hzl-core 0.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 (177) hide show
  1. package/dist/__tests__/backup/backup-restore.test.d.ts +2 -0
  2. package/dist/__tests__/backup/backup-restore.test.d.ts.map +1 -0
  3. package/dist/__tests__/backup/backup-restore.test.js +200 -0
  4. package/dist/__tests__/backup/backup-restore.test.js.map +1 -0
  5. package/dist/__tests__/backup/import-export.test.d.ts +2 -0
  6. package/dist/__tests__/backup/import-export.test.d.ts.map +1 -0
  7. package/dist/__tests__/backup/import-export.test.js +341 -0
  8. package/dist/__tests__/backup/import-export.test.js.map +1 -0
  9. package/dist/__tests__/concurrency/stress.test.d.ts +2 -0
  10. package/dist/__tests__/concurrency/stress.test.d.ts.map +1 -0
  11. package/dist/__tests__/concurrency/stress.test.js +274 -0
  12. package/dist/__tests__/concurrency/stress.test.js.map +1 -0
  13. package/dist/__tests__/concurrency/worker.d.ts +2 -0
  14. package/dist/__tests__/concurrency/worker.d.ts.map +1 -0
  15. package/dist/__tests__/concurrency/worker.js +84 -0
  16. package/dist/__tests__/concurrency/worker.js.map +1 -0
  17. package/dist/__tests__/migrations/upgrade.test.d.ts +2 -0
  18. package/dist/__tests__/migrations/upgrade.test.d.ts.map +1 -0
  19. package/dist/__tests__/migrations/upgrade.test.js +203 -0
  20. package/dist/__tests__/migrations/upgrade.test.js.map +1 -0
  21. package/dist/__tests__/projections/rebuild-equivalence.test.d.ts +2 -0
  22. package/dist/__tests__/projections/rebuild-equivalence.test.d.ts.map +1 -0
  23. package/dist/__tests__/projections/rebuild-equivalence.test.js +276 -0
  24. package/dist/__tests__/projections/rebuild-equivalence.test.js.map +1 -0
  25. package/dist/__tests__/properties/invariants.test.d.ts +2 -0
  26. package/dist/__tests__/properties/invariants.test.d.ts.map +1 -0
  27. package/dist/__tests__/properties/invariants.test.js +314 -0
  28. package/dist/__tests__/properties/invariants.test.js.map +1 -0
  29. package/dist/db/connection.d.ts +13 -0
  30. package/dist/db/connection.d.ts.map +1 -0
  31. package/dist/db/connection.js +52 -0
  32. package/dist/db/connection.js.map +1 -0
  33. package/dist/db/connection.test.d.ts +2 -0
  34. package/dist/db/connection.test.d.ts.map +1 -0
  35. package/dist/db/connection.test.js +63 -0
  36. package/dist/db/connection.test.js.map +1 -0
  37. package/dist/db/migrations/v2.d.ts +2 -0
  38. package/dist/db/migrations/v2.d.ts.map +1 -0
  39. package/dist/db/migrations/v2.js +4 -0
  40. package/dist/db/migrations/v2.js.map +1 -0
  41. package/dist/db/migrations.d.ts +4 -0
  42. package/dist/db/migrations.d.ts.map +1 -0
  43. package/dist/db/migrations.js +45 -0
  44. package/dist/db/migrations.js.map +1 -0
  45. package/dist/db/migrations.test.d.ts +2 -0
  46. package/dist/db/migrations.test.d.ts.map +1 -0
  47. package/dist/db/migrations.test.js +75 -0
  48. package/dist/db/migrations.test.js.map +1 -0
  49. package/dist/db/schema.d.ts +3 -0
  50. package/dist/db/schema.d.ts.map +1 -0
  51. package/dist/db/schema.js +114 -0
  52. package/dist/db/schema.js.map +1 -0
  53. package/dist/events/store.d.ts +33 -0
  54. package/dist/events/store.d.ts.map +1 -0
  55. package/dist/events/store.js +81 -0
  56. package/dist/events/store.js.map +1 -0
  57. package/dist/events/store.test.d.ts +2 -0
  58. package/dist/events/store.test.d.ts.map +1 -0
  59. package/dist/events/store.test.js +138 -0
  60. package/dist/events/store.test.js.map +1 -0
  61. package/dist/events/types.d.ts +106 -0
  62. package/dist/events/types.d.ts.map +1 -0
  63. package/dist/events/types.js +87 -0
  64. package/dist/events/types.js.map +1 -0
  65. package/dist/events/validation.test.d.ts +2 -0
  66. package/dist/events/validation.test.d.ts.map +1 -0
  67. package/dist/events/validation.test.js +83 -0
  68. package/dist/events/validation.test.js.map +1 -0
  69. package/dist/fixtures/sample-data.d.ts +16 -0
  70. package/dist/fixtures/sample-data.d.ts.map +1 -0
  71. package/dist/fixtures/sample-data.js +148 -0
  72. package/dist/fixtures/sample-data.js.map +1 -0
  73. package/dist/index.d.ts +27 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +44 -0
  76. package/dist/index.js.map +1 -0
  77. package/dist/index.test.d.ts +2 -0
  78. package/dist/index.test.d.ts.map +1 -0
  79. package/dist/index.test.js +102 -0
  80. package/dist/index.test.js.map +1 -0
  81. package/dist/projections/comments-checkpoints.d.ts +11 -0
  82. package/dist/projections/comments-checkpoints.d.ts.map +1 -0
  83. package/dist/projections/comments-checkpoints.js +33 -0
  84. package/dist/projections/comments-checkpoints.js.map +1 -0
  85. package/dist/projections/comments-checkpoints.test.d.ts +2 -0
  86. package/dist/projections/comments-checkpoints.test.d.ts.map +1 -0
  87. package/dist/projections/comments-checkpoints.test.js +72 -0
  88. package/dist/projections/comments-checkpoints.test.js.map +1 -0
  89. package/dist/projections/dependencies.d.ts +12 -0
  90. package/dist/projections/dependencies.d.ts.map +1 -0
  91. package/dist/projections/dependencies.js +39 -0
  92. package/dist/projections/dependencies.js.map +1 -0
  93. package/dist/projections/dependencies.test.d.ts +2 -0
  94. package/dist/projections/dependencies.test.d.ts.map +1 -0
  95. package/dist/projections/dependencies.test.js +97 -0
  96. package/dist/projections/dependencies.test.js.map +1 -0
  97. package/dist/projections/engine.d.ts +18 -0
  98. package/dist/projections/engine.d.ts.map +1 -0
  99. package/dist/projections/engine.js +56 -0
  100. package/dist/projections/engine.js.map +1 -0
  101. package/dist/projections/engine.test.d.ts +2 -0
  102. package/dist/projections/engine.test.d.ts.map +1 -0
  103. package/dist/projections/engine.test.js +92 -0
  104. package/dist/projections/engine.test.js.map +1 -0
  105. package/dist/projections/rebuild.d.ts +4 -0
  106. package/dist/projections/rebuild.d.ts.map +1 -0
  107. package/dist/projections/rebuild.js +26 -0
  108. package/dist/projections/rebuild.js.map +1 -0
  109. package/dist/projections/rebuild.test.d.ts +2 -0
  110. package/dist/projections/rebuild.test.d.ts.map +1 -0
  111. package/dist/projections/rebuild.test.js +59 -0
  112. package/dist/projections/rebuild.test.js.map +1 -0
  113. package/dist/projections/search.d.ts +11 -0
  114. package/dist/projections/search.d.ts.map +1 -0
  115. package/dist/projections/search.js +39 -0
  116. package/dist/projections/search.js.map +1 -0
  117. package/dist/projections/search.test.d.ts +2 -0
  118. package/dist/projections/search.test.d.ts.map +1 -0
  119. package/dist/projections/search.test.js +78 -0
  120. package/dist/projections/search.test.js.map +1 -0
  121. package/dist/projections/tags.d.ts +12 -0
  122. package/dist/projections/tags.d.ts.map +1 -0
  123. package/dist/projections/tags.js +41 -0
  124. package/dist/projections/tags.js.map +1 -0
  125. package/dist/projections/tags.test.d.ts +2 -0
  126. package/dist/projections/tags.test.d.ts.map +1 -0
  127. package/dist/projections/tags.test.js +69 -0
  128. package/dist/projections/tags.test.js.map +1 -0
  129. package/dist/projections/tasks-current.d.ts +14 -0
  130. package/dist/projections/tasks-current.d.ts.map +1 -0
  131. package/dist/projections/tasks-current.js +110 -0
  132. package/dist/projections/tasks-current.js.map +1 -0
  133. package/dist/projections/tasks-current.test.d.ts +2 -0
  134. package/dist/projections/tasks-current.test.d.ts.map +1 -0
  135. package/dist/projections/tasks-current.test.js +215 -0
  136. package/dist/projections/tasks-current.test.js.map +1 -0
  137. package/dist/projections/types.d.ts +13 -0
  138. package/dist/projections/types.d.ts.map +1 -0
  139. package/dist/projections/types.js +2 -0
  140. package/dist/projections/types.js.map +1 -0
  141. package/dist/services/backup-service.d.ts +16 -0
  142. package/dist/services/backup-service.d.ts.map +1 -0
  143. package/dist/services/backup-service.js +114 -0
  144. package/dist/services/backup-service.js.map +1 -0
  145. package/dist/services/search-service.d.ts +27 -0
  146. package/dist/services/search-service.d.ts.map +1 -0
  147. package/dist/services/search-service.js +33 -0
  148. package/dist/services/search-service.js.map +1 -0
  149. package/dist/services/search-service.test.d.ts +2 -0
  150. package/dist/services/search-service.test.d.ts.map +1 -0
  151. package/dist/services/search-service.test.js +66 -0
  152. package/dist/services/search-service.test.js.map +1 -0
  153. package/dist/services/task-service.d.ts +147 -0
  154. package/dist/services/task-service.d.ts.map +1 -0
  155. package/dist/services/task-service.js +442 -0
  156. package/dist/services/task-service.js.map +1 -0
  157. package/dist/services/task-service.test.d.ts +2 -0
  158. package/dist/services/task-service.test.d.ts.map +1 -0
  159. package/dist/services/task-service.test.js +399 -0
  160. package/dist/services/task-service.test.js.map +1 -0
  161. package/dist/services/validation-service.d.ts +29 -0
  162. package/dist/services/validation-service.d.ts.map +1 -0
  163. package/dist/services/validation-service.js +74 -0
  164. package/dist/services/validation-service.js.map +1 -0
  165. package/dist/services/validation-service.test.d.ts +2 -0
  166. package/dist/services/validation-service.test.d.ts.map +1 -0
  167. package/dist/services/validation-service.test.js +67 -0
  168. package/dist/services/validation-service.test.js.map +1 -0
  169. package/dist/utils/id.d.ts +3 -0
  170. package/dist/utils/id.d.ts.map +1 -0
  171. package/dist/utils/id.js +12 -0
  172. package/dist/utils/id.js.map +1 -0
  173. package/dist/utils/id.test.d.ts +2 -0
  174. package/dist/utils/id.test.d.ts.map +1 -0
  175. package/dist/utils/id.test.js +24 -0
  176. package/dist/utils/id.test.js.map +1 -0
  177. package/package.json +83 -0
