hikoutei 0.4.2 → 0.4.3

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 (140) hide show
  1. package/dist/application/sync/outbound/SheetsEffectDispatcher.d.ts +64 -0
  2. package/dist/application/sync/outbound/SheetsEffectDispatcher.d.ts.map +1 -0
  3. package/dist/application/sync/outbound/SheetsEffectDispatcher.js +440 -0
  4. package/dist/application/sync/outbound/SheetsEffectDispatcher.js.map +1 -0
  5. package/dist/application/sync/service/SyncServiceBootstrap.d.ts +3 -4
  6. package/dist/application/sync/service/SyncServiceBootstrap.d.ts.map +1 -1
  7. package/dist/application/sync/service/SyncServiceBootstrap.js +7 -4
  8. package/dist/application/sync/service/SyncServiceBootstrap.js.map +1 -1
  9. package/dist/application/sync/telemetry/syncTiming.d.ts +20 -33
  10. package/dist/application/sync/telemetry/syncTiming.d.ts.map +1 -1
  11. package/dist/application/sync/telemetry/syncTiming.js +14 -12
  12. package/dist/application/sync/telemetry/syncTiming.js.map +1 -1
  13. package/dist/infrastructure/storage/errors.d.ts +10 -5
  14. package/dist/infrastructure/storage/errors.d.ts.map +1 -1
  15. package/dist/infrastructure/storage/errors.js +10 -7
  16. package/dist/infrastructure/storage/errors.js.map +1 -1
  17. package/dist/infrastructure/storage/index.d.ts +6 -4
  18. package/dist/infrastructure/storage/index.d.ts.map +1 -1
  19. package/dist/infrastructure/storage/index.js +3 -2
  20. package/dist/infrastructure/storage/index.js.map +1 -1
  21. package/dist/infrastructure/storage/sqlite/schema.d.ts +12 -11
  22. package/dist/infrastructure/storage/sqlite/schema.d.ts.map +1 -1
  23. package/dist/infrastructure/storage/sqlite/schema.js +17 -109
  24. package/dist/infrastructure/storage/sqlite/schema.js.map +1 -1
  25. package/dist/infrastructure/storage/state/canonical/canonicalCommit.d.ts +2 -2
  26. package/dist/infrastructure/storage/state/canonical/canonicalCommit.d.ts.map +1 -1
  27. package/dist/infrastructure/storage/state/canonical/canonicalCommit.js +2 -2
  28. package/dist/infrastructure/storage/state/canonical/canonicalCommit.js.map +1 -1
  29. package/dist/infrastructure/storage/state/mapped/mappedPersistenceContext.d.ts +1 -2
  30. package/dist/infrastructure/storage/state/mapped/mappedPersistenceContext.d.ts.map +1 -1
  31. package/dist/infrastructure/storage/state/mapped/mappedPersistenceContext.js +1 -2
  32. package/dist/infrastructure/storage/state/mapped/mappedPersistenceContext.js.map +1 -1
  33. package/dist/infrastructure/storage/state/observation/observationCanonical.d.ts +1 -1
  34. package/dist/infrastructure/storage/state/observation/observationCanonical.d.ts.map +1 -1
  35. package/dist/infrastructure/storage/state/observation/observationQuarantine.d.ts +1 -1
  36. package/dist/infrastructure/storage/state/observation/observationQuarantine.d.ts.map +1 -1
  37. package/dist/infrastructure/storage/state/observation/observationQuarantine.js +1 -1
  38. package/dist/infrastructure/storage/state/observation/observationQuarantine.js.map +1 -1
  39. package/dist/infrastructure/storage/state/observation/observationTypes.d.ts +1 -1
  40. package/dist/infrastructure/storage/state/observation/observationTypes.d.ts.map +1 -1
  41. package/dist/infrastructure/storage/state/observation/observationValidation.d.ts +1 -1
  42. package/dist/infrastructure/storage/state/observation/observationValidation.d.ts.map +1 -1
  43. package/dist/infrastructure/storage/state/observation/observationWriter.d.ts +1 -1
  44. package/dist/infrastructure/storage/state/observation/observationWriter.d.ts.map +1 -1
  45. package/dist/infrastructure/storage/state/observation/observationWriter.js +1 -2
  46. package/dist/infrastructure/storage/state/observation/observationWriter.js.map +1 -1
  47. package/dist/infrastructure/storage/state/resolution/resolutionWriter.d.ts +1 -2
  48. package/dist/infrastructure/storage/state/resolution/resolutionWriter.d.ts.map +1 -1
  49. package/dist/infrastructure/storage/state/resolution/resolutionWriter.js +1 -2
  50. package/dist/infrastructure/storage/state/resolution/resolutionWriter.js.map +1 -1
  51. package/dist/infrastructure/storage/state/resolution/resolutionWriterContracts.d.ts +1 -1
  52. package/dist/infrastructure/storage/state/resolution/resolutionWriterContracts.d.ts.map +1 -1
  53. package/dist/infrastructure/storage/state/resolution/resolutionWriterHelpers.d.ts +1 -1
  54. package/dist/infrastructure/storage/state/resolution/resolutionWriterHelpers.d.ts.map +1 -1
  55. package/dist/infrastructure/storage/state/resolution/resolutionWriterHelpers.js +1 -1
  56. package/dist/infrastructure/storage/state/resolution/resolutionWriterHelpers.js.map +1 -1
  57. package/dist/infrastructure/storage/state/resolution/resolutionWriterSql.d.ts +1 -1
  58. package/dist/infrastructure/storage/state/resolution/resolutionWriterSql.d.ts.map +1 -1
  59. package/dist/infrastructure/storage/state/resolution/resolutionWriterSql.js +2 -2
  60. package/dist/infrastructure/storage/state/resolution/resolutionWriterSql.js.map +1 -1
  61. package/dist/infrastructure/storage/sync/shared/spreadsheetAuthority.d.ts +1 -1
  62. package/dist/infrastructure/storage/sync/shared/spreadsheetAuthority.d.ts.map +1 -1
  63. package/dist/infrastructure/storage/sync/shared/spreadsheetAuthority.js +1 -1
  64. package/dist/infrastructure/storage/sync/shared/spreadsheetAuthority.js.map +1 -1
  65. package/dist/infrastructure/storage/sync/shared/syncRegistry.d.ts +1 -1
  66. package/dist/infrastructure/storage/sync/shared/syncRegistry.js +1 -1
  67. package/package.json +13 -7
  68. package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.d.ts +0 -66
  69. package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.d.ts.map +0 -1
  70. package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.js +0 -123
  71. package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.js.map +0 -1
  72. package/dist/application/sync/outbound/effects/SyncEffectSupervisor.d.ts +0 -111
  73. package/dist/application/sync/outbound/effects/SyncEffectSupervisor.d.ts.map +0 -1
  74. package/dist/application/sync/outbound/effects/SyncEffectSupervisor.js +0 -369
  75. package/dist/application/sync/outbound/effects/SyncEffectSupervisor.js.map +0 -1
  76. package/dist/application/sync/outbound/effects/SyncEffectWorker.d.ts +0 -127
  77. package/dist/application/sync/outbound/effects/SyncEffectWorker.d.ts.map +0 -1
  78. package/dist/application/sync/outbound/effects/SyncEffectWorker.js +0 -552
  79. package/dist/application/sync/outbound/effects/SyncEffectWorker.js.map +0 -1
  80. package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.d.ts +0 -85
  81. package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.d.ts.map +0 -1
  82. package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.js +0 -74
  83. package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.js.map +0 -1
  84. package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.d.ts +0 -31
  85. package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.d.ts.map +0 -1
  86. package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.js +0 -222
  87. package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.js.map +0 -1
  88. package/dist/application/sync/outbound/effects/SyncEffectWorkerHelpers.d.ts +0 -14
  89. package/dist/application/sync/outbound/effects/SyncEffectWorkerHelpers.d.ts.map +0 -1
  90. package/dist/application/sync/outbound/effects/SyncEffectWorkerHelpers.js +0 -25
  91. package/dist/application/sync/outbound/effects/SyncEffectWorkerHelpers.js.map +0 -1
  92. package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.d.ts +0 -61
  93. package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.d.ts.map +0 -1
  94. package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.js +0 -296
  95. package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.js.map +0 -1
  96. package/dist/application/sync/outbound/effects/SyncEffectWorkerTiming.d.ts +0 -17
  97. package/dist/application/sync/outbound/effects/SyncEffectWorkerTiming.d.ts.map +0 -1
  98. package/dist/application/sync/outbound/effects/SyncEffectWorkerTiming.js +0 -80
  99. package/dist/application/sync/outbound/effects/SyncEffectWorkerTiming.js.map +0 -1
  100. package/dist/application/sync/outbound/effects/SyncEffectWorkerTransitions.d.ts +0 -13
  101. package/dist/application/sync/outbound/effects/SyncEffectWorkerTransitions.d.ts.map +0 -1
  102. package/dist/application/sync/outbound/effects/SyncEffectWorkerTransitions.js +0 -248
  103. package/dist/application/sync/outbound/effects/SyncEffectWorkerTransitions.js.map +0 -1
  104. package/dist/infrastructure/storage/sync/outbound/effectOutbox.d.ts +0 -141
  105. package/dist/infrastructure/storage/sync/outbound/effectOutbox.d.ts.map +0 -1
  106. package/dist/infrastructure/storage/sync/outbound/effectOutbox.js +0 -318
  107. package/dist/infrastructure/storage/sync/outbound/effectOutbox.js.map +0 -1
  108. package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.d.ts +0 -143
  109. package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.d.ts.map +0 -1
  110. package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.js +0 -16
  111. package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.js.map +0 -1
  112. package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.d.ts +0 -33
  113. package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.d.ts.map +0 -1
  114. package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.js +0 -259
  115. package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.js.map +0 -1
  116. package/dist/infrastructure/storage/sync/outbound/effectOutboxSupport.d.ts +0 -27
  117. package/dist/infrastructure/storage/sync/outbound/effectOutboxSupport.d.ts.map +0 -1
  118. package/dist/infrastructure/storage/sync/outbound/effectOutboxSupport.js +0 -316
  119. package/dist/infrastructure/storage/sync/outbound/effectOutboxSupport.js.map +0 -1
  120. package/dist/infrastructure/storage/sync/shared/writerLease.d.ts +0 -76
  121. package/dist/infrastructure/storage/sync/shared/writerLease.d.ts.map +0 -1
  122. package/dist/infrastructure/storage/sync/shared/writerLease.js +0 -189
  123. package/dist/infrastructure/storage/sync/shared/writerLease.js.map +0 -1
  124. package/docs/advanced-sheets-gateway-concurrency-problem.md +0 -434
  125. package/docs/architecture.md +0 -224
  126. package/docs/ci.md +0 -293
  127. package/docs/code-guidelines.md +0 -248
  128. package/docs/development.md +0 -74
  129. package/docs/gateway-removal-inventory.md +0 -147
  130. package/docs/git-workflow.md +0 -224
  131. package/docs/google-sheets-sync-scaling-strategy.md +0 -459
  132. package/docs/mikro-orm-adapter-spike.md +0 -89
  133. package/docs/quick-start.md +0 -137
  134. package/docs/sql-layer-plan.md +0 -59
  135. package/docs/sync-bulk-write-benchmark.md +0 -2117
  136. package/docs/sync-observability.md +0 -100
  137. package/docs/task-queue-write-model.md +0 -640
  138. package/docs/typed-sheets-mvp-scope-2026-06-29.md +0 -490
  139. package/docs/typed-sheets-plan.md +0 -417
  140. package/docs/write-and-synchronization-flow.md +0 -179
