bitwrench 2.0.31 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +210 -125
  2. package/dist/bitwrench-bccl.cjs.js +349 -188
  3. package/dist/bitwrench-bccl.cjs.min.js +2 -39
  4. package/dist/bitwrench-bccl.cjs.min.js.gz +0 -0
  5. package/dist/bitwrench-bccl.esm.js +349 -188
  6. package/dist/bitwrench-bccl.esm.min.js +2 -39
  7. package/dist/bitwrench-bccl.esm.min.js.gz +0 -0
  8. package/dist/bitwrench-bccl.umd.js +349 -188
  9. package/dist/bitwrench-bccl.umd.min.js +2 -39
  10. package/dist/bitwrench-bccl.umd.min.js.gz +0 -0
  11. package/dist/bitwrench-code-edit.cjs.js +17 -6
  12. package/dist/bitwrench-code-edit.cjs.min.js +2 -20
  13. package/dist/bitwrench-code-edit.es5.js +8 -3
  14. package/dist/bitwrench-code-edit.es5.min.js +2 -19
  15. package/dist/bitwrench-code-edit.esm.js +17 -6
  16. package/dist/bitwrench-code-edit.esm.min.js +2 -19
  17. package/dist/bitwrench-code-edit.umd.js +17 -6
  18. package/dist/bitwrench-code-edit.umd.min.js +2 -19
  19. package/dist/bitwrench-code-edit.umd.min.js.gz +0 -0
  20. package/dist/bitwrench-debug.js +1 -1
  21. package/dist/bitwrench-debug.min.js +1 -1
  22. package/dist/bitwrench-lean.cjs.js +2492 -1628
  23. package/dist/bitwrench-lean.cjs.min.js +2 -80
  24. package/dist/bitwrench-lean.cjs.min.js.gz +0 -0
  25. package/dist/bitwrench-lean.es5.js +2740 -1838
  26. package/dist/bitwrench-lean.es5.min.js +2 -49
  27. package/dist/bitwrench-lean.es5.min.js.gz +0 -0
  28. package/dist/bitwrench-lean.esm.js +2492 -1628
  29. package/dist/bitwrench-lean.esm.min.js +2 -80
  30. package/dist/bitwrench-lean.esm.min.js.gz +0 -0
  31. package/dist/bitwrench-lean.umd.js +2492 -1628
  32. package/dist/bitwrench-lean.umd.min.js +2 -80
  33. package/dist/bitwrench-lean.umd.min.js.gz +0 -0
  34. package/dist/bitwrench-util-color.cjs.js +251 -0
  35. package/dist/bitwrench-util-color.cjs.min.js +3 -0
  36. package/dist/bitwrench-util-color.es5.js +256 -0
  37. package/dist/bitwrench-util-color.es5.min.js +3 -0
  38. package/dist/bitwrench-util-color.esm.js +241 -0
  39. package/dist/bitwrench-util-color.esm.min.js +3 -0
  40. package/dist/bitwrench-util-color.umd.js +257 -0
  41. package/dist/bitwrench-util-color.umd.min.js +3 -0
  42. package/dist/bitwrench-util-css.cjs.js +2 -1
  43. package/dist/bitwrench-util-css.cjs.min.js +2 -21
  44. package/dist/bitwrench-util-css.es5.js +2 -1
  45. package/dist/bitwrench-util-css.es5.min.js +2 -20
  46. package/dist/bitwrench-util-css.esm.js +2 -1
  47. package/dist/bitwrench-util-css.esm.min.js +1 -19
  48. package/dist/bitwrench-util-css.umd.js +2 -1
  49. package/dist/bitwrench-util-css.umd.min.js +2 -20
  50. package/dist/bitwrench-util-css.umd.min.js.gz +0 -0
  51. package/dist/bitwrench.cjs.js +2826 -1801
  52. package/dist/bitwrench.cjs.min.js +2 -99
  53. package/dist/bitwrench.cjs.min.js.gz +0 -0
  54. package/dist/bitwrench.css +403 -479
  55. package/dist/bitwrench.d.ts +70 -73
  56. package/dist/bitwrench.es5.js +3106 -2020
  57. package/dist/bitwrench.es5.min.js +2 -67
  58. package/dist/bitwrench.es5.min.js.gz +0 -0
  59. package/dist/bitwrench.esm.js +2826 -1801
  60. package/dist/bitwrench.esm.min.js +2 -99
  61. package/dist/bitwrench.esm.min.js.gz +0 -0
  62. package/dist/bitwrench.min.css +1 -1
  63. package/dist/bitwrench.umd.js +2826 -1801
  64. package/dist/bitwrench.umd.min.js +2 -99
  65. package/dist/bitwrench.umd.min.js.gz +0 -0
  66. package/dist/builds.json +222 -134
  67. package/dist/bwserve.cjs.js +308 -298
  68. package/dist/bwserve.d.ts +157 -0
  69. package/dist/bwserve.esm.js +309 -299
  70. package/dist/sri.json +54 -46
  71. package/docs/README.md +6 -3
  72. package/docs/app-patterns.md +7 -6
  73. package/docs/bitwrench-for-wasm.md +53 -54
  74. package/docs/bitwrench-mcp.md +2 -2
  75. package/docs/bitwrench-northstar-principles.md +406 -0
  76. package/docs/bitwrench-taco-schema-discussion.md +2 -2
  77. package/docs/bitwrench_api.md +191 -106
  78. package/docs/bitwrench_typescript_usage.md +5 -5
  79. package/docs/bw-attach.md +29 -75
  80. package/docs/bwserve.md +200 -168
  81. package/docs/cli.md +36 -12
  82. package/docs/component-cheatsheet.md +2 -2
  83. package/docs/component-library.md +4 -4
  84. package/docs/component-lifecycle.md +234 -0
  85. package/docs/drift-lint.md +268 -0
  86. package/docs/framework-translation-table.md +4 -4
  87. package/docs/llm-bitwrench-guide.md +60 -50
  88. package/docs/routing.md +11 -13
  89. package/docs/state-management.md +110 -109
  90. package/docs/taco-format.md +13 -14
  91. package/docs/theming.md +13 -3
  92. package/docs/thinking-in-bitwrench.md +858 -983
  93. package/docs/tutorial-bwserve.md +37 -36
  94. package/docs/tutorial-embedded.md +10 -21
  95. package/docs/tutorial-website.md +2 -2
  96. package/package.json +38 -7
  97. package/readme.html +262 -160
  98. package/src/bitwrench-bccl-entry.js +2 -2
  99. package/src/bitwrench-bccl.js +346 -185
  100. package/src/bitwrench-code-edit.js +16 -5
  101. package/src/bitwrench-color-utils.js +117 -181
  102. package/src/bitwrench-file-ops.js +2 -2
  103. package/src/bitwrench-lean.js +4 -3
  104. package/src/bitwrench-router.js +5 -2
  105. package/src/bitwrench-styles.js +420 -504
  106. package/src/bitwrench-util-color.js +240 -0
  107. package/src/bitwrench-util-css.js +1 -0
  108. package/src/bitwrench-utils.js +4 -0
  109. package/src/bitwrench.d.ts +70 -73
  110. package/src/bitwrench.h +5 -0
  111. package/src/bitwrench.js +1939 -933
  112. package/src/bwserve/attach.js +0 -1
  113. package/src/bwserve/bwclient.js +172 -32
  114. package/src/bwserve/bwshell.js +0 -4
  115. package/src/bwserve/client.js +59 -220
  116. package/src/bwserve/index.js +78 -42
  117. package/src/bwserve.d.ts +157 -0
  118. package/src/bwserve.h +5 -0
  119. package/src/cli/attach.js +12 -75
  120. package/src/cli/convert.js +2 -2
  121. package/src/cli/serve.js +37 -35
  122. package/src/generate-css.js +1 -1
  123. package/src/mcp/knowledge.js +4 -4
  124. package/src/mcp/live.js +21 -13
  125. package/src/mcp/tools.js +0 -1
  126. package/src/version.js +3 -7
