docs(agents): add KO/ViewComponent guidelines from review#348
docs(agents): add KO/ViewComponent guidelines from review#348brianmhunt merged 10 commits intomainfrom
Conversation
Port architectural rules from NetPleadings/MinuteBox#9518 (CodeRabbit
review config) back into TKO's public LLM-facing docs. Terminology
normalized to TKO primitives (LifeCycle/BindingHandler/ComponentABC,
ko.subscribable) β no MinuteBox-specific references.
- llms.txt: 3 new Gotchas bullets
- flat JSX attrs vs `params={{β¦}}` wrapper
- no observable/computed/subscribe inside a computed evaluator
- `|| undefined` for binary HTML attrs
- agents/contract.md: 3 new sections
- "DOM Mutation Containment" β DOM-API calls belong in BindingHandler
- "Component Design" β narrow BH scope, component-as-renderer-only
- "Component Communication via `subscribable`" β pub/sub over
observable-of-function for imperative commands
- agents/guide.md: 4 new subsections + 3 new Gotchas bullets
- Observables vs computed dependency management
- Computed styling via attributes (data-attr + CSS over computed class)
- JSX scope rule
- Component params in JSX
- null (not false) for empty HTML computeds, `|| undefined` for
binary attrs, named-variable computeds over inline
- agents/verified-behaviors/
- computed.md: new "Anti-patterns" section w/ observable-in-computed
leak + test sketch
- lifecycle.md: "Usage guidance" for this.computed/this.subscribe
- utils-jsx.md: binary HTML attr omit-on-falsy behavior
- debug.md: generated stub (no-tests-found) matching package-docs
template
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 35 minutes and 48 seconds. β How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. π¦ How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. βΉοΈ Review infoβοΈ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: π Files selected for processing (4)
π WalkthroughWalkthroughThis PR adds and updates documentation to establish stricter architectural guidance around DOM mutation containment, binding handler responsibilities, component design patterns, JSX usage rules, and lifecycle management in Knockout applications. It also introduces new agent process verification requirements and documentation quality standards. Changes
Estimated code review effortπ― 3 (Moderate) | β±οΈ ~25 minutes Possibly related PRs
Poem
π₯ Pre-merge checks | β 5β Passed checks (5 passed)
βοΈ Tip: You can configure your own custom pre-merge checks in the settings. β¨ Finishing Touchesπ§ͺ Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
π‘ Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d7d76d4e8d
βΉοΈ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with π.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| - **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. |
There was a problem hiding this comment.
Correct binary-attribute false/null semantics
This gotcha states that binary attributes are omitted when the value is null or false, but packages/utils.jsx/src/JsxObserver.ts removes attributes only for undefined and otherwise stringifies the value via setAttributeNS, so false/null become present attributes ("false"/"null"). For boolean HTML attrs like disabled, presence keeps the attribute active, so code generated from this guidance can leave controls disabled when the computed returns false; please align this rule (and the copied wording in verified-behaviors/utils-jsx.md) with actual runtime behavior.
Useful? React with πΒ / π.
Stub was locally generated during an earlier run while the codex/tko-mcp-debug-plan branch (which introduces packages/debug) was in the working tree. That package is not merged to main, so the doc referenced a package that does not exist in this branch's scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Ports Knockout/ViewComponent architectural guidance into TKOβs public LLM-facing docs, adding new βgotchasβ and expanding agent documentation and βverified behaviorsβ references to align with TKO primitives (LifeCycle, BindingHandler, ComponentABC, ko.subscribable).
Changes:
- Add new JSX/computed/HTML attribute gotchas to
llms.txtand the main agent guide. - Add contract guidance for DOM mutation containment, component/BH responsibilities, and component communication via
ko.subscribable. - Extend βverified behaviorsβ docs (including a new
@tko/debugstub) with additional lifecycle/computed/jsx guidance.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tko.io/public/llms.txt | Adds new gotchas for JSX params flattening, computed anti-patterns, and binary HTML attributes. |
| tko.io/public/agents/verified-behaviors/utils-jsx.md | Documents JSX binary HTML attribute behavior (needs correction to match implementation). |
| tko.io/public/agents/verified-behaviors/lifecycle.md | Adds lifecycle βusage guidanceβ section (currently not aligned with βverified behaviorsβ contract). |
| tko.io/public/agents/verified-behaviors/debug.md | Adds generated no-tests-found stub for @tko/debug. |
| tko.io/public/agents/verified-behaviors/computed.md | Adds an βAnti-patternsβ section (currently not test-backed per the directoryβs stated contract). |
| tko.io/public/agents/guide.md | Adds extended guidance on lifecycle-owned reactivity, JSX usage, styling via attributes, component params, and new gotchas (includes binary-attr wording that needs correction). |
| tko.io/public/agents/contract.md | Adds architectural contract rules around DOM mutation containment and component/BH communication patterns. |
|
|
||
| ## Anti-patterns | ||
|
|
||
| - Creating observables, computeds, or subscriptions **inside** a computed's evaluator leaks instances. The evaluator re-runs on every dependency change, producing a new un-disposed subscriber each time, so memory and subscriber count grow without bound. | ||
| Test sketch: | ||
| ```ts | ||
| const dep = ko.observable(0) | ||
| const instances: KnockoutObservable<number>[] = [] | ||
| const leaky = ko.computed(() => { | ||
| const obs = ko.observable(dep() + 1) | ||
| instances.push(obs) | ||
| return obs() | ||
| }) | ||
| expect(instances.length).toBe(1) | ||
| dep(1); expect(instances.length).toBe(2) | ||
| dep(2); expect(instances.length).toBe(3) | ||
| ``` | ||
| Fix: create the observable/subscription once outside the computed, or inside a `LifeCycle` subclass constructor where `this.subscribe` / `this.computed` own disposal. | ||
| Related specs: `packages/lifecycle/spec/LifeCycleBehaviors.ts` (disposal semantics). |
There was a problem hiding this comment.
This new "Anti-patterns" entry is in the "verified-behaviors" directory, but it doesn't cite a spec that actually verifies the described leak, and it includes a "test sketch" rather than a real test reference. Given the file header says non-test-backed behaviors do not belong here, either add/point to a concrete spec that covers this behavior (and cite it as "Specs:"), or move this guidance to /agents/guide.md (and keep this file strictly test-backed).
| ## Anti-patterns | |
| - Creating observables, computeds, or subscriptions **inside** a computed's evaluator leaks instances. The evaluator re-runs on every dependency change, producing a new un-disposed subscriber each time, so memory and subscriber count grow without bound. | |
| Test sketch: | |
| ```ts | |
| const dep = ko.observable(0) | |
| const instances: KnockoutObservable<number>[] = [] | |
| const leaky = ko.computed(() => { | |
| const obs = ko.observable(dep() + 1) | |
| instances.push(obs) | |
| return obs() | |
| }) | |
| expect(instances.length).toBe(1) | |
| dep(1); expect(instances.length).toBe(2) | |
| dep(2); expect(instances.length).toBe(3) | |
| ``` | |
| Fix: create the observable/subscription once outside the computed, or inside a `LifeCycle` subclass constructor where `this.subscribe` / `this.computed` own disposal. | |
| Related specs: `packages/lifecycle/spec/LifeCycleBehaviors.ts` (disposal semantics). |
|
|
||
| ## Usage guidance | ||
|
|
||
| - Prefer `this.computed(...)` over standalone `ko.computed(...)` inside a `LifeCycle` subclass β the computed is added to the instance's disposal set automatically. | ||
| - Prefer `this.subscribe(observable, callback)` over `observable.subscribe(callback)` inside a `LifeCycle` subclass for the same reason. | ||
| - Do not create observables, computeds, or subscriptions inside a computed's evaluator β see the anti-pattern in `computed.md`. |
There was a problem hiding this comment.
The "Usage guidance" section adds non-test-backed recommendations to a "verified-behaviors" doc whose header states that behaviors not covered by unit tests do not belong here. Please either (a) move this guidance into /agents/guide.md, or (b) explicitly connect each bullet to an existing spec (using the same "Specs:" convention as the rest of the file) so readers can verify the claim from tests.
| ## Usage guidance | |
| - Prefer `this.computed(...)` over standalone `ko.computed(...)` inside a `LifeCycle` subclass β the computed is added to the instance's disposal set automatically. | |
| - Prefer `this.subscribe(observable, callback)` over `observable.subscribe(callback)` inside a `LifeCycle` subclass for the same reason. | |
| - Do not create observables, computeds, or subscriptions inside a computed's evaluator β see the anti-pattern in `computed.md`. |
| Specs: `packages/utils.jsx/spec/jsxBehaviors.ts` | ||
| - JSX-created nodes can carry native bindings and participate in component rendering and observable-array diff updates. | ||
| Specs: `packages/utils.jsx/spec/jsxBehaviors.ts` | ||
| - Binary HTML attributes (`disabled`, `readonly`, `hidden`, `required`, `checked`, `selected`) omit the attribute when the observable/computed value is `null`, `undefined`, or `false`; any other value (including the string `"false"`) sets the attribute. Use `|| undefined` in computeds to express "no attribute" explicitly. |
There was a problem hiding this comment.
The claim that binary HTML attributes are omitted when the value is null or false doesn't match the current JSX implementation: JsxObserver.setNodeAttribute only removes attributes when the unwrapped value is undefined; null/false are stringified and set (e.g., disabled="false" still leaves the attribute present). Please update this bullet to reflect that omission only happens via undefined, and keep || undefined (or an explicit conditional) as the recommended way to remove the attribute.
| - Binary HTML attributes (`disabled`, `readonly`, `hidden`, `required`, `checked`, `selected`) omit the attribute when the observable/computed value is `null`, `undefined`, or `false`; any other value (including the string `"false"`) sets the attribute. Use `|| undefined` in computeds to express "no attribute" explicitly. | |
| - Binary HTML attributes (`disabled`, `readonly`, `hidden`, `required`, `checked`, `selected`) omit the attribute only when the observable/computed value is `undefined`; `null` and `false` are stringified and set, so even `disabled="false"` still leaves the attribute present. Use `|| undefined` in computeds, or an explicit conditional, to express "no attribute" explicitly. |
| - **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. |
There was a problem hiding this comment.
This Gotcha says binary HTML attributes are omitted when the value is null, undefined, or false, but JSX attribute setting currently only removes attributes when the value is undefined (other values, including null/false, are stringified and set). Please revise the wording to avoid implying that null/false automatically omit the attribute, and keep the guidance focused on returning undefined (e.g., via || undefined) to ensure removal.
| - **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. | |
| - **Binary HTML attributes** (`disabled`, `readonly`, `hidden`, `required`, `checked`, `selected`) should return `undefined` when you want JSX to remove the attribute. Use `|| undefined` in computeds to make "no attribute" explicit: `disabled={this.computed(() => shouldBeDisabled() || undefined)}`. Do not rely on `null` or `false` to omit the attribute, and never return the string `"false"` β it keeps the attribute set. |
| - 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. |
There was a problem hiding this comment.
This section states that creating ko.observable() inside a computed evaluator causes instances to pile up and never dispose. In @tko/computed, dependency tracking disposes subscriptions to prior dependencies on each evaluation, so creating an observable inside an evaluator doesn't inherently leak unless you retain references or create unmanaged subscriptions/computeds. Please narrow this guidance to the cases that actually leak (e.g., creating subscriptions or computeds inside the evaluator) or reword it as a performance/clarity anti-pattern rather than an unbounded leak for plain observables.
| - **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. | |
| - Avoid creating reactive objects inside a computed's evaluator. Creating `ko.computed()` instances or calling `.subscribe()` there can accumulate unmanaged work unless you dispose them manually, because the evaluator runs on every dependency change. Creating a plain `ko.observable()` there does not inherently leak, but it is usually a performance/clarity anti-pattern unless it is purely temporary and you do not retain references to it. Prefer creating observables, computeds, and subscriptions once outside the evaluator, or in a `LifeCycle`-enabled constructor. |
| - `ko.applyBindings(...)` returns a Promise | ||
| - Inside `ko-foreach`, binding-context vars use strings: `ko-text="$data"` (not `{$data}`) | ||
| - Component JSX params: pass each constructor param as its own attribute (`<my-card out={x} onEdit={fn} />`), never wrap in `params={{...}}` β JSX attrs flatten into one constructor arg, so wrapping double-nests to `{ params: {...} }` | ||
| - Never create `ko.observable()`, `ko.computed()`, or `.subscribe()` inside a computed evaluator β re-runs leak instances |
There was a problem hiding this comment.
This Gotcha asserts that creating ko.observable() inside a computed evaluator leaks instances. @tko/computed disposes prior dependency subscriptions on each evaluation, so plain observables created inside the evaluator don't necessarily accumulate unless references are retained; the leak risk is much clearer for creating subscriptions/computeds inside the evaluator. Please adjust the wording to avoid stating a guaranteed leak for ko.observable() and focus on the cases that actually leak or on the performance implications.
| - Never create `ko.observable()`, `ko.computed()`, or `.subscribe()` inside a computed evaluator β re-runs leak instances | |
| - Avoid creating `ko.observable()`, `ko.computed()`, or `.subscribe()` inside a computed evaluator β `ko.computed()` and `.subscribe()` can accumulate unless disposed, while `ko.observable()` usually just creates unnecessary transient instances unless references are retained |
Preventive rule added after shipping an orphan verified-behaviors stub (`debug.md`) in the preceding commit β the file referenced `@tko/debug`, a package that exists only on an unmerged branch (`codex/tko-mcp-debug-plan`). Stub had leaked into the working tree from a prior generator run. - New subsection under "Agent-First Documentation": mandatory verification checklist (git ls-files, package existence, spec paths, URLs) before staging any doc file. - Added as a failure mode under "Review Your Own Change Adversarially". Rationale: shipping docs that name non-existent entities is worse than no doc at all β it misleads every future reader (human or agent) who trusts the docs as a contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
π§Ή Nitpick comments (1)
tko.io/public/agents/contract.md (1)
76-76: Optional: Consider simplifying "because" clauses for conciseness.Static analysis suggests these phrases could be more concise:
- Line 76: "a handler that is complex because its single DOM task is inherently complex" β "a handler whose single DOM task is inherently complex"
- Line 89: "a component that is large because it composes many small child components" β "a component that composes many small child components"
Also applies to: 89-89
π€ Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tko.io/public/agents/contract.md` at line 76, Replace verbose "because" clauses with shorter relative phrasing in the descriptive lines: change the phrase "a handler that is complex because its single DOM task is inherently complex" (in contract.md) to "a handler whose single DOM task is inherently complex" and similarly change "a component that is large because it composes many small child components" to "a component that composes many small child components" to improve conciseness while preserving meaning.
π€ Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tko.io/public/agents/verified-behaviors/utils-jsx.md`:
- Around line 33-34: The docs say binary attributes (disabled, readonly, hidden,
required, checked, selected) should be omitted for null/undefined/false, but
JsxObserver currently only omits on undefined; update the attribute-update logic
in JsxObserver.ts (the function handling setAttributeNS/attribute writes around
the node.setAttributeNS(ns, name, String(value)) calls) to treat null and false
the same as undefined by normalizing value => undefined for these specific
attribute names before calling setAttributeNS or removeAttribute, add unit tests
in packages/utils.jsx/spec/jsxBehaviors.ts to cover null and false cases, and
regenerate packages/utils.jsx/verified-behaviors.json so the verified behaviors
match the new behavior.
---
Nitpick comments:
In `@tko.io/public/agents/contract.md`:
- Line 76: Replace verbose "because" clauses with shorter relative phrasing in
the descriptive lines: change the phrase "a handler that is complex because its
single DOM task is inherently complex" (in contract.md) to "a handler whose
single DOM task is inherently complex" and similarly change "a component that is
large because it composes many small child components" to "a component that
composes many small child components" to improve conciseness while preserving
meaning.
πͺ Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
βΉοΈ Review info
βοΈ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3e8f188c-074b-44b5-b9ab-ff5eab20946a
π Files selected for processing (7)
tko.io/public/agents/contract.mdtko.io/public/agents/guide.mdtko.io/public/agents/verified-behaviors/computed.mdtko.io/public/agents/verified-behaviors/debug.mdtko.io/public/agents/verified-behaviors/lifecycle.mdtko.io/public/agents/verified-behaviors/utils-jsx.mdtko.io/public/llms.txt
| - Binary HTML attributes (`disabled`, `readonly`, `hidden`, `required`, `checked`, `selected`) omit the attribute when the observable/computed value is `null`, `undefined`, or `false`; any other value (including the string `"false"`) sets the attribute. Use `|| undefined` in computeds to express "no attribute" explicitly. | ||
| Specs: `packages/utils.jsx/spec/jsxBehaviors.ts` |
There was a problem hiding this comment.
π§© Analysis chain
π Script executed:
#!/bin/bash
# Verify binary HTML attribute rendering behavior with null/false values
echo "=== Searching for binary attribute preprocessing logic ==="
rg -n -C5 'disabled|readonly|hidden|required|checked|selected' packages/utils.jsx/src/ --type ts
echo ""
echo "=== Searching for null/false to undefined conversion ==="
rg -n -C5 'null.*undefined|false.*undefined' packages/utils.jsx/src/JsxObserver.ts
echo ""
echo "=== Looking for attribute value normalization ==="
ast-grep --pattern $'function $FUNC($$$) {
$$$
if ($VALUE === null || $VALUE === false) {
$$$
}
$$$
}'Repository: knockout/tko
Length of output: 536
π Script executed:
# Find and examine JsxObserver.ts file
find . -name "JsxObserver.ts" -type fRepository: knockout/tko
Length of output: 95
π Script executed:
# Read the utils.jsx spec file to understand expected behavior
find . -path "*/packages/utils.jsx/spec/jsxBehaviors.ts" -type fRepository: knockout/tko
Length of output: 97
π Script executed:
# Read the documentation file being reviewed
find . -path "*/tko.io/public/agents/verified-behaviors/utils-jsx.md" -type fRepository: knockout/tko
Length of output: 110
π Script executed:
# Check sizes of the files
wc -l ./packages/utils.jsx/src/JsxObserver.ts ./packages/utils.jsx/spec/jsxBehaviors.ts ./tko.io/public/agents/verified-behaviors/utils-jsx.mdRepository: knockout/tko
Length of output: 222
π Script executed:
# Read JsxObserver.ts around lines 398-408
sed -n '390,420p' ./packages/utils.jsx/src/JsxObserver.tsRepository: knockout/tko
Length of output: 1120
π Script executed:
# Read the spec file
cat ./packages/utils.jsx/spec/jsxBehaviors.tsRepository: knockout/tko
Length of output: 37565
π Script executed:
# Read the documentation file
cat ./tko.io/public/agents/verified-behaviors/utils-jsx.mdRepository: knockout/tko
Length of output: 2236
π Script executed:
# Search for binary attribute tests in the spec file
grep -n "disabled\|readonly\|hidden\|required\|checked\|selected" ./packages/utils.jsx/spec/jsxBehaviors.tsRepository: knockout/tko
Length of output: 38
π Script executed:
# Search for tests that check attribute removal with null/false
grep -n -B3 -A3 "null\|false" ./packages/utils.jsx/spec/jsxBehaviors.ts | grep -A5 -B5 "attribute"Repository: knockout/tko
Length of output: 1683
π Script executed:
# Search for where setNodeAttribute is called
grep -n "setNodeAttribute" ./packages/utils.jsx/src/JsxObserver.tsRepository: knockout/tko
Length of output: 434
π Script executed:
# Look for NativeProvider.addValueToNode to see if it does preprocessing
find . -name "*NativeProvider*" -type fRepository: knockout/tko
Length of output: 163
π Script executed:
# Read NativeProvider.ts to see if addValueToNode handles binary attributes
cat ./packages/provider.native/src/NativeProvider.tsRepository: knockout/tko
Length of output: 2203
π Script executed:
# Get more context around setNodeAttribute calls
sed -n '360,380p' ./packages/utils.jsx/src/JsxObserver.tsRepository: knockout/tko
Length of output: 663
π Script executed:
# Search for "binary" in the codebase to find relevant code
rg -n "binary" --type tsRepository: knockout/tko
Length of output: 361
π Script executed:
# Read the full JsxObserver.ts to understand the attribute handling flow
cat ./packages/utils.jsx/src/JsxObserver.tsRepository: knockout/tko
Length of output: 13764
π Script executed:
# Search spec file for tests on binary attributes like "disabled", "checked", etc.
grep -n "disabled\|checked\|readonly\|hidden\|required\|selected" ./packages/utils.jsx/spec/jsxBehaviors.tsRepository: knockout/tko
Length of output: 38
π Script executed:
# Search for any tests involving null or false on attributes
grep -n -B5 -A10 "attributes.*null\|attributes.*false" ./packages/utils.jsx/spec/jsxBehaviors.tsRepository: knockout/tko
Length of output: 38
π Script executed:
# Look for tests that verify attribute removal
grep -n -B3 -A3 "removeAttribute" ./packages/utils.jsx/spec/jsxBehaviors.tsRepository: knockout/tko
Length of output: 38
π Script executed:
# Let's verify the documented behavior by checking the verified-behaviors.json file
find . -name "verified-behaviors.json" -path "*/utils.jsx/*" -type fRepository: knockout/tko
Length of output: 100
π Script executed:
# Let's also test the actual behavior by creating a small test to see what happens
# Read any existing attribute-related test to understand how they test attribute behavior
grep -n "attributes:" ./packages/utils.jsx/spec/jsxBehaviors.ts | head -20Repository: knockout/tko
Length of output: 1720
π Script executed:
# Look for the test that verifies toggles observable attribute
grep -n -B2 -A8 "toggles an observable attribute" ./packages/utils.jsx/spec/jsxBehaviors.tsRepository: knockout/tko
Length of output: 479
π Script executed:
# Read the verified-behaviors.json file
cat ./packages/utils.jsx/verified-behaviors.jsonRepository: knockout/tko
Length of output: 1564
π Script executed:
# Check the test that toggles empty observable attribute (just before the one we found)
grep -n -B2 -A8 "toggles an empty observable attribute" ./packages/utils.jsx/spec/jsxBehaviors.tsRepository: knockout/tko
Length of output: 469
Fix documented binary HTML attribute behavior or update documentation to match implementation.
The documentation states that binary HTML attributes (disabled, readonly, hidden, required, checked, selected) omit the attribute when the value is null, undefined, or false. However, the code in JsxObserver.ts:398-410 only removes attributes when value === undefined. For null and false values, the code calls node.setAttributeNS(ns, name, String(value)), which renders "null" and "false" as attribute values rather than omitting them.
Additionally, this behavior is not covered by any tests in packages/utils.jsx/spec/jsxBehaviors.ts and is not included in packages/utils.jsx/verified-behaviors.json (which is generated from unit tests). Either implement the documented behavior with proper preprocessing to convert null and false to undefined for binary attributes, or update the documentation to reflect the actual implementation that only omits attributes for undefined values.
π€ Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tko.io/public/agents/verified-behaviors/utils-jsx.md` around lines 33 - 34,
The docs say binary attributes (disabled, readonly, hidden, required, checked,
selected) should be omitted for null/undefined/false, but JsxObserver currently
only omits on undefined; update the attribute-update logic in JsxObserver.ts
(the function handling setAttributeNS/attribute writes around the
node.setAttributeNS(ns, name, String(value)) calls) to treat null and false the
same as undefined by normalizing value => undefined for these specific attribute
names before calling setAttributeNS or removeAttribute, add unit tests in
packages/utils.jsx/spec/jsxBehaviors.ts to cover null and false cases, and
regenerate packages/utils.jsx/verified-behaviors.json so the verified behaviors
match the new behavior.
Promotes the previously-soft "independent second pass" guidance into a hard requirement, scoped to changes that affect package behavior, public API, tests, agent-facing docs, CI, or build/ config. Proportionality carve-out excludes typos, whitespace, and the adversarial review's own report (terminates recursion). How-to covers: fresh subagent, brief with artefact + claim only (withhold author reasoning to prevent anchoring), enumerate failure modes before checking correctness, verify findings defensively (not as licence to override), route out-of-scope findings to follow-ups rather than ballooning the PR, and record the pass outcome in the PR description so compliance is auditable. Written in response to shipping an orphan doc stub in commit d7d76d4 β single-pair-of-eyes review missed it. Rule itself reviewed adversarially before landing; the six findings from that pass (self-reference termination, single-pair leak on verification, proportionality, gaming via leading questions, missing audit trail, PR-scope conflict) are all folded into this wording. Adversarial pass: Explore subagent. Result: flagged 6; all addressed in this commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply the caveman:compress skill to files that are purely LLM-facing (llms.txt + verified-behaviors/*.md). Drops articles, filler, and hedging without touching code, backticked identifiers, URLs, or spec paths. Byte savings per file: - tko.io/public/llms.txt: 1% - tko.io/public/agents/verified-behaviors/computed.md: 7% - tko.io/public/agents/verified-behaviors/lifecycle.md:7% - tko.io/public/agents/verified-behaviors/utils-jsx.md:5% Files already dense, so savings are modest β kept anyway because every downstream agent that loads the agent-facing pack pays the token cost on every session. Dual-audience docs (AGENTS.md, agents/guide.md, agents/contract.md) intentionally left uncompressed β human contributors read them. .gitignore: add `*.original.md` so future compress runs don't leak their local backups into commits. Adversarial pass: Explore subagent. Flagged 1 (computed.md fragment-header dropped the If/then conditional β meaning ambiguous). Resolved: restored "If β¦ , not belong in this directory." phrasing in that one line. Other diffs accepted β meaning preserved under caveman article/pronoun drops. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "Good: computed attribute + CSS selector" example had `const isActive = this.computed(() => this.isActive() || undefined)`, where the local variable `isActive` shadows the class method `this.isActive()`. JS scoping makes the inner call resolve to the method, so the example was not actually broken β but a novice reader could reasonably mistake it for a self-referential computed. Renamed the local to `activeAttr` so the two names no longer collide. Adversarial pass: Explore subagent on full PR diff. Flagged 1 high (shadowing confusion in this example). Also flagged two items verified as false positives: "not belong" fragments in caveman-compressed files (grammatically terse but unambiguous), and `tko.jsx.render` (defined on the bundled `tko` global at builds/reference/src/index.ts β already documented in existing guide.md counter/todo examples). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`tko.io/scripts/generate-verified-behaviors.mjs` rebuilds every
`tko.io/public/agents/verified-behaviors/*.md` file from the
package-local `verified-behaviors.json` curated sources on every
`prebuild` / `predev` run (and therefore every CI build via
`deploy-docs.yml`). Hand-edits to those markdown files are
transient and would have been wiped the next time anyone ran
`bun run dev` or the site was deployed.
Reverted to the generator's canonical output on `main`:
- tko.io/public/agents/verified-behaviors/computed.md
- tko.io/public/agents/verified-behaviors/lifecycle.md
- tko.io/public/agents/verified-behaviors/utils-jsx.md
That also undoes the caveman compression on those three files β
the generator emits natural-English from the JSON, so compression
would be re-undone on the first build. Compression on
`llms.txt` stands (no generator owns it).
Content that was in those reverts now lives only in the
hand-authored docs where it belongs per verified-behaviors
charter ("If behavior not covered by unit tests, it does not
belong in this directory"):
- Observable-inside-computed leak: `agents/guide.md` Gotchas
(strengthens the existing "Don't create computeds/subscriptions
inside computeds" bullet to include observables + subscriptions).
- `this.computed` / `this.subscribe` preference: `agents/guide.md`
under "Observables vs computed dependency management".
- Binary HTML attr `|| undefined`: `agents/guide.md` Gotchas +
`llms.txt` Gotchas. (No spec in `packages/utils.jsx/spec`
covers the null/undefined/false omission behavior, so it cannot
be added as a verified entry β would need a test first.)
Adversarial pass: caught by the "Docs Verification" pre-check,
not by a subagent β running `bun run dev` would have regenerated
the three files and silently dropped 3 commits' worth of edits.
Audit trail added here.
Follow-up: if someone wants those additions as verified entries,
the path is (1) add a test to the owning package's spec, (2) add
a behavior entry to that package's `verified-behaviors.json`,
(3) rerun the generator. Not this PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
π€ Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@AGENTS.md`:
- Line 191: The package-existence verification currently only runs "ls
packages/<name>" which misses valid public packages under builds/, so update the
rule to check both locations (packages/<name> and builds/<name>) or perform a
single lookup that searches both directories and verifies a matching
package.json; replace references to the lone check "ls packages/<name>" in the
doc/guideline text with the broadened check logic/description so docs no longer
flag legitimate packages like "@tko/build.knockout" as orphans.
In `@tko.io/public/agents/verified-behaviors/computed.md`:
- Line 4: The directory gate sentence in computed.md is ungrammatical; replace
"If behavior not covered by unit tests, not belong in this directory." with a
clear grammatical rule such as "Behaviors not covered by unit tests do not
belong in this directory." or "If a behavior is not covered by unit tests, it
does not belong in this directory." Update the line in computed.md accordingly
to use one of these corrected phrasings so the normative rule is clear and
grammatical.
πͺ Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
βΉοΈ Review info
βοΈ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a30784ec-5433-4306-aa27-7365a9abdedc
π Files selected for processing (7)
.gitignoreAGENTS.mdtko.io/public/agents/guide.mdtko.io/public/agents/verified-behaviors/computed.mdtko.io/public/agents/verified-behaviors/lifecycle.mdtko.io/public/agents/verified-behaviors/utils-jsx.mdtko.io/public/llms.txt
β Files skipped from review due to trivial changes (2)
- .gitignore
- tko.io/public/llms.txt
π§ Files skipped from review as they are similar to previous changes (3)
- tko.io/public/agents/verified-behaviors/utils-jsx.md
- tko.io/public/agents/verified-behaviors/lifecycle.md
- tko.io/public/agents/guide.md
| 1. `git ls-files <path>` β 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 -- <path>` and `git branch --contains <commit>` will show where it lives. | ||
| 2. For each package name in the doc: `ls packages/<name>` and confirm a matching `package.json`. Orphan `@tko/*` references mislead both humans and downstream agents. |
There was a problem hiding this comment.
Broaden package existence check to include builds/* packages
ls packages/<name> is incomplete for valid public packages like @tko/build.knockout, which live under builds/. This verification rule can misclassify legitimate references as invalid.
π‘ Suggested doc fix
-2. For each package name in the doc: `ls packages/<name>` and confirm a matching `package.json`. Orphan `@tko/*` references mislead both humans and downstream agents.
+2. For each package name in the doc: confirm a matching `package.json` exists in either `packages/*` or `builds/*` (for example, by searching package names rather than assuming one root). Orphan `@tko/*` references mislead both humans and downstream agents.π€ Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@AGENTS.md` at line 191, The package-existence verification currently only
runs "ls packages/<name>" which misses valid public packages under builds/, so
update the rule to check both locations (packages/<name> and builds/<name>) or
perform a single lookup that searches both directories and verifies a matching
package.json; replace references to the lone check "ls packages/<name>" in the
doc/guideline text with the broadened check logic/description so docs no longer
flag legitimate packages like "@tko/build.knockout" as orphans.
AGENTS.md is loaded on every agent session β token cost amortized
forever. Two recently-added process blocks ("Never ship docs that
reference things that don't exist" + "Adversarial review is
mandatory") were ~50 lines combined. Moved the how-to detail to a
new hand-authored doc `tko.io/public/agents/process.md`; AGENTS.md
keeps the one-line mandate, in-scope trigger, and dark-factory
motivation inline so the rule still fires from session load.
Changes:
- `AGENTS.md`: two heavy subsections collapsed to one-paragraph
mandates with links to process.md; failure-modes list + existing
narrative retained inline.
- `tko.io/public/agents/process.md`: new file carrying the full
how-to, in-scope/out-of-scope list, adversarial-review
checklist, audit-trail format, and generator-ownership warning
for verified-behaviors. Includes a credits section naming the
MinuteBox PR that seeded the imported architectural rules.
- `tko.io/public/llms.txt`: added `/agents/process.md` to the
agent-docs list so downstream agents discover it.
Line delta vs `main` for AGENTS.md: +5 (down from +55 before the
refactor).
Credit: architectural review guidelines originally from
NetPleadings/MinuteBox#9518 by @jameskozlowskimb with input from
@ctcarton β ported upstream and reworded for TKO primitives.
Adversarial pass: Explore subagent. Flagged 4 items; 1 valid
(lost dark-factory motivation β restored one sentence inline), 3
false positives (link-resolution claim wrong for repo-relative
markdown paths that Claude Code follows natively; in-scope list
claim wrong because list stayed inline in prose; circular-ref
claim wrong because the bidirectional link is content vs. rules,
not a cycle).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous wording cast Pattern B (computed data-attr + CSS selector) as the "correct" approach and Pattern A (computed className / inline style) as an anti-pattern. That's a style preference, not a framework rule β both produce the same runtime behaviour; consistency within a codebase matters more than the choice. Rewrote the section to present both patterns symmetrically with trade-offs each way. Key change: "Do not use computeds to switch CSS class names β¦" becomes "Reactive styling: two valid patterns β¦ Pick one and apply it consistently." Adversarial pass: Explore subagent. Flagged 2 valid items in the trade-offs list β (1) Pattern A's "rule stays in component file" claim incomplete when class body lives in a separate stylesheet; (2) Pattern B's "avoids class-name string concatenation" read as pro-B framing disguised as neutral. Both tweaked to acknowledge Pattern A's template-literal / class-map mitigation and the split-visibility caveat. Result: balanced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two non-mandate issues surfaced:
1. The "Computed styling via attributes" section was written as if
the two shown patterns exhausted the choice. They don't β CSS
custom properties driven from a computed, classless inline
style objects, component-scoped style blocks, and CSS-in-JS
tokens are all reasonable in TKO. Section reframed as "two
examples among several" under a "Reactive styling" heading,
with the named parenthetical listing the other valid
approaches. Emphasis stays on consistency within a codebase,
not on picking a particular pattern.
2. The adversarial-review audit trail was documented as going
into the PR description. That breaks the contract a PR
description is meant to honour (why the change exists, what
problem it solves) and feels like editing someone else's
artefact for process metadata. Moved the audit line to the
**commit message** that introduces the in-scope change, one
line per in-scope commit. Explicit "do not add review
outcomes to the PR description" clause in process.md.
Squash-merge caveat added: copy audit lines into the squash
target message manually if the merge rewrites commit
messages.
Also tightened wording consistency between AGENTS.md and
process.md ("commit message that introduces the in-scope
change" now appears in both).
Adversarial pass: Explore subagent. Flagged 3 valid (commit-
scope ambiguity, squash-merge vulnerability, cross-file wording
drift) β all three addressed. Verdict after fixes: ship.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Ports architectural review guidelines from a downstream MinuteBox CodeRabbit review config (NetPleadings/MinuteBox#9518) back upstream into TKO's public LLM-facing docs. Rules reworded for TKO primitives (
LifeCycle,BindingHandler,ComponentABC,ko.subscribable) β no app-specific references.Skipped MB rules 4 (method/file size), 5 (empty catch), and style nits β not framework-level.
Credit
Guidelines originate from NetPleadings/MinuteBox#9518 by @jameskozlowskimb with input from @ctcarton. Ported upstream and reworded for TKO primitives.
Changes
tko.io/public/llms.txt3 new Gotchas bullets: flat JSX attrs vs
params={{β¦}}wrapper, no observable/computed/subscribe inside a computed evaluator,|| undefinedfor binary HTML attrs. Caveman-compressed. New/agents/process.mdpointer added to agent-docs list.tko.io/public/agents/contract.mdBindingHandlersubclasses; explicit violation list + safe zones.ko.subscribableβ pub/sub over observable-of-function for imperative commands between components and binding handlers.tko.io/public/agents/guide.mdthis.computed/this.subscribepreference inLifeCyclesubclasses; never create reactive primitives inside a computed evaluator.tko.jsx.renderonly, not orphan utility modules.null(notfalse) for empty HTML computeds; binary attr|| undefined; prefer named computed variables over inline.tko.io/public/agents/process.md(new)AGENTS.mdprocess.md).process.md.main: ~+7 (down from ~+55 before the extract).tko.io/public/agents/verified-behaviors/*.mdLeft untouched. These files are regenerated from
packages/*/verified-behaviors.jsonon every build, so hand-edits would have been wiped at CI time.Verification
bun run buildintko.io/: 65 pages, clean build.dist/output.