diff --git a/.gitignore b/.gitignore
index 2e2c2be6..cf688e41 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@ desktop.ini
perf/*
*.orig
*.bak
+*.original.md
.DS_Store
npm-debug.log
node_modules
diff --git a/AGENTS.md b/AGENTS.md
index 98418830..6307527e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -176,6 +176,8 @@ changes — update **both** the Starlight docs (for humans) and the agent guide
(for agents). The agent guide should be token-efficient: dense, code-first,
minimal prose.
+**Before staging any doc, verify every package, spec path, and URL it names exists on the target branch.** Pay extra attention to untracked or generated files in the working tree. Full checklist: [`tko.io/public/agents/process.md`](tko.io/public/agents/process.md#never-ship-docs-that-reference-things-that-dont-exist-on-the-target-branch).
+
## Docs Verification
When validating `tko.io` documentation changes with the local docs site:
@@ -202,7 +204,9 @@ Avoid scope creep. If an improvement would balloon the PR, file a follow-up issu
## Review Your Own Change Adversarially
-Before declaring a change done, steelman the case against it. Ask what could go wrong, what assumption could be false, what future goal it quietly forecloses, what coverage or signal it weakens, who it surprises. Get an independent second pass (a colleague, a subagent where available) on changes that touch framework internals, test coverage, public APIs, or docs the whole team relies on.
+Before declaring a change done, steelman the case against it. Ask what could go wrong, what assumption could be false, what future goal it quietly forecloses, what coverage or signal it weakens, who it surprises.
+
+**Adversarial review is mandatory for in-scope changes** (code, tests, public API, agent-facing docs, CI, `tools/build.ts`, `vitest.config.ts`, `biome.json`, landing commit messages). A single pair of eyes (yours) is not enough in a dark factory — the missing human reviewer has to be replaced by a second agent that was not told what "good" looks like and is asked only "where is this wrong?". Spawn a fresh subagent, brief it with the artefact + claim only (no author reasoning), bias toward flagging, verify any findings defensively, and record the outcome at the end of the commit message that introduces the in-scope change — one audit line per in-scope commit, never the PR description (that's for *why* the change exists, not reviewer ceremony). Out of scope: typos, whitespace, comment corrections. Full how-to and audit-trail format: [`tko.io/public/agents/process.md`](tko.io/public/agents/process.md#adversarial-review-is-mandatory).
This is the ceiling on "Always Improve": that section pushes toward *more* in a PR; this one pushes toward *scrutiny* of what's in it. Use both — improve in scope, audit the scope itself here.
@@ -217,6 +221,7 @@ Failure modes specific to a published low-level framework, worth probing every t
- Patching the symptom, not the root cause
- Unrelated refactors or opportunistic redesigns that balloon the PR (the "Always Improve" bar is *small, low-risk, in-scope* fixes — anything larger belongs in its own PR)
- Silent assumptions about environment, timing, or ordering
+- Docs that reference packages, APIs, or spec paths that do not exist on the target branch (see "Agent-First Documentation" → "Never ship docs that reference things that don't exist on the target branch")
If the change doesn't survive a ten-minute attempt to poke holes in it, it's not ready.
diff --git a/tko.io/public/agents/contract.md b/tko.io/public/agents/contract.md
index 9e88e8a9..69a47359 100644
--- a/tko.io/public/agents/contract.md
+++ b/tko.io/public/agents/contract.md
@@ -40,6 +40,80 @@ In those cases:
- let the custom binding read observables and update the DOM
- avoid making the custom binding the authoritative owner of app state
+## DOM Mutation Containment
+
+DOM mutation and direct DOM-API calls belong inside a `BindingHandler` (a class that extends `LifeCycle` and is registered as a binding). They do not belong in component view models, utility modules, or arbitrary class methods.
+
+Violations (do not do outside a `BindingHandler`):
+- `document.createElement`, `document.body.appendChild` — bypasses the binding pipeline
+- `element.querySelector` / `querySelectorAll` — except to find a mount root for `applyBindings`
+- `element.style.*`, `element.classList.*` — use the `style` / `css` bindings
+- `element.addEventListener` — use `click`, `event:{…}`, or a binding handler
+- `appendChild` / `insertBefore` / `replaceWith` — use `foreach`, `if`, `template`
+- `element.innerHTML = …` — use `text` or (when trusted markup is the point) `html`; also flag XSS if content is external
+- `element.focus()` / `element.select()` — use `hasFocus` or a focus-orchestration binding
+- `requestAnimationFrame` for DOM scheduling — move inside a binding that owns the frame loop
+- Storing `HTMLElement` references as component view-model fields — the element belongs to the binding
+
+Where these calls are safe:
+- inside a class that extends `BindingHandler` (its `constructor`/`init`/`update` receive the owning element)
+- reading observables whose values drive DOM updates via bindings — that is the whole point
+- direct DOM reads in tests/verification code after bindings are active
+
+Binding handlers are the prescribed escape hatch for imperative DOM work: they receive the exact element, participate in the lifecycle, and dispose cleanly. Mutating the DOM from elsewhere creates a second source of truth for DOM state and the reactive graph loses sight of it.
+
+## Component Design: Binding Handlers and Component View Models
+
+### Binding handlers — narrow scope, one DOM task
+
+Each `BindingHandler` subclass should do one thing to its element. A handler that sanitizes HTML *and* walks the DOM for citations *and* renders diagrams *and* rewrites tables should be split into separate handlers, each composable on the same element.
+
+Violations:
+- a binding handler performing multiple unrelated DOM operations — split by concern
+- using a binding handler where a component with JSX would suffice — prefer declarative JSX when the rendering can be expressed that way
+
+Not a violation:
+- a handler that is complex because its single DOM task is inherently complex (rich-text editor, canvas renderer, third-party widget bridge)
+
+### Component view models — rendering only
+
+A component view model (a class registered with `components.register(...)`, typically extending `ComponentABC`) should contain only code that produces its template output. Data transformation, parsing, business rules, and utilities belong in standalone `.ts` files the component imports.
+
+Violations:
+- template method exceeding ~80 lines of JSX without decomposition — extract nested child components
+- manipulating the DOM directly from a component method — delegate to a binding handler
+- non-rendering logic (parsing, domain rules, formatting of structured data) embedded in the component — move to utilities
+- instantiating a child component with `params={{ ... }}` wrapping every prop (see "Component Params in JSX" in `/agents/guide.md`)
+
+Not a violation:
+- a component that is large because it composes many small child components
+- simple inline computeds that only map data for the template (`const label = ko.pureComputed(() => format(value()))`)
+
+## Component Communication via `subscribable`
+
+When a component needs to trigger an imperative action inside a binding handler (for example "print this iframe", "scroll to top", "focus on demand"), pass a `ko.subscribable` owned by the component into the handler. The handler subscribes in its constructor and disposes the subscription in `dispose`.
+
+Do not model the command as an `observable` holding a function (`observable<(() => void) | null>(null)`). Function-valued observables capture closures that may retain DOM references, muddle cleanup, and do not broadcast.
+
+```js
+// Component owns the channel and fires events
+class PrintableCard extends ComponentABC {
+ printChannel = new ko.subscribable()
+ onPrintClick = () => this.printChannel.notifySubscribers(null, 'print')
+}
+
+// Binding handler subscribes and disposes with its lifecycle
+class PrintIframe extends BindingHandler {
+ constructor (params) {
+ super(params)
+ const channel = this.value
+ this.addDisposable(channel.subscribe(() => this.$element.contentWindow.print(), null, 'print'))
+ }
+}
+```
+
+The element reference stays inside the binding handler, `dispose` cleans up naturally, and multiple elements can listen on the same channel without coordination.
+
## Security Preference
- Prefer `text` over `html`.
diff --git a/tko.io/public/agents/guide.md b/tko.io/public/agents/guide.md
index 3346dcbb..fdfc0fcd 100644
--- a/tko.io/public/agents/guide.md
+++ b/tko.io/public/agents/guide.md
@@ -33,6 +33,13 @@ ko.when(viewModel.isReady, () => console.log('Ready'))
ko.when(() => viewModel.isReady() && viewModel.hasData()).then(() => console.log('Ready'))
```
+### Observables vs computed dependency management
+
+- Bare `ko.observable()` and `ko.observableArray()` are safe anywhere — they are containers and do not re-evaluate.
+- Inside a class using the `LifeCycle` mixin (for example `BindingHandler`, `ComponentABC`, or anything produced by `LifeCycle.mixInto`), prefer `this.computed(...)` over standalone `ko.computed(...)` and `this.subscribe(observable, cb)` over `observable.subscribe(cb)` — both are auto-disposed when the instance is torn down.
+- In plain view models without `LifeCycle`, `ko.computed(...)` and `observable.subscribe(...)` are fine; call `dispose()` on them when you are done.
+- **Never** create `ko.observable()`, `ko.computed()`, or call `.subscribe()` inside a computed's evaluator. The evaluator runs on every dependency change, so new observables/subscriptions pile up and never dispose — memory and CPU grow unbounded. Create them once outside the computed, or in a `LifeCycle`-enabled constructor.
+
## Bindings
Activate with `ko.applyBindings(viewModel, element)`.
@@ -65,6 +72,69 @@ When the goal is to demonstrate TKO itself, keep the state flow inside observabl
- If an example contrasts reactive models, the counters and highlighted state should also be observable-driven so the example demonstrates the pattern instead of bypassing it.
- The line to avoid is using the DOM itself as the mutable source of truth after bindings are active.
+### Reactive styling
+
+Reactive state that drives visual variations can be expressed several ways in TKO. Two common patterns are shown below; they are examples, not an exhaustive list (CSS custom properties written from a computed, classless inline style objects, component-scoped style blocks, and CSS-in-JS tokens are all reasonable too). **What matters more than the specific pattern is that a codebase applies one consistently** for the same kind of state — mixing approaches for identical problems is what hurts readability.
+
+**Example A — computed class name / inline style.** The computed returns the class name or style string directly. The rule sits next to the reactive expression.
+
+```tsx
+const className = this.computed(() => this.isActive() ? classes.active : classes.inactive)
+return
...
+```
+
+**Example B — computed attribute, CSS selector does the switching.** The computed returns a flag value; CSS reads the attribute and applies styles.
+
+```tsx
+const activeAttr = this.computed(() => this.isActive() || undefined)
+return ...
+```
+
+```css
+.my-component { opacity: 0.5; }
+.my-component[data-active] { opacity: 1; }
+```
+
+**Trade-offs between these two.**
+- A keeps the rule ("active → this class") visible in the component file; refactors that rename classes stay local. Split visibility if the class body lives in a separate stylesheet.
+- B keeps styling in CSS and pairs naturally with the `|| undefined` pattern for binary attributes (see Gotchas); adding a new visual variant means editing CSS, not the component.
+- B sidesteps class-name string concatenation inside computeds for complex multi-state styling. A can avoid the same footgun with template literals or a class-map helper; it just doesn't get it for free.
+- A reads more directly when one class encapsulates many unrelated properties that B would need multiple attribute selectors to reach.
+
+Classic `data-bind` writers for the same effects: `css:` / `style:` for A, `attr:` for B (`data-bind="attr: { 'data-active': activeAttr }"`).
+
+### JSX scope rule
+
+Use JSX inside component view models (classes registered with `components.register(...)`, typically extending `ComponentABC`) or in functions whose output is immediately passed to `tko.jsx.render(...)` and mounted. Do not leave JSX in standalone utility functions that outlive a component lifecycle — without `LifeCycle`, subscriptions and computeds created for that JSX have no owner to dispose them.
+
+```tsx
+// Good: component owns the JSX and its lifecycle
+class MyCard extends ComponentABC {
+ template = () =>
+}
+
+// Also fine: top-level render for a fixed mount
+const { node } = tko.jsx.render()
+document.getElementById('root').appendChild(node)
+
+// Avoid: JSX returned from a utility module without a clear owner
+export const myView = () =>
+```
+
+### Component params in JSX
+
+When instantiating a component from JSX, pass each constructor parameter as its own attribute. Do **not** wrap them in a single `params={{ ... }}` attribute — JSX collects attributes into one object and hands that object to the component constructor, so wrapping in `params` delivers `{ params: { … } }` instead of the flat shape the constructor expects.
+
+```tsx
+// Bad — wrapped in params
+
+
+// Good — each prop is its own attribute
+
+```
+
+The constructor receives `{ output, onView, onEdit }` directly.
+
## Classic data-bind parsing and CSP
Classic `data-bind` parsing is provider-driven. Use `DataBindProvider` when you need binding strings, and combine it with other providers through `MultiProvider` as needed.
@@ -222,6 +292,9 @@ tko.applyBindings({ removeTodo: t => todos.remove(t) }, root)
- **Observable children in JSX** are reactive — when an observable's value changes, the DOM updates. If it becomes `undefined`, a placeholder comment is rendered.
- **Cannot apply bindings twice** to the same DOM node — throws error.
- **`dispose()` on computed/subscriptions** stops updates. After disposal, reading returns last cached value.
+- **HTML-producing computeds** should return `null` for empty output, not `false` or an empty fragment — bindings and JSX observers handle `null` cleanly.
+- **Binary HTML attributes** (`disabled`, `readonly`, `hidden`, `required`, `checked`, `selected`) omit the attribute when the value is `null`, `undefined`, or `false`. Use `|| undefined` in computeds to make "no attribute" explicit: `disabled={this.computed(() => shouldBeDisabled() || undefined)}`. Never return the string `"false"` — it keeps the attribute set.
+- **Prefer named computed variables over inline computeds in JSX.** A computed created inline in JSX is easier to accidentally recreate on each render than one bound to a class field or `const`.
## Testing
diff --git a/tko.io/public/agents/process.md b/tko.io/public/agents/process.md
new file mode 100644
index 00000000..a84fb7a8
--- /dev/null
+++ b/tko.io/public/agents/process.md
@@ -0,0 +1,76 @@
+# Agent Process Rules
+
+Mandatory workflow rules for AI agents working in the TKO repo. AGENTS.md
+references this file for the detail of each rule; the one-line mandate lives
+there so it fires on session load. Read the matching section here before
+declaring work complete.
+
+## Never ship docs that reference things that don't exist on the target branch
+
+Before including a doc file — especially any untracked/generated file you find
+in the working tree — verify every package, export, class, function, spec path,
+or URL it names actually exists on the branch you are merging into (normally
+`main`).
+
+Mandatory checks before staging a docs file:
+
+1. `git ls-files ` — is it tracked? If not, where did it come from?
+ - If it was emitted by a generator (e.g. `tko.io/scripts/generate-verified-behaviors.mjs`), re-run the generator on a clean checkout of the target branch and diff. If the generator does not produce it, it is stale — do not ship it.
+ - If it was hand-written on a different branch, confirm that branch has merged into the target. `git log --all -- ` and `git branch --contains ` will show where it lives.
+2. For each package name in the doc: `ls packages/` and confirm a matching `package.json`. Orphan `@tko/*` references mislead both humans and downstream agents.
+3. For each spec path in the doc: the file must exist at the named location.
+4. For each external URL in the doc: it is OK to trust user-provided URLs, but do not invent them.
+
+The failure mode is shipping a doc that promises behaviour the code does not
+deliver. That is worse than no doc at all — it poisons every future reader
+(human or agent) that trusts the docs as a contract. When in doubt, leave it
+out and open a follow-up.
+
+Also note: `tko.io/public/agents/verified-behaviors/*.md` are regenerated from
+`packages/*/verified-behaviors.json` on every `prebuild` / `predev` / CI build.
+Hand-edits there are transient — the generator wins. Edit the JSON source, or
+put hand-authored guidance in `guide.md` / `contract.md` instead.
+
+## Adversarial review is mandatory
+
+A single pair of eyes (yours) is not enough in a dark factory. If small teams
+plus agents are going to maintain what once took a big team, the missing human
+reviewer has to be replaced by a second agent that was not told what "good"
+looks like and is asked only "where is this wrong?".
+
+**In scope** (always run the pass):
+- Code changes in `packages/*` or `builds/*`
+- Test additions, deletions, or env changes
+- Public `@tko/*` API surface
+- Docs in `tko.io/public/` and agent-facing files (`llms.txt`, `agents/*`)
+- CI workflows, `tools/build.ts`, `vitest.config.ts`, `biome.json`
+- Changesets and commit messages that land a PR
+
+**Out of scope** (proportionality):
+- Typos, whitespace, comment corrections that do not change behavior or public surface
+- The adversarial review itself — its report is not an artefact that needs its own review. One pass per change closes the loop.
+
+**How to run the pass**, every time it is in scope:
+
+- Spawn a fresh subagent (`Agent` tool, specialized `subagent_type` when one fits, otherwise `Explore`). Do **not** let the agent that produced the change also sign it off — that is a null check.
+- Brief the reviewer with the **artefact (diff or file) and the claim it makes** ("this PR does X"). Do **not** include your reasoning for why it works, the commit message you intend to write, or the PR title. Anchoring the reviewer to your conclusion defeats the pass.
+- Prompt it to enumerate likely failure modes *before* reviewing for correctness (e.g. "list the three most likely ways this could break, then check each"). Ask: "where is this wrong, what would break, what would mislead a future reader?" Bias toward flagging.
+- Apply the AGENTS.md failure-modes list *and* the domain-specific checklist for the artefact type (docs → "Never ship docs that reference things that don't exist"; tests → disposal leaks + env assumptions; public API → backwards-compat + changeset; etc.).
+- If the reviewer returns a finding, **verify the specific claim** (re-run the command, read the named file, grep for the named symbol) — do not rely on your own prior reasoning to dismiss it. Subagents produce false positives, so verification is defensive, not a licence to override. If after verification you still believe the finding is wrong, record the reasoning in the PR description or as a code comment so a future reader (or maintainer) can judge it; do not silently reject.
+- If the pass surfaces a finding that belongs in a separate PR, file a follow-up or spawn a task rather than expanding the current change — keep "Keep PRs focused" intact.
+- Record that the pass was run. A single line at the end of the **commit message** that introduces the in-scope change is enough:
+ `Adversarial pass: . Result: clean` or
+ `Adversarial pass: . Flagged : . Resolved: .`
+ If a PR has multiple in-scope commits, each gets its own audit line; do not batch them. The commit message is the right home: it travels with the change in `git log` forever, stays granular to what was reviewed, and keeps process metadata out of the PR description (which is for *why* the change exists and *what* it does — not for reviewer ceremony). Do not add review outcomes to the PR description. Without *some* audit trail, compliance is unverifiable and the rule is trivially gamed; the commit message is a cheap, out-of-the-way place to leave it.
+ Caveat for squash-merge repos: squashing collapses per-commit audit lines into the squash target's message. That is acceptable as long as the lines survive the squash; if the squash message is auto-truncated or rewritten, copy the audit lines into it manually before merging.
+- Only after the pass returns clean (or returns findings that you have verified and addressed, deferred to a follow-up, or consciously rejected with documented reasoning) may you declare the work done.
+
+## Credits
+
+Architectural review guidelines at `agents/contract.md` ("DOM Mutation
+Containment", "Component Design", "Component Communication via
+`subscribable`") and related Gotchas in `agents/guide.md` and `llms.txt`
+originate from code-review rules in the MinuteBox project
+([NetPleadings/MinuteBox#9518](https://github.com/NetPleadings/MinuteBox/pull/9518),
+@jameskozlowskimb) with input from @ctcarton, ported upstream and reworded for
+TKO primitives.
diff --git a/tko.io/public/llms.txt b/tko.io/public/llms.txt
index b6d1ce88..c320f2bf 100644
--- a/tko.io/public/llms.txt
+++ b/tko.io/public/llms.txt
@@ -12,12 +12,13 @@
## Agent Docs
-- /agents/soul.md — why Knockout works the way it does (design philosophy)
+- /agents/soul.md — why Knockout works (design philosophy)
- /agents/guide.md — API reference, gotchas, examples
- /agents/glossary.md — domain terms, concepts, packages
-- /agents/testing.md — how to run and verify TKO code
+- /agents/testing.md — run + verify TKO code
- /agents/contract.md — state/binding/DOM architecture
-- /agents/options.md — ko.options.* — when to use defineOption vs core Options fields
+- /agents/options.md — `ko.options.*` — when to use `defineOption` vs core Options fields
+- /agents/process.md — mandatory agent workflow (doc-ref verification, adversarial review)
- /agents/verified-behaviors/index.md — test-backed behavior contracts
- /agents/sample-tsx.html — minimal browser TSX scaffold
- /examples/ — interactive self-contained HTML examples
@@ -44,8 +45,11 @@
## Gotchas
- Derived `ko-*` values must stay observable/computed — inline expressions freeze
-- `ko.applyBindings(...)` returns a Promise
+- `ko.applyBindings(...)` returns Promise
- Inside `ko-foreach`, binding-context vars use strings: `ko-text="$data"` (not `{$data}`)
+- Component JSX params: pass each constructor param as own attribute (``), never wrap in `params={{...}}` — JSX attrs flatten into one constructor arg, wrapping double-nests to `{ params: {...} }`
+- Never create `ko.observable()`, `ko.computed()`, or `.subscribe()` inside computed evaluator — re-runs leak instances
+- Binary HTML attrs (`disabled`, `hidden`, …): return `|| undefined` from computed; `false` renders string `"false"`, keeps attribute
## Browser JSX (esbuild-wasm)
@@ -61,4 +65,4 @@ const result = await esbuild.transform(tsxCode, {
- Docs: /observables/ · /computed/ · /bindings/ · /components/
- Playground: /playground
-- GitHub: https://github.com/knockout/tko
+- GitHub: https://github.com/knockout/tko
\ No newline at end of file