package/docs/cli.md CHANGED
@@ -189,32 +189,56 @@ bwcli serve -v
189
189
  Your app sends bwserve protocol messages (JSON) to the input port. All connected browsers update in real time.
190
190
 
191
191
  ```bash
192
- # Patch a value:
192
+ # Patch a value (v2.1 wire protocol — ref, discriminated fields, v:1 stamp):
193
193
  curl -X POST http://localhost:9000 \
194
194
  -H "Content-Type: application/json" \
195
- -d '{"type":"patch","target":"temp","content":"23.5 C"}'
195
+ -d '{"v":1,"type":"patch","ref":"temp","text":"23.5 C"}'
196
196
 
197
197
  # Batch update:
198
198
  curl -X POST http://localhost:9000 \
199
- -d '{"type":"batch","ops":[
200
- {"type":"patch","target":"temp","content":"23.5"},
201
- {"type":"patch","target":"humidity","content":"67%"}
199
+ -d '{"v":1,"type":"batch","ops":[
200
+ {"v":1,"type":"patch","ref":"temp","text":"23.5"},
201
+ {"v":1,"type":"patch","ref":"humidity","text":"67%"}
202
202
  ]}'
203
203
 
204
204
  # r-prefix relaxed JSON is also accepted (for C/embedded):
205
- curl -X POST http://localhost:9000 -d "r{'type':'patch','target':'temp','content':'23.5'}"
205
+ curl -X POST http://localhost:9000 -d "r{'v':1,'type':'patch','ref':'temp','text':'23.5'}"
206
206
  ```
207
207
 
208
+ ### Interactive Commands
209
+
210
+ Messages with a `"command"` field are interactive -- they return a JSON response (not broadcast). Use these to inspect or query connected clients:
211
+
212
+ ```bash
213
+ # List connected clients
214
+ curl -X POST http://localhost:9000 -d '{"command":"clients"}'
215
+
216
+ # Eval a JS expression in the browser
217
+ curl -X POST http://localhost:9000 -d '{"command":"query","code":"document.title"}'
218
+
219
+ # DOM tree snapshot
220
+ curl -X POST http://localhost:9000 -d '{"command":"tree","selector":"#app","depth":2}'
221
+
222
+ # Take a screenshot (requires --allow-screenshot)
223
+ curl -X POST http://localhost:9000 -d '{"command":"screenshot"}'
224
+ ```
225
+
226
+ When no `clientId` is specified, the command routes to the most recently connected client.
227
+
208
228
  ### Options