@@ -1,417 +0,0 @@
1
- # typed-sheets Plan
2
-
3
- ## 1. Project Positioning
4
-
5
- `typed-sheets`는 Google Sheets를 초기 MVP, 내부 운영툴, 저트래픽 어드민, 프로토타입의 lightweight database처럼 사용할 때 필요한 TypeScript repository layer다.
6
-
7
- 핵심 포지션은 다음이다.
8
-
9
- > Typed repository and safe write layer for Google Sheets-backed MVPs.
10
-
11
- 이 프로젝트는 Google Sheets를 MySQL/Postgres 대체재로 주장하지 않는다. 또한 `google-spreadsheet`나 `@googleapis/sheets` 같은 API client를 대체하려는 것도 아니다.
12
-
13
- 핵심 차별점은 Google Sheets를 DB처럼 쓸 때 조용히 깨지는 상태를 성공으로 처리하지 않는 것이다.
14
-
15
- - Sheet header와 TypeScript schema의 drift 감지
16
- - row 데이터를 typed object로 변환
17
- - key column 기반 repository API 제공
18
- - `_version` column 기반 optimistic locking
19
- - Google Sheets API quota를 고려한 확장 가능한 구조
20
- - 향후 Apps Script `LockService` write gateway 확장 가능성
21
-
22
- ## 2. Existing Ecosystem Comparison
23
-
24
- ### `google-spreadsheet`
25
-
26
- - Google Sheets API를 편하게 다루기 위한 wrapper다.
27
- - row/cell 조작은 편하지만 repository abstraction은 아니다.
28
- - schema drift, key uniqueness, optimistic locking을 라이브러리의 중심 책임으로 보지 않는다.
29
-
30
- `typed-sheets`에서는 이 라이브러리를 내부 adapter 구현체로 사용할 수 있다.
31
-
32
- ### `@googleapis/sheets`
33
-
34
- - Google 공식 Sheets API client다.
35
- - 저수준 API 호출, 인증, range read/write에 적합하다.
36
- - typed repository, schema validation, stale write protection은 직접 구현해야 한다.
37
-
38
- `typed-sheets`에서는 production adapter의 가장 보수적인 기반이 될 수 있다.
39
-
40
- ### `spreadsheet-orm`
41
-
42
- - Google Spreadsheet를 ORM처럼 다루려는 비교적 가까운 시도다.
43
- - query builder, schema management 등 ORM 방향의 기능을 포함한다.
44
- - `typed-sheets`는 더 좁게 가는 것이 좋다.
45
-
46
- `typed-sheets`의 MVP는 ORM 전체가 아니라 다음만 강하게 잡는다.
47
-
48
- - header drift detection
49
- - typed row parsing
50
- - key-based repository
51
- - `_version` optimistic locking
52
-
53
- ## 3. MVP API Review
54
-
55
- 제안된 MVP API는 크지 않다. 오히려 프로젝트의 차별점을 증명하기에 적절한 최소 범위다.
56
-
57
- 초기 API:
58
-
59
- ```ts
60
- import { createSheetRepository, text, number, boolean } from "typed-sheets";
61
-
62
- const users = createSheetRepository({
63
- sheetName: "Users",
64
- key: "id",
65
- columns: {
66
- id: text(),
67
- email: text(),
68
- age: number().optional(),
69
- active: boolean(),
70
- _version: number(),
71
- },
72
- });
73
-
74
- await users.assertSchema();
75
-
76
- const all = await users.findAll();
77
- const user = await users.findById("u1");
78
-
79
- await users.insert({
80
- id: "u1",
81
- email: "a@test.com",
82
- age: 20,
83
- active: true,
84
- _version: 1,
85
- });
86
-
87
- await users.update("u1", current => ({
88
- ...current,
89
- age: current.age + 1,
90
- }));
91
- ```
92
-
93
- 단, 실제 구현에서는 `createSheetRepository`에 adapter를 명시적으로 주입하는 형태가 더 좋다.
94
-
95
- ```ts
96
- const users = createSheetRepository({
97
- adapter,
98
- sheetName: "Users",
99
- key: "id",
100
- columns: {
101
- id: text(),
102
- email: text(),
103
- age: number().optional(),
104
- active: boolean(),
105
- _version: number(),
106
- },
107
- });
108
- ```
109
-
110
- 이렇게 하면 core repository logic을 Google API, OAuth, network와 분리해 fake adapter로 테스트할 수 있다.
111
-
112
- ## 4. MVP Scope
113
-
114
- MVP에 포함할 기능:
115
-
116
- - schema definition API
117
- - header 검증
118
- - 필수 column 누락 감지
119
- - 중복 header 감지
120
- - key column 누락 감지
121
- - row parsing
122
- - string, number, boolean parser
123
- - optional column
124
- - `findAll`
125
- - `findById`
126
- - `insert`
127
- - `update`
128
- - `_version` 기반 optimistic locking
129
- - `SchemaDriftError`
130
- - `ConflictError`
131
- - `ParseError`
132
- - fake adapter 기반 테스트
133
-
134
- MVP에서 제외할 기능:
135
-
136
- - relation / join
137
- - lazy loading
138
- - SQL-like query language
139
- - migration engine
140
- - transaction manager
141
- - multi-row atomic transaction
142
- - Apps Script 자동 설치
143
- - Apps Script Web App gateway
144
- - cache
145
- - request collapse
146
- - retry/backoff
147
- - browser support
148
- - dashboard UI
149
-
150
- ## 5. Google Sheets를 DB처럼 쓸 때의 한계
151
-
152
- Google Sheets는 spreadsheet이지 database가 아니다. 따라서 다음 한계를 명확히 문서화해야 한다.
153
-
154
- - storage layer에 primary key / unique constraint가 없다.
155
- - multi-row transaction이 없다.
156
- - 여러 사용자가 동시에 수정할 때 lost update가 발생할 수 있다.
157
- - 비개발자가 header를 변경하거나 삭제할 수 있다.
158
- - 값 타입이 느슨해서 string, number, boolean, date가 섞일 수 있다.
159
- - formula, formatted value, raw value가 다를 수 있다.
160
- - 전체 row scan이 커질수록 느려지고 quota를 많이 쓴다.
161
- - Google Sheets API quota와 timeout에 영향을 받는다.
162
- - Google OAuth, service account, 공유 권한 설정이 운영 변수가 된다.
163
- - Apps Script를 붙이면 OAuth 승인, Workspace 정책, script quota, 배포 문제가 추가된다.
164
-
165
- 따라서 이 프로젝트는 다음 사용처에만 적합하다고 명확히 제한해야 한다.
166
-
167
- - 초기 MVP
168
- - 내부 운영툴
169
- - 저트래픽 어드민
170
- - 비개발자가 직접 수정하는 운영 데이터
171
- - 프로토타입
172
-
173
- ## 6. Domain / Application / Adapter 분리 구조
174
-
175
- 권장 구조:
176
-
177
- ```txt
178
- src/
179
- index.ts
180
- domain/ # 정규화, 평가, 충돌, 상태 규칙
181
- shared/ # 공통 상수·인코딩·상태 계약
182
- application/
183
- orm/ # 사용자 ORM facade와 mapping/flush
184
- sync/ # gateway, worker, reconciliation
185
- adapter/ # persistence와 Sheets provider 계약/구현
186
- infrastructure/
187
- storage/ # SQLite schema, canonical state, outbox
188
- ```
189
-
190
- ### Domain 책임
191
-
192
- `domain`은 Google API와 SQLite 구현을 몰라야 한다.
193
-
194
- 책임:
195
-
196
- - schema definition 해석
197
- - header validation
198
- - duplicate header detection
199
- - required column validation
200
- - key column validation
201
- - row parsing
202
- - duplicate key detection
203
- - repository method 구현
204
- - `_version` optimistic locking
205
- - typed error throw
206
-
207
- ### Application 책임
208
-
209
- application은 domain 규칙과 adapter/infrastructure를 조율한다.
210
-
211
- 책임:
212
-
213
- - ORM entity lifecycle과 flush 계획
214
- - effect worker와 reconciliation 실행
215
- - Apps Script gateway operation 조합
216
-
217
- ### Adapter 책임
218
-
219
- adapter는 Google Sheets와 통신하는 부분만 담당한다.
220
-
221
- 책임:
222
-
223
- - 인증
224
- - spreadsheet 접근
225
- - sheet 조회
226
- - range read/write
227
- - append row
228
- - update row
229
- - batch API 사용 여부
230
- - retry/backoff
231
- - cache/request collapse
232
- - Apps Script gateway 호출
233
-
234
- 초기 adapter port 예시:
235
-
236
- ```ts
237
- export type SheetCell = string | number | boolean | null;
238
-
239
- export interface SheetSnapshot {
240
- headers: string[];
241
- rows: SheetCell[][];
242
- }
243
-
244
- export interface SheetAdapter {
245
- readSheet(sheetName: string): Promise<SheetSnapshot>;
246
- appendRow(sheetName: string, row: SheetCell[]): Promise<void>;
247
- updateRow(sheetName: string, rowNumber: number, row: SheetCell[]): Promise<void>;
248
- }
249
- ```
250
-
251
- `rowNumber`는 Google Sheet 기준 1-based row number로 둔다. header가 1행이므로 data row는 2행부터 시작한다.
252
-
253
- ## 7. Optimistic Locking 설계
254
-
255
- `_version` column은 MVP에서 필수로 두는 것이 좋다.
256
-
257
- 기본 흐름:
258
-
259
- 1. `update(id, updater)` 호출
260
- 2. sheet를 읽는다.
261
- 3. key column으로 row를 찾는다.
262
- 4. 현재 `_version`을 기억한다.
263
- 5. updater를 적용한다.
264
- 6. write 직전 같은 row를 다시 읽거나 adapter-level compare-and-set을 수행한다.
265
- 7. version이 바뀌었으면 `ConflictError`를 던진다.
266
- 8. version이 같으면 `_version + 1`로 write한다.
267
-
268
- MVP fake adapter에서는 read-between-write 시나리오를 테스트할 수 있게 만들어 stale write를 증명한다.
269
-
270
- 실제 Google Sheets API만으로는 완전한 atomic compare-and-set이 어렵다. 따라서 MVP에서는 stale write risk를 줄이는 repository-level fencing으로 시작하고, 2차 확장에서 Apps Script `LockService` 기반 serialized write gateway로 강화한다.
271
-
272
- ## 8. Error 설계
273
-
274
- ### `SchemaDriftError`
275
-
276
- 다음 상황에서 발생:
277
-
278
- - header 중복
279
- - required column 누락
280
- - key column 누락
281
- - `_version` column 누락
282
- - duplicate key 발견
283
-
284
- ### `ParseError`
285
-
286
- 다음 상황에서 발생:
287
-
288
- - required value가 비어 있음
289
- - number parser 실패
290
- - boolean parser 실패
291
- - `_version` 값이 number가 아님
292
-
293
- ### `ConflictError`
294
-
295
- 다음 상황에서 발생:
296
-
297
- - update 중 `_version`이 변경됨
298
- - stale row write가 감지됨
299
-
300
- ## 9. Test Scenarios
301
-
302
- 테스트는 실제 Google API 없이 fake adapter로 먼저 작성한다.
303
-
304
- ### Schema tests
305
-
306
- - `assertSchema`는 모든 column이 있으면 성공한다.
307
- - required column이 없으면 `SchemaDriftError`를 던진다.
308
- - key column이 없으면 `SchemaDriftError`를 던진다.
309
- - `_version` column이 없으면 `SchemaDriftError`를 던진다.
310
- - duplicate header가 있으면 `SchemaDriftError`를 던진다.
311
-
312
- ### Parsing tests
313
-
314
- - text column을 string으로 파싱한다.
315
- - number column을 number로 파싱한다.
316
- - boolean column을 boolean으로 파싱한다.
317
- - optional column은 empty value를 `undefined`로 파싱한다.
318
- - required column이 비어 있으면 `ParseError`를 던진다.
319
- - number 변환에 실패하면 `ParseError`를 던진다.
320
- - boolean 변환에 실패하면 `ParseError`를 던진다.
321
-
322
- ### Repository read tests
323
-
324
- - `findAll`은 typed object 배열을 반환한다.
325
- - `findById`는 key가 일치하는 row를 반환한다.
326
- - `findById`는 없으면 `null`을 반환한다.
327
- - duplicate key가 있으면 `SchemaDriftError`를 던진다.
328
-
329
- ### Insert tests
330
-
331
- - `insert`는 header 순서대로 row를 append한다.
332
- - duplicate key insert는 `SchemaDriftError` 또는 별도 duplicate key error를 던진다.
333
- - insert row parse/serialization 실패를 테스트한다.
334
-
335
- ### Update tests
336
-
337
- - `update`는 현재 row를 updater에 넘긴다.
338
- - `update`는 `_version`을 1 증가시킨다.
339
- - `update`는 header 순서대로 row를 write한다.
340
- - target row가 없으면 `null`을 반환한다.
341
- - write 전 version이 바뀌면 `ConflictError`를 던진다.
342
-
343
- ## 10. Implementation Order
344
-
345
- 권장 구현 순서:
346
-
347
- 1. `package.json`, `tsconfig`, test runner 설정
348
- 2. `SheetAdapter` interface 정의
349
- 3. error class 정의
350
- 4. column schema primitive 구현
351
- 5. schema validation 구현
352
- 6. row parsing 구현
353
- 7. `findAll`, `findById` 구현
354
- 8. `insert` 구현
355
- 9. `_version` 기반 `update` 구현
356
- 10. fake adapter 테스트 작성
357
- 11. README 정리
358
- 12. 실제 Google Sheets adapter는 MVP core 검증 후 별도 단계로 진행
359
-
360
- ## 11. Expected MVP Size
361
-
362
- 목표 규모:
363
-
364
- - production TypeScript: 500~900 lines
365
- - tests: 700~1,200 lines
366
- - README/docs/config: 400~700 lines
367
- - total MVP: 1,500~2,800 lines
368
-
369
- 핵심은 기능 수가 아니라 다음을 테스트로 증명하는 것이다.
370
-
371
- - schema drift를 조용히 통과시키지 않는다.
372
- - parse failure를 조용히 통과시키지 않는다.
373
- - duplicate key를 조용히 통과시키지 않는다.
374
- - stale write를 조용히 통과시키지 않는다.
375
-
376
- ## 12. Later Extensions
377
-
378
- MVP 이후 후보:
379
-
380
- 1. read cache / request collapse
381
- 2. timeout / retry / exponential backoff
382
- 3. `@googleapis/sheets` adapter
383
- 4. `google-spreadsheet` adapter
384
- 5. Apps Script 자동 설치 CLI
385
- 6. Apps Script `LockService` 기반 serialized write gateway
386
- 7. schema drift report
387
- 8. GitHub Action
388
- 9. Google Sheet template generator
389
- 10. `_createdAt`, `_updatedAt` system columns
390
- 11. soft delete
391
- 12. audit log sheet
392
-
393
- ## 13. Apps Script 방향
394
-
395
- 향후 CLI 예시:
396
-
397
- ```sh
398
- npx typed-sheets init --spreadsheet-id xxx
399
- ```
400
-
401
- 목표:
402
-
403
- - Google OAuth 로그인
404
- - 대상 spreadsheet 확인
405
- - bound Apps Script project 생성
406
- - Apps Script 코드 업로드
407
- - `LockService` 기반 write function 설치
408
- - 설치 검증
409
-
410
- 가능한 공식 API 흐름:
411
-
412
- - `projects.create`
413
- - `projects.updateContent`
414
- - spreadsheet file id를 parent로 사용해 bound script 생성
415
- - 필요한 경우 installable trigger 생성
416
-
417
- MVP에는 포함하지 않는다. OAuth, Workspace 정책, 권한 승인, Apps Script API 활성화 문제 때문에 core 안정성을 먼저 증명한 뒤 진행한다.
@@ -1,179 +0,0 @@
1
- # Write and Synchronization Flow
2
-
3
- Hikoutei commits local state first and materializes Google Sheets changes
4
- asynchronously. The root ORM API is SQLite-only; the internal sync service owns
5
- all Sheet communication.
6
-
7
- ## Public ORM flow
8
-
9
- ```text
10
- em.persist(entity) / em.remove(entity)
11
- │
12
- ▼
13
- SQLite transaction
14
- └─ entity table
15
- │
16
- ▼
17
- return to application
18
- ```
19
-
20
- `createTypedSheets()` never contacts Google Sheets and does not require a Sheet
21
- route, provider client, or provisioner. Reads always come from SQLite.
22
-
23
- ## Internal sync service flow
24
-
25
- The service-side bootstrap uses the same SQLite adapter and adds the canonical
26
- sync state and durable outbox to the flush transaction:
27
-
28
- ```text
29
- EntityManager.flush()
30
- │
31
- ▼
32
- SQLite transaction
33
- ├─ entity table
34
- ├─ canonical sync state
35
- ├─ projection registry/state
36
- └─ durable Sheet effect outbox
37
- │
38
- ▼
39
- internal effect supervisor
40
- ├─ claim with a lease
41
- ├─ send a bounded direct REST batch to the sync provider
42
- ├─ persist uncertain delivery and schedule a durable postcondition probe
43
- └─ mark the effect applied, terminally failed, or recoverably pending
44
- │
45
- ▼
46
- sync provider (googleSheetsApi) ──▶ Google Sheets
47
- ```
48
-
49
- A successful `flush()` means that SQLite accepted the local change and queued
50
- its projection effect. It does not mean that the remote Sheet write has
51
- completed.
52
-
53
- ## Fast append, update, and delete
54
-
55
- New System_State rows use the bounded fast append operation where possible.
56
- Updates and deletes use guarded effects with expected visible revision/hash
57
- evidence. The same internal worker handles retries, response-loss recovery, and
58
- reconciliation. Each physical route's effects are split into bounded sub-batches
59
- so one provider call returns a complete result set instead of a partial prefix.
60
- These operation types are implementation details and are not methods on the
61
- public EntityManager.
62
-
63
- ### Outbound provider
64
-
65
- The outbound half of the worker runs through one Google Sheets API provider
66
- (`googleSheetsApi`): a single service-account provider owns provisioning,
67
- outbound effects, table reads, row anchors, and User_Input observation with
68
- no Apps Script at all. The legacy Apps Script gateway and the
69
- `appsScript`/`googleApiWorker` options were removed; existing deployments
70
- should switch to `googleSheetsApi`.
71
-
72
- The direct provider implements the established sync semantics in TypeScript: bulk
73
- preflight with fail-closed validation (headers, anchors, identities,
74
- receipts), receipt replay/idempotency, visible/candidate/repair guards,
75
- full-row deletion guards, one atomic `spreadsheets.batchUpdate` per
76
- applicable target mutation + receipt write, and receipt-backed postcondition
77
- recovery. It disables SDK auto-retry (the durable worker owns retries),
78
- spaces request starts at 1,100 ms per class (reads and writes separately),
79
- and trims a serialized batch over ~2 MB to an order-preserving prefix with
80
- `hasMore`. Transport outcomes are classified by the shared
81
- `classifyTransportOutcome` boundary: pre-mutation 4xx rejections are explicit
82
- remote failures; timeouts, network errors, 408/429/5xx, and malformed 2xx
83
- replies stay delivery-uncertain and are probed like any provider response loss.
84
- This mode is not a performance claim; the raw-transport experiments under
85
- `scripts/bench/` measure the unguarded API path without receipts or
86
- compare-and-set.
87
-
88
- ## Inbound User_Input flow
89
-
90
- The internal service polls registered `User_Input` projections on a bounded
91
- interval. By default each pass runs an adaptive values-only preflight: it reads
92
- the cheap value surface, compares it with canonical SQLite state, and only
93
- escalates a table to the metadata-preserving snapshot read when the preflight
94
- cannot certify it as unchanged:
95
-
96
- ```text
97
- User_Input polling
98
- ├─ values-only preflight over registered projections
99
- ├─ escalate changed/ambiguous/schema tables to full metadata
100
- ├─ normalize literal/blank cells and retain formula/merge/error metadata
101
- ├─ resolve business-key row binding
102
- ├─ validate ownership and field revisions
103
- ├─ classify accepted, conflict, stale, or quarantine
104
- ├─ record Sync_Conflicts evidence
105
- ├─ CAS-resolve conflicts in favor of canonical SQLite state
106
- └─ persist accepted observation and entity mutation in SQLite
107
- ```
108
-
109
- The preflight never accepts or persists edits. It escalates a table whenever a
110
- row changed, a business key is unknown or duplicated, an expected active entity
111
- is missing from the projection, or any cell fails its literal/blank type check,
112
- so the existing quarantine and conflict rules stay authoritative. Because the
113
- values-only read drops formula, merged, and error metadata, the bootstrap also
114
- forces a periodic metadata-preserving safety full scan (default one minute,
115
- configurable) so invalid cells are rechecked and quarantined rather than
116
- mistaken for literal user edits. The coordinator records any overdue safety-scan
117
- lag, including a scan delayed by writer-lease contention, through internal
118
- polling telemetry without changing the SQLite write boundary.
119
-
120
- Accepted observation writes update canonical state and the application entity in
121
- the same SQLite transaction. They do not enqueue a duplicate User_Input effect;
122
- only the required system projection repair/materialization is considered.
123
- Conflicts, stale writes, duplicate keys, and malformed cells remain visible in
124
- SQLite evidence tables. A conflict is not left open indefinitely: the internal
125
- resolver submits a fenced `acknowledge_system` command using the current
126
- canonical revision, active candidate hash, and candidate epoch. When it applies,
127
- it clears the candidate pointer and appends both the guarded canonical
128
- User_Input rewrite and the `Sync_Conflicts` audit effect in the same SQLite
129
- transaction. If the CAS is stale, the newer candidate remains authoritative for
130
- the next observation pass rather than being overwritten.
131
-
132
- ## Provisioning and provider boundary
133
-
134
- Projection provisioning is an internal service-start operation. The bootstrap
135
- generates route registrations and headers for the required System_State,
136
- User_Input, and Sync_Conflicts projections, verifies remote schema drift, and
137
- starts workers only after provisioning and unresolved-conflict backfill succeed.
138
- The full direct provider provisions the tabs itself — creating missing tabs,
139
- initializing truly-empty tabs, and failing closed on header drift — with no
140
- Apps Script involved. The legacy Apps Script gateway mode was removed; the
141
- service-account `googleSheetsApi` provider is the only sync path.
142
-
143
- Sheet consistency is not provided by cross-request Sheet transactions; Google
144
- Sheets offers no serializable isolation between separate API calls. It comes
145
- from the hidden effect-receipt tab (each effect id and payload hash is
146
- recorded with its visible evidence), effect-id/payload-hash dedupe (a
147
- replayed or concurrent effect is recognized as already applied and never
148
- double-materialized), the SQLite durable outbox (effects survive restarts and
149
- are delivered at-least-once), fencing (spreadsheet authority epoch/token and
150
- worker/effect leases reject stale writers), and postcondition recovery
151
- (uncertain deliveries are probed until receipt-backed visible evidence
152
- matches).
153
-
154
- Applications do not import or call the provider client, protocol, operation
155
- builders, polling functions, provisioning interfaces, or the direct Sheets
156
- API provider.
157
-
158
- In direct mode the same spreadsheet must be reachable by the service account
159
- (the full provider owns provisioning, outbound writes, and observation): one
160
- SQLite runtime is the single authoritative writer for the whole spreadsheet.
161
- The tracked live scenario
162
- (`scripts/ci/run-api-scenario.mjs --backend live --outbound direct`) verifies
163
- provisioning from an empty spreadsheet, append/update/delete delivery, mapped
164
- User_Input polling, stale-edit CAS guards, anchor evidence, and cleanup
165
- without any Apps Script deployment.
166
-
167
- ## Failure model
168
-
169
- An HTTP timeout, non-JSON response, 404, or lost connection does not prove that
170
- a Sheet write failed. The worker persists `delivery_uncertain` with a durable
171
- probe schedule and dispatch identity, then promotes the effect only after
172
- receipt-backed visible evidence is read. An explicit structured remote failure
173
- uses the terminal failure path. SQLite also records a per-spreadsheet authority
174
- epoch/token; provider mutations carrying an older token are rejected. A pass that only
175
- requeued uncertain work (a response-loss or postcondition-unapplied loop) backs
176
- off with a bounded, jittered delay that resets as soon as forward progress
177
- resumes; lease expiry and recovery keep effects live during the backoff. The
178
- SQLite commit is the application success boundary; remote delivery is
179
- at-least-once and asynchronous.