docs(ai): ai.md becomes a router; invariants move to AGENTS.md; budgets gated

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.
This commit is contained in:
2026-10-29 11:08:07 -07:00
parent a311c8feb1
commit c9ac348976
6 changed files with 187 additions and 279 deletions
+104 -49
View File
@@ -1,7 +1,8 @@
# AGENTS — how to work in this repo # AGENTS — how to work in this repo
This file is auto-read at the start of every session. The project keeps its Auto-read at the start of every session: the rules, the invariants that break the product, and
memory as a linked map of markdown files — read it before doing anything. 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 ## ⚠️ 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.* > *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 **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 work lands directly on `main`, committed per work unit. No `taskNN-*` branches, no PRs. The scope
`taskNN-*` branches, no PRs. The scope discipline above is about test focus, discipline above is about test focus, not git mechanics.
not git mechanics.
**Commit / push policy (2026-08-30, user directive):** commit every work unit — **Commit / push policy (2026-08-30, user directive):** commit every work unit —
committing is expected and never needs extra permission. Pushing is different: 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 session's agreed checkpoint(s), not per-commit. When a milestone warrants a
push the user will say so (or say "push it"). push the user will say so (or say "push it").
MUST read the architecture guide before planning or modifying the codebase: ## Onboarding (in order — and no further)
[`ai.md`](ai.md) (+ [`schema.md`](schema.md) for the memory-map conventions).
MUST read the coding guide before writing tests or code: the [Working 1. [`HANDOFF.md`](HANDOFF.md) — current state: branch, what's in flight, next action, landmines.
rules](#working-rules) in this file — this repo has no `CODING.md`; **Trust it**; no branch hunting, no file-scanning to re-derive what it already says.
conventions live here and in `ai.md`. 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. `<dir>/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). **Why it is this short:** the mandatory read used to be ~56k tokens of map, so sessions paid it
2. [`ai.md`](ai.md) — the AI guide: architecture, patterns, decisions, current state. and then skimmed — which is exactly how stale facts survive. Read the one subsystem you touch.
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. `<dir>/index.md` — the index for any directory you're about to touch.
**If `HANDOFF.md` exists, trust it as current state** — no `fsck`, no branch ## Reading budget
hunting, no file-scanning to re-derive what it already states, unless it points
at a problem. 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/<subsystem>.md`, `MyMistakes.md`, or `git log`.
## Working rules ## Working rules
- **Never work without the map.** If the map contradicts the code, the code wins - **Never work without the map.** If the map contradicts the code, the code wins and the facts
and the map gets fixed in the same change (stale facts are corrected, not appended). your change **falsifies** are corrected in the same commit (stale facts are corrected, not
- **Every feature change ships with its memory update in the SAME commit:** appended). Cosmetic drift — a stale task number, a heading — may wait for a sweep; it is not a
`ai.md` for architecture/patterns, `TASKS.md` for status, index files when reason to stall a unit.
layout changes. No code commit without its docs — a follow-up "docs backfill" - **Memory rides with the change, not as a unit of its own.** Correct what the change
commit is a broken rule, not a style. falsifies in the same commit; a broad restatement can be batched at a milestone.
- **Rewrite `HANDOFF.md` at session end, compaction, or any interruption.** - **`HANDOFF.md` is ≤40 lines and is overwritten, never appended:** branch, in flight, dirty
Never end a session with uncommitted work unrecorded — the handoff names the files, next action, new landmines. History belongs to `git log`.
branch, the dirty files, and why it stopped. - **The first time a fact costs a hunt, record it where it will be found:** paths / ports /
- **The first time a fact costs a hunt (secrets path, DB path, port, recovery secrets / build commands → `docs/ai/environment.md`; subsystem traps → that subsystem's
source), record it** in `ai.md`/indexes/`HANDOFF.md` so the next session never `docs/ai` file; one-off recipes → `MyMistakes.md` → Recipes registry.
re-hunts it. - **Derived-solution rule (record once, then grep):** when you work out a reusable solution — a
- **Derived-solution rule (record once, then grep):** the moment you work out a recipe, workaround, or how-to (e.g. how to shrink and re-encode an image for the README) —
reusable solution — a recipe, workaround, or how-to (e.g. how to shrink and write it into `MyMistakes.md` **in the same session**, and **grep `MyMistakes.md` first** on
re-encode an image for the README) — write it into `MyMistakes.md` **in the same any "I've done this before but have to figure it out again" wall. An un-recorded solution
session**, before you finish the task. And always **grep `MyMistakes.md` first** guarantees the user hears it derived again (the image-shrink incident, 2026-08-29).
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.
- **Follow existing conventions** — MVVM, `RelayCommand` for actions, - **Follow existing conventions** — MVVM, `RelayCommand` for actions,
`ViewModelBase.SetProperty<T>()`, all styles in `Themes/Controls.xaml` `ViewModelBase.SetProperty<T>()`, all styles in `Themes/Controls.xaml`
(merged once in `App.xaml`; never duplicate per-window). (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 - **Spin guard (objectively triggered):** a SECOND failed remedy for the same
symptom = STOP. No third guess over the same code. Research the established 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`, 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 then code it. Repeated failure is a research trigger, not a persistence trigger.
trigger, not a persistence trigger.
- **Response style: no default planning template.** Do the work, then report - **Response style: no default planning template.** Do the work, then report
what changed and what's next. No "Plans & Pitfalls", pros/cons tables, or 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`). step-by-step plans unless the user asks for a plan first. A short diff-style summary beats a
- **No-Fluff Mode is available on request** — unpadded, ruthless review that proposal document every time.
argues rather than reassures (see `ai.md`). Optional, never the default. - **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 ## 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 ### Pre-commit audit
Before every commit, run `scripts/scope-check.sh` with the declared file list. One gate: `./scripts/verify.sh "<file1>" "<file2>" ...` with the declared scope. It runs the doc
If any file appears in `git diff` but not in the declared scope, either justify budgets, a **clean** build (0 warnings), the test suite, and `scope-check.sh`. Docs-only units run
it or revert the change. The script enforces what the rule demands. `scope-check.sh` alone.
```bash ```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 ## Build
+39 -203
View File
@@ -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 > Read [`AGENTS.md`](AGENTS.md) first: the rules and the invariants. This file is the **map** —
> status and YouTube API research in [`TASKS.md`](TASKS.md); brand/marketing > what the product is, how it is built, and where each subsystem's truth lives. It carries no
> palette and launch strategy in [`MARCOM.md`](MARCOM.md) (gitignored — read > narrative and no status.
> it before any branding or marketing work); directory maps in each folder's
> `index.md`. Reading order: this file → `TASKS.md` → `MARCOM.md` → `<dir>/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.
## Response style ## What it is
No default "Plans & Pitfalls" / planning boilerplate. Respond directly and A Windows-only WPF (.NET 8) app that live-streams to YouTube. Five fixed scenes
concisely: **do the queued work, then report what changed and what's next.** (Starting / Live / BRB / Chat / Ending), a pure C# software compositor over an always-1920×1080
Skip feature pitch, step-by-step implementation plans, pros/cons tables, and master, a bundled-ffmpeg encoder pushing RTMP, and a WASAPI audio chain. Free forever, GPL-3.0,
"potential pitfalls" sections unless the user explicitly asks for a plan first. no account creation — it links an existing YouTube channel. One-click go-live is the product;
A short diff-style summary beats a proposal document every time. 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 Windows `dotnet.exe` from WSL; takes are read in **Release**. Full detail in
OBS, CEV, and WPF ecosystems long before us. Two consequences: [`docs/ai/environment.md`](docs/ai/environment.md).
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.
**Derived-solution rule (2026-08-29, driven by the image-shrink incident):** when ```bash
you work out any one-off, reusable solution (recipe / workaround / how-to), write "/mnt/c/Program Files/dotnet/dotnet.exe" build "C:\Users\gramp\Documents\Code\projects\ytLive\ytLive.csproj"
it into `MyMistakes.md` → **Recipes registry that same session**, and grep it "/mnt/c/Program Files/dotnet/dotnet.exe" run -c Release
before ever re-deriving. A solution recorded once ends the loop; an un-recorded "/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"
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.
`./scripts/verify.sh "<files>"` is the one pre-commit gate (doc budgets, clean build at 0
warnings, tests, scope).
## Architecture ## 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** | | `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** | | `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 | | `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`) | | `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 | | `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 | Subsystem | File | Read when |
hermetic real-`MainWindow` test rule live in |---|---|---|
**[`docs/ai/environment.md`](docs/ai/environment.md)** — read it before building, running, or | 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 |
adding a test. | 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) ·
`<dir>/index.md` (per-directory map).
The brand/naming split, AI transparency, the GPL licensing prohibitions (do not violate), the ## Current state
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`.
[`HANDOFF.md`](HANDOFF.md) — branch, what's in flight, the next action, live landmines. Do not
restate it here.
+4 -4
View File
@@ -1,9 +1,9 @@
# docs/ai — index # docs/ai — index
Subsystems split out of [`ai.md`](../../ai.md) (2026-10-26) so a session pays only for the Subsystems split out of [`ai.md`](../../ai.md) so a session pays only for the subsystem its task
subsystem its task touches. The split is a **verbatim move**: history, traps and measurements are touches. `ai.md` is now a **router** (what the product is, the architecture table, where truth
unchanged, and `ai.md` keeps a short stub carrying the load-bearing invariants plus a link here, so lives) and `AGENTS.md` carries the invariants that hold across all of them, so no rule is
no rule is reachable only from a file you forgot to open. reachable only from a file you forgot to open.
Read the file whose row matches the task. There is no required reading order. Read the file whose row matches the task. There is no required reading order.
+3 -3
View File
@@ -1,11 +1,11 @@
# docs — index # docs — index
Reference material that is too large for [`ai.md`](../ai.md)'s front half. Nothing here is Reference material that would bloat the router. Nothing here is mandatory reading on session
mandatory reading on session start; open the file whose subsystem your task touches. start; open the file whose subsystem your task touches.
| Path | Purpose | | 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) | | `ytLlive-preview.jpg` | Preview screenshot used by [`README.md`](../README.md) |
See [`schema.md`](../schema.md) for the memory-map conventions. See [`schema.md`](../schema.md) for the memory-map conventions.
+15 -16
View File
@@ -16,15 +16,15 @@ that every other memory file follows. Read `ai.md` first; this file explains
| File | Role | Entry point? | | 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 | | `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` | 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" |
| `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: branch, in flight, next action, landmines (≤40 lines, overwritten never appended) | yes — trust it as current state |
| `HANDOFF.md` | Current operational state: what's in flight, landmines, next step, secret/DB/port locations | 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 |
| `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 |
| `schema.md` | This file: the conventions below | when in doubt | | `schema.md` | This file: the conventions below | when in doubt |
| `<dir>/index.md` | Per-directory map (progressive disclosure): what lives there + links | when diving into code | | `<dir>/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 | — | | `Views/` | Reserved for Views; currently empty | — |
## Conventions ## Conventions
@@ -46,16 +46,15 @@ that every other memory file follows. Read `ai.md` first; this file explains
## Reading order ## Reading order
1. `AGENTS.md` — auto-read on-ramp; it points you here. 1. `AGENTS.md` — auto-read on-ramp: rules + invariants; it points you here.
2. `ai.md` — what this app is, how it's built, current state, open decisions. 2. `HANDOFF.md` — current state. Trust it; don't re-derive it.
3. `TASKS.md` — what's done, what's next, and the YouTube API constraints. 3. `ai.md` — the router: architecture and where truth lives.
4. `<dir>/index.md` — to understand a directory before reading its files. 4. The one `docs/ai/*.md` file that matches the task.
5. The source files themselves — always the authority over any memory file. 5. `<dir>/index.md` — before reading that directory's files.
6. The source files themselves — always the authority over any memory file.
## Integration rule ## Integration rule
Every feature change ships with its memory update **in the same commit**: Every feature change ships with what it **falsifies** corrected in the same commit — a path, an
`ai.md` for architecture / patterns, `TASKS.md` for status, index files when invariant, a status line. Broader restatement may be batched at a milestone. The point is that
the layout changes. No code commit without its docs — a follow-up "docs the map never stays wrong; it is not a tax on every commit.
backfill" commit is a broken rule, not a style. That is what keeps the map
accurate enough to trust next session.
+22 -4
View File
@@ -8,7 +8,8 @@
# against fresh compiler output. # against fresh compiler output.
# #
# Requires WINDOWS dotnet (never Linux dotnet — it re-downloads the windowsdesktop packs # 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 set -euo pipefail
@@ -18,7 +19,24 @@ DOTNET="/mnt/c/Program Files/dotnet/dotnet.exe"
ROOT="$(cd "$(dirname "$0")/.." && pwd)" ROOT="$(cd "$(dirname "$0")/.." && pwd)"
ROOTWIN="$(wslpath -w "$ROOT")" 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/<subsystem>.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 "$DOTNET" clean "$ROOTWIN\\ytLive.csproj" > /dev/null
OUT="$("$DOTNET" build "$ROOTWIN\\ytLive.csproj" 2>&1)" OUT="$("$DOTNET" build "$ROOTWIN\\ytLive.csproj" 2>&1)"
if echo "$OUT" | grep -qI ": warning"; then 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 "$OUT" | grep -E "Build succeeded" || { echo "$OUT" | tail -5; exit 1; }
echo "✓ Build clean, 0 warnings." 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" 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 "$DOTNET" build "$ROOTWIN\\ytLive.Tests\\ytLive.Tests.csproj" > /dev/null
set +e 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)." echo "⚠ Test exit=$TEST_EXIT but only the 2 known pre-existing failures present (OK)."
fi fi
echo "══ 3/3 SCOPE CHECK ══" echo "══ 4/4 SCOPE CHECK ══"
"$ROOT/scripts/scope-check.sh" "$@" "$ROOT/scripts/scope-check.sh" "$@"
echo "" echo ""