209
229
 
210
230
  ```
211
- bwcli serve [options]
231
+ bwcli serve [dir] [options]
212
232
 
213
233
  Options:
214
234
  -p, --port <number> Web port for browsers (default: 8080)
215
- --input-port <number> Input port for protocol messages (default: 9000)
235
+ -l, --listen <number> Input port for protocol messages (default: 9000)
236
+ -b, --bind <address> Host/address to bind to (default: 0.0.0.0)
216
237
  --stdin Read messages from stdin instead of input port
217
238
  -t, --theme <name> Theme preset or hex colors
239
+ --title <string> Page title (default: "bwcli serve")
240
+ --no-dir-list Disable directory listings
241
+ --allow-screenshot Enable client.screenshot() capability
218
242
  --open Open browser on start
219
243
  -v, --verbose Verbose output (shows all messages)
220
244
  -h, --help Print help
@@ -232,7 +256,7 @@ Both strict JSON and r-prefix relaxed JSON are accepted on the input port. See [
232
256
 
233
257
  ## The `bwcli attach` subcommand — Remote Debugging REPL
234
258
 
235
- `bwcli attach` provides a built-in terminal-based debugger for any bitwrench page. It starts a bwserve instance and waits for a browser to connect via a drop-in `<script>` tag. Once connected, you get an interactive REPL for evaluating JS, inspecting the DOM, taking screenshots, and listening to events.
259
+ `bwcli attach` provides a built-in terminal-based debugger for any bitwrench page. It starts a bwserve instance and waits for a browser to connect via a drop-in `<script>` tag. Once connected, you get an interactive REPL for inspecting the DOM, mounting components, taking screenshots, and listening to events.
236
260
 
237
261
  ### Usage
238
262
 
@@ -265,15 +289,13 @@ The drop-in script automatically loads bitwrench if it's not already on the page
265
289
  Once connected, you get a `bw>` prompt:
266
290
 
267
291
  ```
268
- bw> document.title # Evaluate JS expression
269
292
  bw> /tree #app 2 # Show DOM tree
270
293
  bw> /screenshot body page.png # Capture screenshot (requires --allow-screenshot)
271
294
  bw> /mount #app card {"title":"Hi"} # Mount BCCL component
272
- bw> /render #app {"t":"h1","c":"Hi"} # Render TACO
295
+ bw> /render #app {"t":"h1","c":"Hi"} # Render TACO at selector
273
296
  bw> /patch counter 42 # Update element text
274
297
  bw> /listen button click # Watch DOM events
275
298
  bw> /unlisten button click # Stop watching
276
- bw> /exec alert('hello') # Execute JS (fire-and-forget)
277
299
  bw> /clients # List connected clients
278
300
  bw> /help # Command reference
279
301
  bw> /quit # Exit
@@ -291,6 +313,8 @@ Options:
291
313
  -h, --help Print help
292
314
  ```
293
315
 
316
+ Bare JS evaluation and the `/exec` command were removed in v2.1. Non-slash input is no longer evaluated. Use the slash commands above for all interactions.
317
+
294
318
  For the complete guide, see [bwcli attach documentation](bw-attach.md).
295
319
 
296
320
  ## Page layout
@@ -1,7 +1,7 @@
1
1
  # Component Cheat Sheet
2
2
 
3
3
  > **Before you write custom TACO for a common UI pattern, check this list.**
4
- > Bitwrench ships ready-made `make*()` factories. Each returns a Level 0 TACO object.
4
+ > Bitwrench ships ready-made `make*()` factories. Each returns a plain TACO object.
5
5
 
6
6
  ## Full Component Table
7
7
 
@@ -92,7 +92,7 @@ bw.message('#my-carousel', 'goToSlide', 2);
92
92
  bw.message('.bw_uuid_abc123', 'next');
93
93
  ```
94
94
 