@@ -0,0 +1,92 @@
1
+ // packages/hzl-core/src/projections/engine.test.ts
2
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
3
+ import Database from 'better-sqlite3';
4
+ import { ProjectionEngine } from './engine.js';
5
+ import { runMigrations } from '../db/migrations.js';
6
+ import { EventStore } from '../events/store.js';
7
+ import { EventType } from '../events/types.js';
8
+ describe('ProjectionEngine', () => {
9
+ let db;
10
+ let eventStore;
11
+ let engine;
12
+ beforeEach(() => {
13
+ db = new Database(':memory:');
14
+ runMigrations(db);
15
+ eventStore = new EventStore(db);
16
+ });
17
+ afterEach(() => {
18
+ db.close();
19
+ });
20
+ it('registers and applies projectors', () => {
21
+ const applied = [];
22
+ const testProjector = {
23
+ name: 'test',
24
+ apply: (event) => { applied.push(event); },
25
+ };
26
+ engine = new ProjectionEngine(db);
27
+ engine.register(testProjector);
28
+ const event = eventStore.append({
29
+ task_id: 'TASK1',
30
+ type: EventType.TaskCreated,
31
+ data: { title: 'Test', project: 'inbox' },
32
+ });
33
+ engine.applyEvent(event);
34
+ expect(applied).toHaveLength(1);
35
+ expect(applied[0].event_id).toBe(event.event_id);
36
+ });
37
+ it('tracks projection state', () => {
38
+ const testProjector = {
39
+ name: 'test_state',
40
+ apply: () => { },
41
+ };
42
+ engine = new ProjectionEngine(db);
43
+ engine.register(testProjector);
44
+ const event = eventStore.append({
45
+ task_id: 'TASK1',
46
+ type: EventType.TaskCreated,
47
+ data: { title: 'Test', project: 'inbox' },
48
+ });
49
+ engine.applyEvent(event);
50
+ engine.updateProjectionState('test_state', event.rowid);
51
+ const state = engine.getProjectionState('test_state');
52
+ expect(state?.last_event_id).toBe(event.rowid);
53
+ });
54
+ it('applies multiple projectors in registration order', () => {
55
+ const order = [];
56
+ const projector1 = {
57
+ name: 'first',
58
+ apply: () => { order.push('first'); },
59
+ };
60
+ const projector2 = {
61
+ name: 'second',
62
+ apply: () => { order.push('second'); },
63
+ };
64
+ engine = new ProjectionEngine(db);
65
+ engine.register(projector1);
66
+ engine.register(projector2);
67
+ const event = eventStore.append({
68
+ task_id: 'TASK1',
69
+ type: EventType.TaskCreated,
70
+ data: { title: 'Test', project: 'inbox' },
71
+ });
72
+ engine.applyEvent(event);
73
+ expect(order).toEqual(['first', 'second']);
74
+ });
75
+ it('getEventsSince returns events after given id', () => {
76
+ engine = new ProjectionEngine(db);
77
+ const e1 = eventStore.append({
78
+ task_id: 'TASK1',
79
+ type: EventType.TaskCreated,
80
+ data: { title: 'Test 1', project: 'inbox' },
81
+ });
82
+ const e2 = eventStore.append({
83
+ task_id: 'TASK2',
84
+ type: EventType.TaskCreated,
85
+ data: { title: 'Test 2', project: 'inbox' },
86
+ });
87
+ const events = engine.getEventsSince(e1.rowid, 100);
88
+ expect(events).toHaveLength(1);
89
+ expect(events[0].event_id).toBe(e2.event_id);
90
+ });
91
+ });
92
+ //# sourceMappingURL=engine.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.test.js","sourceRoot":"","sources":["../../src/projections/engine.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { ProjectionEngine } from './engine.js';
3
+ export declare function rebuildAllProjections(db: Database.Database, engine: ProjectionEngine): void;
4
+ //# sourceMappingURL=rebuild.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rebuild.d.ts","sourceRoot":"","sources":["../../src/projections/rebuild.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,MAAM,EAAE,gBAAgB,GACvB,IAAI,CA2BN"}
@@ -0,0 +1,26 @@
1
+ const BATCH_SIZE = 1000;
2
+ export function rebuildAllProjections(db, engine) {
3
+ const projectors = engine.getProjectors();
4
+ for (const projector of projectors) {
5
+ if (projector.reset) {
6
+ projector.reset(db);
7
+ }
8
+ }
9
+ db.exec('DELETE FROM projection_state');
10
+ let lastId = 0;
11
+ while (true) {
12
+ const events = engine.getEventsSince(lastId, BATCH_SIZE);
13
+ if (events.length === 0)
14
+ break;
15
+ for (const event of events) {
16
+ engine.applyEvent(event);
17
+ lastId = event.rowid;
18
+ }
19
+ }
20
+ for (const projector of projectors) {
21
+ if (lastId > 0) {
22
+ engine.updateProjectionState(projector.name, lastId);
23
+ }
24
+ }
25
+ }
26
+ //# sourceMappingURL=rebuild.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rebuild.js","sourceRoot":"","sources":["../../src/projections/rebuild.ts"],"names":[],"mappings":"AAIA,MAAM,UAAU,GAAG,IAAI,CAAC;AAExB,MAAM,UAAU,qBAAqB,CACnC,EAAqB,EACrB,MAAwB;IAExB,MAAM,UAAU,GAAG,MAAM,CAAC,aAAa,EAAE,CAAC;IAE1C,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,SAAS,CAAC,KAAK,EAAE,CAAC;YACpB,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IAED,EAAE,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;IAExC,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QACzD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM;QAE/B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;YACzB,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC;QACvB,CAAC;IACH,CAAC;IAED,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;YACf,MAAM,CAAC,qBAAqB,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=rebuild.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rebuild.test.d.ts","sourceRoot":"","sources":["../../src/projections/rebuild.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,59 @@
1
+ // packages/hzl-core/src/projections/rebuild.test.ts
2
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
3
+ import Database from 'better-sqlite3';
4
+ import { rebuildAllProjections } from './rebuild.js';
5
+ import { ProjectionEngine } from './engine.js';
6
+ import { TasksCurrentProjector } from './tasks-current.js';
7
+ import { DependenciesProjector } from './dependencies.js';
8
+ import { TagsProjector } from './tags.js';
9
+ import { CommentsCheckpointsProjector } from './comments-checkpoints.js';
10
+ import { SearchProjector } from './search.js';
11
+ import { runMigrations } from '../db/migrations.js';
12
+ import { EventStore } from '../events/store.js';
13
+ import { EventType, TaskStatus } from '../events/types.js';
14
+ describe('rebuildAllProjections', () => {
15
+ let db;
16
+ let eventStore;
17
+ let engine;
18
+ beforeEach(() => {
19
+ db = new Database(':memory:');
20
+ runMigrations(db);
21
+ eventStore = new EventStore(db);
22
+ engine = new ProjectionEngine(db);
23
+ engine.register(new TasksCurrentProjector());
24
+ engine.register(new DependenciesProjector());
25
+ engine.register(new TagsProjector());
26
+ engine.register(new CommentsCheckpointsProjector());
27
+ engine.register(new SearchProjector());
28
+ });
29
+ afterEach(() => {
30
+ db.close();
31
+ });
32
+ it('rebuilds all projections from events', () => {
33
+ const e1 = eventStore.append({
34
+ task_id: 'TASK1',
35
+ type: EventType.TaskCreated,
36
+ data: { title: 'Test', project: 'inbox', tags: ['tag1'], depends_on: ['DEP1'] },
37
+ });
38
+ engine.applyEvent(e1);
39
+ const e2 = eventStore.append({
40
+ task_id: 'TASK1',
41
+ type: EventType.StatusChanged,
42
+ data: { from: TaskStatus.Backlog, to: TaskStatus.Ready },
43
+ });
44
+ engine.applyEvent(e2);
45
+ // Manually corrupt projections
46
+ db.exec('DELETE FROM tasks_current');
47
+ db.exec('DELETE FROM task_tags');
48
+ db.exec('DELETE FROM task_dependencies');
49
+ // Rebuild
50
+ rebuildAllProjections(db, engine);
51
+ // Verify restoration
52
+ const task = db.prepare('SELECT * FROM tasks_current WHERE task_id = ?').get('TASK1');
53
+ expect(task.title).toBe('Test');
54
+ expect(task.status).toBe('ready');
55
+ const tags = db.prepare('SELECT tag FROM task_tags WHERE task_id = ?').all('TASK1');
56
+ expect(tags.map(t => t.tag)).toContain('tag1');
57
+ });
58
+ });
59
+ //# sourceMappingURL=rebuild.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rebuild.test.js","sourceRoot":"","sources":["../../src/projections/rebuild.test.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACrE,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAC;AACzE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAE3D,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,IAAI,EAAqB,CAAC;IAC1B,IAAI,UAAsB,CAAC;IAC3B,IAAI,MAAwB,CAAC;IAE7B,UAAU,CAAC,GAAG,EAAE;QACd,EAAE,GAAG,IAAI,QAAQ,CAAC,UAAU,CAAC,CAAC;QAC9B,aAAa,CAAC,EAAE,CAAC,CAAC;QAClB,UAAU,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;QAChC,MAAM,GAAG,IAAI,gBAAgB,CAAC,EAAE,CAAC,CAAC;QAClC,MAAM,CAAC,QAAQ,CAAC,IAAI,qBAAqB,EAAE,CAAC,CAAC;QAC7C,MAAM,CAAC,QAAQ,CAAC,IAAI,qBAAqB,EAAE,CAAC,CAAC;QAC7C,MAAM,CAAC,QAAQ,CAAC,IAAI,aAAa,EAAE,CAAC,CAAC;QACrC,MAAM,CAAC,QAAQ,CAAC,IAAI,4BAA4B,EAAE,CAAC,CAAC;QACpD,MAAM,CAAC,QAAQ,CAAC,IAAI,eAAe,EAAE,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,EAAE,CAAC,KAAK,EAAE,CAAC;IACb,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC;YAC3B,OAAO,EAAE,OAAO;YAChB,IAAI,EAAE,SAAS,CAAC,WAAW;YAC3B,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC,MAAM,CAAC,EAAE;SAChF,CAAC,CAAC;QACH,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAEtB,MAAM,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC;YAC3B,OAAO,EAAE,OAAO;YAChB,IAAI,EAAE,SAAS,CAAC,aAAa;YAC7B,IAAI,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,OAAO,EAAE,EAAE,EAAE,UAAU,CAAC,KAAK,EAAE;SACzD,CAAC,CAAC;QACH,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAEtB,+BAA+B;QAC/B,EAAE,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;QACrC,EAAE,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;QACjC,EAAE,CAAC,IAAI,CAAC,+BAA+B,CAAC,CAAC;QAEzC,UAAU;QACV,qBAAqB,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QAElC,qBAAqB;QACrB,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,+CAA+C,CAAC,CAAC,GAAG,CAAC,OAAO,CAAQ,CAAC;QAC7F,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAElC,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,6CAA6C,CAAC,CAAC,GAAG,CAAC,OAAO,CAAU,CAAC;QAC7F,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { PersistedEventEnvelope } from '../events/store.js';
3
+ import type { Projector } from './types.js';
4
+ export declare class SearchProjector implements Projector {
5
+ name: string;
6
+ apply(event: PersistedEventEnvelope, db: Database.Database): void;
7
+ reset(db: Database.Database): void;
8
+ private handleTaskCreated;
9
+ private handleTaskUpdated;
10
+ }
11
+ //# sourceMappingURL=search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/projections/search.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAK5C,qBAAa,eAAgB,YAAW,SAAS;IAC/C,IAAI,SAAY;IAEhB,KAAK,CAAC,KAAK,EAAE,sBAAsB,EAAE,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,IAAI;IAWjE,KAAK,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,IAAI;IAIlC,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,iBAAiB;CAgB1B"}
@@ -0,0 +1,39 @@
1
+ import { EventType } from '../events/types.js';
2
+ const SEARCHABLE_FIELDS = new Set(['title', 'description']);
3
+ export class SearchProjector {
4
+ name = 'search';
5
+ apply(event, db) {
6
+ switch (event.type) {
7
+ case EventType.TaskCreated:
8
+ this.handleTaskCreated(event, db);
9
+ break;
10
+ case EventType.TaskUpdated:
11
+ this.handleTaskUpdated(event, db);
12
+ break;
13
+ }
14
+ }
15
+ reset(db) {
16
+ db.exec('DELETE FROM task_search');
17
+ }
18
+ handleTaskCreated(event, db) {
19
+ const data = event.data;
20
+ db.prepare(`
21
+ INSERT INTO task_search (task_id, title, description)
22
+ VALUES (?, ?, ?)
23
+ `).run(event.task_id, data.title, data.description ?? '');
24
+ }
25
+ handleTaskUpdated(event, db) {
26
+ const data = event.data;
27
+ if (!SEARCHABLE_FIELDS.has(data.field))
28
+ return;
29
+ const task = db.prepare('SELECT title, description FROM tasks_current WHERE task_id = ?').get(event.task_id);
30
+ if (!task)
31
+ return;
32
+ db.prepare('DELETE FROM task_search WHERE task_id = ?').run(event.task_id);
33
+ db.prepare(`
34
+ INSERT INTO task_search (task_id, title, description)
35
+ VALUES (?, ?, ?)
36
+ `).run(event.task_id, task.title, task.description ?? '');
37
+ }
38
+ }
39
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/projections/search.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC;AAE5D,MAAM,OAAO,eAAe;IAC1B,IAAI,GAAG,QAAQ,CAAC;IAEhB,KAAK,CAAC,KAA6B,EAAE,EAAqB;QACxD,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;YACnB,KAAK,SAAS,CAAC,WAAW;gBACxB,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBAClC,MAAM;YACR,KAAK,SAAS,CAAC,WAAW;gBACxB,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBAClC,MAAM;QACV,CAAC;IACH,CAAC;IAED,KAAK,CAAC,EAAqB;QACzB,EAAE,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;IACrC,CAAC;IAEO,iBAAiB,CAAC,KAA6B,EAAE,EAAqB;QAC5E,MAAM,IAAI,GAAG,KAAK,CAAC,IAAW,CAAC;QAC/B,EAAE,CAAC,OAAO,CAAC;;;KAGV,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;IAC5D,CAAC;IAEO,iBAAiB,CAAC,KAA6B,EAAE,EAAqB;QAC5E,MAAM,IAAI,GAAG,KAAK,CAAC,IAAW,CAAC;QAC/B,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO;QAE/C,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CACrB,gEAAgE,CACjE,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAA8D,CAAC;QAElF,IAAI,CAAC,IAAI;YAAE,OAAO;QAElB,EAAE,CAAC,OAAO,CAAC,2CAA2C,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC3E,EAAE,CAAC,OAAO,CAAC;;;KAGV,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;IAC5D,CAAC;CACF"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=search.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.test.d.ts","sourceRoot":"","sources":["../../src/projections/search.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,78 @@
1
+ // packages/hzl-core/src/projections/search.test.ts
2
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
3
+ import Database from 'better-sqlite3';
4
+ import { SearchProjector } from './search.js';
5
+ import { TasksCurrentProjector } from './tasks-current.js';
6
+ import { runMigrations } from '../db/migrations.js';
7
+ import { EventStore } from '../events/store.js';
8
+ import { EventType } from '../events/types.js';
9
+ describe('SearchProjector', () => {
10
+ let db;
11
+ let eventStore;
12
+ let tasksProjector;
13
+ let searchProjector;
14
+ beforeEach(() => {
15
+ db = new Database(':memory:');
16
+ runMigrations(db);
17
+ eventStore = new EventStore(db);
18
+ tasksProjector = new TasksCurrentProjector();
19
+ searchProjector = new SearchProjector();
20
+ });
21
+ afterEach(() => {
22
+ db.close();
23
+ });
24
+ describe('task_created', () => {
25
+ it('indexes title and description', () => {
26
+ const event = eventStore.append({
27
+ task_id: 'TASK1',
28
+ type: EventType.TaskCreated,
29
+ data: {
30
+ title: 'Implement authentication',
31
+ project: 'inbox',
32
+ description: 'Add OAuth2 support',
33
+ },
34
+ });
35
+ tasksProjector.apply(event, db);
36
+ searchProjector.apply(event, db);
37
+ const results = db.prepare("SELECT task_id FROM task_search WHERE task_search MATCH 'authentication'").all();
38
+ expect(results.map(r => r.task_id)).toContain('TASK1');
39
+ });
40
+ });
41
+ describe('task_updated', () => {
42
+ it('updates index when title changes', () => {
43
+ const createEvent = eventStore.append({
44
+ task_id: 'TASK1',
45
+ type: EventType.TaskCreated,
46
+ data: { title: 'Original title', project: 'inbox' },
47
+ });
48
+ tasksProjector.apply(createEvent, db);
49
+ searchProjector.apply(createEvent, db);
50
+ const updateEvent = eventStore.append({
51
+ task_id: 'TASK1',
52
+ type: EventType.TaskUpdated,
53
+ data: { field: 'title', new_value: 'Updated title' },
54
+ });
55
+ tasksProjector.apply(updateEvent, db);
56
+ searchProjector.apply(updateEvent, db);
57
+ const oldResults = db.prepare("SELECT task_id FROM task_search WHERE task_search MATCH 'Original'").all();
58
+ expect(oldResults).toHaveLength(0);
59
+ const newResults = db.prepare("SELECT task_id FROM task_search WHERE task_search MATCH 'Updated'").all();
60
+ expect(newResults.map(r => r.task_id)).toContain('TASK1');
61
+ });
62
+ });
63
+ describe('reset', () => {
64
+ it('clears search index', () => {
65
+ const event = eventStore.append({
66
+ task_id: 'TASK1',
67
+ type: EventType.TaskCreated,
68
+ data: { title: 'Test', project: 'inbox' },
69
+ });
70
+ tasksProjector.apply(event, db);
71
+ searchProjector.apply(event, db);
72
+ searchProjector.reset(db);
73
+ const count = db.prepare('SELECT COUNT(*) as cnt FROM task_search').get();
74
+ expect(count.cnt).toBe(0);
75
+ });
76
+ });
77
+ });
78
+ //# sourceMappingURL=search.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.test.js","sourceRoot":"","sources":["../../src/projections/search.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { PersistedEventEnvelope } from '../events/store.js';
3
+ import type { Projector } from './types.js';
4
+ export declare class TagsProjector implements Projector {
5
+ name: string;
6
+ apply(event: PersistedEventEnvelope, db: Database.Database): void;
7
+ reset(db: Database.Database): void;
8
+ private handleTaskCreated;
9
+ private handleTaskUpdated;
10
+ private insertTags;
11
+ }
12
+ //# sourceMappingURL=tags.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tags.d.ts","sourceRoot":"","sources":["../../src/projections/tags.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C,qBAAa,aAAc,YAAW,SAAS;IAC7C,IAAI,SAAU;IAEd,KAAK,CAAC,KAAK,EAAE,sBAAsB,EAAE,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,IAAI;IAWjE,KAAK,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,IAAI;IAIlC,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,UAAU;CAQnB"}
@@ -0,0 +1,41 @@
1
+ import { EventType } from '../events/types.js';
2
+ export class TagsProjector {
3
+ name = 'tags';
4
+ apply(event, db) {
5
+ switch (event.type) {
6
+ case EventType.TaskCreated:
7
+ this.handleTaskCreated(event, db);
8
+ break;
9
+ case EventType.TaskUpdated:
10
+ this.handleTaskUpdated(event, db);
11
+ break;
12
+ }
13
+ }
14
+ reset(db) {
15
+ db.exec('DELETE FROM task_tags');
16
+ }
17
+ handleTaskCreated(event, db) {
18
+ const data = event.data;
19
+ const tags = data.tags;
20
+ if (!tags || tags.length === 0)
21
+ return;
22
+ this.insertTags(db, event.task_id, tags);
23
+ }
24
+ handleTaskUpdated(event, db) {
25
+ const data = event.data;
26
+ if (data.field !== 'tags')
27
+ return;
28
+ const newTags = data.new_value;
29
+ db.prepare('DELETE FROM task_tags WHERE task_id = ?').run(event.task_id);
30
+ if (newTags && newTags.length > 0) {
31
+ this.insertTags(db, event.task_id, newTags);
32
+ }
33
+ }
34
+ insertTags(db, taskId, tags) {
35
+ const insertStmt = db.prepare('INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)');
36
+ for (const tag of tags) {
37
+ insertStmt.run(taskId, tag);
38
+ }
39
+ }
40
+ }
41
+ //# sourceMappingURL=tags.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tags.js","sourceRoot":"","sources":["../../src/projections/tags.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,MAAM,OAAO,aAAa;IACxB,IAAI,GAAG,MAAM,CAAC;IAEd,KAAK,CAAC,KAA6B,EAAE,EAAqB;QACxD,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;YACnB,KAAK,SAAS,CAAC,WAAW;gBACxB,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBAClC,MAAM;YACR,KAAK,SAAS,CAAC,WAAW;gBACxB,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBAClC,MAAM;QACV,CAAC;IACH,CAAC;IAED,KAAK,CAAC,EAAqB;QACzB,EAAE,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;IACnC,CAAC;IAEO,iBAAiB,CAAC,KAA6B,EAAE,EAAqB;QAC5E,MAAM,IAAI,GAAG,KAAK,CAAC,IAAW,CAAC;QAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,IAA4B,CAAC;QAC/C,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAEvC,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC3C,CAAC;IAEO,iBAAiB,CAAC,KAA6B,EAAE,EAAqB;QAC5E,MAAM,IAAI,GAAG,KAAK,CAAC,IAAW,CAAC;QAC/B,IAAI,IAAI,CAAC,KAAK,KAAK,MAAM;YAAE,OAAO;QAElC,MAAM,OAAO,GAAG,IAAI,CAAC,SAAqB,CAAC;QAC3C,EAAE,CAAC,OAAO,CAAC,yCAAyC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACzE,IAAI,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClC,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC;IAEO,UAAU,CAAC,EAAqB,EAAE,MAAc,EAAE,IAAc;QACtE,MAAM,UAAU,GAAG,EAAE,CAAC,OAAO,CAC3B,8DAA8D,CAC/D,CAAC;QACF,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,UAAU,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tags.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tags.test.d.ts","sourceRoot":"","sources":["../../src/projections/tags.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,69 @@
1
+ // packages/hzl-core/src/projections/tags.test.ts
2
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
3
+ import Database from 'better-sqlite3';
4
+ import { TagsProjector } from './tags.js';
5
+ import { runMigrations } from '../db/migrations.js';
6
+ import { EventStore } from '../events/store.js';
7
+ import { EventType } from '../events/types.js';
8
+ describe('TagsProjector', () => {
9
+ let db;
10
+ let eventStore;
11
+ let projector;
12
+ beforeEach(() => {
13
+ db = new Database(':memory:');
14
+ runMigrations(db);
15
+ eventStore = new EventStore(db);
16
+ projector = new TagsProjector();
17
+ });
18
+ afterEach(() => {
19
+ db.close();
20
+ });
21
+ describe('task_created with tags', () => {
22
+ it('inserts tag rows', () => {
23
+ const event = eventStore.append({
24
+ task_id: 'TASK1',
25
+ type: EventType.TaskCreated,
26
+ data: {
27
+ title: 'Test',
28
+ project: 'inbox',
29
+ tags: ['urgent', 'backend'],
30
+ },
31
+ });
32
+ projector.apply(event, db);
33
+ const tags = db.prepare('SELECT tag FROM task_tags WHERE task_id = ? ORDER BY tag').all('TASK1');
34
+ expect(tags.map(t => t.tag)).toEqual(['backend', 'urgent']);
35
+ });
36
+ });
37
+ describe('task_updated tags field', () => {
38
+ it('replaces all tags', () => {
39
+ const createEvent = eventStore.append({
40
+ task_id: 'TASK1',
41
+ type: EventType.TaskCreated,
42
+ data: { title: 'Test', project: 'inbox', tags: ['old1', 'old2'] },
43
+ });
44
+ projector.apply(createEvent, db);
45
+ const updateEvent = eventStore.append({
46
+ task_id: 'TASK1',
47
+ type: EventType.TaskUpdated,
48
+ data: { field: 'tags', new_value: ['new1', 'new2', 'new3'] },
49
+ });
50
+ projector.apply(updateEvent, db);
51
+ const tags = db.prepare('SELECT tag FROM task_tags WHERE task_id = ? ORDER BY tag').all('TASK1');
52
+ expect(tags.map(t => t.tag)).toEqual(['new1', 'new2', 'new3']);
53
+ });
54
+ });
55
+ describe('reset', () => {
56
+ it('clears all tag data', () => {
57
+ const event = eventStore.append({
58
+ task_id: 'TASK1',
59
+ type: EventType.TaskCreated,
60
+ data: { title: 'Test', project: 'inbox', tags: ['tag1'] },
61
+ });
62
+ projector.apply(event, db);
63
+ projector.reset(db);
64
+ const count = db.prepare('SELECT COUNT(*) as cnt FROM task_tags').get();
65
+ expect(count.cnt).toBe(0);
66
+ });
67
+ });
68
+ });
69
+ //# sourceMappingURL=tags.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tags.test.js","sourceRoot":"","sources":["../../src/projections/tags.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { PersistedEventEnvelope } from '../events/store.js';
3
+ import type { Projector } from './types.js';
4
+ export declare class TasksCurrentProjector implements Projector {
5
+ name: string;
6
+ apply(event: PersistedEventEnvelope, db: Database.Database): void;
7
+ reset(db: Database.Database): void;
8
+ private handleTaskCreated;
9
+ private handleStatusChanged;
10
+ private handleTaskMoved;
11
+ private handleTaskUpdated;
12
+ private handleTaskArchived;
13
+ }
14
+ //# sourceMappingURL=tasks-current.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tasks-current.d.ts","sourceRoot":"","sources":["../../src/projections/tasks-current.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAK5C,qBAAa,qBAAsB,YAAW,SAAS;IACrD,IAAI,SAAmB;IAEvB,KAAK,CAAC,KAAK,EAAE,sBAAsB,EAAE,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,IAAI;IAoBjE,KAAK,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,IAAI;IAIlC,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,mBAAmB;IAgD3B,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,iBAAiB;IAgBzB,OAAO,CAAC,kBAAkB;CAS3B"}