kerfjs 4.4.0 → 4.5.0-beta.1

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 (84) hide show
  1. package/CHANGELOG.md +284 -104
  2. package/LICENSE +1 -1
  3. package/README.md +73 -54
  4. package/ai/cursorrules +130 -48
  5. package/ai/manifest.json +63 -5
  6. package/ai/skill.md +148 -65
  7. package/dist/actions.d.ts +1 -1
  8. package/dist/actions.js +4 -4
  9. package/dist/actions.js.map +1 -1
  10. package/dist/array-signal.js +5 -5
  11. package/dist/async.js +34 -24
  12. package/dist/async.js.map +1 -1
  13. package/dist/attach.d.ts +11 -8
  14. package/dist/attach.js +54 -4
  15. package/dist/attach.js.map +1 -1
  16. package/dist/{chunk-QIP723L4.js → chunk-5WRGJZV6.js} +4 -3
  17. package/dist/chunk-5WRGJZV6.js.map +1 -0
  18. package/dist/{chunk-GY4XV2UV.js → chunk-BDX3R4OM.js} +4 -3
  19. package/dist/chunk-BDX3R4OM.js.map +1 -0
  20. package/dist/{chunk-VVDJLWMP.js → chunk-CEQMZYLR.js} +2 -2
  21. package/dist/chunk-CEQMZYLR.js.map +1 -0
  22. package/dist/{chunk-KEZTD6H4.js → chunk-E5R5GNKE.js} +15 -5
  23. package/dist/chunk-E5R5GNKE.js.map +1 -0
  24. package/dist/{chunk-MRYM3O3V.js → chunk-ELXVRKY2.js} +12 -9
  25. package/dist/chunk-ELXVRKY2.js.map +1 -0
  26. package/dist/{chunk-U32TFTGZ.js → chunk-MK42GLPV.js} +3 -3
  27. package/dist/chunk-MK42GLPV.js.map +1 -0
  28. package/dist/{chunk-SUPUPSBE.js → chunk-QFUNWHKH.js} +42 -20
  29. package/dist/chunk-QFUNWHKH.js.map +1 -0
  30. package/dist/{chunk-SRWQKB33.js → chunk-V757BT6U.js} +206 -111
  31. package/dist/chunk-V757BT6U.js.map +1 -0
  32. package/dist/{chunk-YHH7OUFA.js → chunk-WKIPLNVO.js} +3 -3
  33. package/dist/chunk-WKIPLNVO.js.map +1 -0
  34. package/dist/{chunk-3APBEVHF.js → chunk-Y2FOYPBV.js} +3 -3
  35. package/dist/{chunk-3APBEVHF.js.map → chunk-Y2FOYPBV.js.map} +1 -1
  36. package/dist/{chunk-SAYPJ6XR.js → chunk-ZOIERTUW.js} +10 -6
  37. package/dist/chunk-ZOIERTUW.js.map +1 -0
  38. package/dist/dev.d.ts +9 -6
  39. package/dist/dev.js +66 -19
  40. package/dist/dev.js.map +1 -1
  41. package/dist/html.d.ts +1 -1
  42. package/dist/html.js +8 -7
  43. package/dist/html.js.map +1 -1
  44. package/dist/index.d.ts +1 -1
  45. package/dist/index.js +28 -19
  46. package/dist/index.js.map +1 -1
  47. package/dist/jsx-runtime.js +5 -5
  48. package/dist/list.d.ts +1 -1
  49. package/dist/list.js +304 -213
  50. package/dist/list.js.map +1 -1
  51. package/dist/overlay.d.ts +190 -280
  52. package/dist/overlay.js +471 -364
  53. package/dist/overlay.js.map +1 -1
  54. package/dist/remount.d.ts +5 -3
  55. package/dist/remount.js +29 -10
  56. package/dist/remount.js.map +1 -1
  57. package/dist/router.d.ts +1 -1
  58. package/dist/router.js +73 -28
  59. package/dist/router.js.map +1 -1
  60. package/dist/scope.d.ts +4 -3
  61. package/dist/scope.js +11 -10
  62. package/dist/scope.js.map +1 -1
  63. package/dist/testing.js +4 -4
  64. package/dist/timing.d.ts +3 -2
  65. package/dist/timing.js +5 -5
  66. package/dist/timing.js.map +1 -1
  67. package/llms.txt +13 -6
  68. package/package.json +35 -13
  69. package/setup/cli.mjs +90 -0
  70. package/setup/index.d.mts +37 -0
  71. package/setup/index.mjs +1347 -0
  72. package/setup/jsonc.mjs +201 -0
  73. package/setup/state.schema.json +56 -0
  74. package/dist/chunk-GY4XV2UV.js.map +0 -1
  75. package/dist/chunk-KEZTD6H4.js.map +0 -1
  76. package/dist/chunk-MRYM3O3V.js.map +0 -1
  77. package/dist/chunk-QIP723L4.js.map +0 -1
  78. package/dist/chunk-SAYPJ6XR.js.map +0 -1
  79. package/dist/chunk-SRWQKB33.js.map +0 -1
  80. package/dist/chunk-SUPUPSBE.js.map +0 -1
  81. package/dist/chunk-U32TFTGZ.js.map +0 -1
  82. package/dist/chunk-VVDJLWMP.js.map +0 -1
  83. package/dist/chunk-YHH7OUFA.js.map +0 -1
  84. /package/dist/{attrSelector-Cmu2ZoGO.d.ts → attr-Cmu2ZoGO.d.ts} +0 -0