95
- See [State Management -- Level 1.5](state-management.md#level-15-component-handles) for the full handle/slots guide.
95
+ See [State Management -- Component Handles](state-management.md#component-handles) for the full handle/slots guide.
96
96
 
97
97
  ## How to Use Slots
98
98
 
@@ -334,7 +334,7 @@ bw.makeButton({
334
334
  text: 'Click Me',
335
335
  variant: 'primary', // 'primary' | 'secondary' | 'success' | 'danger' |
336
336
  // 'warning' | 'info' | 'light' | 'dark' |
337
- // 'outline-primary' | 'outline-secondary' | ...
337
+ // 'outline_primary' | 'outline_secondary' | ...
338
338
  size: '', // 'sm' or 'lg'
339
339
  disabled: false,
340
340
  onclick: function() {},
@@ -1049,7 +1049,7 @@ Build your own with `o.handle` and `o.slots`:
1049
1049
  }
1050
1050
  ```
1051
1051
 
1052
- See [State Management -- Level 1.5](state-management.md#level-15-component-handles) for the full guide.
1052
+ See [State Management -- Component Handles](state-management.md#component-handles) for the full guide.
1053
1053
 
1054
1054
  ---
1055
1055
 
@@ -1068,7 +1068,7 @@ Most components accept a `variant` prop. The available variants are:
1068
1068
  | `light` | Light background |
1069
1069
  | `dark` | Dark background |
1070
1070
 
1071
- Buttons also support outline variants: `outline-primary`, `outline-secondary`, etc.
1071
+ Buttons also support outline variants: `outline_primary`, `outline_secondary`, etc.
1072
1072
 
1073
1073
  ## Composition
1074
1074
 
@@ -1078,7 +1078,7 @@ Because every factory returns a TACO object, you compose components with standar
1078
1078
  // Functions as component factories
1079
1079
  function userRow(user) {
1080
1080
  return {
1081
- t: 'div', a: { class: 'bw-card' }, c: [
1081
+ t: 'div', a: { class: 'bw_bccl_card' }, c: [
1082
1082
  bw.makeAvatar({ initials: user.name[0], size: 'sm' }),
1083
1083
  { t: 'span', c: user.name },
1084
1084
  bw.makeBadge({ text: user.role, variant: 'info' })
@@ -0,0 +1,234 @@
1
+ # Component Lifecycle Walkthrough
2
+
3
+ A stats card, taken through every phase of a component's life — define, create,
4
+ mount, update, unmount — using the v2.1 API. If you read one document after the
5
+ README, read this one: it is the shortest complete tour of how bitwrench
6
+ components actually work.
7
+
8
+ ---
9
+
10
+ ## Phase 1: Define
11
+
12
+ A stats card that displays a label and a formatted number.
13
+
14
+ ```javascript
15
+ function makeStatsCard(config) {
16
+ return {
17
+ t: 'div', a: { class: 'stats-card' },
18
+ c: [
19
+ { t: 'h3', a: { class: 'card-title' }, c: config.title || '' },
20
+ { t: 'span', a: { class: 'card-value' }, c: formatValue(config.value) }
21
+ ],
22
+ o: {
23
+ type: 'stats-card',
24
+ state: { value: config.value || 0 },
25
+ slots: { title: '.card-title', value: '.card-value' },
26
+ handle: {
27
+ update: function(el, data) {
28
+ if (data.value !== undefined) {
29
+ el._bw_state.value = data.value;
30
+ el.bw.setValue(formatValue(data.value));
31
+ }
32
+ if (data.title !== undefined) {
33
+ el.bw.setTitle(data.title);
34
+ }
35
+ }
36
+ }
37
+ }
38
+ };
39
+ }
40
+
41
+ function formatValue(v) {
42
+ return '$' + Number(v).toLocaleString();
43
+ }
44
+ ```
45
+
46
+ What we have: a factory function that returns a TACO. Plain data (minus the
47
+ handle function). The factory captures `config` but does NOT create DOM, wire
48
+ lifecycle, or touch the document. This is just an object.
49
+
50
+ **What goes where:**
51
+
52
+ - `t, a, c` — structure (tag, attributes, content)
53
+ - `o.type` — typed discovery: elements get a `bw_is_component_stats-card` class,
54
+ findable via `querySelectorAll`
55
+ - `o.state` — local mutable state (value), initialized from config
56
+ - `o.slots` — cached DOM targets. Auto-generates `el.bw.setTitle()`,
57
+ `el.bw.getTitle()`, `el.bw.setValue()`, `el.bw.getValue()`
58
+ - `o.handle.update` — the public "give me new data" method. Does formatting
59
+ logic, then delegates to slot setters for the DOM update
60
+
61
+ **Slot vs handle interaction:** slots generate simple setters that replace DOM
62
+ content. The `update` handle method adds logic (formatting) then calls slot
63
+ setters. Both live on `el.bw`. They don't collide because they have different
64
+ names: `setValue` (slot) vs `update` (handle).
65
+
66
+ The handle named `update` is special: `bw.update(ref, data)` dispatches to
67
+ `el.bw.update(data)` automatically. Define an `update` handle and your
68
+ component works with the dispatch API for free.
69
+
70
+ ---
71
+
72
+ ## Phase 2+3: Create + Hydrate
73
+
74
+ ```javascript
75
+ var taco = makeStatsCard({ title: 'Revenue', value: 50000 });
76
+ var node = bw.create(taco);
77
+ ```
78
+
79
+ After this call, `node` is a **detached** DOM element:
80
+
81
+ - `node.tagName` === `'DIV'`
82
+ - `node.className` includes `stats-card bw_lc bw_is_component bw_uuid_xxxx`
83
+ - `node._bw_state` === `{ value: 50000 }`
84
+ - `node.bw.update` exists (bound handle method)
85
+ - `node.bw.setValue` / `node.bw.setTitle` exist (slot setters, cached selectors)
86
+ - `node.bw.getValue` / `node.bw.getTitle` exist (slot getters)
87
+ - The h3 contains `Revenue`, the span contains `$50,000`
88
+ - **NOT in the document.** Not registered. `o.mounted` has NOT fired.
89
+
90
+ You rarely call `bw.create()` directly — this is shown for understanding.
91
+
92
+ ---
93
+
94
+ ## Phase 4: Mount
95
+
96
+ ```javascript
97
+ var el = bw.mount('#dashboard', taco);
98
+ ```
99
+
100
+ `bw.mount()` unmounts any previous children of `#dashboard`, creates the
101
+ element, appends it, and runs the mount pass. It returns the root element.
102
+ (`bw.DOM()` is the same function under its other name.)
103
+
104
+ After mount:
105
+
106
+ - Element is in the document inside `#dashboard`
107
+ - Its UUID is registered — findable via `bw.el('bw_uuid_xxxx')`
108
+ - `o.mounted` would have fired if this component had one (it doesn't)
109
+ - Element is alive: `el.bw.update({ value: 75000 })` works
110
+
111
+ **Try it in devtools:** select the card in the Elements panel and type
112
+ `$0._bw_state` — the component's state, no browser extension required.
113
+ `$0.bw` shows its public methods.
114
+
115
+ ---
116
+
117
+ ## Phase 5: Update
118
+
119
+ Five ways, ordered by cost and coupling:
120
+
121
+ ### Direct handle method call (cheapest)
122
+
123
+ ```javascript
124
+ el.bw.update({ value: 75000 });
125
+ // card shows '$75,000'; el._bw_state.value === 75000
126
+ ```
127
+
128
+ ### Slot setter (bypasses the handle's logic)
129
+
130
+ ```javascript
131
+ el.bw.setValue('SOLD OUT');
132
+ // card shows 'SOLD OUT' (raw, no formatting)
133
+ // el._bw_state.value is NOT updated — slot setters don't touch state
134
+ ```
135
+
136
+ This is intentional. Slot setters are low-level; handle methods are the public
137
+ API. If you call `setValue()` directly, keeping state consistent is on you.
138
+
139
+ ### Dispatch via bw.update
140
+
141
+ ```javascript
142
+ bw.update(el, { value: 100000, title: 'Profit' });
143
+ // dispatches to el.bw.update({ ... })
144
+ ```
145
+
146
+ Works identically by UUID, without holding the element reference:
147
+
148
+ ```javascript
149
+ var uuid = bw.getUUID(el);
150
+ bw.update(uuid, { value: 100000 });
151
+ ```
152
+
153
+ ### Dispatch via bw.message (explicit method name)
154
+
155
+ ```javascript
156
+ bw.message(el, 'update', { value: 200000 });
157
+ // same as el.bw.update({ value: 200000 })
158
+ ```
159
+
160
+ ### Pub/sub (decoupled)
161
+
162
+ ```javascript
163
+ bw.sub('revenue:change', function(data) {
164
+ el.bw.update(data);
165
+ }, el);
166
+
167
+ // elsewhere:
168
+ bw.pub('revenue:change', { value: 999999 });
169
+ ```
170
+
171
+ The third argument to `bw.sub()` ties the subscription to the element — when
172
+ the element is unmounted, the subscription is removed automatically.
173
+
174
+ Note what this component never needs: `bw.refresh()`. It has no `o.render`,
175
+ because every update it supports is surgical. Reach for `o.render` +
176
+ `bw.refresh(el)` when the component's *structure* changes with state, not just
177
+ its values.
178
+
179
+ ---
180
+
181
+ ## Phase 6: Unmount
182
+
183
+ ```javascript
184
+ bw.remove(el);
185
+ ```
186
+
187
+ This unmounts and removes the element. The unmount pass:
188
+
189
+ - Fires `o.unmount` if it existed (ours doesn't)
190
+ - Unregisters the UUID
191
+ - Unsubscribes all pub/sub subscriptions tied to this element
192
+ - Deletes `_bw_state`, `_bw_render`, `_bw_type`, and `el.bw`
193
+ - Removes the element from the DOM
194
+
195
+ After this, `el` is an inert DOM node with no bitwrench properties.
196
+
197
+ ---
198
+
199
+ ## The factory as reusable template
200
+
201
+ ```javascript
202
+ var revenue = bw.mount('#stats', makeStatsCard({ title: 'Revenue', value: 50000 }));
203
+ var users = bw.mount('#stats', makeStatsCard({ title: 'Users', value: 1234 }));
204
+ var orders = bw.mount('#stats', makeStatsCard({ title: 'Orders', value: 89 }));
205
+ ```
206
+
207
+ Three independent instances. Each has its own state, its own UUID, its own slot
208
+ targets. The handle function is shared by reference, but `el` (the first
209
+ argument) is bound per instance. State is per instance (`el._bw_state`).
210
+
211
+ To update all three at once, publish:
212
+
213
+ ```javascript
214
+ bw.pub('dashboard:refresh', newData);
215
+ // each card subscribed to this topic updates independently
216
+ ```
217
+
218
+ ---
219
+
220
+ ## The design in one paragraph
221
+
222
+ The TACO was consumed at mount time — bitwrench does not keep a copy of it.
223
+ Everything the component is now lives on the DOM element itself: its state
224
+ (`el._bw_state`), its public API (`el.bw`), its identity (`bw_uuid_*` class).
225
+ There is no shadow tree to reconcile and no framework instance to look up.
226
+ That's why updates are explicit and cheap — you (or the component itself) talk
227
+ directly to the element — and why any component can be inspected, driven, or
228
+ debugged from the browser console with no tooling.
229
+
230
+ ## See also
231
+
232
+ - [State Management](state-management.md) — the full component model, `o.render` + `bw.refresh()`
233
+ - [Thinking in Bitwrench §7](thinking-in-bitwrench.md) — lifecycle options and the update cost spectrum
234
+ - [Component Library](component-library.md) — the built-in BCCL factories, which follow this exact pattern
@@ -0,0 +1,268 @@
1
+ # drift-lint -- keeping the docs honest
2
+
3
+ `tools/drift-lint.js` is a repo-specific consistency checker. Where ESLint asks
4
+ "is this code well-formed?", drift-lint asks **"do the docs still tell the truth?"**
5
+
6
+ It exists because documentation rots silently. Code that calls a removed function
7
+ fails a test; a doc that *teaches* a removed function fails a user, months later,
8
+ with no error pointing back here. The v2.1 alignment release cleaned up exactly
9
+ this kind of rot -- drift-lint is what makes that cleanup permanent instead of a
10
+ one-time event. Every API removal, rename, or terminology ruling gets encoded as
11
+ a rule, and from then on the stale form physically cannot reach a release.
12
+
13
+ It also extracts the real public API from `src/` at runtime and cross-references
14
+ every `bw.XXX()` call in scanned files against it. If a doc references a function
15
+ that doesn't exist in the source, drift-lint catches it -- no manual rule needed.
16
+
17
+ ## Usage
18
+
19
+ ```
20
+ npm run lint:drift # run all rules
21
+ node tools/drift-lint.js # same thing, direct
22
+ node tools/drift-lint.js --verbose # show honored ignore blocks and config excludes
23
+ node tools/drift-lint.js --list-rules # print all rules with descriptions
24
+ node tools/drift-lint.js --help # CLI help
25
+ ```
26
+
27
+ ## When it runs
28
+
29
+ | Trigger | Why there |
30
+ |---------|-----------|
31
+ | `npm run lint:drift` | Direct invocation |
32
+ | `posttest` (after every `npm test`) | Cheapest possible feedback loop |
33
+ | `postbuild` (after every `npm run build`) | Generated artifacts (e.g. `readme.html`) are scanned fresh |
34
+ | `tools/release.js` step 4 | A stale doc cannot ship |
35
+
36
+ It scans ~120 files in well under a second. No browsers, no network, no flake --
37
+ it is always safe to make blocking.
38
+
39
+ ## What it scans
40
+
41
+ - **Directories (recursive):** `docs/`, `pages/`, `examples/`, `embedded_python/`
42
+ - **Root files:** `README.md`, `CONTRIBUTING.md`, `ABOUT.md`, `readme.html`, `llms.txt`, `agents.md`
43
+ - **Extensions:** `.md`, `.html`, `.js`, `.py`, `.sh`, `.ts`, `.txt`
44
+ - **Skipped:** `node_modules`, `dist`, `coverage`, `.git`, and `dev/` (internal
45
+ notes are allowed to discuss stale patterns — the SUPERSEDED archives live there)
46
+
47
+ `src/` is deliberately out of scope: working code is ESLint's jurisdiction, and
48
+ several rules (like the wire-format field renames) would false-positive against
49
+ the implementation that has to handle both forms.
50
+
51
+ `readme.html` is scanned even though it is generated: if someone fixes
52
+ `README.md` but forgets `npm run build:readme`, the mirror goes stale — which is
53
+ itself drift, and the postbuild hook catches it.
54
+
55
+ ## Rule kinds
56
+
57
+ ### Name rules
58
+
59
+ A token that should no longer appear. The simplest and most common kind:
60
+
61
+ <!-- drift-lint:ignore-start: documenting the rule format requires showing banned tokens -->
62
+ ```javascript
63
+ {
64
+ id: 'toggleStyles',
65
+ pattern: /toggleStyles/g,
66
+ message: 'bw.toggleStyles() → bw.toggleThemeMode()',
67
+ contextExclude: [
68
+ /removed|was removed|renamed|no longer|SUPERSEDED/i
69
+ ]
70
+ }
71
+ ```
72
+ <!-- drift-lint:ignore-end -->
73
+
74
+ Fields:
75
+
76
+ | Field | Meaning |
77
+ |-------|---------|
78
+ | `id` | Short name, shown in the failure report |
79
+ | `pattern` | Regex tested line by line |
80
+ | `message` | What to do instead — write it as `old → new` |
81
+ | `contextExclude` | Regexes that exempt a matching line (e.g. a removal note legitimately names the removed API) |
82
+ | `fileFilter` | Only scan matching paths (e.g. `/\.md$/`) |
83
+ | `fileExclude` | Skip matching paths entirely |
84
+
85
+ ### Structural rules
86
+
87
+ An anti-pattern *shape* rather than a banned name — every individual token is
88
+ fine, the combination is wrong. Structural rules add two fields: `followedBy`
89
+ (evidence regex) and `within` (line window). The rule fires when the anchor
90
+ matches and the evidence appears within N lines:
91
+
92
+ ```javascript
93
+ {
94
+ id: 'mounted-event-wiring',
95
+ pattern: /\bmounted\s*:/, // anchor
96
+ followedBy: /\.addEventListener\(\s*['"](?:click|input|...)['"]/, // evidence
97
+ within: 3,
98
+ message: 'event handler wired in o.mounted is lost on bw.refresh() — use a: { onclick: fn }',
99
+ fileFilter: /\.(html|js)$/,
100
+ contextExclude: [
101
+ /['"`].*addEventListener.*['"`]/, // quoted demo code in comparison pages
102
+ /window\.addEventListener|document\.addEventListener/ // page-level listeners are fine
103
+ ]
104
+ }
105
+ ```
106
+
107
+ For structural rules, `contextExclude` is applied to the **evidence line** —
108
+ that's where false-positive context lives (a quoted code string, a
109
+ window-level listener).
110
+
111
+ This rule enforces the #1 documented mistake in the lifecycle model: handlers
112
+ attached via `addEventListener` inside `o.mounted` are silently lost when the
113
+ component re-renders via `bw.refresh()`. Handlers belong in `a: { onclick: fn }`,
114
+ which bitwrench re-attaches on every render.
115
+
116
+ ### API rules
117
+
118
+ The stale-api rule is different from name and structural rules: it has no
119
+ hand-written pattern. Instead, drift-lint extracts the real public API from
120
+ `src/` at startup and checks every `bw.XXX()` call in scanned files against it.
121
+
122
+ What it extracts:
123
+ - `bw.XXX = function` assignments from all `src/bitwrench*.js` files
124
+ - Object-literal properties on the `bw` object (e.g. `getVersion`, `version`)
125
+ - `export function XXX` from `bitwrench-bccl.js` (become `bw.makeXxx`)
126
+ - `export function XXX` from `bitwrench-color-utils.js` (become `bw.XXX`)
127
+
128
+ What it skips:
129
+ - `el.bw.XXX()` -- handle/slot method calls on component instances
130
+ - `bw._xxx()` -- private/internal names
131
+ - Lines mentioning "removed", "deprecated", "renamed", etc.
132
+ - Generic naming patterns like `bw.makeXxx()` (convention, not a literal call)
133
+
134
+ This rule requires no maintenance. When you add, rename, or remove a public
135
+ function in source, the rule automatically picks up the change. If a doc still
136
+ references the old name, drift-lint flags it on the next run.
137
+
138
+ Run `node tools/drift-lint.js --list-rules` to see the current API count.
139
+
140
+ ## The ignore pragma
141
+
142
+ Sometimes a scanned file must legitimately contain a banned pattern — this
143
+ document is the canonical example. Others: documenting how old versions worked,
144
+ counter-examples shown for discussion, quoting other frameworks' APIs. Wrap the
145
+ block and **give a reason**:
146
+
147
+ ```markdown
148
+ <!-- drift-lint:ignore-start: comparison table quotes the old 2.0 API on purpose -->
149
+ ...exempt content...
150
+ <!-- drift-lint:ignore-end -->
151
+ ```
152
+
153
+ Any comment style works — the scanner matches the pragma text itself, so `//`,
154
+ `#`, `/* */`, and `<!-- -->` are all fine:
155
+
156
+ ```javascript
157
+ // drift-lint:ignore-start: demo intentionally shows the WRONG pattern for teaching
158
+ ...
159
+ // drift-lint:ignore-end
160
+ ```
161
+
162
+ For a single line, use `ignore-next-line` instead of a block:
163
+
164
+ ```javascript
165
+ // drift-lint:ignore-next-line: comparing bitwrench to reactive frameworks
166
+ const comparison = "Unlike reactive frameworks, bitwrench uses explicit updates";
167
+ ```
168
+
169
+ Guard rails, so exemptions can't silently swallow more than intended:
170
+
171
+ - `ignore-start` without a reason → **warning** (the run still passes — a
172
+ hotfix shouldn't be blocked on prose — but the nag prints on every run
173
+ until a reason is added. Pragmas without context become archaeology:
174
+ months later nobody knows what the exemption was protecting.)
175
+ - `ignore-start` never closed → **error** (prevents accidentally exempting the rest of a file)
176
+ - `ignore-end` without a start → **error**
177
+ - nested `ignore-start` → **error**
178
+ - `node tools/drift-lint.js --verbose` lists every honored block with its
179
+ file, line range, and reason — audit them occasionally; each one is debt
180
+ - the summary line always shows the honored-block count
181
+
182
+ Prefer `contextExclude` on the rule over pragmas in files: an exclusion encodes
183
+ *why* a context is acceptable once, centrally; pragmas scatter exemptions
184
+ through the tree. Use the pragma when a single file has a unique, legitimate
185
+ need the rule shouldn't generalize.
186
+
187
+ ## Config file (.drift-lint-config.json)
188
+
189
+ <!-- drift-lint:ignore-start: documenting the config feature requires showing rule IDs and example patterns -->
190
+ For files that structurally and permanently contain a banned pattern (e.g. a
191
+ design philosophy doc that will always discuss reactive systems comparatively),
192
+ use the config file instead of scattering pragmas:
193
+
194
+ ```json
195
+ {
196
+ "exclude": {
197
+ "reactive-self": ["docs/bitwrench-northstar-principles.md"],
198
+ "*": ["docs/legacy-migration-guide.md"]
199
+ }
200
+ }
201
+ ```
202
+ <!-- drift-lint:ignore-end -->
203
+
204
+ - Keys are rule IDs (from the `id` field in RULES). `"*"` excludes from all rules.
205
+ - Values are arrays of relative paths (from repo root, forward slashes).
206
+ - `--verbose` reports which config excludes are active.
207
+
208
+ Use config excludes when a file's relationship to a rule is structural — the
209
+ file will always legitimately contain the pattern, and re-wording to avoid it
210
+ would make the document worse. Use pragmas for isolated one-off exemptions
211
+ within an otherwise-scanned file.
212
+
213
+ ## Adding a rule
214
+
215
+ When you remove, rename, or re-decide something user-facing:
216
+
217
+ 1. Add the rule to `RULES` in `tools/drift-lint.js` in the same commit as the
218
+ change. The rule *is* the enforcement half of the decision.
219
+ 2. Write the message as the migration: `old → new`, not just "don't".
220
+ 3. Run `npm run lint:drift` — it will list every place the old form survives.
221
+ Fix them all in the same commit if feasible.
222
+ 4. Add `contextExclude` entries only for patterns of legitimate use you actually
223
+ observed, not hypothetical ones.
224
+ 5. If a doc must keep the old form (removal notes, migration guides,
225
+ comparison tables), prefer a `contextExclude`; reach for the pragma only
226
+ for one-off cases.
227
+
228
+ ## Current rule inventory
229
+
230
+ <!-- drift-lint:ignore-start: the inventory table must name the banned patterns it documents -->
231
+
232
+ | Rule | Catches | Correct form |
233
+ |------|---------|--------------|
234
+ | `client.render` / `client.exec` / `client.query` / `client.register` | 2.0 bwserve client APIs | `client.mount()`; exec/query/register removed |
235
+ | `bw.createDOM` | 2.0 name | `bw.create()` |
236
+ | `bw.cleanup` | 2.0 name | `bw.unmount()` |
237
+ | `bw.component` | removed API | TACO `o:` options |
238
+ | `data-bw-action` | 2.0 action attribute | `bw_act_*` CSS class |
239
+ | `allowExec` / `--allow-exec` / `/exec` | removed exec surface | removed in v2.1 |
240
+ | `wire:target` / `wire:node` | 2.0 wire field names | `"ref"` / `"taco"` |
241
+ | `parseRJSON` | 2.0 name | `bw.parseJSONFlex()` |
242
+ | `levels-taxonomy` | "Level 0/1/2" component taxonomy | descriptive stage names |
243
+ | `toggleStyles` | removed API | `bw.toggleThemeMode()` |
244
+ | `bw_card-bare` / `bw_btn-bare` | pre-rename BCCL class names in docs | `bw_bccl_card` / `bw_bccl_btn` (the bare forms remain valid as stylesheet classes) |
245
+ | `bw-container` | hyphen class form | `bw_container` (underscore canonical) |
246
+ | `normalizeClass` | never-shipped API | remove reference |
247
+ | `three-level` | stale component-model count | describe stages, don't count |
248
+ | `outline-hyphen` | hyphen variant spelling | `outline_primary` etc. |
249
+ | `reactive-self` | "reactive" as self-description | "explicit stateful" / "state + explicit re-render" (comparative uses are excluded) |
250
+ | `getHandle` | removed API | handles live on `el.bw` directly |
251
+ | `update-as-rerender` | prose claiming `bw.update()` re-renders | `bw.update()` dispatches; re-render is `bw.refresh()` |
252
+ | `mounted-event-wiring` | structural: DOM event handlers wired in `o.mounted` | `a: { onclick: fn }` |
253
+ | `stale-api` | `bw.XXX()` in docs where XXX is not in the public API | auto-detected from `src/` -- no manual rule needed |
254
+
255
+ <!-- drift-lint:ignore-end -->
256
+
257
+ ## Design constraints
258
+
259
+ - **Zero dependencies, zero flake.** Plain-string scanning only. If a rule
260
+ needs an AST, it belongs in ESLint; if it needs a browser, it belongs in
261
+ Playwright. Drift-lint stays fast enough that nobody is ever tempted to
262
+ skip it.
263
+ - **Every rule earns its place by having fired at least once.** Rules encode
264
+ real drift that actually happened (or a decision actually made), not
265
+ hypothetical hygiene.
266
+ - **False positives are rule bugs.** If a legitimate line trips a rule, fix
267
+ the rule (`contextExclude`), don't pragma around it — the next legitimate
268
+ use will trip it again.