From c9ac3489766d1c2eb65b9fc37abf9db086df6ebc Mon Sep 17 00:00:00 2001 From: gramps Date: Thu, 29 Oct 2026 11:08:07 -0700 Subject: [PATCH] docs(ai): ai.md becomes a router; invariants move to AGENTS.md; budgets gated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ai.md 228 → 64 lines. It now carries only: what the product is, the build commands, the architecture table, a "where the truth lives" table, and a pointer to HANDOFF. No narrative, no status, no duplicated invariants. AGENTS.md (195 lines) is now the single always-read file: - the 20 invariants that break output, wipe user data, or ship a legal/trust breach — one line each, pointing at the doc that explains it (they replace 110 lines of invariant stubs that duplicated docs/ai/*.md verbatim) - Response style + No-Fluff Mode moved in from ai.md (rules belong in the rules file) - onboarding is now: HANDOFF → ai.md router → the ONE docs/ai file → dir index; TASKS.md is read when the question is "what's next", not to write code - memory rides with the change but is no longer a unit of its own; HANDOFF is ≤40 lines and overwritten, never appended Anti-rebloat gate: scripts/verify.sh gains step 1/4, a doc-budget check (AGENTS ≤210, ai ≤100; HANDOFF/TASKS caps land with the queue trim). A commit that pads a capped file instead of moving detail fails the gate. schema.md and docs/index.md corrected for the new roles and reading order. Docs-only unit: scope-check run, no build. --- AGENTS.md | 153 +++++++++++++++++++---------- ai.md | 242 ++++++++-------------------------------------- docs/ai/index.md | 8 +- docs/index.md | 6 +- schema.md | 31 +++--- scripts/verify.sh | 26 ++++- 6 files changed, 187 insertions(+), 279 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b67f07d..d694a67 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,8 @@ # AGENTS — how to work in this repo -This file is auto-read at the start of every session. The project keeps its -memory as a linked map of markdown files — read it before doing anything. +Auto-read at the start of every session: the rules, the invariants that break the product, and +the map to everything else. `ai.md` is the router, `docs/ai/*.md` is the subsystem truth, +`HANDOFF.md` is current state. ## ⚠️ CRITICAL: The Good Dog Rule @@ -16,9 +17,8 @@ When the user asks for changes that would involve multiple integration tests: > *A good dog learns one trick perfectly before learning the next.* **No feature branches pre-1.0 (decided 2026-08-24):** solo dev, pre-1.0 — all -work lands directly on `main`, committed and pushed per work unit. No -`taskNN-*` branches, no PRs. The scope discipline above is about test focus, -not git mechanics. +work lands directly on `main`, committed per work unit. No `taskNN-*` branches, no PRs. The scope +discipline above is about test focus, not git mechanics. **Commit / push policy (2026-08-30, user directive):** commit every work unit — committing is expected and never needs extra permission. Pushing is different: @@ -27,48 +27,53 @@ that means commits land locally every partial; `git push` happens at the session's agreed checkpoint(s), not per-commit. When a milestone warrants a push the user will say so (or say "push it"). -MUST read the architecture guide before planning or modifying the codebase: -[`ai.md`](ai.md) (+ [`schema.md`](schema.md) for the memory-map conventions). +## Onboarding (in order — and no further) -MUST read the coding guide before writing tests or code: the [Working -rules](#working-rules) in this file — this repo has no `CODING.md`; -conventions live here and in `ai.md`. +1. [`HANDOFF.md`](HANDOFF.md) — current state: branch, what's in flight, next action, landmines. + **Trust it**; no branch hunting, no file-scanning to re-derive what it already says. +2. [`ai.md`](ai.md) — the router: what the product is, the architecture table, and where each + subsystem's truth lives. +3. **The one** [`docs/ai/`](docs/ai/index.md) file that matches your task — not all of them. + The routing table at the top of `ai.md` picks it. +4. `/index.md` — the index for any directory you're about to touch. -## Onboarding (in order) +`TASKS.md` is the queue. Read it when the user asks what's next or status is part of the +question — it is not required reading to write code. -1. [`schema.md`](schema.md) — the memory-map conventions (what lives where, how to keep it true). -2. [`ai.md`](ai.md) — the AI guide: architecture, patterns, decisions, current state. -3. [`TASKS.md`](TASKS.md) — the task catalog (index + queue state); open items and API research facts are in [`TASKS/`](TASKS/). -4. [`HANDOFF.md`](HANDOFF.md) — current operational state: what's in flight, landmines, next step. -5. `/index.md` — the index for any directory you're about to touch. +**Why it is this short:** the mandatory read used to be ~56k tokens of map, so sessions paid it +and then skimmed — which is exactly how stale facts survive. Read the one subsystem you touch. -**If `HANDOFF.md` exists, trust it as current state** — no `fsck`, no branch -hunting, no file-scanning to re-derive what it already states, unless it points -at a problem. +## Reading budget + +Enforced by `scripts/verify.sh`; a commit that pads instead of replacing fails the gate. + +| File | Cap | Role | +|---|---|---| +| `AGENTS.md` | 210 lines | rules + invariants, always read | +| `ai.md` | 100 lines | router only — no narrative, no status | +| `HANDOFF.md` | 40 lines | current state, overwritten every time | +| `TASKS.md` | 120 lines | the active queue; done work moves to `TASKS/archive.md` | + +If a fact does not fit, it belongs in `docs/ai/.md`, `MyMistakes.md`, or `git log`. ## Working rules -- **Never work without the map.** If the map contradicts the code, the code wins - and the map gets fixed in the same change (stale facts are corrected, not appended). -- **Every feature change ships with its memory update in the SAME commit:** - `ai.md` for architecture/patterns, `TASKS.md` for status, index files when - layout changes. No code commit without its docs — a follow-up "docs backfill" - commit is a broken rule, not a style. -- **Rewrite `HANDOFF.md` at session end, compaction, or any interruption.** - Never end a session with uncommitted work unrecorded — the handoff names the - branch, the dirty files, and why it stopped. -- **The first time a fact costs a hunt (secrets path, DB path, port, recovery - source), record it** in `ai.md`/indexes/`HANDOFF.md` so the next session never - re-hunts it. -- **Derived-solution rule (record once, then grep):** the moment you work out a - reusable solution — a recipe, workaround, or how-to (e.g. how to shrink and - re-encode an image for the README) — write it into `MyMistakes.md` **in the same - session**, before you finish the task. And always **grep `MyMistakes.md` first** - when you hit a "I've done this before but have to figure it out again" wall. A - solution recorded once ends the re-derivation loop; an un-recorded solution is a - guarantee you will re-derive it and make the user sit through it again. This is - the fix for the image-shrink incident (2026-08-29): the recipe was never written - down, so it had to be worked out from scratch a second time. +- **Never work without the map.** If the map contradicts the code, the code wins and the facts + your change **falsifies** are corrected in the same commit (stale facts are corrected, not + appended). Cosmetic drift — a stale task number, a heading — may wait for a sweep; it is not a + reason to stall a unit. +- **Memory rides with the change, not as a unit of its own.** Correct what the change + falsifies in the same commit; a broad restatement can be batched at a milestone. +- **`HANDOFF.md` is ≤40 lines and is overwritten, never appended:** branch, in flight, dirty + files, next action, new landmines. History belongs to `git log`. +- **The first time a fact costs a hunt, record it where it will be found:** paths / ports / + secrets / build commands → `docs/ai/environment.md`; subsystem traps → that subsystem's + `docs/ai` file; one-off recipes → `MyMistakes.md` → Recipes registry. +- **Derived-solution rule (record once, then grep):** when you work out a reusable solution — a + recipe, workaround, or how-to (e.g. how to shrink and re-encode an image for the README) — + write it into `MyMistakes.md` **in the same session**, and **grep `MyMistakes.md` first** on + any "I've done this before but have to figure it out again" wall. An un-recorded solution + guarantees the user hears it derived again (the image-shrink incident, 2026-08-29). - **Follow existing conventions** — MVVM, `RelayCommand` for actions, `ViewModelBase.SetProperty()`, all styles in `Themes/Controls.xaml` (merged once in `App.xaml`; never duplicate per-window). @@ -83,13 +88,63 @@ at a problem. - **Spin guard (objectively triggered):** a SECOND failed remedy for the same symptom = STOP. No third guess over the same code. Research the established answer externally, cite its URL in the commit message AND in `MyMistakes.md`, - then code it. You do not know everything — repeated failure is a research - trigger, not a persistence trigger. + then code it. Repeated failure is a research trigger, not a persistence trigger. - **Response style: no default planning template.** Do the work, then report what changed and what's next. No "Plans & Pitfalls", pros/cons tables, or - step-by-step plans unless the user asks for a plan first (see `ai.md`). -- **No-Fluff Mode is available on request** — unpadded, ruthless review that - argues rather than reassures (see `ai.md`). Optional, never the default. + step-by-step plans unless the user asks for a plan first. A short diff-style summary beats a + proposal document every time. +- **No-Fluff Mode is available on request.** Invoke with "no-fluff mode" (or similar) for + ruthless review instead of reassurance: strip pleasantries, emojis and padding; treat the + user's input as a draft to deconstruct; give unvarnished truth. Occasional and + explicitly-invoked — never the default. + +## Invariants that break the product + +Violating any of these breaks output, wipes a user's data, or ships a legal/trust breach. Each +line points at the file that explains it. + +- **The Background is one locked layer per scene** — index 0, never addable/removable/reorderable; + Live is `DisplayCapture`, every other canonical scene is static art. Never feed a capture row to + the `ActiveBackgroundImage` overlay. → `docs/ai/capture.md` +- **Every global overlay composites on every render path** — pump, preview/thumbnails, scene dump. + A feature wired into one path is silently absent from the output. → `docs/ai/overlays.md` +- **`VideoFrame.Epoch` bumps on every frame from a recycling producer**, or the pump's paste cache + and full-render signature serve stale pixels. → `docs/ai/frame-pipeline.md` +- **Never demote threads inside `Parallel.For`** — `StripPool` owns four `BelowNormal` threads; + strips below 64 rows run inline. → `docs/ai/frame-pipeline.md` +- **Submit never blocks:** the encoder queue is bounded, the newest frame is dropped and counted, + and `StopAsync` flushes the encoder before awaiting the pump. → `docs/ai/frame-pipeline.md` +- **Audio closes before video:** `StopStream` runs `StopLive` (audio EOF) before stopping the pump; + the reverse order stalls on pipe backpressure. → `docs/ai/output.md` +- **rawvideo is paced by arrival, never by a timestamp** — no `-re`, and `nextTick` is never + rebased to wall-now (that turns a hitch into a silent time-lapse). → `docs/ai/output.md` +- **The encoder is probed, never libx264** — NVENC → QSV → AMF → OpenH264; no static or GPL codec + path. → `docs/ai/output.md` +- **REC and ON-AIR are radio-exclusive** — one ffmpeg, one output, in the UI and the VM. + → `docs/ai/output.md` +- **ffmpeg is bundled and separate** — never downloaded at runtime, never linked into the app; the + pin stays `lgpl-shared`. → `docs/ai/product.md` +- **A modal's falsy result never falls through into its side effect** — Cancel on the recording + save dialog deletes the temp footage. → `docs/ai/output.md` +- **Any test that constructs `MainWindow` must point `LayoutPathOverride` at a temp DB** and clear + it in `finally`, or it overwrites the user's real layout. → `docs/ai/environment.md` +- **The vertical tier is geometry only**, and the Chat camera is rect-locked in the MODEL + (`VerticalShapeLocked`), not just in the UI. → `docs/ai/capture.md` +- **Never override the user's camera choice** — there is deliberately no fallback camera. + → `docs/ai/capture.md` +- **Chat is rendered, never stored** — no history, no moderation log, no reward payloads; the + Store 11.12 certification answer depends on it. → `docs/ai/product.md` +- **Full trust only** — a packaged build flipped to `appContainer` silently loses recordings. + → `docs/ai/product.md` +- **A `PopupRoot` has no visual parent** — ancestor walks must fall back to + `LogicalTreeHelper.GetParent`, or a popup row click reads as an outside click. + → `docs/ai/ui-surface.md` +- **Licensing: no non-CC0 icon art, no dropped `THIRD-PARTY-NOTICES.txt`, no + licence-incompatible dependency, no unvetted ML model.** → `docs/ai/product.md` +- **The private-only broadcast lock is deliberate** — do not unlock it, do not raise it as a + question; it ends at v1 by the creator's ruling. → `docs/ai/product.md` +- **Read render numbers in Release only** — Debug pixel paths are several× slower. + → `docs/ai/environment.md` ## Scope Lock @@ -115,12 +170,12 @@ don't make it. A stable file touched by an unrelated task is a bug, not a featur ### Pre-commit audit -Before every commit, run `scripts/scope-check.sh` with the declared file list. -If any file appears in `git diff` but not in the declared scope, either justify -it or revert the change. The script enforces what the rule demands. +One gate: `./scripts/verify.sh "" "" ...` with the declared scope. It runs the doc +budgets, a **clean** build (0 warnings), the test suite, and `scope-check.sh`. Docs-only units run +`scope-check.sh` alone. ```bash -./scripts/scope-check.sh "Models/Scene.cs" "ViewModels/MainViewModel.cs" "MainWindow.xaml" +./scripts/verify.sh "Models/Scene.cs" "ViewModels/MainViewModel.cs" "MainWindow.xaml" ``` ## Build diff --git a/ai.md b/ai.md index 2c533b5..f51f9df 100644 --- a/ai.md +++ b/ai.md @@ -1,58 +1,30 @@ -# ytLlive — AI Guide +# LlamaCasty (repo `ytLive`) — AI guide & router -> Memory map entry point. Conventions live in [`schema.md`](schema.md); task -> status and YouTube API research in [`TASKS.md`](TASKS.md); brand/marketing -> palette and launch strategy in [`MARCOM.md`](MARCOM.md) (gitignored — read -> it before any branding or marketing work); directory maps in each folder's -> `index.md`. Reading order: this file → `TASKS.md` → `MARCOM.md` → `/index.md` → source. -> -> **Subsystem detail is split out:** large blocks of this file now live under -> [`docs/ai/`](docs/ai/index.md) and this file keeps a short invariant stub + link where each was -> removed. Open [`docs/ai/index.md`](docs/ai/index.md) to find the right one, or ignore it entirely -> until your task touches a moved subsystem. +> Read [`AGENTS.md`](AGENTS.md) first: the rules and the invariants. This file is the **map** — +> what the product is, how it is built, and where each subsystem's truth lives. It carries no +> narrative and no status. -## Response style +## What it is -No default "Plans & Pitfalls" / planning boilerplate. Respond directly and -concisely: **do the queued work, then report what changed and what's next.** -Skip feature pitch, step-by-step implementation plans, pros/cons tables, and -"potential pitfalls" sections unless the user explicitly asks for a plan first. -A short diff-style summary beats a proposal document every time. +A Windows-only WPF (.NET 8) app that live-streams to YouTube. Five fixed scenes +(Starting / Live / BRB / Chat / Ending), a pure C# software compositor over an always-1920×1080 +master, a bundled-ffmpeg encoder pushing RTMP, and a WASAPI audio chain. Free forever, GPL-3.0, +no account creation — it links an existing YouTube channel. One-click go-live is the product; +OBS is the escape hatch for anything bigger. -## Derivative work (spin guard mandated in AGENTS.md) +## Build, run, test -Nothing we build is novel — streaming/overlay/WebView2 problems were solved by -OBS, CEV, and WPF ecosystems long before us. Two consequences: -1. **Proactive:** new features start with a quick external scan, not head-first - design (citation goes in the commit message). -2. **Objectively triggered spin guard:** a second failed fix for the same symptom - means STOP theorizing and research the established answer externally; there is - no unique bug in this repo that the wider ecosystem hasn't hit. -This project's hard lesson: the web-source bounding saga (2026-08) burned 9 -commits rediscovering that a browser source is a *fixed canvas* — OBS keeps it at -a stable page size, crops/hugs content via the box, and clips at the edge. The -apparent "gap" that ended the saga was a widget's own CSS glow effect, not a bug. +Windows `dotnet.exe` from WSL; takes are read in **Release**. Full detail in +[`docs/ai/environment.md`](docs/ai/environment.md). -**Derived-solution rule (2026-08-29, driven by the image-shrink incident):** when -you work out any one-off, reusable solution (recipe / workaround / how-to), write -it into `MyMistakes.md` → **Recipes registry that same session**, and grep it -before ever re-deriving. A solution recorded once ends the loop; an un-recorded -one guarantees the user hears it derived again (see `AGENTS.md` → Working rules → 🔬). - - -## No-Fluff Mode (on demand) - -Invoke with "no-fluff mode" (or similar) when you want ruthless review instead -of reassurance. In that mode: - -- Strip all polite pleasantries, emojis, transitions, and conversational padding. -- Treat the user's input as a draft to be methodically deconstructed or - strengthened — argue, correct, and sharpen rather than agree. -- Give unvarnished truth, not reassurance. - -This is an occasional, explicitly-invoked mode — **never the default**. The -default response style above stays in effect unless invoked. +```bash +"/mnt/c/Program Files/dotnet/dotnet.exe" build "C:\Users\gramp\Documents\Code\projects\ytLive\ytLive.csproj" +"/mnt/c/Program Files/dotnet/dotnet.exe" run -c Release +"/mnt/c/Program Files/dotnet/dotnet.exe" vstest "C:\Users\gramp\Documents\Code\projects\ytLive\ytLive.Tests\bin\Debug\net8.0-windows10.0.19041.0\ytLive.Tests.dll" +``` +`./scripts/verify.sh ""` is the one pre-commit gate (doc budgets, clean build at 0 +warnings, tests, scope). ## Architecture @@ -62,167 +34,31 @@ C# / WPF (.NET 8) following MVVM: |------|------| | `Models/` | Plain data types — Scene, Source (incl. `ClipShape`, `IsMirrored`, `VideoImageSource`), QualityOption, StreamConfig, StreamHealth, YouTubeChannel, ChatMessage, **Socials (`SocialService` enum + `SocialEntry`/`SocialsConfig` + `SocialServiceIcons`) — the social bar** | | `ViewModels/` | MainViewModel — `public partial class`, one file per functional area (Scenes, Background, Webcam, Audio, Trax, Socials, Streaming, Chat, Overlays, Account, License, Recording — split complete, see `ViewModels/index.md`); **Chat.cs is a thin delegating facade over `Services/ChatOverlayLayer.cs` (Commit G, first true decomposition), and now also over `Services/AlertOverlayLayer.cs` (TASK 43 — the native alert box, same true-component pattern)**; GoLiveViewModel, ReuseImageViewModel, CameraPickerViewModel, **SocialsDialogViewModel** | -| `Services/` | YouTube OAuth2, stream/broadcast management, live chat polling, LayoutStore (SQLite), **SocialValidator (`ISocialValidator` seam + `HttpSocialValidator` default)**, **webcam: `VideoFrame` seam + `CameraDeviceInfo`/`ICameraEnumerator`/`ICameraFrameSource` interfaces + `MediaCaptureCameraEnumerator`/`MediaCaptureFrameSource` (WinRT) + `CameraManager`**, **screen capture: `IFullScreenDetector`/`Win32FullScreenDetector` + `IScreenCaptureSource`/`ScreenCaptureFrameSource` (WinRT GraphicsCapture) + `ScreenCaptureManager` + `ScreenCaptureSourceFactory` + `Direct3D11Helper`/`CaptureInterop` (COM bridges)**, **media source: `IMediaFrameSource` + `MediaVideoSource` (spawns ffmpeg `rawvideo` BGRA decode) + `MediaVideoSourceManager` (refcount-by-path session owner) + pure `RawVideoFrameReader` + `IDecodeProcess`/`FfmpegDecodeProcess` process seam (binary-stdout mirror of `IEncoderProcess`; see "Media source")**, **compositor: `SceneCompositor` + `CompositorOptions` + pure `StretchMath` + `StaticPixelCache` (see [`docs/ai/frame-pipeline.md`](docs/ai/frame-pipeline.md) → "Scene compositor")**, **alerts: `ChatOverlayLayer` + `AlertOverlayLayer`/`AlertRenderer` (the native celebration-zone overlay, TASK 43 — see "Native events & alerts")**, **audio: `IAudioSource` seam + `WasapiLoopbackAudioSource`/`WasapiMicAudioSource` (NAudio WASAPI) + `AudioMixer` + pure `AudioLevelMeter`/`WaveToFloat`/`VoiceFilterChain`/`LowShelfFilter`/`HighShelfFilter`/`NoiseGate`/`Compressor`/`AutoDucker`/`AudioRingBuffer`/`TinyResampler`/`AudioSyncDelay` + `MusicPlayer` + `ITraxPlayer` (TRAX toggle seam — FakePlayer in tests; the dot is green/red/gray and the error scrolls terse in the desktop-audio bar; MusicVolume fixed 0.5) + `IAudioPipeWriter`/`NamedPipeAudioWriter` (see [`docs/ai/output.md`](docs/ai/output.md) → "Live audio capture")**, **encoder: `IFfmpegEncoder`/`FfmpegEncoder` + `IEncoderProcess`/`FfmpegEncoderProcess` + `IFfmpegLocator`/`FfmpegLocator` + pure `FfmpegArgs`/`FfmpegProgressParser`/`FfmpegEncoderPicker` + the `FramePump` frame producer (see [`docs/ai/output.md`](docs/ai/output.md) → "Live encoder" + [`docs/ai/frame-pipeline.md`](docs/ai/frame-pipeline.md) → "Live frame pipeline")**, **notifications: `INotificationService` seam (`AppNotificationSeverity` Info/Success/Warning/Error) + `NotificationService` (Notification.Wpf toasts, see "Toast notifications")** | +| `Services/` | YouTube OAuth2, stream/broadcast management, live chat polling, LayoutStore (SQLite), **SocialValidator (`ISocialValidator` seam + `HttpSocialValidator` default)**, **webcam: `VideoFrame` seam + `CameraDeviceInfo`/`ICameraEnumerator`/`ICameraFrameSource` interfaces + `MediaCaptureCameraEnumerator`/`MediaCaptureFrameSource` (WinRT) + `CameraManager`**, **screen capture: `IFullScreenDetector`/`Win32FullScreenDetector` + `IScreenCaptureSource`/`ScreenCaptureFrameSource` (WinRT GraphicsCapture) + `ScreenCaptureManager` + `ScreenCaptureSourceFactory` + `Direct3D11Helper`/`CaptureInterop` (COM bridges)**, **media source: `IMediaFrameSource` + `MediaVideoSource` (spawns ffmpeg `rawvideo` BGRA decode) + `MediaVideoSourceManager` (refcount-by-path session owner) + pure `RawVideoFrameReader` + `IDecodeProcess`/`FfmpegDecodeProcess` process seam (binary-stdout mirror of `IEncoderProcess`; see [`TASKS/task-21-media-source.md`](TASKS/task-21-media-source.md))**, **compositor: `SceneCompositor` + `CompositorOptions` + pure `StretchMath` + `StaticPixelCache` (see [`docs/ai/frame-pipeline.md`](docs/ai/frame-pipeline.md) → "Scene compositor")**, **alerts: `ChatOverlayLayer` + `AlertOverlayLayer`/`AlertRenderer` (the native celebration-zone overlay, TASK 43 — see [`docs/ai/overlays.md`](docs/ai/overlays.md))**, **audio: `IAudioSource` seam + `WasapiLoopbackAudioSource`/`WasapiMicAudioSource` (NAudio WASAPI) + `AudioMixer` + pure `AudioLevelMeter`/`WaveToFloat`/`VoiceFilterChain`/`LowShelfFilter`/`HighShelfFilter`/`NoiseGate`/`Compressor`/`AutoDucker`/`AudioRingBuffer`/`TinyResampler`/`AudioSyncDelay` + `MusicPlayer` + `ITraxPlayer` (TRAX toggle seam — FakePlayer in tests; the dot is green/red/gray and the error scrolls terse in the desktop-audio bar; MusicVolume fixed 0.5) + `IAudioPipeWriter`/`NamedPipeAudioWriter` (see [`docs/ai/output.md`](docs/ai/output.md) → "Live audio capture")**, **encoder: `IFfmpegEncoder`/`FfmpegEncoder` + `IEncoderProcess`/`FfmpegEncoderProcess` + `IFfmpegLocator`/`FfmpegLocator` + pure `FfmpegArgs`/`FfmpegProgressParser`/`FfmpegEncoderPicker` + the `FramePump` frame producer (see [`docs/ai/output.md`](docs/ai/output.md) → "Live encoder" + [`docs/ai/frame-pipeline.md`](docs/ai/frame-pipeline.md) → "Live frame pipeline")**, **notifications: `INotificationService` seam (`AppNotificationSeverity` Info/Success/Warning/Error) + `NotificationService` (Notification.Wpf toasts, see "Toast notifications")** | | `Helpers/` | ViewModelBase (INotifyPropertyChanged), RelayCommand, ImageCache, AppLog (file logger), FocusPreservingListBox, OAuthCredentials, **TokenStore (DPAPI session persistence)**, **BuildStamp (wordmark release counter `#N`, incremented by `GenerateBuildStamp` from the local `BuildCounter.txt`; per-build GUID/time are logged to startup.log)**, visibility converters | | `Themes/` | `Controls.xaml` — the single dark-theme source, merged once in `App.xaml` (see `Themes/index.md`) | | `MainWindow.xaml` | Dark theme; top bar + health banner; work area has a fixed 360px left column and preview column, top-left readouts/resolution and scene strip above preview, lower left panel + preview; bottom bar is an empty spacer | -### Environment, LAN, build & tests — moved +## Where the truth lives -Server IPs and SSH, the Windows `dotnet.exe` build commands, the test suite map and the -hermetic real-`MainWindow` test rule live in -**[`docs/ai/environment.md`](docs/ai/environment.md)** — read it before building, running, or -adding a test. +| Subsystem | File | Read when | +|---|---|---| +| Capture: backdrop, webcam, background effects, vertical tier | [`docs/ai/capture.md`](docs/ai/capture.md) | touching any frame source, the Background row, camera acquisition, or the 9:16 geometry | +| Compositor, render budget, frame pump | [`docs/ai/frame-pipeline.md`](docs/ai/frame-pipeline.md) | changing the render loop, per-frame cost, pacing, backpressure, or dropped/juddering frames | +| ffmpeg, encoder, recording, audio chain | [`docs/ai/output.md`](docs/ai/output.md) | ffmpeg args, REC/ON-AIR, the audio mix, loudness or sync | +| Social bar, alerts, chat overlays | [`docs/ai/overlays.md`](docs/ai/overlays.md) | anything drawn on top of the output | +| Go-live, broadcasts, health, chat API | [`docs/ai/youtube-api.md`](docs/ai/youtube-api.md) | go-live, broadcast lifecycle, health polling, streaming close-out | +| Control surface, theming, transitions | [`docs/ai/ui-surface.md`](docs/ai/ui-surface.md) | anything the creator looks at | +| Brand, licensing, monetization, distribution | [`docs/ai/product.md`](docs/ai/product.md) | branding, pricing, licensing, packaging | +| Hosts, build commands, test map | [`docs/ai/environment.md`](docs/ai/environment.md) | building, running, adding a test | +| Routes for every subsystem | [`docs/ai/index.md`](docs/ai/index.md) | you want the full table with a "read when" per file | -### Product, brand, licensing & distribution — moved +Lookup, not reading: [`MyMistakes.md`](MyMistakes.md) (grep first on any "did this before?" wall) · +[`TASKS.md`](TASKS.md) (the queue) · `TASKS/` (per-task detail and API research) · +`/index.md` (per-directory map). -The brand/naming split, AI transparency, the GPL licensing prohibitions (do not violate), the -free-forever monetization posture, auth/account assumptions, and packaging/distribution live -in **[`docs/ai/product.md`](docs/ai/product.md)**. - -### YouTube Live API — moved - -Go-live ordering, the broadcast-binding fix, the private-only lock, health parsing, end-stream -pre-flight, Test mode and chat-insert shape live in -**[`docs/ai/youtube-api.md`](docs/ai/youtube-api.md)**. - -### Overlays: social bar, alerts, chat — moved - -The social bar, the native alert box (six animations, alert clip, ticker) and the chat box -conventions live in **[`docs/ai/overlays.md`](docs/ai/overlays.md)**. - -### UI surface — moved - -The control surface, scene strip, preview rail, hotkeys, broadcast drawer, toasts, transitions, -brand logo, launch geometry and the theme/properties conventions live in -**[`docs/ai/ui-surface.md`](docs/ai/ui-surface.md)**. - -### Capture — backdrop, webcam, effects & the vertical tier — moved - -Screen/game/window backdrop capture, the webcam seam and sessions, the MediaPipe background effects -and the vertical tier's per-scene camera placement live in -**[`docs/ai/capture.md`](docs/ai/capture.md)** — read it before touching capture, cameras, the -Background row, or anything the 9:16 tier re-places. - -Invariants that hold even if you never open that file: - -- **The Background is a locked per-screen layer:** exactly one per canonical scene, named - "Background", pinned at index 0, never addable/removable/reorderable — Live is `DisplayCapture`, - every other screen is static art. `NormalizeBackgrounds` heals any layout to that model on load. -- **Never feed a capture row to the `ActiveBackgroundImage` overlay.** That layer is fed by - static-art rows only; a flavor-blind `IsBackground` lookup there permanently covers the live - desktop/game feed (the 2026-08-23 regression). -- **`window:`/`picker:` capture keys are session-scoped and stripped on load** (a recycled HWND - would capture the wrong window), and an explicit window pin beats auto game/desktop detection - while that window is alive. -- **The WinRT interop paths are settled:** read frame bytes with `WindowsRuntimeMarshal.TryGetDataUnsafe` - (the `IMemoryBufferByteAccess` ComImport throws `Invalid cast` on every frame under CsWinRT) and - build the D3D device via `CreateDirect3D11DeviceFromDXGIDevice` (the - QI-for-`IDirect3DDxgiInterfaceAccess` trick returns E_NOINTERFACE on current Windows). -- **`VideoFrame` + the capture interfaces are the seam** — nothing above the WinRT layer leaks WinRT - types; capture is CPU-first (`MemoryPreference.Cpu`, `SharedReadOnly`), the reader-subtype ladder - tries each candidate under its OWN catch (one rejected format must never abort acquisition), and - there is deliberately **no fallback camera** — the user's pick is never overridden. -- **One webcam identity app-wide, placements per scene.** `IsWebcamAttainable` needs a live lock, not - just a saved id; the app keeps its own base ref, so removing every placement leaves the session up. -- **`CameraManager` is the ONE background-effects processing point** — segment once per raw frame, - render per-scene variants keyed by effect+strength, mask smoothed per camera session (0.75 new / - 0.25 previous). The raw path stays untouched so a slow model never stalls the webcam, and the - bundled model's provenance is fixed (do not swap in PINTO #109). -- **The vertical tier is geometry only:** `BeginVerticalLayout`/`EndVerticalLayout` park and swap the - live placement, so the compositor and `PreviewPane.xaml` never learn about tiers, and the stored - X/Y/W/H/ClipShape columns stay the **16:9** layout. The Chat camera is rect-only through the MODEL - (`VerticalShapeLocked`), not merely through a hidden button. - -### Rendering budget, measurement & the compositor — moved - -Frame cadence, per-element composite timing, the scene dump, the render budget and the -`SceneCompositor` design live in **[`docs/ai/frame-pipeline.md`](docs/ai/frame-pipeline.md)** — -read it before changing the render loop, the compositor, or anything that moves per-frame cost. - -Invariants that hold even if you never open that file: - -- **Every global overlay must be composited on EVERY render path** (pump, preview/thumbnails, scene - dump — `SceneCompositor.Overlay` is shared for all three). A feature wired into one path is silently - absent from the output; that bug (flash + social bar "preview-only forever", 2026-09-26) is why the - rule exists. -- **`VideoFrame.Epoch` is not optional.** A producer that recycles buffers MUST bump `Epoch` per - frame, or `FramePump`'s buffer-identity paste cache and the full-render signature serve stale - pixels. `BrandFlashPresenter` reuses one 8MB master buffer and stamps `Epoch` per read for exactly - this reason. -- **`StripPool` owns four dedicated `BelowNormal` background threads** for strip-parallel - compositing; strips below 64 rows run inline. Never demote threads inside `Parallel.For` — it runs - on thread-pool threads and starves the scheduler (that mistake once broke audio-mix timing). -- **Read render numbers in Release only.** Debug pixel paths are several× slower; a Release 1080p - composite measures ~1.0–1.1ms against the 16.67ms budget, and only Release takes are evidence. - -### Output — ffmpeg, encoder, recording & audio — moved - -The bundled `ffmpeg.exe` decision, the encoder subprocess (`FfmpegEncoder`/`IEncoderProcess`), local -recording, and the whole WASAPI capture → filter → mix → named-pipe audio chain live in -**[`docs/ai/output.md`](docs/ai/output.md)** — read it before touching ffmpeg args, the encoder -seams, REC/ON-AIR, or the audio mix. - -Invariants that hold even if you never open that file: - -- **ffmpeg is BUNDLED, never downloaded or fetched at runtime.** `IFfmpegLocator` checks the bundle - first, PATH second, and throws loudly if neither exists; `GuardBundledFfmpeg` fails the build when - the folder is missing. `TheLocatorCarriesNoNetworkCapability` prevents the old download design - (Store policy 10.2.2, and it 404'd in production on 2026-09-01) from coming back. -- **Args stay pure** (`FfmpegArgs.Build`) and **never use `-re`** — the pump is the pacer; a second - clock on the rawvideo demux fights it. Keyframes `-g fps×4` with a closed GOP, `-bf 0`, yuv420p. -- **Encoder choice is probed, never libx264** (GPL): NVENC → QSV → AMF → OpenH264. Never add a static - or GPL codec path — see the licensing guardrails in `docs/ai/product.md`. -- **REC and ON-AIR are radio-exclusive by creator ruling.** The dual-output machinery stays generic, - but the UI can never arm both: one ffmpeg, one output, "we're not them". -- **A modal's falsy result must never fall through into its side effect.** Cancel on the recording - save dialog DISCARDS the temp file explicitly (the bug that kept recordings under the default name). -- **Torn-down sessions must leave no lit pill:** every pump death / go-live prep failure runs the full - `StopStream` teardown, and `AudioMixer.StopLive` is idempotent for rollbacks that never started. -- **Audio stop order:** `StopStream` closes the audio pipe (`StopLive` → audio EOF) **BEFORE** stopping - the frame pump (video EOF); the reverse order stalls on pipe backpressure. -- **Audio is honest by measurement, not by ear:** meters show true dBFS (`ToDisplay`), the mix runs - `SpeechLeveler` + `MasterLimiter` (−1 dBFS ceiling), and gains are `Func` seams read live each tick - (`AudioGainProvider`) — a decorative volume control is a bug. Judge loudness with the bundled - ffmpeg (`ebur128`), never the in-app meter alone. - -### The live frame pipeline — moved - -`FramePump` and the whole path from frame producer to encoder (pacing, pool/paste caches, slice -history, teardown, health stats) live in -**[`docs/ai/frame-pipeline.md`](docs/ai/frame-pipeline.md)**. - -Invariants that hold even if you never open that file: - -- **rawvideo is stamped by ARRIVAL, never by a timestamp.** ffmpeg assigns pts from frame order at - the declared fps, so supply rate = output speed. The pump duplicates the previous frame on a missed - deadline (correct duration) and **never resets `nextTick` to wall-now** — rebasing turns a hitch - into a silent time-lapse (take-2 lesson, 2026-09-01). -- **Submit never blocks.** The encoder queue is bounded and a drain task owns the stdin write + - flush; on overflow the **newest frame is dropped and counted** (`DroppedFrames`) — overload costs - content, never a frozen or smeared session. -- **Stop order:** `StopAsync` stops the encoder first — flushing the pending queue to EOF so the file - finalizes with no accepted frame lost — **before** awaiting the pump loop; the reverse order - deadlocked on pipe backpressure. -- **Judge output in Release, by the burned `_outputIndex`** — the 6-digit counter burned into each - frame is what lets a recorded file be audited for drops after the fact; stats alone are not proof. - - -### Current limitations / TODOs - -- `Helpers/OAuthCredentials.cs` contains the real ClientId/ClientSecret. Auth is complete and the session **persists via Windows DPAPI** (`Helpers/TokenStore.cs` → `%APPDATA%\ytLlive\ytLlive.auth`, CurrentUser scope), reloaded best-effort at startup with a proactive refresh of a near-expiry access token. Sign-in/Change Account lives **inside the Start Stream dialog** (two-state flow — no separate Connect button). Sign-out is **explicit only**, and that means **Logout — NOT Change Account**: `StopStream()` does NOT clear the session — TASK 18's 2026-08-29 reversal ("stopping a recording leaves the creator signed in") — the older "graceful End signs out" line here was stale and is corrected against the code (2026-09-01). `YouTubeAuthService` takes an optional `HttpClient` + `sessionChanged` + `openBrowser` callback (test seams + save hook; services are still constructed in `MainViewModel`). **Change Account must never sign out first** (fixed 2026-09-27): it used to `await SignOutYouTubeAsync(); await SignInAsync();`, and since `SignOutYouTubeAsync` → `TokenStore.Clear()` **deletes** `ytLlive.auth`, cancelling in the Google chooser left the creator logged out of a working account. It is now just `await SignInAsync()` — success overwrites the token via the `sessionChanged` hook, cancel/failure leaves the session untouched. The OAuth flow also **releases its loopback callback port on retry**: abandoning the browser leaves the attempt holding `localhost:8765` for its 5-minute timeout, so a new `AuthenticateAsync` cancels and awaits the abandoned attempt before binding, serialised by a `SemaphoreSlim` (`_authGate`) — without that gate, two overlapping calls both see a free port and race to bind it (the exact failure the takeover prevents) -- **Google Cloud project ownership (recorded 2026-09-27, do not re-hunt):** OAuth ClientId is `198374416003-s8tm3rb5csh91quvlg6mnec6srmrr6md.apps.googleusercontent.com`, so the **Cloud project number is `198374416003`**. That project is owned by the **`llamachile.shop@gmail.com`** ("gramps") Google account. ⛔ **This is NOT the same account as the YouTube channel a creator signs into the app with** — console access and channel identity are separate logins, and assuming they are the same was the trap that stalled the search. Direct link that resolves it in one hop: `https://console.cloud.google.com/apis/credentials?project=198374416003`. If the picker shows no match, it defaults to the **"Recent"** tab — switch to **"All"** before concluding the project is missing. -- Scene/source/asset layout + the social bar persist (SQLite, schema v8); the OAuth session persists (DPAPI); the paid-unlock state persists (LayoutStore Settings table — `LicenseKey`/`LicenseValidatedAt`/`IsPremium`, 14-day offline grace) -- `YouTubeStreamService` manages the **variable reusable stream** (shipped 2026-08-16): `GetOrCreateReusableStreamAsync` lists `liveStreams?mine=true` and reuses the existing `cdn.isReusable` stream, creating it only on first use (`resolution=variable`, `frameRate=variable`); the stream is cached via `LayoutStore` (`SaveReusableStream`/`LoadReusableStream`, Settings table) and bound at broadcast insert (`boundStreamId`). Health (shipped 2026-08-16): `GetStreamHealthAsync(streamId)` polls `liveStreams?part=status` for `healthStatus` + `configurationIssues[]` → `StreamHealth`; the real API nests them under `status.healthStatus = {status, lastUpdateTimeSeconds, configurationIssues[]}` — the parse accepts that object shape and the legacy flat-string shape (fix 2026-09-22: the object shape crashed every poll with `requires an element of type 'String'`); banner decision in pure `StreamHealthReporter` -- Webcam capture is shipped (milestone 1); the live desktop/game backdrop is shipped (ship task #1); **the output compositor (TASK 4 ship step 1) is SHIPPED**, **the FFmpeg locator (TASK 4 ship step 2) is SHIPPED**, **the encoder + RTMP push (TASK 4 ship step 3) is SHIPPED**, **WASAPI audio capture (TASK 4 ship step 4) is SHIPPED**, **frame-pipeline wiring (TASK 4 ship step 5) is SHIPPED**, **health stats (TASK 4 ship step 6) is SHIPPED**, **one-click go-live + private-only enforcement (TASK 4 ship step 7) is SHIPPED** — TASK 4 (RTMP Ingest) is fully done; full plan in `TASKS.md` -- `StreamConfig` defaults (`TargetBitrate=6000`, `Resolution="1920x1080"`) are stale — the live dropdown drives `StreamHealth.CurrentBitrate`/`FPS` instead -- **v1 task queue (2026-08-19):** tasks 19-23 added for v1 feature completeness; TASK 3.18 (chat box) shipped; TASK 16 (infinity display) shipped; TASK 10 (Polar billing) shipped — steps 1-7 (Velopack bootstrap, PolarLicenseService, license entry UI, LayoutStore entitlement, BrandFlash toggle, startup re-validation, PremiumUrl wired); Velopack update URL pending: - - TASK 10: Polar billing — license key entry, offline entitlement, brand flash gating, Velopack auto-updates - - TASK 19/23 merged: Control Surface UX — director's control room with an explicit EDIT toggle, scene strip above the preview, readouts in the top-left cell, and preview-rail drawers; current behavior is in `docs/ai/ui-surface.md` - - TASK 20: Hotkeys (global keyboard shortcuts) — the single biggest UX gap - - Broadcast metadata pull-out + launch geometry — shipped 2026-08-24 ("Text" tab on the Live screen; see `docs/ai/ui-surface.md`) - - TASK 21: Media source (video file playback) — starting soon videos, BRB loops - - TASK 22: Audio sync offset — lip-sync correction for USB mics/capture cards - - These were identified through competitive analysis and are table-stakes for any streaming software in 2025-2026. Full details in `TASKS.md`. +## Current state +[`HANDOFF.md`](HANDOFF.md) — branch, what's in flight, the next action, live landmines. Do not +restate it here. diff --git a/docs/ai/index.md b/docs/ai/index.md index d02f6e8..0c8b6e1 100644 --- a/docs/ai/index.md +++ b/docs/ai/index.md @@ -1,9 +1,9 @@ # docs/ai — index -Subsystems split out of [`ai.md`](../../ai.md) (2026-10-26) so a session pays only for the -subsystem its task touches. The split is a **verbatim move**: history, traps and measurements are -unchanged, and `ai.md` keeps a short stub carrying the load-bearing invariants plus a link here, so -no rule is reachable only from a file you forgot to open. +Subsystems split out of [`ai.md`](../../ai.md) so a session pays only for the subsystem its task +touches. `ai.md` is now a **router** (what the product is, the architecture table, where truth +lives) and `AGENTS.md` carries the invariants that hold across all of them, so no rule is +reachable only from a file you forgot to open. Read the file whose row matches the task. There is no required reading order. diff --git a/docs/index.md b/docs/index.md index 06c313e..62f2f12 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,11 +1,11 @@ # docs — index -Reference material that is too large for [`ai.md`](../ai.md)'s front half. Nothing here is -mandatory reading on session start; open the file whose subsystem your task touches. +Reference material that would bloat the router. Nothing here is mandatory reading on session +start; open the file whose subsystem your task touches. | Path | Purpose | |------|---------| -| [`ai/`](ai/index.md) | Split-out subsystems of the AI guide — each started life verbatim inside `ai.md`, which keeps a short invariant stub pointing here | +| [`ai/`](ai/index.md) | Subsystem truth for the AI guide — one file per subsystem, routed from [`ai.md`](../ai.md) | | `ytLlive-preview.jpg` | Preview screenshot used by [`README.md`](../README.md) | See [`schema.md`](../schema.md) for the memory-map conventions. diff --git a/schema.md b/schema.md index fb0fb64..48da346 100644 --- a/schema.md +++ b/schema.md @@ -16,15 +16,15 @@ that every other memory file follows. Read `ai.md` first; this file explains | File | Role | Entry point? | |------|------|--------------| -| `AGENTS.md` | Auto-read on-ramp for AI sessions — points to the map and the working rules | **always read first** | +| `AGENTS.md` | Auto-read on-ramp: working rules, **the invariants that break the product**, response style | **always read first** | +| `ai.md` | **Router** — what the product is, the architecture table, a "where the truth lives" table. No narrative, no status (≤100 lines, gated) | yes — the map | | `README.md` | Human-facing intro: what the app is, how to run it, roadmap | no | -| `ai.md` | **AI guide + session handoff** — architecture, patterns, decisions, the cognitive map home | **yes — start here** | -| `TASKS.md` | **Catalog/index** for the task queue. The actual task files live in [`TASKS/`](TASKS/) (one file per task); `TASKS.md` carries the status table, open items, and research facts pointer. Read this for queue state; open task files for full detail. | yes — for status | -| `HANDOFF.md` | Current operational state: what's in flight, landmines, next step, secret/DB/port locations | yes — trust it as current state | -| `MyMistakes.md` | Per-task failure log **+ RECIPES REGISTRY** (derived-solution rule): one-off solutions / how-tos get recorded here the same session, then grepp'd before ever re-deriving them (see `AGENTS.md` 🔬) | grep first on any "did this before?" wall | +| `TASKS.md` | The active queue. Full task detail and research live in [`TASKS/`](TASKS/); done work in `TASKS/archive.md` | when the task is "what's next" | +| `HANDOFF.md` | Current operational state: branch, in flight, next action, landmines (≤40 lines, overwritten never appended) | yes — trust it as current state | +| `MyMistakes.md` | Failure log **+ RECIPES REGISTRY**: one-off solutions / how-tos get recorded here the same session, then grepp'd before ever re-deriving them | grep first on any "did this before?" wall | | `schema.md` | This file: the conventions below | when in doubt | | `/index.md` | Per-directory map (progressive disclosure): what lives there + links | when diving into code | -| `docs/ai/*.md` | **Split-out subsystems** of `ai.md` — verbatim moves (from 2026-10-26); `ai.md` keeps a short invariant stub + link, and [`docs/ai/index.md`](docs/ai/index.md) routes | when the task touches a moved subsystem | +| `docs/ai/*.md` | **Subsystem truth** — one file per subsystem (capture, frame pipeline, output, overlays, youtube-api, ui-surface, product, environment). `ai.md` routes; [`docs/ai/index.md`](docs/ai/index.md) lists them with a "read when" | when the task touches that subsystem | | `Views/` | Reserved for Views; currently empty | — | ## Conventions @@ -46,16 +46,15 @@ that every other memory file follows. Read `ai.md` first; this file explains ## Reading order -1. `AGENTS.md` — auto-read on-ramp; it points you here. -2. `ai.md` — what this app is, how it's built, current state, open decisions. -3. `TASKS.md` — what's done, what's next, and the YouTube API constraints. -4. `/index.md` — to understand a directory before reading its files. -5. The source files themselves — always the authority over any memory file. +1. `AGENTS.md` — auto-read on-ramp: rules + invariants; it points you here. +2. `HANDOFF.md` — current state. Trust it; don't re-derive it. +3. `ai.md` — the router: architecture and where truth lives. +4. The one `docs/ai/*.md` file that matches the task. +5. `/index.md` — before reading that directory's files. +6. The source files themselves — always the authority over any memory file. ## Integration rule -Every feature change ships with its memory update **in the same commit**: -`ai.md` for architecture / patterns, `TASKS.md` for status, index files when -the layout changes. No code commit without its docs — a follow-up "docs -backfill" commit is a broken rule, not a style. That is what keeps the map -accurate enough to trust next session. +Every feature change ships with what it **falsifies** corrected in the same commit — a path, an +invariant, a status line. Broader restatement may be batched at a milestone. The point is that +the map never stays wrong; it is not a tax on every commit. diff --git a/scripts/verify.sh b/scripts/verify.sh index dd706c0..3f6bfc6 100644 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -8,7 +8,8 @@ # against fresh compiler output. # # Requires WINDOWS dotnet (never Linux dotnet — it re-downloads the windowsdesktop packs -# over the slow 9p bridge; see ai.md -> Run). Reads the Windows path from the WSL mount. +# over the slow 9p bridge; see docs/ai/environment.md -> Run). Reads the Windows path from the +# WSL mount. set -euo pipefail @@ -18,7 +19,24 @@ DOTNET="/mnt/c/Program Files/dotnet/dotnet.exe" ROOT="$(cd "$(dirname "$0")/.." && pwd)" ROOTWIN="$(wslpath -w "$ROOT")" -echo "══ 1/3 CLEAN BUILD (expect 0 warnings) ══" +echo "══ 1/4 DOC BUDGETS ══" +# The map stays cheap to read. A commit that PADS a capped file instead of replacing or +# moving detail fails here (see AGENTS.md -> Reading budget). +check_budget() { + local rel="$1" cap="$2" lines + [ -f "$ROOT/$rel" ] || { echo "✗ $rel is missing"; exit 1; } + lines=$(wc -l < "$ROOT/$rel") + if [ "$lines" -gt "$cap" ]; then + echo "✗ $rel is $lines lines (cap $cap)." + echo " Move the detail to docs/ai/.md, MyMistakes.md or git log — do not pad." + exit 1 + fi + printf " ✓ %-12s %3s/%s lines\n" "$rel" "$lines" "$cap" +} +check_budget "AGENTS.md" 210 +check_budget "ai.md" 100 + +echo "══ 2/4 CLEAN BUILD (expect 0 warnings) ══" "$DOTNET" clean "$ROOTWIN\\ytLive.csproj" > /dev/null OUT="$("$DOTNET" build "$ROOTWIN\\ytLive.csproj" 2>&1)" if echo "$OUT" | grep -qI ": warning"; then @@ -29,7 +47,7 @@ fi echo "$OUT" | grep -E "Build succeeded" || { echo "$OUT" | tail -5; exit 1; } echo "✓ Build clean, 0 warnings." -echo "══ 2/3 TEST SUITE ══" +echo "══ 3/4 TEST SUITE ══" TESTDLL="$ROOTWIN\\ytLive.Tests\\bin\\Debug\\net8.0-windows10.0.19041.0\\ytLive.Tests.dll" "$DOTNET" build "$ROOTWIN\\ytLive.Tests\\ytLive.Tests.csproj" > /dev/null set +e @@ -52,7 +70,7 @@ if [ "$TEST_EXIT" -ne 0 ]; then echo "⚠ Test exit=$TEST_EXIT but only the 2 known pre-existing failures present (OK)." fi -echo "══ 3/3 SCOPE CHECK ══" +echo "══ 4/4 SCOPE CHECK ══" "$ROOT/scripts/scope-check.sh" "$@" echo ""