@@ -0,0 +1,201 @@
1
+ function syntaxError(label, index, message) {
2
+ throw new Error(`${label} is invalid JSONC at offset ${index}: ${message}.`);
3
+ }
4
+
5
+ export function parseJsoncDocument(source, label = 'JSONC document') {
6
+ let index = 0;
7
+ const skipTrivia = () => {
8
+ while (index < source.length) {
9
+ if (/\s/.test(source[index])) {
10
+ index += 1;
11
+ continue;
12
+ }
13
+ if (source.startsWith('//', index)) {
14
+ index = source.indexOf('\n', index + 2);
15
+ if (index < 0) index = source.length;
16
+ continue;
17
+ }
18
+ if (source.startsWith('/*', index)) {
19
+ const end = source.indexOf('*/', index + 2);
20
+ if (end < 0) syntaxError(label, index, 'unterminated block comment');
21
+ index = end + 2;
22
+ continue;
23
+ }
24
+ break;
25
+ }
26
+ };
27
+ const parseString = () => {
28
+ const start = index++;
29
+ while (index < source.length) {
30
+ if (source[index] === '\\') {
31
+ index += 2;
32
+ continue;
33
+ }
34
+ if (source[index++] === '"') {
35
+ try {
36
+ return {
37
+ type: 'string',
38
+ start,
39
+ end: index,
40
+ value: JSON.parse(source.slice(start, index)),
41
+ };
42
+ } catch {
43
+ syntaxError(label, start, 'invalid string');
44
+ }
45
+ }
46
+ }
47
+ syntaxError(label, start, 'unterminated string');
48
+ };
49
+ const parseValue = () => {
50
+ skipTrivia();
51
+ const start = index;
52
+ if (source[index] === '"') return parseString();
53
+ if (source[index] === '{') {
54
+ index += 1;
55
+ const properties = new Map();
56
+ const value = {};
57
+ skipTrivia();
58
+ while (source[index] !== '}') {
59
+ if (index >= source.length)
60
+ syntaxError(label, start, 'unterminated object');
61
+ if (source[index] !== '"')
62
+ syntaxError(label, index, 'object keys must be quoted strings');
63
+ const propertyStart = index;
64
+ const keyNode = parseString();
65
+ skipTrivia();
66
+ if (source[index++] !== ':')
67
+ syntaxError(label, index - 1, 'expected a colon');
68
+ const valueNode = parseValue();
69
+ if (properties.has(keyNode.value))
70
+ syntaxError(label, keyNode.start, `duplicate key ${keyNode.value}`);
71
+ const property = {
72
+ key: keyNode.value,
73
+ start: propertyStart,
74
+ end: valueNode.end,
75
+ valueNode,
76
+ comma: null,
77
+ };
78
+ properties.set(keyNode.value, property);
79
+ value[keyNode.value] = valueNode.value;
80
+ skipTrivia();
81
+ if (source[index] === ',') {
82
+ property.comma = index++;
83
+ skipTrivia();
84
+ if (source[index] === '}') break;
85
+ } else if (source[index] !== '}')
86
+ syntaxError(label, index, 'expected a comma or closing brace');
87
+ }
88
+ index += 1;
89
+ return { type: 'object', start, end: index, value, properties };
90
+ }
91
+ if (source[index] === '[') {
92
+ index += 1;
93
+ const items = [];
94
+ const value = [];
95
+ skipTrivia();
96
+ while (source[index] !== ']') {
97
+ if (index >= source.length)
98
+ syntaxError(label, start, 'unterminated array');
99
+ const item = parseValue();
100
+ items.push(item);
101
+ value.push(item.value);
102
+ skipTrivia();
103
+ if (source[index] === ',') {
104
+ index += 1;
105
+ skipTrivia();
106
+ if (source[index] === ']') break;
107
+ } else if (source[index] !== ']')
108
+ syntaxError(label, index, 'expected a comma or closing bracket');
109
+ }
110
+ index += 1;
111
+ return { type: 'array', start, end: index, value, items };
112
+ }
113
+ const match = source
114
+ .slice(index)
115
+ .match(
116
+ /^(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)/,
117
+ );
118
+ if (!match) syntaxError(label, index, 'expected a value');
119
+ index += match[0].length;
120
+ return {
121
+ type: 'primitive',
122
+ start,
123
+ end: index,
124
+ value: JSON.parse(match[0]),
125
+ };
126
+ };
127
+
128
+ const root = parseValue();
129
+ skipTrivia();
130
+ if (index !== source.length)
131
+ syntaxError(label, index, 'unexpected content after the root value');
132
+ return { root, value: root.value };
133
+ }
134
+
135
+ const lineIndent = (source, position) => {
136
+ const start = source.lastIndexOf('\n', position - 1) + 1;
137
+ return source.slice(start, position).match(/^\s*/)[0];
138
+ };
139
+
140
+ const formattedValue = (value, indent, childIndent, newline) => {
141
+ const unit = childIndent.slice(indent.length) || ' ';
142
+ const formatted = JSON.stringify(value, null, unit);
143
+ return formatted.replaceAll('\n', `${newline}${childIndent}`);
144
+ };
145
+
146
+ export function jsoncValueEdit(node, value, source) {
147
+ const newline = source.includes('\r\n') ? '\r\n' : '\n';
148
+ return {
149
+ start: node.start,
150
+ end: node.end,
151
+ text: formattedValue(value, '', lineIndent(source, node.start), newline),
152
+ };
153
+ }
154
+
155
+ export function jsoncInsertProperties(source, objectNode, entries) {
156
+ if (!entries.length) return [];
157
+ const newline = source.includes('\r\n') ? '\r\n' : '\n';
158
+ const close = objectNode.end - 1;
159
+ const closeLineStart = source.lastIndexOf('\n', close - 1) + 1;
160
+ const closePrefix = source.slice(closeLineStart, close);
161
+ const closeIndent = /^\s*$/.test(closePrefix)
162
+ ? closePrefix
163
+ : lineIndent(source, close);
164
+ const firstProperty = objectNode.properties.values().next().value;
165
+ const childIndent = firstProperty
166
+ ? lineIndent(source, firstProperty.start)
167
+ : `${closeIndent}${source.includes('\t') ? '\t' : ' '}`;
168
+ const insertAt = /^\s*$/.test(closePrefix) ? closeLineStart : close;
169
+ const rendered = entries
170
+ .map(
171
+ ([key, value]) =>
172
+ `${childIndent}${JSON.stringify(key)}: ${formattedValue(value, closeIndent, childIndent, newline)}`,
173
+ )
174
+ .join(`,${newline}`);
175
+ const edits = [
176
+ {
177
+ start: insertAt,
178
+ end: insertAt,
179
+ text: `${insertAt === close ? newline : ''}${rendered}${newline}${closeIndent}`,
180
+ },
181
+ ];
182
+ const properties = [...objectNode.properties.values()];
183
+ const last = properties.at(-1);
184
+ if (last && last.comma === null)
185
+ edits.push({
186
+ start: last.valueNode.end,
187
+ end: last.valueNode.end,
188
+ text: ',',
189
+ });
190
+ return edits;
191
+ }
192
+
193
+ export function applyJsoncEdits(source, edits) {
194
+ return [...edits]
195
+ .sort((left, right) => right.start - left.start || right.end - left.end)
196
+ .reduce(
197
+ (output, edit) =>
198
+ `${output.slice(0, edit.start)}${edit.text}${output.slice(edit.end)}`,
199
+ source,
200
+ );
201
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/brianwestphal/kerf/main/setup/state.schema.json",
4
+ "title": "Kerf AI setup state",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "setupVersion", "packageManager", "packages"],
7
+ "properties": {
8
+ "schemaVersion": { "const": 2 },
9
+ "setupVersion": { "type": "string", "minLength": 1 },
10
+ "packageManager": { "enum": ["npm", "pnpm", "yarn"] },
11
+ "packageManagerVersion": { "type": "string", "minLength": 1 },
12
+ "packageManagerVariant": { "enum": ["classic", "berry"] },
13
+ "packages": {
14
+ "type": "object",
15
+ "minProperties": 1,
16
+ "additionalProperties": {
17
+ "type": "object",
18
+ "required": [
19
+ "mode",
20
+ "package",
21
+ "packagePath",
22
+ "managed",
23
+ "resolutions"
24
+ ],
25
+ "properties": {
26
+ "mode": { "enum": ["core", "ui"] },
27
+ "package": { "type": "string", "minLength": 1 },
28
+ "packagePath": { "type": "string", "minLength": 1 },
29
+ "managed": {
30
+ "type": "object",
31
+ "additionalProperties": {
32
+ "type": "object",
33
+ "required": ["sha256"],
34
+ "properties": {
35
+ "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
36
+ },
37
+ "additionalProperties": false
38
+ }
39
+ },
40
+ "resolutions": {
41
+ "type": "object",
42
+ "additionalProperties": { "enum": ["keep", "kerf"] }
43
+ }
44
+ },
45
+ "additionalProperties": false
46
+ }
47
+ }
48
+ },
49
+ "additionalProperties": false,
50
+ "allOf": [
51
+ {
52
+ "if": { "required": ["packageManagerVariant"] },
53
+ "then": { "properties": { "packageManager": { "const": "yarn" } } }
54
+ }
55
+ ]
56
+ }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/segment.ts"],"names":[],"mappings":"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","file":"chunk-GY4XV2UV.js","sourcesContent":["/**\n * `Segment` — kerf's structured render output.\n *\n * The JSX runtime emits a `SafeHtml` wrapping a `Segment`. Most renders\n * produce a single static segment (just an HTML string), which behaves\n * exactly like a string for backward compatibility. When the tree\n * contains a list (`each()`) or a parent whose children include a list,\n * the runtime emits a structured segment that `mount()` can dispatch\n * on — running its native keyed reconciler for the list parts and\n * leaving the static surrounds to the general-purpose diff.\n *\n * Why have a structured form at all: the perf bottleneck for huge\n * keyed lists isn't the per-row JSX work (which `each()` already\n * memoizes). It's that flattening every render's whole tree to one\n * big HTML string forces a full `innerHTML` parse and a tree walk\n * over rows we know are unchanged. The segment shape lets mount()\n * skip both for the list parts.\n */\n\nimport type { Binding } from './bindings.js';\n\nexport type Segment = StaticSegment | ListSegment | MixedSegment;\n\nexport interface StaticSegment {\n kind: 'static';\n html: string;\n}\n\nexport interface ListItem {\n /**\n * The row's object identity. Used by the reconciler to match new items\n * against live DOM nodes across renders. Unchanged ref → reuse the\n * existing live node; replaced ref → build a fresh node.\n */\n ref: object;\n /**\n * KF-294: the row's fine-grained binding specs (signals in row attrs/text).\n * Undefined for granular-path rows (which snapshot in this spike). The\n * snapshot reconciler wires these to the fresh row node and disposes them\n * when the row is removed.\n */\n bindings?: Binding[];\n /**\n * Optional cache-invalidation key that captures external state affecting\n * this row's render (e.g. selection class). Different cacheKey on the\n * same `ref` triggers a cache miss for that row. `undefined` when the\n * user didn't pass a `key` callback to `each()`.\n */\n cacheKey: unknown;\n html: string;\n}\n\nexport interface ListSegment {\n kind: 'list';\n id: string;\n items: ListItem[];\n /**\n * Optional granular patches (KF-92). When present, the list reconciler\n * applies these directly to the existing binding instead of doing a\n * full classify+reconcile pass. Emitted by `each()` when bound to an\n * `arraySignal`. Mutually exclusive with the `items` snapshot in the\n * sense that the snapshot is treated as informational/fall-back when\n * patches are present.\n */\n patches?: ArrayPatchInternal[];\n /**\n * KF-388: the identity of the data this list renders — the `arraySignal`\n * instance, or `undefined` for a plain array.\n *\n * A list's `id` is its call-order index, so a render that changes how many\n * `each()` calls precede this one hands this segment a DIFFERENT list's\n * binding. Patches are only meaningful against the binding they were queued\n * for, so the reconciler compares this against the binding's recorded source\n * before trusting the patch queue. It is an identity check, not a value\n * check — the instance is never read.\n */\n source?: object;\n}\n\n/**\n * Internal patch shape used inside list segments. Mirrors `ArrayPatch<T>`\n * from `array-signal.ts` but typed against `object` so the segment layer\n * doesn't need to be generic. `update` / `insert` patches carry the row's\n * pre-rendered HTML — `each()` renders them at JSX-evaluation time inside a\n * try/catch so a throwing render falls back to the snapshot path (KF-99)\n * instead of leaving the signal and DOM divergent.\n */\nexport type ArrayPatchInternal =\n | { type: 'update'; index: number; item: object; html: string; bindings?: Binding[] }\n | { type: 'insert'; index: number; item: object; html: string; bindings?: Binding[] }\n | { type: 'remove'; index: number }\n | { type: 'move'; from: number; to: number }\n | { type: 'replace'; items: readonly object[] };\n\n/**\n * Narrowed patch aliases. Array indexing loses the union discriminant, so\n * the granular reconciler casts through these instead of restating the full\n * object type at every site — a field rename is then a one-place edit.\n */\nexport type UpdatePatch = Extract<ArrayPatchInternal, { type: 'update' }>;\nexport type InsertPatch = Extract<ArrayPatchInternal, { type: 'insert' }>;\n\nexport interface MixedSegment {\n kind: 'mixed';\n parts: Segment[];\n}\n\n/**\n * Flatten a segment to a complete HTML string. Used for first render\n * (bulk innerHTML), for SSR-style consumption via `toString()`, and\n * for diagnostics.\n *\n * If `withMarkers` is set, list segments are wrapped in\n * `<!--kf-list:{id}-->` comments so the post-parse walk can find each\n * list's live parent. Plain (non-marker) flatten is what JSX consumers\n * see when they call `.toString()` on the SafeHtml.\n */\n/**\n * Comment-marker prefix emitted before each list (`<!--kf-list:{id}-->`).\n * `mount()` consumes it when binding lists from the live DOM — the emitter\n * (here) and the consumer must agree byte-for-byte, so both import this one\n * constant. Part of the reserved marker namespace (see `bindings.ts`).\n */\nexport const LIST_MARKER_PREFIX = 'kf-list:';\n\nexport function flatten(segment: Segment, withMarkers: boolean): string {\n if (segment.kind === 'static') return segment.html;\n if (segment.kind === 'list') {\n const items = segment.items.map((i) => i.html).join('');\n return withMarkers ? `<!--${LIST_MARKER_PREFIX}${segment.id}-->${items}` : items;\n }\n return segment.parts.map((p) => flatten(p, withMarkers)).join('');\n}\n\n/**\n * Variant of `flatten` for the static-only diff path on subsequent\n * renders. Lists are reduced to a single marker comment with no items\n * inside — the actual list children stay in the live DOM and are\n * reconciled separately. Keeping list items out of this string is\n * what makes the morph cheap on huge lists where most rows are\n * unchanged.\n */\nexport function flattenWithoutListItems(segment: Segment): string {\n if (segment.kind === 'static') return segment.html;\n if (segment.kind === 'list') return `<!--${LIST_MARKER_PREFIX}${segment.id}-->`;\n return segment.parts.map(flattenWithoutListItems).join('');\n}\n\n/** Collect every `ListSegment` in the tree, keyed by its id. */\nexport function collectLists(\n segment: Segment,\n out: Map<string, ListSegment> = new Map(),\n): Map<string, ListSegment> {\n if (segment.kind === 'list') out.set(segment.id, segment);\n else if (segment.kind === 'mixed') {\n for (const part of segment.parts) collectLists(part, out);\n }\n return out;\n}\n\n/**\n * Combine a list of child segments into the smallest equivalent\n * representation: collapses adjacent statics into one static, returns\n * a single static if everything is static, otherwise a mixed segment\n * with statics coalesced.\n */\nexport function mergeChildSegments(parts: Segment[]): Segment {\n if (parts.length === 0) return { kind: 'static', html: '' };\n if (parts.every((p) => p.kind === 'static')) {\n return {\n kind: 'static',\n html: parts.map((p) => (p as StaticSegment).html).join(''),\n };\n }\n const merged: Segment[] = [];\n let coalesced = '';\n for (const p of parts) {\n if (p.kind === 'static') {\n coalesced += p.html;\n } else {\n if (coalesced !== '') {\n merged.push({ kind: 'static', html: coalesced });\n coalesced = '';\n }\n merged.push(p);\n }\n }\n if (coalesced !== '') merged.push({ kind: 'static', html: coalesced });\n return { kind: 'mixed', parts: merged };\n}\n\n/**\n * Wrap a child segment with surrounding open/close tags from the\n * parent JSX element. Used by the JSX runtime when constructing\n * `_jsx(tag, ...)` output.\n */\nexport function wrapWithTags(child: Segment, openTag: string, closeTag: string): Segment {\n if (child.kind === 'static') {\n return { kind: 'static', html: openTag + child.html + closeTag };\n }\n if (child.kind === 'mixed') {\n return {\n kind: 'mixed',\n parts: [\n { kind: 'static', html: openTag },\n ...child.parts,\n { kind: 'static', html: closeTag },\n ],\n };\n }\n return {\n kind: 'mixed',\n parts: [\n { kind: 'static', html: openTag },\n child,\n { kind: 'static', html: closeTag },\n ],\n };\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/delegate.ts"],"names":[],"mappings":";;;AA+CA,IAAM,YAAA,uBAAmB,GAAA,CAAY;AAAA,EACnC,OAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACA,OAAA;AAAA,EACA,YAAA;AAAA,EACA;AACF,CAAC,CAAA;AAqBD,SAAS,mBAAA,CAAoB,UAAkB,EAAA,EAAkB;AAC/D,EAAA,IAAI;AACF,IAAA,QAAA,CAAS,aAAA,CAAc,KAAK,CAAA,CAAE,OAAA,CAAQ,QAAQ,CAAA;AAAA,EAChD,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,EAAG,EAAE,CAAA,oBAAA,EAAuB,QAAQ,CAAA,2EAAA;AAAA,KAEtC;AAAA,EACF;AACF;AAQA,SAAS,YAAA,CACP,MAAA,EACA,QAAA,EACA,OAAA,EACA,KAAA,EACwB;AACxB,EAAA,OAAO,CAAC,KAAA,KAAuB;AAC7B,IAAA,MAAM,SAAS,KAAA,CAAM,MAAA;AACrB,IAAA,IAAI,EAAE,kBAAkB,OAAA,CAAA,EAAU;AAClC,IAAA,MAAM,OAAA,GAAU,KAAA,KAAU,QAAA,GACrB,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA,GAAI,MAAA,GAAS,IAAA,GACrC,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA;AAC3B,IAAA,IAAI,OAAA,KAAY,IAAA,IAAQ,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA,EAAG;AAChD,MAAA,OAAA,CAAQ,OAAO,OAAY,CAAA;AAAA,IAC7B;AAAA,EACF,CAAA;AACF;AAuBO,SAAS,QAAA,CACd,MAAA,EACA,IAAA,EACA,QAAA,EACA,SACA,OAAA,EACY;AACZ,EAAA,mBAAA,CAAoB,UAAU,UAAU,CAAA;AACxC,EAAA,QAAA,CAAS,mBAAmB,UAAU,CAAA;AACtC,EAAA,MAAM,WAAW,YAAA,CAAa,MAAA,EAAQ,UAAU,OAAA,EAAS,OAAA,EAAS,SAAS,SAAS,CAAA;AACpF,EAAA,MAAM,OAAA,GAAU,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA;AACrC,EAAA,MAAA,CAAO,gBAAA,CAAiB,IAAA,EAAM,QAAA,EAAU,OAAO,CAAA;AAC/C,EAAA,OAAO,MAAM;AACX,IAAA,MAAA,CAAO,mBAAA,CAAoB,IAAA,EAAM,QAAA,EAAU,OAAO,CAAA;AAAA,EACpD,CAAA;AACF;AAuBO,SAAS,eAAA,CACd,MAAA,EACA,IAAA,EACA,QAAA,EACA,SACA,OAAA,EACY;AACZ,EAAA,mBAAA,CAAoB,UAAU,iBAAiB,CAAA;AAC/C,EAAA,QAAA,CAAS,mBAAmB,iBAAiB,CAAA;AAC7C,EAAA,MAAM,WAAW,YAAA,CAAa,MAAA,EAAQ,UAAU,OAAA,EAAS,OAAA,EAAS,SAAS,SAAS,CAAA;AACpF,EAAA,MAAA,CAAO,gBAAA,CAAiB,IAAA,EAAM,QAAA,EAAU,IAAI,CAAA;AAC5C,EAAA,OAAO,MAAM;AACX,IAAA,MAAA,CAAO,mBAAA,CAAoB,IAAA,EAAM,QAAA,EAAU,IAAI,CAAA;AAAA,EACjD,CAAA;AACF","file":"chunk-KEZTD6H4.js","sourcesContent":["/**\n * Tiny event-delegation helpers. Replace per-element `addEventListener` calls\n * (which don't survive morph re-renders for nodes the diff creates) with one\n * listener at the morph-root that dispatches via `closest()`.\n *\n * Three-tier listener model:\n *\n * - Tier 1 (bubbling events) — use `delegate()`.\n * click, input, change, submit, mousedown/up, keydown/up, pointerdown/up/move,\n * drag*, drop, contextmenu, wheel, copy/paste/cut, focusin/focusout.\n *\n * `delegate()` also auto-promotes the well-known non-bubbling event\n * types (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`,\n * `mouseleave`) to the capture phase under the hood, so the call site\n * looks identical for \"interactive thing happens on a descendant\"\n * regardless of whether that event bubbles. Selector matching stays\n * `closest()`-style — the same as for bubbling events — so a wrapper\n * selector like `'.field-row'` still matches when the event fires on\n * a descendant `<input>`.\n *\n * - Tier 2 (explicit capture) — use `delegateCapture()`.\n * The escape hatch for cases the auto-promotion list doesn't cover\n * (custom non-bubbling events) or when you want capture-phase\n * interception. Selector matching is `closest()`-style by default —\n * the same walk-up as `delegate()`, and it passes the matched ancestor\n * (not the raw target) to the handler — so a click on any descendant of\n * the selected element climbs to it. Pass `{ match: 'direct' }` to opt\n * into strict `matches()`-style matching (fire only when the event lands\n * on the exact element the selector identifies).\n *\n * - Tier 3 (per-element instances / library-owned subtrees) — mark the\n * host element with `data-morph-skip` and manage the library's\n * lifecycle directly. No delegation helper applies.\n */\n\nimport { devHooks } from './dev-hooks.js';\n\n/**\n * Event types that don't bubble and so wouldn't reach a root-level\n * bubble-phase listener. `delegate()` flips to capture for these; the\n * caller doesn't need to know or care.\n *\n * Membership is conservative — it covers the cases that \"should obviously\n * work\" with delegate() but otherwise don't. For exotic non-bubbling events\n * (custom events, less-common DOM events) the explicit `delegateCapture()`\n * remains the escape hatch.\n */\nconst NON_BUBBLING = new Set<string>([\n 'focus',\n 'blur',\n 'scroll',\n 'load',\n 'error',\n 'mouseenter',\n 'mouseleave',\n]);\n\n/**\n * How the selector is matched against the event's target:\n *\n * - `'closest'` (the default for both helpers) — walk UP from `event.target`\n * via `closest(selector)`, firing for the nearest matching ancestor inside\n * `rootEl`. This is the delegation behavior you almost always want: a click\n * on an icon inside a button fires the button's handler.\n * - `'direct'` — strict `matches()` match: fire only when `event.target`\n * itself matches the selector, with no walk-up.\n */\nexport interface DelegateOptions {\n match?: 'closest' | 'direct';\n}\n\n/**\n * Validate a CSS selector at registration time, so a typo throws immediately\n * with the bad selector quoted instead of producing a cryptic DOMException\n * the first time a matching event fires.\n */\nfunction assertValidSelector(selector: string, fn: string): void {\n try {\n document.createElement('div').matches(selector);\n } catch {\n throw new Error(\n `${fn}: invalid selector \"${selector}\". `\n + 'Pass a valid CSS selector (e.g. \\'[data-action=\"add\"]\\', \\'.btn\\', \\'input\\').',\n );\n }\n}\n\n/**\n * Build the shared root-level listener used by both helpers. Resolves the\n * event's target to a matched element (walk-up `closest()` or strict\n * `matches()`, per `match`), requires the match to be inside `rootEl`, then\n * fires `handler(event, matched)`.\n */\nfunction makeListener<T extends Element>(\n rootEl: HTMLElement,\n selector: string,\n handler: (event: Event, target: T) => void,\n match: 'closest' | 'direct',\n): (event: Event) => void {\n return (event: Event): void => {\n const target = event.target;\n if (!(target instanceof Element)) return;\n const matched = match === 'direct'\n ? (target.matches(selector) ? target : null)\n : target.closest(selector);\n if (matched !== null && rootEl.contains(matched)) {\n handler(event, matched as T);\n }\n };\n}\n\n/**\n * Delegation that \"just works\" for both bubbling and the common non-bubbling\n * events. Installs ONE listener on `rootEl`; for known non-bubblers (see\n * `NON_BUBBLING` above) the listener is registered on the capture phase so\n * it actually reaches the target, otherwise on the bubble phase. Either way,\n * matching walks up from `event.target` via `closest(selector)` and fires\n * `handler(event, matched)` if the match is inside `rootEl`.\n *\n * Pass `{ match: 'direct' }` to fire only when `event.target` itself matches\n * the selector (no walk-up); the default is `'closest'`.\n *\n * The generic `T` narrows the second handler argument to the expected element\n * type — `delegate<HTMLButtonElement>(root, 'click', 'button', (e, btn) => btn.value)`\n * — so consumers can avoid casts. Defaults to `Element` for untyped calls.\n *\n * Returns a disposer that removes the listener.\n *\n * Usage (pseudo-code — see examples for live ones):\n * delegate(rootEl, 'click', '[data-action=\"add\"]', handlerFn);\n * delegate(rootEl, 'focus', 'input', handlerFn); // auto-capture\n */\nexport function delegate<T extends Element = Element>(\n rootEl: HTMLElement,\n type: string,\n selector: string,\n handler: (event: Event, target: T) => void,\n options?: DelegateOptions,\n): () => void {\n assertValidSelector(selector, 'delegate');\n devHooks.delegateInEffect?.('delegate');\n const listener = makeListener(rootEl, selector, handler, options?.match ?? 'closest');\n const capture = NON_BUBBLING.has(type);\n rootEl.addEventListener(type, listener, capture);\n return () => {\n rootEl.removeEventListener(type, listener, capture);\n };\n}\n\n/**\n * Capture-phase delegation — the escape hatch for custom non-bubbling events\n * (ones `delegate()`'s auto-promotion list doesn't know about) and for\n * capture-phase interception (run before any descendant's bubble-phase\n * handler). Reaches descendants of `rootEl` that match `selector` regardless\n * of how many times the diff has rebuilt them.\n *\n * Selector matching is `closest()`-style by default — the same walk-up as\n * `delegate()`, and it passes the matched ancestor (not the raw target) to\n * the handler — so a click on any descendant of the selected element climbs\n * to it. Pass `{ match: 'direct' }` to opt into strict `matches()`-style\n * matching (fire only when the event lands on the exact element the selector\n * identifies, with no walk-up).\n *\n * The generic `T` narrows the second handler argument to the expected element\n * type, mirroring `delegate<T>()`. Defaults to `Element` for untyped calls.\n *\n * Usage (pseudo-code — see examples for live ones):\n * delegateCapture(rootEl, 'focus', 'input, textarea', handlerFn);\n * delegateCapture(rootEl, 'click', '.exact', handlerFn, { match: 'direct' });\n */\nexport function delegateCapture<T extends Element = Element>(\n rootEl: HTMLElement,\n type: string,\n selector: string,\n handler: (event: Event, target: T) => void,\n options?: DelegateOptions,\n): () => void {\n assertValidSelector(selector, 'delegateCapture');\n devHooks.delegateInEffect?.('delegateCapture');\n const listener = makeListener(rootEl, selector, handler, options?.match ?? 'closest');\n rootEl.addEventListener(type, listener, true);\n return () => {\n rootEl.removeEventListener(type, listener, true);\n };\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/array-signal.ts"],"names":[],"mappings":"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","file":"chunk-MRYM3O3V.js","sourcesContent":["/**\n * `arraySignal(initial)` — granular collection signal.\n *\n * A keyed-list-friendly variant of `signal()` that emits typed patch events\n * for every mutation (update / insert / remove / move / replace). When such\n * a signal is bound to `each(...)` inside a `mount()`, the keyed list\n * reconciler applies just the patches against the live DOM — no per-item\n * iteration, no `classifyItems` Map build, no LIS pass over unchanged rows.\n *\n * const rows = arraySignal<Row>([]);\n *\n * rows.update(42, (r) => ({ ...r, label: 'changed' })); // 1 update event\n * rows.insert(0, { id: 'x', ... }); // 1 insert event\n * rows.remove(7); // 1 remove event\n * rows.move(3, 0); // 1 move event\n * rows.replace([...]); // falls back to snapshot reconcile\n *\n * Read-side semantics match a regular signal: `arraySig.value` is a\n * snapshot, and reads inside `effect()` / `computed()` register as\n * dependencies, so derived values keep working.\n */\n\nimport { bumpItemVersion } from './item-version.js';\nimport type { Signal } from './reactive.js';\nimport { signal } from './reactive.js';\n\n/** A single granular mutation event. */\nexport type ArrayPatch<T> =\n | { type: 'update'; index: number; item: T }\n | { type: 'insert'; index: number; item: T }\n | { type: 'remove'; index: number }\n | { type: 'move'; from: number; to: number }\n | { type: 'replace'; items: readonly T[] };\n\n/**\n * Cross-bundle brand for `ArraySignal` instances. `each()` and the\n * granular reconciler check for this brand instead of `instanceof\n * ArraySignal`, so the main `kerfjs` barrel can detect arraySignal\n * inputs without importing the class at runtime — the class lives\n * only in the `kerfjs/array-signal` subpath, so apps that don't need\n * granular collections shed ~1 KB.\n *\n * Same `Symbol.for(...)`-based pattern as `SafeHtml` (KF-14): cross-\n * bundle-safe, zero-cost runtime check.\n */\nexport const ARRAY_SIGNAL_BRAND = Symbol.for('kerfjs.ArraySignal');\n\nexport class ArraySignal<T> {\n private _items: T[];\n private _version: Signal<number>;\n private _patches: ArrayPatch<T>[];\n // Branded so `isArraySignal()` recognizes instances from any copy of this module.\n readonly [ARRAY_SIGNAL_BRAND] = true as const;\n\n constructor(initial: readonly T[] = []) {\n this._items = [...initial];\n this._version = signal(0);\n this._patches = [];\n }\n\n /** Read-only snapshot. Reads inside an effect/computed register a dependency. */\n get value(): readonly T[] {\n // Touch the version signal so signals-core treats reads as tracked.\n void this._version.value;\n return this._items;\n }\n\n /**\n * Replace the item at `index` with `fn(currentItem)`. Emits one `update`\n * patch. Both styles work: returning a fresh object (idiomatic) invalidates\n * the row by identity, and mutating `item` in place and returning it works\n * too — a per-item content version (KF-418) makes the same-ref change visible\n * to every consumer's row memo.\n */\n update(index: number, fn: (item: T) => T): void {\n if (index < 0 || index >= this._items.length) {\n throw new Error(\n `arraySignal.update: index ${index} out of bounds [0, ${this._items.length}).`,\n );\n }\n const next = fn(this._items[index]);\n this._items[index] = next;\n this._patches.push({ type: 'update', index, item: next });\n // KF-418: a same-ref update (fn mutates and returns the same object) is\n // invisible to the row memo, which is keyed on object identity. Bump the\n // item's content version so every consumer — this list, another list over\n // this signal, a second mount, a plain-array filter() view — re-renders it.\n // Non-object items (an arraySignal<number> used as a plain signal) are\n // skipped by bumpItemVersion — they can't be each() rows (KF-419).\n bumpItemVersion(next);\n this._version.value++;\n }\n\n /** Insert `item` at `index`. Existing items at index..N shift right. Emits one `insert` patch. */\n insert(index: number, item: T): void {\n if (index < 0 || index > this._items.length) {\n throw new Error(\n `arraySignal.insert: index ${index} out of bounds [0, ${this._items.length}].`,\n );\n }\n this._items.splice(index, 0, item);\n this._patches.push({ type: 'insert', index, item });\n this._version.value++;\n }\n\n /** Append `item` at the end. Sugar for `insert(items.length, item)`. */\n push(item: T): void {\n this.insert(this._items.length, item);\n }\n\n /** Remove and return the item at `index`. Emits one `remove` patch. */\n remove(index: number): T {\n if (index < 0 || index >= this._items.length) {\n throw new Error(\n `arraySignal.remove: index ${index} out of bounds [0, ${this._items.length}).`,\n );\n }\n const [removed] = this._items.splice(index, 1);\n this._patches.push({ type: 'remove', index });\n this._version.value++;\n return removed;\n }\n\n /** Move the item at `from` to position `to`. Emits one `move` patch (no-op when from === to). */\n move(from: number, to: number): void {\n if (from === to) return;\n if (from < 0 || from >= this._items.length || to < 0 || to >= this._items.length) {\n throw new Error(\n `arraySignal.move: indices out of bounds (from=${from}, to=${to}, length=${this._items.length}).`,\n );\n }\n const [item] = this._items.splice(from, 1);\n this._items.splice(to, 0, item);\n this._patches.push({ type: 'move', from, to });\n this._version.value++;\n }\n\n /** Replace every item. Emits one `replace` patch — the granular reconciler falls back to a full keyed diff for this case. */\n replace(items: readonly T[]): void {\n this._items = [...items];\n this._patches.push({ type: 'replace', items: this._items });\n this._version.value++;\n }\n\n /**\n * @internal Used by `each()` when binding this signal to a list. Returns\n * the queue of granular patches issued since the previous call, then\n * clears the queue. Best paired with a single binding — a second consumer\n * in the same render gets an empty array (which forces the snapshot\n * fall-back path, which is correct but slower).\n */\n _consumePatches(): ArrayPatch<T>[] {\n const out = this._patches;\n this._patches = [];\n return out;\n }\n}\n\n/** Construct an array signal seeded with `initial`. */\nexport function arraySignal<T>(initial: readonly T[] = []): ArraySignal<T> {\n return new ArraySignal(initial);\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/item-version.ts"],"names":[],"mappings":";AA8BA,IAAM,QAAA,uBAAe,OAAA,EAAwB;AAC7C,IAAI,YAAA,GAAe,KAAA;AAWZ,SAAS,gBAAgB,IAAA,EAAqB;AACnD,EAAA,IAAI,SAAS,IAAA,IAAS,OAAO,SAAS,QAAA,IAAY,OAAO,SAAS,UAAA,EAAa;AAC/E,EAAA,YAAA,GAAe,IAAA;AACf,EAAA,QAAA,CAAS,IAAI,IAAA,EAAA,CAAO,QAAA,CAAS,IAAI,IAAI,CAAA,IAAK,KAAK,CAAC,CAAA;AAClD;AAGO,SAAS,YAAY,IAAA,EAAsB;AAChD,EAAA,OAAO,YAAA,GAAe,QAAA,CAAS,GAAA,CAAI,IAAI,KAAK,CAAA,GAAI,CAAA;AAClD","file":"chunk-QIP723L4.js","sourcesContent":["/**\n * Per-item content version — makes a same-ref `arraySignal.update()` visible to\n * the row memo (KF-418).\n *\n * kerf memoizes `each()` rows by object IDENTITY (`WeakMap<item, CacheEntry>`),\n * so a same-ref mutation — `update(i, r => { r.x = 1; return r })` — changes the\n * row's content without changing its identity and is invisible to the memo. The\n * KF-414 fix repaired only the single list that drained the patch; every other\n * consumer (a second list over the same signal, a second `mount()`, a\n * plain-array `filter()` view of the same items) still trusted its own memo\n * entry, keyed on an identity that didn't change, and rendered stale forever.\n *\n * `arraySignal.update()` bumps the returned item's version here; each row's\n * `CacheEntry` records the version it rendered at, and a cache hit now requires\n * the version to still match. A same-ref update therefore re-renders the row in\n * EVERY consumer, because they all read the same version off the same item ref.\n *\n * Why a shared module rather than a field on `ArraySignal`:\n * - the main-bundle `each()` must read the version, and importing the\n * `kerfjs/array-signal` subpath into the main bundle would defeat KF-95\n * (arraySignal only ships when the consumer imports it). This module lives in\n * the main bundle; the subpath imports it, not the other way round.\n * - keying on the item object (not the signal) is what lets a plain-array\n * `filter()` view — which has no reference to the arraySignal — still see the\n * bump: it holds the same item refs.\n *\n * `anyVersioned` keeps the common case free: until some `update()` actually\n * bumps a version, every lookup returns 0 without touching the WeakMap, so a app\n * that never mutates in place pays nothing per row.\n */\nconst versions = new WeakMap<object, number>();\nlet anyVersioned = false;\n\n/**\n * Bump `item`'s content version. Called by `arraySignal.update()` on the item it\n * returns. A no-op for any value that isn't a valid WeakMap key — a primitive\n * item (an `arraySignal<number>` used purely as a standalone signal) can never be\n * an `each()` row, since rows are memoized by object identity, so it needs no\n * content version. Guarding here keeps `update()` from throwing `Invalid value\n * used as weak map key` and leaves `anyVersioned` false until a real object is\n * versioned (so the primitive path stays a zero-cost lookup).\n */\nexport function bumpItemVersion(item: unknown): void {\n if (item === null || (typeof item !== 'object' && typeof item !== 'function')) return;\n anyVersioned = true;\n versions.set(item, (versions.get(item) ?? 0) + 1);\n}\n\n/** `item`'s current content version — 0 if it has never been same-ref-updated. */\nexport function itemVersion(item: object): number {\n return anyVersioned ? versions.get(item) ?? 0 : 0;\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/store.ts"],"names":[],"mappings":";;;;AAsCA,IAAM,WAAyC,EAAC;AAEzC,SAAS,YACd,IAAA,EACyB;AACzB,EAAA,MAAM,QAAA,GAA2B,MAAA,CAAO,IAAA,CAAK,OAAA,EAAS,CAAA;AAKtD,EAAA,MAAM,UAAkC,QAAA,CAAS,SAAA,GAAY,EAAE,MAAA,EAAQ,OAAM,GAAI,IAAA;AAEjF,EAAA,MAAM,GAAA,GAAM,CAAC,IAAA,KAAuB;AAMlC,IAAA,MAAM,QAAQ,QAAA,CAAS,UAAA;AACvB,IAAA,MAAM,GAAA,GAAM,KAAA,GAAQ,KAAA,CAAM,IAAI,CAAA,GAAI,IAAA;AAClC,IAAA,IAAI,SAAS,QAAA,CAAS,SAAA,GAAY,QAAA,CAAS,KAAA,EAAO,KAAK,OAAO,CAAA;AAC9D,IAAA,QAAA,CAAS,KAAA,GAAQ,GAAA;AAAA,EACnB,CAAA;AAOA,EAAA,MAAM,MAAM,MAAwB;AAClC,IAAA,MAAM,IAAI,QAAA,CAAS,KAAA;AACnB,IAAA,MAAM,WAAW,QAAA,CAAS,aAAA;AAC1B,IAAA,IAAI,QAAA,IAAY,CAAA,KAAM,IAAA,IAAQ,OAAO,MAAM,QAAA,EAAU;AACnD,MAAA,OAAO,SAAS,CAAoB,CAAA;AAAA,IACtC;AACA,IAAA,OAAO,CAAA;AAAA,EACT,CAAA;AAEA,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,OAAA,CAAQ,GAAA,EAAK,GAAG,CAAA;AAErC,EAAA,MAAM,KAAA,GAAiC;AAAA,IACrC,KAAA,EAAO,QAAA;AAAA,IACP,OAAA;AAAA,IACA,KAAA,GAAQ;AACN,MAAA,QAAA,CAAS,KAAA,GAAQ,KAAK,OAAA,EAAQ;AAAA,IAChC;AAAA,GACF;AAEA,EAAA,QAAA,CAAS,KAAK,KAAK,CAAA;AACnB,EAAA,OAAO,KAAA;AACT;AAOO,SAAS,cAAA,GAAuB;AACrC,EAAA,KAAA,MAAW,CAAA,IAAK,QAAA,EAAU,CAAA,CAAE,KAAA,EAAM;AACpC;AAMO,SAAS,kBAAA,GAA2B;AACzC,EAAA,QAAA,CAAS,MAAA,GAAS,CAAA;AACpB","file":"chunk-SAYPJ6XR.js","sourcesContent":["/**\n * `defineStore({ initial, actions })` — composable testable stores layered on\n * top of `reactive.ts`'s signals.\n *\n * Three rules:\n * 1. `state` is read-only. Consumers read via `state.value` or subscribe via\n * `effect()`. They cannot write directly.\n * 2. `actions` is the only mutation surface. All writes go through named\n * action functions. This is what makes stores testable — assert against\n * actions, not against arbitrary writes.\n * 3. `reset()` resets to `initial()`. Always defined; tests use it for\n * setup, lifecycle hooks (route change, sign-out, etc.) use it for\n * tear-down.\n *\n * A module-level registry tracks every store created via `defineStore()`;\n * `resetAllStores()` walks the registry and calls each `reset()`. Useful for\n * tests + project-switch / logout / route-reset scenarios where every piece\n * of client state should return to its initial shape.\n */\n\nimport { devHooks, type WarnOnceContext } from './dev-hooks.js';\nimport type { ReadonlySignal, Signal } from './reactive.js';\nimport { signal } from './reactive.js';\n\nexport interface Store<TState, TActions> {\n /** Read-only reactive view. Consumers read `state.value` or subscribe via `effect()`. */\n readonly state: ReadonlySignal<TState>;\n /** Named mutators — the only way to change state. */\n readonly actions: TActions;\n /** Reset state to `initial()`. Used by tests and lifecycle hooks. */\n reset(): void;\n}\n\ninterface DefineStoreSpec<TState, TActions> {\n initial: () => TState;\n actions: (set: (next: TState) => void, get: () => Readonly<TState>) => TActions;\n}\n\nconst REGISTRY: Array<{ reset: () => void }> = [];\n\nexport function defineStore<TState, TActions>(\n spec: DefineStoreSpec<TState, TActions>,\n): Store<TState, TActions> {\n const internal: Signal<TState> = signal(spec.initial());\n // KF-212: per-store one-shot dedup for the opt-in narrow-set warning.\n // Default off; consumers opt in via `KERF_DEV_WARN_NARROW_SET=1` in dev.\n // Allocated only when the diagnostics are installed — production never pays\n // for an object it will never read.\n const warnCtx: WarnOnceContext | null = devHooks.narrowSet ? { warned: false } : null;\n\n const set = (next: TState): void => {\n // With `kerfjs/dev` installed, `next` may carry proxies handed back by the\n // `get()` trap (e.g. `set({ ...get(), count: 1 })`). Unwrap them so the\n // internal signal only ever holds a plain object — the narrow-set warning\n // and every consumer read see raw state, never a Proxy. Production stores\n // the bare reference.\n const toRaw = devHooks.storeToRaw;\n const raw = toRaw ? toRaw(next) : next;\n if (warnCtx) devHooks.narrowSet?.(internal.value, raw, warnCtx);\n internal.value = raw;\n };\n // With the diagnostics installed, wrap the reference returned to actions in a\n // deep read-only Proxy so that `get().count = 42` / `get().nested.x = 1`\n // (documented Rule 8 violations) throw a `TypeError` instead of silently\n // landing on the underlying state without notifying subscribers. The live\n // state object is never frozen or mutated, so external references to it stay\n // writable. Production returns the bare reference — no proxy, no wrapping.\n const get = (): Readonly<TState> => {\n const v = internal.value;\n const readonly = devHooks.storeReadonly;\n if (readonly && v !== null && typeof v === 'object') {\n return readonly(v as TState & object);\n }\n return v;\n };\n\n const actions = spec.actions(set, get);\n\n const store: Store<TState, TActions> = {\n state: internal,\n actions,\n reset() {\n internal.value = spec.initial();\n },\n };\n\n REGISTRY.push(store);\n return store;\n}\n\n/**\n * Reset every store registered via `defineStore()` to its `initial()` value.\n * Used by tests and by application lifecycle hooks (project switch, logout,\n * route reset).\n */\nexport function resetAllStores(): void {\n for (const s of REGISTRY) s.reset();\n}\n\n/**\n * Test helper — clears the registry. Exposed via the `kerfjs/testing` subpath,\n * not the main `kerfjs` entry. Unit tests use it to isolate stores between cases.\n */\nexport function clearStoreRegistry(): void {\n REGISTRY.length = 0;\n}\n"]}