hikoutei 0.3.2 → 0.3.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 (211) hide show
  1. package/README.md +12 -2
  2. package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmMappedObservation.d.ts +2 -0
  3. package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmMappedObservation.d.ts.map +1 -1
  4. package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmMappedObservation.js +5 -0
  5. package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmMappedObservation.js.map +1 -1
  6. package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPolling.d.ts +52 -0
  7. package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPolling.d.ts.map +1 -1
  8. package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPolling.js +195 -13
  9. package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPolling.js.map +1 -1
  10. package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingFastPath.d.ts +24 -0
  11. package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingFastPath.d.ts.map +1 -0
  12. package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingFastPath.js +124 -0
  13. package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingFastPath.js.map +1 -0
  14. package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingInspection.js +1 -1
  15. package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingInspection.js.map +1 -1
  16. package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingPersistence.d.ts +15 -2
  17. package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingPersistence.d.ts.map +1 -1
  18. package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingPersistence.js +27 -3
  19. package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingPersistence.js.map +1 -1
  20. package/dist/adapter/sheets/providers/apps-script-gateway/errors.d.ts +1 -0
  21. package/dist/adapter/sheets/providers/apps-script-gateway/errors.d.ts.map +1 -1
  22. package/dist/adapter/sheets/providers/apps-script-gateway/errors.js +1 -0
  23. package/dist/adapter/sheets/providers/apps-script-gateway/errors.js.map +1 -1
  24. package/dist/adapter/sheets/providers/apps-script-gateway/index.d.ts +2 -0
  25. package/dist/adapter/sheets/providers/apps-script-gateway/index.d.ts.map +1 -1
  26. package/dist/adapter/sheets/providers/apps-script-gateway/index.js +1 -0
  27. package/dist/adapter/sheets/providers/apps-script-gateway/index.js.map +1 -1
  28. package/dist/adapter/sheets/providers/apps-script-gateway/operations/effect/effectOperation.d.ts +3 -1
  29. package/dist/adapter/sheets/providers/apps-script-gateway/operations/effect/effectOperation.d.ts.map +1 -1
  30. package/dist/adapter/sheets/providers/apps-script-gateway/operations/effect/effectOperation.js +9 -0
  31. package/dist/adapter/sheets/providers/apps-script-gateway/operations/effect/effectOperation.js.map +1 -1
  32. package/dist/adapter/sheets/providers/apps-script-gateway/operations/effect/effectOperationScript.d.ts.map +1 -1
  33. package/dist/adapter/sheets/providers/apps-script-gateway/operations/effect/effectOperationScript.js +137 -20
  34. package/dist/adapter/sheets/providers/apps-script-gateway/operations/effect/effectOperationScript.js.map +1 -1
  35. package/dist/adapter/sheets/providers/apps-script-gateway/operations/observation/observationOperation.js +82 -34
  36. package/dist/adapter/sheets/providers/apps-script-gateway/operations/observation/observationOperation.js.map +1 -1
  37. package/dist/adapter/sheets/providers/apps-script-gateway/operations/write/batchAppendOperation.d.ts +31 -0
  38. package/dist/adapter/sheets/providers/apps-script-gateway/operations/write/batchAppendOperation.d.ts.map +1 -0
  39. package/dist/adapter/sheets/providers/apps-script-gateway/operations/write/batchAppendOperation.js +567 -0
  40. package/dist/adapter/sheets/providers/apps-script-gateway/operations/write/batchAppendOperation.js.map +1 -0
  41. package/dist/adapter/sheets/providers/apps-script-gateway/operations/write/fastAppendOperation.d.ts +12 -17
  42. package/dist/adapter/sheets/providers/apps-script-gateway/operations/write/fastAppendOperation.d.ts.map +1 -1
  43. package/dist/adapter/sheets/providers/apps-script-gateway/operations/write/fastAppendOperation.js +27 -213
  44. package/dist/adapter/sheets/providers/apps-script-gateway/operations/write/fastAppendOperation.js.map +1 -1
  45. package/dist/adapter/sheets/providers/apps-script-gateway/transport/operationClient.d.ts.map +1 -1
  46. package/dist/adapter/sheets/providers/apps-script-gateway/transport/operationClient.js +81 -8
  47. package/dist/adapter/sheets/providers/apps-script-gateway/transport/operationClient.js.map +1 -1
  48. package/dist/adapter/sheets/providers/apps-script-gateway/transport/operationSyncGateway.d.ts +1 -6
  49. package/dist/adapter/sheets/providers/apps-script-gateway/transport/operationSyncGateway.d.ts.map +1 -1
  50. package/dist/adapter/sheets/providers/apps-script-gateway/transport/operationSyncGateway.js +15 -9
  51. package/dist/adapter/sheets/providers/apps-script-gateway/transport/operationSyncGateway.js.map +1 -1
  52. package/dist/application/orm/mapping/projection.js +1 -1
  53. package/dist/application/orm/mapping/projection.js.map +1 -1
  54. package/dist/application/orm/persistence/flush/flushCoordinator.d.ts.map +1 -1
  55. package/dist/application/orm/persistence/flush/flushCoordinator.js +1 -0
  56. package/dist/application/orm/persistence/flush/flushCoordinator.js.map +1 -1
  57. package/dist/application/orm/persistence/lifecycle/entityLifecycle.js +1 -1
  58. package/dist/application/orm/persistence/lifecycle/entityLifecycle.js.map +1 -1
  59. package/dist/application/sync/gateway/SyncGatewayBootstrap.d.ts +3 -1
  60. package/dist/application/sync/gateway/SyncGatewayBootstrap.d.ts.map +1 -1
  61. package/dist/application/sync/gateway/SyncGatewayBootstrap.js +3 -1
  62. package/dist/application/sync/gateway/SyncGatewayBootstrap.js.map +1 -1
  63. package/dist/application/sync/gateway/conflictProjection.d.ts +13 -0
  64. package/dist/application/sync/gateway/conflictProjection.d.ts.map +1 -0
  65. package/dist/application/sync/gateway/conflictProjection.js +58 -0
  66. package/dist/application/sync/gateway/conflictProjection.js.map +1 -0
  67. package/dist/application/sync/gateway/conflictProjectionRegistration.d.ts +11 -0
  68. package/dist/application/sync/gateway/conflictProjectionRegistration.d.ts.map +1 -0
  69. package/dist/application/sync/gateway/conflictProjectionRegistration.js +70 -0
  70. package/dist/application/sync/gateway/conflictProjectionRegistration.js.map +1 -0
  71. package/dist/application/sync/gateway/coordinator/CoordinatedSyncGateway.d.ts +102 -0
  72. package/dist/application/sync/gateway/coordinator/CoordinatedSyncGateway.d.ts.map +1 -0
  73. package/dist/application/sync/gateway/coordinator/CoordinatedSyncGateway.js +221 -0
  74. package/dist/application/sync/gateway/coordinator/CoordinatedSyncGateway.js.map +1 -0
  75. package/dist/application/sync/gateway/coordinator/asyncMutex.d.ts +51 -0
  76. package/dist/application/sync/gateway/coordinator/asyncMutex.d.ts.map +1 -0
  77. package/dist/application/sync/gateway/coordinator/asyncMutex.js +65 -0
  78. package/dist/application/sync/gateway/coordinator/asyncMutex.js.map +1 -0
  79. package/dist/application/sync/gateway/coordinator/coordinatorTelemetry.d.ts +31 -0
  80. package/dist/application/sync/gateway/coordinator/coordinatorTelemetry.d.ts.map +1 -0
  81. package/dist/application/sync/gateway/coordinator/coordinatorTelemetry.js +13 -0
  82. package/dist/application/sync/gateway/coordinator/coordinatorTelemetry.js.map +1 -0
  83. package/dist/application/sync/gateway/syncGateway.d.ts +26 -6
  84. package/dist/application/sync/gateway/syncGateway.d.ts.map +1 -1
  85. package/dist/application/sync/gateway/syncGateway.js +10 -2
  86. package/dist/application/sync/gateway/syncGateway.js.map +1 -1
  87. package/dist/application/sync/gateway/transportClassification.d.ts +62 -0
  88. package/dist/application/sync/gateway/transportClassification.d.ts.map +1 -0
  89. package/dist/application/sync/gateway/transportClassification.js +133 -0
  90. package/dist/application/sync/gateway/transportClassification.js.map +1 -0
  91. package/dist/application/sync/inbound/autoSystemConflictResolution.d.ts +26 -0
  92. package/dist/application/sync/inbound/autoSystemConflictResolution.d.ts.map +1 -0
  93. package/dist/application/sync/inbound/autoSystemConflictResolution.js +335 -0
  94. package/dist/application/sync/inbound/autoSystemConflictResolution.js.map +1 -0
  95. package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.d.ts +50 -0
  96. package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.d.ts.map +1 -0
  97. package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.js +97 -0
  98. package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.js.map +1 -0
  99. package/dist/application/sync/outbound/effects/SyncEffectSupervisor.d.ts +4 -1
  100. package/dist/application/sync/outbound/effects/SyncEffectSupervisor.d.ts.map +1 -1
  101. package/dist/application/sync/outbound/effects/SyncEffectSupervisor.js +35 -1
  102. package/dist/application/sync/outbound/effects/SyncEffectSupervisor.js.map +1 -1
  103. package/dist/application/sync/outbound/effects/SyncEffectWorker.d.ts +14 -2
  104. package/dist/application/sync/outbound/effects/SyncEffectWorker.d.ts.map +1 -1
  105. package/dist/application/sync/outbound/effects/SyncEffectWorker.js +190 -31
  106. package/dist/application/sync/outbound/effects/SyncEffectWorker.js.map +1 -1
  107. package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.d.ts +25 -2
  108. package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.d.ts.map +1 -1
  109. package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.js +25 -2
  110. package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.js.map +1 -1
  111. package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.d.ts +14 -7
  112. package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.d.ts.map +1 -1
  113. package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.js +118 -14
  114. package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.js.map +1 -1
  115. package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.d.ts +30 -9
  116. package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.d.ts.map +1 -1
  117. package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.js +102 -18
  118. package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.js.map +1 -1
  119. package/dist/application/sync/outbound/effects/SyncEffectWorkerTransitions.d.ts.map +1 -1
  120. package/dist/application/sync/outbound/effects/SyncEffectWorkerTransitions.js +49 -14
  121. package/dist/application/sync/outbound/effects/SyncEffectWorkerTransitions.js.map +1 -1
  122. package/dist/application/sync/outbound/reconciliation/ReconciliationScanner.d.ts.map +1 -1
  123. package/dist/application/sync/outbound/reconciliation/ReconciliationScanner.js +97 -17
  124. package/dist/application/sync/outbound/reconciliation/ReconciliationScanner.js.map +1 -1
  125. package/dist/application/sync/service/SyncServiceBootstrap.d.ts +17 -0
  126. package/dist/application/sync/service/SyncServiceBootstrap.d.ts.map +1 -1
  127. package/dist/application/sync/service/SyncServiceBootstrap.js +125 -10
  128. package/dist/application/sync/service/SyncServiceBootstrap.js.map +1 -1
  129. package/dist/application/sync/service/contracts.d.ts +2 -0
  130. package/dist/application/sync/service/contracts.d.ts.map +1 -1
  131. package/dist/application/sync/telemetry/syncTiming.d.ts +6 -0
  132. package/dist/application/sync/telemetry/syncTiming.d.ts.map +1 -1
  133. package/dist/application/sync/telemetry/syncTiming.js +2 -0
  134. package/dist/application/sync/telemetry/syncTiming.js.map +1 -1
  135. package/dist/domain/conflict/index.d.ts +1 -1
  136. package/dist/domain/conflict/index.d.ts.map +1 -1
  137. package/dist/domain/conflict/index.js +1 -1
  138. package/dist/domain/conflict/index.js.map +1 -1
  139. package/dist/domain/conflict/transitions.d.ts +3 -0
  140. package/dist/domain/conflict/transitions.d.ts.map +1 -1
  141. package/dist/domain/conflict/transitions.js +3 -2
  142. package/dist/domain/conflict/transitions.js.map +1 -1
  143. package/dist/domain/model/constants.d.ts +1 -0
  144. package/dist/domain/model/constants.d.ts.map +1 -1
  145. package/dist/domain/model/constants.js +1 -0
  146. package/dist/domain/model/constants.js.map +1 -1
  147. package/dist/infrastructure/storage/index.d.ts +4 -2
  148. package/dist/infrastructure/storage/index.d.ts.map +1 -1
  149. package/dist/infrastructure/storage/index.js +2 -1
  150. package/dist/infrastructure/storage/index.js.map +1 -1
  151. package/dist/infrastructure/storage/sqlite/migrateSchema.d.ts.map +1 -1
  152. package/dist/infrastructure/storage/sqlite/migrateSchema.js +59 -1
  153. package/dist/infrastructure/storage/sqlite/migrateSchema.js.map +1 -1
  154. package/dist/infrastructure/storage/sqlite/schema.d.ts +12 -1
  155. package/dist/infrastructure/storage/sqlite/schema.d.ts.map +1 -1
  156. package/dist/infrastructure/storage/sqlite/schema.js +47 -3
  157. package/dist/infrastructure/storage/sqlite/schema.js.map +1 -1
  158. package/dist/infrastructure/storage/sqlite/schemaTypes.d.ts +2 -2
  159. package/dist/infrastructure/storage/sqlite/schemaTypes.d.ts.map +1 -1
  160. package/dist/infrastructure/storage/state/canonical/canonicalCommit.js +1 -1
  161. package/dist/infrastructure/storage/state/canonical/canonicalCommit.js.map +1 -1
  162. package/dist/infrastructure/storage/state/mapped/mappedPersistenceSql.d.ts +1 -0
  163. package/dist/infrastructure/storage/state/mapped/mappedPersistenceSql.d.ts.map +1 -1
  164. package/dist/infrastructure/storage/state/mapped/mappedPersistenceSql.js +1 -1
  165. package/dist/infrastructure/storage/state/mapped/mappedPersistenceSql.js.map +1 -1
  166. package/dist/infrastructure/storage/state/observation/observationAudit.js +5 -1
  167. package/dist/infrastructure/storage/state/observation/observationAudit.js.map +1 -1
  168. package/dist/infrastructure/storage/state/resolution/resolutionWriter.d.ts +10 -0
  169. package/dist/infrastructure/storage/state/resolution/resolutionWriter.d.ts.map +1 -1
  170. package/dist/infrastructure/storage/state/resolution/resolutionWriter.js +106 -20
  171. package/dist/infrastructure/storage/state/resolution/resolutionWriter.js.map +1 -1
  172. package/dist/infrastructure/storage/state/resolution/resolutionWriterContracts.d.ts +9 -1
  173. package/dist/infrastructure/storage/state/resolution/resolutionWriterContracts.d.ts.map +1 -1
  174. package/dist/infrastructure/storage/state/resolution/resolutionWriterContracts.js +3 -0
  175. package/dist/infrastructure/storage/state/resolution/resolutionWriterContracts.js.map +1 -1
  176. package/dist/infrastructure/storage/state/resolution/resolutionWriterSql.d.ts +11 -0
  177. package/dist/infrastructure/storage/state/resolution/resolutionWriterSql.d.ts.map +1 -1
  178. package/dist/infrastructure/storage/state/resolution/resolutionWriterSql.js +35 -0
  179. package/dist/infrastructure/storage/state/resolution/resolutionWriterSql.js.map +1 -1
  180. package/dist/infrastructure/storage/sync/outbound/effectOutbox.d.ts +12 -4
  181. package/dist/infrastructure/storage/sync/outbound/effectOutbox.d.ts.map +1 -1
  182. package/dist/infrastructure/storage/sync/outbound/effectOutbox.js +46 -15
  183. package/dist/infrastructure/storage/sync/outbound/effectOutbox.js.map +1 -1
  184. package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.d.ts +24 -0
  185. package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.d.ts.map +1 -1
  186. package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.js +1 -0
  187. package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.js.map +1 -1
  188. package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.d.ts +9 -7
  189. package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.d.ts.map +1 -1
  190. package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.js +44 -8
  191. package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.js.map +1 -1
  192. package/dist/infrastructure/storage/sync/outbound/effectOutboxSupport.d.ts +3 -1
  193. package/dist/infrastructure/storage/sync/outbound/effectOutboxSupport.d.ts.map +1 -1
  194. package/dist/infrastructure/storage/sync/outbound/effectOutboxSupport.js +35 -2
  195. package/dist/infrastructure/storage/sync/outbound/effectOutboxSupport.js.map +1 -1
  196. package/dist/infrastructure/storage/sync/outbound/reconciliationSql.js +1 -0
  197. package/dist/infrastructure/storage/sync/outbound/reconciliationSql.js.map +1 -1
  198. package/dist/infrastructure/storage/sync/shared/spreadsheetAuthority.d.ts +31 -0
  199. package/dist/infrastructure/storage/sync/shared/spreadsheetAuthority.d.ts.map +1 -0
  200. package/dist/infrastructure/storage/sync/shared/spreadsheetAuthority.js +92 -0
  201. package/dist/infrastructure/storage/sync/shared/spreadsheetAuthority.js.map +1 -0
  202. package/dist/infrastructure/storage/sync/shared/writerLease.js +3 -1
  203. package/dist/infrastructure/storage/sync/shared/writerLease.js.map +1 -1
  204. package/docs/advanced-sheets-gateway-concurrency-problem.md +434 -0
  205. package/docs/architecture.md +53 -11
  206. package/docs/google-sheets-sync-scaling-strategy.md +459 -0
  207. package/docs/quick-start.md +15 -1
  208. package/docs/sync-bulk-write-benchmark.md +520 -0
  209. package/docs/sync-observability.md +39 -3
  210. package/docs/write-and-synchronization-flow.md +76 -17
  211. package/package.json +1 -1
@@ -1,5 +1,386 @@
1
1
  # Sync bulk-write benchmark
2
2
 
3
+ ## Black-box server and Locust workload
4
+
5
+ The test-only server harness treats Hikoutei as a server-side library rather
6
+ than calling the sync worker directly from a benchmark script:
7
+
8
+ ```sh
9
+ node --env-file=.env .local/hikoutei-load-server.mjs
10
+ ```
11
+
12
+ It binds to `127.0.0.1:8787` by default and reports a run-specific persistent
13
+ SQLite path, System_State/User_Input tab names, and JSONL log path from
14
+ `GET /health`. It does not delete the database or Sheet data on shutdown.
15
+
16
+ Run the mixed workload from another terminal:
17
+
18
+ ```sh
19
+ locust -f .local/locustfile.py \
20
+ --host http://127.0.0.1:8787 \
21
+ --users 20 \
22
+ --spawn-rate 5 \
23
+ --run-time 5m \
24
+ --headless
25
+ ```
26
+
27
+ The Locust user weights are read 40%, create 20%, application update 25%, and
28
+ User_Input simulation 15%. The User_Input task calls the test-only
29
+ `/__test/user-input` endpoint, which changes the real Sheet and leaves the
30
+ normal polling service to apply the value to SQLite. The server records HTTP,
31
+ flush, Gateway, polling, error, and over-30-second events in the JSONL log;
32
+ the over-30-second observer never stops the server or workload. Inspect
33
+ `GET /metrics` after Locust stops, then stop the server with `Ctrl-C`; the
34
+ persistent run data remains for inspection.
35
+
36
+ ## 2026-08-03 — black-box Locust run and configuration diagnosis
37
+
38
+ - Server run: `load-1785732937019-23536468`
39
+ - Log: `.local/hikoutei-load-load-1785732937019-23536468.jsonl`
40
+ - Workload: `.local/locustfile.py` mixed CRUD/User_Input tasks
41
+ - Final HTTP log: **1,508** requests; 1,201 successful responses and 307
42
+ failures, including 264 creates, 582 reads, 375 updates, and 209 User_Input
43
+ requests
44
+ - Gateway log: **444** requests; 246 successes, 190 operation failures, and 8
45
+ timeouts; 4 requests exceeded 30 seconds
46
+ - Final local state snapshot: **264** SQLite entity rows, 332 applied effects,
47
+ 7 blocked candidates, 601 pending effects, and 234 processing effects
48
+
49
+ The test did create visible tabs, but the server read a different
50
+ `TYPED_SHEETS_GATEWAY_SHEET_ID` from the local `.env` than the spreadsheet ID
51
+ provided for the intended target. On the spreadsheet actually used by the
52
+ server, the following tabs were visible: `TS_Load_load-1785732937019-23536468_System`
53
+ (265 rows including the header) and `TS_Load_load-1785732937019-23536468_Input`
54
+ (26 rows including the header). The apparent missing table was therefore a
55
+ configuration-target mismatch, not a provisioning omission. The intended sheet
56
+ ID must be placed in `.env` before the next run; the shared secret must also be
57
+ valid for that deployed gateway/sheet configuration.
58
+
59
+ The run was not a clean functional pass. Concurrent User_Input simulation and
60
+ polling contended on the Apps Script observation lock, producing repeated
61
+ `Could not acquire the sync observation gateway lock` errors; the resulting
62
+ failed effects then blocked later ORM writes with `user_input projection is
63
+ blocked: latest effect is failed`. The persistent SQLite file and actual test
64
+ Sheet data were retained for diagnosis.
65
+
66
+ ## 2026-08-03 — lock-refactor Locust run
67
+
68
+ - Branch: `perf/adaptive-sync-performance`
69
+ - Server run: `load-1785737483461-793bddd2`
70
+ - Exact server command: `node --env-file=.env .local/hikoutei-load-server.mjs`
71
+ - Exact Locust command: `/usr/local/bin/locust -f .local/locustfile.py --host http://127.0.0.1:8787`
72
+ (user count and duration were controlled from the Locust UI)
73
+ - Log: `.local/hikoutei-load-load-1785737483461-793bddd2.jsonl`
74
+ - Database: `.local/hikoutei-load-load-1785737483461-793bddd2.sqlite`
75
+ - Backend: local Node `v24.3.0`, persistent SQLite/MikroORM, deployed Apps Script
76
+ gateway, and the real target Sheet
77
+ - Scenario: mixed read/create/update/User_Input workload; HTTP activity ran from
78
+ `2026-08-03T06:12:25Z` through `2026-08-03T06:17:44Z` (about 5m 19s)
79
+ - Setup: one provisioning Gateway call, 4,013 ms, excluded from steady-state
80
+ counts below
81
+
82
+ | Metric | No-setup / steady-state result | Full recorded run |
83
+ | --- | ---: | ---: |
84
+ | HTTP requests | 4,883 workload requests | 4,907 through workload stop, including 21 health, 2 metrics, and 1 unknown request |
85
+ | HTTP success/failure | 3,119 / 1,764 workload responses | 3,142 / 1,765 responses |
86
+ | Gateway requests | 763 sync calls; 718 success / 45 failure | 764 including setup |
87
+ | HTTP latency | p50 5.16 ms; p95 2,820.89 ms | max 60,004.28 ms |
88
+ | Gateway latency | p50 2,312 ms; p95 30,314 ms | max 60,004 ms; 35 over 30 s |
89
+
90
+ Workload response breakdown: create 505 successful and 522 failed; read 1,957
91
+ successful; update 582 successful and 603 failed; User_Input 75 returned 202,
92
+ 598 returned 404 before the row was projected, and 41 returned 500. Gateway
93
+ failures during the workload were 22 invalid responses, 15 `method_not_allowed`
94
+ responses, 6 timeouts, and 2 remote operation failures. No
95
+ `Could not acquire the sync observation gateway lock` message occurred during
96
+ the workload window. One such lock error appeared later while background
97
+ polling continued after Locust stopped.
98
+
99
+ The dominant workload failure was projection poisoning: 1,124 HTTP operation
100
+ errors reported `user_input projection is blocked: latest effect is
101
+ blocked_candidate`. The workload also recorded 7 polling errors for invalid
102
+ observed projection evidence. A post-load metrics snapshot showed 505 entity
103
+ rows, 656 applied outbox effects, 25 blocked candidates, 61 failed effects,
104
+ 1,264 pending effects, and 177 processing effects; this snapshot includes
105
+ background synchronization after the Locust workload stopped. The Locust UI
106
+ process remained open without new workload requests, and the server remained
107
+ alive for diagnosis. All Sheet, SQLite, and JSONL data were retained.
108
+
109
+ ## 2026-08-03 — pending User_Input retry Locust run
110
+
111
+ - Branch: `perf/adaptive-sync-performance`
112
+ - Server run: `load-1785738645774-79e64130`
113
+ - Exact server command: `node --env-file=.env .local/hikoutei-load-server.mjs`
114
+ - Exact Locust command: `/usr/local/bin/locust -f .local/locustfile.py --host http://127.0.0.1:8787`
115
+ (user count and duration were controlled from the Locust UI)
116
+ - Log: `.local/hikoutei-load-load-1785738645774-79e64130.jsonl`
117
+ - Database: `.local/hikoutei-load-load-1785738645774-79e64130.sqlite`
118
+ - Backend: local Node `v24.3.0`, persistent SQLite/MikroORM, deployed Apps Script
119
+ gateway, and the real target Sheet
120
+ - Scenario: mixed read/create/update/User_Input workload with bounded User_Input
121
+ retry/backoff; workload ran from `2026-08-03T06:31:37Z` through
122
+ `2026-08-03T06:36:58Z` (about 5m 21s)
123
+ - Setup: one provisioning call and pre-load worker warm-up excluded from the
124
+ steady-state Gateway counts
125
+
126
+ | Metric | No-setup / steady-state result | Locust result |
127
+ | --- | ---: | ---: |
128
+ | Requests | 7,754 CRUD/User_Input requests | 7,774 total requests |
129
+ | Success/failure | 4,519 / 3,235 workload responses | fail ratio **41.61%** |
130
+ | Gateway | 1,173 calls; 1,165 success / 8 failure | p50 2,132 ms; p95 6,624 ms |
131
+ | HTTP latency | aggregate median 10 ms; p95 2,300 ms | max 52,409 ms |
132
+ | Slow Gateway calls | 12 over 30 seconds | max 52,407 ms |
133
+
134
+ Locust's stopped report had 3,116 successful reads, 1,590 creates with 1,386
135
+ failures, 1,924 updates with 1,698 failures, and 1,124 User_Input attempts
136
+ with 151 failures. Of the User_Input failures, 148 reached the bounded retry
137
+ deadline and 3 were HTTP 500 responses. The harness therefore stopped counting
138
+ most projection-lag 404 responses as immediate Locust failures, but those rows
139
+ still did not become successful Sheet edits: the server log recorded 943 404s
140
+ and 188 successful 202 edits.
141
+
142
+ The main failure remained projection poisoning. The server recorded 3,084
143
+ `user_input projection is blocked: latest effect is blocked_candidate` errors,
144
+ matching the create/update failures. During the workload, the Gateway had 3
145
+ remote operation failures, 4 invalid responses, and 1 `method_not_allowed`
146
+ response; polling also recorded 2 observation-lock acquisition failures and 7
147
+ invalid observed-evidence errors. The retry queue improved classification of
148
+ asynchronous row lag, but it cannot repair a projection whose effect is already
149
+ blocked. A post-load metrics snapshot showed 204 entity rows, 360 applied
150
+ outbox effects, 25 blocked candidates, 2 failed effects, 397 pending effects,
151
+ and 93 processing effects. The Locust state was stopped with zero users; the
152
+ server and all generated data remain available for diagnosis.
153
+
154
+ Compared with the previous lock-refactor run, the retry queue reduced User_Input
155
+ failures reported by Locust from immediate row-not-found failures to 148
156
+ retry-deadline failures, but the underlying blocked-candidate cascade remains
157
+ the next bottleneck. The two observation-lock failures also confirm that the
158
+ remaining contention is in the retained full-observation path, not the
159
+ lock-free values-only read.
160
+
161
+ ## 2026-08-03 — Sync_Conflicts and automatic system-wins Locust run
162
+
163
+ - Branch: `perf/adaptive-sync-performance`
164
+ - Server command: `node --env-file=.env .local/hikoutei-load-server.mjs`
165
+ - Locust command: `/usr/local/bin/locust -f .local/locustfile.py --host http://127.0.0.1:8787` (user count and duration were controlled from the Locust UI)
166
+ - Server run: `load-1785748381328-41f5654c`
167
+ - Log: `.local/hikoutei-load-load-1785748381328-41f5654c.jsonl`
168
+ - Database: `.local/hikoutei-load-load-1785748381328-41f5654c.sqlite`
169
+ - Backend: local Node `v24.3.0`, persistent SQLite/MikroORM, deployed Apps Script gateway, and the real target Sheet
170
+ - Scenario: mixed read/create/update/User_Input workload with mandatory
171
+ `System_State`, `User_Input`, and `Sync_Conflicts` projections
172
+ - Workload window: `2026-08-03T09:13:59.580Z`–`2026-08-03T09:19:17.434Z`
173
+ (about 5m 18s); setup provisioning took 5,436 ms and is excluded below
174
+ - Dataset/result: 456 SQLite entity rows and 127 captured conflicts
175
+
176
+ | Metric | No-setup / steady-state result | Full server record |
177
+ | --- | ---: | ---: |
178
+ | Application HTTP requests | 2,648 workload requests | 2,669 including 21 health requests before the workload endpoint closed |
179
+ | HTTP outcome | 2,068 2xx / 246 projection-lag 404 / 334 500 | 2,089 2xx / 246 404 / 334 500 |
180
+ | Locust-effective failure ratio | **334 / 2,648 = 12.61%** | 404 responses were marked success by the bounded retry queue |
181
+ | Gateway requests | 428 sync calls; 308 success / 120 failure | 429 including setup |
182
+ | Gateway latency | p50 4,344 ms; p95 34,246 ms | max 60,011 ms; 92 over 30 s; 4 over 60 s |
183
+
184
+ Application breakdown was 456 successful creates and 93 failed creates, 1,044
185
+ successful reads, 526 successful updates and 130 failed updates, and 42
186
+ successful User_Input changes, 246 projection-lag 404s, and 111 failed
187
+ User_Input requests. The main errors were 223 requests blocked by a previously
188
+ failed `user_input` effect, 90 invalid Gateway JSON responses, 17
189
+ `Use a signed POST request` responses, and 4 Gateway timeouts. During the
190
+ workload window there were 2 polling evidence errors and 1 effect-worker
191
+ postcondition/claim mismatch. The 404s are not counted as Locust failures by
192
+ `locustfile.py`; they remain bounded pending projection retries.
193
+
194
+ The new conflict path itself behaved correctly in SQLite: all **127/127**
195
+ `sync_conflict` rows ended `RESOLVED`, all **127/127**
196
+ `acknowledge_system` commands ended `applied` with role `sync_operator`, and
197
+ there were **0** active candidate pointers. No `latest effect is
198
+ blocked_candidate` cascade appeared in the server errors; the remaining
199
+ application failures were caused by failed Gateway/materialization effects.
200
+ The durable outbox retained the remote work for recovery. At the post-load
201
+ metrics snapshot (after background synchronization continued), the outbox was
202
+ `applied=390`, `pending=1,267`, `processing=60`, `failed=375`,
203
+ `blocked_candidate=4`, and `superseded=127` at the
204
+ `2026-08-03T09:26:09Z` metrics snapshot. The 127 conflict-audit effects
205
+ were `30 applied`, `77 pending`, and `20 failed`, so SQLite resolution was
206
+ committed even though the remote `Sync_Conflicts` projection had not fully
207
+ converged.
208
+
209
+ This is a functional improvement over the preceding retry-queue run: the
210
+ unresolved conflict/candidate cascade was eliminated and the observed failure
211
+ ratio was lower, but it is **not a clean remote-delivery benchmark**. The
212
+ remaining bottleneck is Apps Script instability/latency (`invalid JSON`,
213
+ `method_not_allowed`, operation failures, and timeouts), which poisoned
214
+ System_State/User_Input effects and left a large durable backlog. The server
215
+ and all Sheet, SQLite, and JSONL artifacts were retained; the server continued
216
+ running after Locust stopped, so later background metrics are explicitly not
217
+ part of the steady-state workload numbers.
218
+
219
+ A later background-only inspection at `2026-08-03T09:50:42Z` found that the
220
+ server was still alive but the remote projection had not converged: SQLite now
221
+ contained **333** resolved conflicts (up from 127 at the workload snapshot),
222
+ with zero active candidate pointers. This increase represents the same
223
+ User_Input edits being observed again while their canonical reconcile effects
224
+ remain failed/pending; it is not evidence that SQLite left conflicts open, but
225
+ it does show that a continuously running worker cannot drain a permanently
226
+ unavailable remote projection. The outbox then stood at `applied=794`,
227
+ `pending=887`, `processing=64`, `failed=553`, `blocked_candidate=4`, and
228
+ `superseded=333`; the server was still handling a long Gateway append when the
229
+ 20-second before/after check was taken. This follow-up is diagnostic and is not
230
+ part of the steady-state workload result above.
231
+
232
+ ## 2026-08-02 — performance branch baseline
233
+
234
+ - Branch: `perf/adaptive-sync-performance`
235
+ - Base: `origin/develop` at `63ebddc`, including the merged contract PRs #155 and #156
236
+ - Environment: macOS arm64, Node `v24.3.0`, npm `11.4.2`
237
+
238
+ ### Local implementation verification (synthetic, no live Sheets)
239
+
240
+ These commands exercise the in-process SQLite authority plus fake/in-memory
241
+ Apps Script gateways. They are correctness and contract checks, not latency
242
+ measurements: no real spreadsheet, credentials, or Apps Script quota is
243
+ involved, so timings are not comparable to the live baselines below.
244
+
245
+ - `npm test` — 30 files, 197 tests passed
246
+ - `npm run typecheck` — passed
247
+ - `npm run typecheck:test` — passed
248
+ - `npm run build` — passed
249
+ - `npm pack --dry-run` — passed
250
+ - `git diff --check` — passed
251
+
252
+ The regression suite now covers the adaptive/outbound work directly with
253
+ synthetic fixtures: the values-only preflight escalation rules, formula/merged/
254
+ error deferral to the periodic safety full scan, safety-scan scheduling and
255
+ coalescing, backlog convergence across worker passes, no full read on unchanged
256
+ adaptive passes, response-loss backoff, and the new internal polling timing
257
+ phases emitted through the diagnostics sink (canonical state read, values-only
258
+ read, fast comparison, full metadata observation, persistence, overall, and safety-scan lag).
259
+
260
+ ### 2026-08-02 — local synthetic latency sample
261
+
262
+ - Branch: `perf/adaptive-sync-performance`
263
+ - Exact command: `./node_modules/.bin/vite-node scripts/bench-local-sync-performance.ts`
264
+ - Backend: in-process SQLite/MikroORM plus `FakeSyncSheetGateway`; no network or Apps Script quota
265
+ - Dataset: 66 unchanged User_Input rows; 7 steady-state samples per mode
266
+ - Setup: entity seeding, effect delivery, and first warm-up pass excluded
267
+
268
+ | Scenario | No-setup / steady-state result | Gateway calls during run |
269
+ | --- | ---: | --- |
270
+ | Adaptive values-only polling | mean **1.604 ms** (min 1.271 / max 2.582) | 7 values-only reads, 0 full snapshots |
271
+ | Full metadata polling | mean **5.642 ms** (min 5.228 / max 6.656) | 0 values-only reads, 7 full snapshots |
272
+ | 66-row outbound update | SQLite flush **68.988 ms**; delivery **216.973 ms**; 66 applied | 8 `applyEffects` calls |
273
+
274
+ The local adaptive sample is about **3.5× faster** than the local full path in
275
+ this no-network fixture, but it is not comparable to the historical live
276
+ 27.7-second versus 2.2-second measurements below. The useful correctness signal
277
+ is that unchanged adaptive samples used no full metadata reads. The live
278
+ measurement below confirms the same behavior through the deployed gateway.
279
+
280
+ ### 2026-08-02 — live Apps Script polling smoke benchmark
281
+
282
+ - Branch: `perf/adaptive-sync-performance`
283
+ - Exact command: `LIVE_POLL_ONLY=1 node --env-file=.env .local/user-input-polling-live.mjs`
284
+ - Slow-request guard: synchronization Gateway requests with `durationMs > 30,000` fail the benchmark; provisioning and cleanup requests are recorded but excluded from this failure gate
285
+ - Backend: deployed Apps Script gateway and real Google Sheet
286
+ - Dataset: 1 entity row, System_State + User_Input projections
287
+ - Setup: temporary-tab provisioning and first safety scan are reported separately;
288
+ steady-state polling excludes provisioning
289
+
290
+ | Scenario | No-setup / steady-state result | Evidence |
291
+ | --- | ---: | --- |
292
+ | Initial unchanged adaptive poll | **2,504 ms** | `mode=adaptive`, `fullMetadataTables=0`, 1 values-only read, 0 full snapshots |
293
+ | Remote edit request | **1,663.4 ms** | Apps Script `setValue()` + `flush()` |
294
+ | Changed adaptive poll | **3,859 ms** | 1,580 ms values-only read + 2,262 ms full metadata read; SQLite status became `approved` |
295
+ | First ORM insert flush | **12.39 ms** | SQLite/outbox commit |
296
+ | Insert remote delivery | **5,185.8 ms** | 2 effects applied |
297
+
298
+ The service's automatic initial safety scan took **2,567 ms** and used one full
299
+ metadata request. The latency guard inspected **8 synchronization Gateway
300
+ requests** and found **0** over the 30-second threshold. Temporary Sheet tabs
301
+ were cleaned up after the run. This live run proves the current harness reaches
302
+ the deployed gateway and that unchanged polling avoids the full metadata request
303
+ while changed polling escalates and persists correctly. The JSON result includes
304
+ `gatewayLatencyGuard` with the threshold and guarded-request summary. A
305
+ functional failure (for example, the SQLite value is not `approved`) is reported
306
+ separately from a `slow_sync_gateway_operation` latency failure.
307
+
308
+ #### Post-guard verification rerun
309
+
310
+ - Commands:
311
+ - `LIVE_POLL_ONLY=1 node --env-file=.env .local/user-input-polling-live.mjs`
312
+ - `LIVE_CRUD_ONLY=1 node --env-file=.env .local/user-input-polling-live.mjs`
313
+ - `node --env-file=.env .local/user-input-polling-live.mjs`
314
+ - Dataset: 1 row per isolated run; provisioning excluded from steady-state values
315
+ - Functional result: all runs completed; User_Input status became `approved`; all
316
+ CRUD deliveries applied 2 effects per operation; no conflicts
317
+
318
+ | Run | No-setup / steady-state result | Guard result |
319
+ | --- | --- | --- |
320
+ | `LIVE_POLL_ONLY=1` | Initial poll **2,304 ms**; changed poll **5,345 ms**; insert delivery **6,194.66 ms** | 8 guarded requests, 0 slow |
321
+ | `LIVE_CRUD_ONLY=1` | Initial poll **2,153 ms**; update delivery **8,515.16 ms**; delete delivery **6,372.73 ms** | 9 guarded requests, 0 slow |
322
+ | Full harness | Initial poll **2,728 ms**; steady polls **3,141.38 / 2,489.48 / 2,227.51 ms**; update delivery **7,593.92 ms**; delete delivery **7,249.12 ms** | 15 guarded requests, 0 slow |
323
+
324
+ #### Quota observation for this verification batch
325
+
326
+ The three live runs above made **38 external Apps Script Web App POSTs** in total:
327
+ 32 synchronization requests, 3 provisioning requests, and 3 cleanup requests. Every observed gateway request returned HTTP 200; no 403/429,
328
+ `RESOURCE_EXHAUSTED`, or quota-related gateway error occurred. The 30-second
329
+ gateway guard also found 0 slow synchronization requests.
330
+
331
+ This is an observed sample, not a remaining-quota counter. The current gateway
332
+ uses Apps Script `SpreadsheetApp`, not the Google Sheets REST API, so the Sheets
333
+ REST request quota cannot be inferred from these POST counts. Apps Script
334
+ service/runtime quotas are account- and project-dependent, and Google does not
335
+ return their remaining amount through this harness. The exact remaining quota
336
+ must be checked in the linked Google Cloud/API or Apps Script execution/usage
337
+ console.
338
+
339
+ ### 2026-08-02 — progressive live bidirectional verification
340
+
341
+ - Branch: `perf/adaptive-sync-performance`
342
+ - Exact command: `node --env-file=.env .local/progressive-bidirectional-live.mjs`
343
+ - Backend: deployed Apps Script gateway and real Google Sheet
344
+ - Matrix: cumulative **1 → 10 → 25 → 66 → 100 rows** in one isolated service
345
+ - Each stage performed both directions: app flush/effect delivery, bulk User_Input
346
+ edit, changed polling, SQLite verification, and one unchanged polling pass
347
+ - Policy: 30-second Gateway requests were recorded but never stopped the matrix;
348
+ all stages completed before the final result was reported
349
+
350
+ | Rows | Added | App flush | Outbound delivery | Changed poll | Unchanged poll | Max Gateway request | Result |
351
+ | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |
352
+ | 1 | 1 | 12.25 ms | 6,304.94 ms | 4,003.09 ms | 1,768.76 ms | 3,510 ms | passed |
353
+ | 10 | 9 | 13.18 ms | 13,100.62 ms | 10,130.85 ms | 2,356.88 ms | 8,121 ms | passed |
354
+ | 25 | 15 | 16.71 ms | 16,375.68 ms | 7,866.09 ms | 2,037.32 ms | 8,462 ms | passed |
355
+ | 66 | 41 | 34.96 ms | 78,406.70 ms | 16,210.92 ms | 1,845.36 ms | 19,087 ms | passed |
356
+ | 100 | 34 | 23.89 ms | 120,235.71 ms | 27,529.99 ms | 1,881.89 ms | 26,694 ms | passed |
357
+
358
+ All stages persisted the expected row count, changed polling applied every
359
+ expected row with one full-metadata escalation and zero conflicts, and unchanged
360
+ polling stayed on the values-only path. The matrix made **47 synchronization
361
+ Gateway requests** (49 including one provisioning and one cleanup request),
362
+ found **0** over 30 seconds, and had no functional or cleanup failure. The 66- and 100-row worker/stage totals exceeded 30 seconds because they
363
+ contained multiple Gateway requests; no individual synchronization Gateway
364
+ request exceeded the threshold.
365
+
366
+ This is a one-row smoke benchmark, not a 66-row throughput result. Network
367
+ latency, Apps Script execution variance, Sheet lock contention, and quota remain
368
+ caveats. The progressive result above is still a single 100-row cumulative run,
369
+ not a repeated production-load benchmark. Historical live comparison baselines
370
+ from earlier branches remain:
371
+
372
+ | Scenario | Setup | No-setup / steady-state result | Source |
373
+ | --- | ---: | ---: | --- |
374
+ | Full snapshot polling, 66 rows | excluded | 27,652 ms steady state | 2026-07-27 phase trace below |
375
+ | Values-only polling, 66 rows | excluded | 2,240 ms steady state | 2026-07-27 lightweight polling below |
376
+ | Outbound 20-order stage | excluded | 28,182 ms / 5.0 rows/s | 2026-07-27 timing run below |
377
+ | Outbound 370-order stage | excluded | 36,865 ms / 30.1 rows/s | 2026-07-27 progressive run below |
378
+
379
+ SQLite remains the application authority and Sheets the asynchronous projection:
380
+ full metadata fidelity, response-loss recovery, and the periodic safety full scan
381
+ stay required while the values-only fast path avoids remote full reads on
382
+ unchanged passes.
383
+
3
384
  ## 2026-07-24 — raw Apps Script write
4
385
 
5
386
  - Branch: `benchmark/apps-script-bulk-write`
@@ -1034,3 +1415,142 @@ events or canonical writes.
1034
1415
  - The result strongly indicates that the current Gateway validation and
1035
1416
  metadata path, rather than raw `setValues()` throughput, is the dominant
1036
1417
  bottleneck.
1418
+
1419
+ ## 2026-08-03 — clean Locust smoke after adaptive-sync implementation
1420
+
1421
+ - Branch: `perf/adaptive-sync-performance`
1422
+ - Command:
1423
+ `locust -f .local/locustfile.py --host http://127.0.0.1:8787 --headless
1424
+ --users 10 --spawn-rate 2 --run-time 60s --csv
1425
+ .local/locust-20260803-231300-clean2 --html
1426
+ .local/locust-20260803-231300-clean2.html --only-summary`
1427
+ - Server: `.local/hikoutei-load-server.mjs`, Node.js 24.3, local SQLite,
1428
+ deployed Apps Script Gateway, fresh run ID `load-20260803-231003-clean2`
1429
+ - Dataset/scenario: fresh SQLite and fresh `System_State`, `User_Input`, and
1430
+ `Sync_Conflicts` projection tabs; 10 mixed Locust users; 2 users/second
1431
+ ramp; 60-second workload window.
1432
+ - No-setup/steady-state scope: server provisioning and startup were excluded;
1433
+ the table below starts after `/health` became ready and includes only the
1434
+ Locust workload window. The separate drain snapshot includes background
1435
+ worker/polling traffic after Locust stopped.
1436
+
1437
+ | Workload | Requests | Failures | p50 | p95 | Maximum | Throughput |
1438
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: |
1439
+ | `GET /users/:id` | 134 | 0 (0%) | 2 ms | 4 ms | 6 ms | 2.29/s |
1440
+ | `POST /users` | 81 | 0 (0%) | 5 ms | 6 ms | 19 ms | 1.38/s |
1441
+ | `PATCH /users/:id` | 76 | 0 (0%) | 5 ms | 8 ms | 22 ms | 1.30/s |
1442
+ | `POST /__test/user-input` | 38 | 8 (21.05%) | 2.2 s | 31.0 s | 31.9 s | 0.65/s |
1443
+ | **Aggregated** | **339** | **8 (2.36%)** | **4 ms** | **2.3 s** | **31.9 s** | **5.78/s** |
1444
+
1445
+ The server-side Gateway snapshot after the workload and approximately two
1446
+ minutes of background drain contained 100 Gateway requests, 34 failures, p50
1447
+ 3.71 s, p95 33.47 s, and maximum 60.00 s. The failures were dominated by
1448
+ intermittent non-JSON/timeout Gateway responses. The SQLite outbox had 306
1449
+ `pending`, 8 `processing`, 2 `applied`, and 2 `superseded` effects at the
1450
+ snapshot; it had not converged, so this is not a successful drain benchmark.
1451
+
1452
+ Compared with the earlier 2,648-request run (12.61% HTTP failures, Gateway
1453
+ p50 4.34 s, p95 34.25 s, maximum 60.01 s), the clean smoke had a lower HTTP
1454
+ failure rate for entity create/read/update traffic, but its User_Input path
1455
+ still failed and the outbox did not drain. The runs are not a production
1456
+ performance comparison until the Gateway deployment is stable and the
1457
+ background outbox converges under the same workload.
1458
+
1459
+ Artifacts:
1460
+
1461
+ - `.local/locust-20260803-231300-clean2_stats.csv`
1462
+ - `.local/locust-20260803-231300-clean2_failures.csv`
1463
+ - `.local/locust-20260803-231300-clean2.html`
1464
+ - `.local/hikoutei-load-load-20260803-231003-clean2.jsonl`
1465
+
1466
+ Caveats: the Gateway returned intermittent HTTP 404/non-JSON responses and
1467
+ 60-second client timeouts during the run; Python 3.9 emitted Locust's
1468
+ end-of-life warning; and the remote deployment was not independently
1469
+ re-deployed during this measurement.
1470
+
1471
+ ## 2026-08-03 — Gateway baseline transport gate (stopped)
1472
+
1473
+ - Branch: `perf/adaptive-sync-performance`
1474
+ - Command:
1475
+ `node --env-file=.env --input-type=module <gateway-baseline-probe>`
1476
+ - Exact probe: 100 sequential (`concurrency=1`) signed `applyOperations` calls
1477
+ using a read-only function that returned the bound spreadsheet ID and an
1478
+ integer nonce; no Sheet rows or tabs were created or changed.
1479
+ - Environment: current built `dist/`, configured Apps Script Gateway URL,
1480
+ configured shared secret, configured spreadsheet ID; secrets and IDs are not
1481
+ recorded here.
1482
+ - No-setup/steady-state scope: every request was measured after the client was
1483
+ constructed; there was no application or Sheet setup work in the request
1484
+ loop.
1485
+
1486
+ | Requests | Success | Failure | Failure rate | p50/p95/maximum | Duration |
1487
+ | ---: | ---: | ---: | ---: | --- | ---: |
1488
+ | 100 | 0 | 100 | **100%** | not applicable (all HTTP 405) | 425,373 ms |
1489
+
1490
+ Every failure decoded as `invalid_sync_gateway_response` with HTTP 405 and the
1491
+ message `Code.gs response was not valid JSON`. This exceeds the 1% stop
1492
+ criterion, so no further live sync/load refactor or User_Input migration should
1493
+ be accepted until the deployed Gateway URL, redirect/method preservation,
1494
+ permissions, deployment version, and quota/runtime path are isolated. This
1495
+ probe is diagnostic only and does not establish Sheet convergence.
1496
+
1497
+ Artifact: `.local/gateway-baseline-100-20260804.json`.
1498
+
1499
+ ## 2026-08-04 — Gateway baseline transport gate (redeployed, passed)
1500
+
1501
+ - Branch: `perf/adaptive-sync-performance`
1502
+ - Command: ephemeral signed no-op probe using the built
1503
+ `AppsScriptOperationClient`; Gateway credentials were supplied only through
1504
+ process environment and are not recorded.
1505
+ - Exact probe: 100 sequential (`concurrency=1`) signed `applyOperations` calls
1506
+ using the same read-only function as the stopped baseline. No Sheet rows or
1507
+ tabs were created or changed.
1508
+ - Environment: redeployed Apps Script Web App `/exec`, current built `dist/`,
1509
+ 60-second client timeout. Setup was excluded from the measured loop.
1510
+
1511
+ | Requests | Success | Failure | Failure rate | p50 | p95 | Maximum | Duration |
1512
+ | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
1513
+ | 100 | 100 | 0 | **0%** | 1,956 ms | 4,511 ms | 20,994 ms | 254,786 ms |
1514
+
1515
+ All responses were valid HTTP 200 JSON envelopes. Every request followed the
1516
+ expected `POST /exec` 302 redirect to the Apps Script `macros/echo` endpoint,
1517
+ which returned HTTP 200. Compared with the previous 100% failure baseline, the
1518
+ redeployment fixed the transport gate. This probe proves transport and signed
1519
+ no-op execution only; it does not establish Sheet write convergence or remove
1520
+ the need for the subsequent single-append, replay, and concurrency checks.
1521
+
1522
+ Artifact: `.local/gateway-baseline-100-20260804-fixed.json`.
1523
+
1524
+ ## 2026-08-04 — Code.gs-only write correctness gate (passed)
1525
+
1526
+ - Branch: `perf/adaptive-sync-performance`
1527
+ - Command: `node .local/gateway-write-verification-live.mjs` with Gateway
1528
+ credentials supplied only through ephemeral process environment; credentials
1529
+ are not recorded.
1530
+ - Backend: redeployed Apps Script Web App `/exec`, built `dist/`, no Advanced
1531
+ Sheets Service or manifest activation.
1532
+ - Scenario: create a uniquely named temporary tab, append one row, replay the
1533
+ same effect, submit a same-effect payload mismatch, submit a duplicate
1534
+ identity, simulate response loss after the remote append response, replay the
1535
+ uncertain effect, inspect row/receipt counts, and delete the temporary tab and
1536
+ newly created receipt tab. Setup and cleanup were outside the correctness
1537
+ assertions; no existing user tab was retained or modified.
1538
+
1539
+ | Check | Result |
1540
+ | --- | --- |
1541
+ | Single append | `applied`, receipt-backed visible hash/revision |
1542
+ | Exact replay | `applied`, target rows 2 including header, one receipt per effect |
1543
+ | Payload mismatch | rejected with `operation_failed` |
1544
+ | Duplicate identity | rejected with `operation_failed` |
1545
+ | Simulated response loss | classified as network uncertainty |
1546
+ | Response-loss replay | `applied`, no duplicate target/receipt row |
1547
+ | Cleanup | temporary target and receipt tabs deleted |
1548
+
1549
+ This is the first live write-path verification of the built-in
1550
+ `SpreadsheetApp` implementation. It proves the default single-`Code.gs` path can
1551
+ append, replay, reject unsafe reuse, and recover a lost response without the
1552
+ Advanced Sheets Service. It does not yet establish multi-user throughput or
1553
+ eliminate the documented two-flush crash window between target and receipt
1554
+ writes.
1555
+
1556
+ Artifact: `.local/gateway-write-verification-20260804.json`.
@@ -10,7 +10,7 @@ API 서버는 JSON 한 줄 로그를 출력한다.
10
10
 
11
11
  | 이벤트 | 의미 |
12
12
  | --- | --- |
13
- | `typed_sheets_sync_worker_report` | 한 번의 worker(백그라운드 처리기) pass가 로컬 outbox를 어떻게 처리했는지 보여준다. `selected`, `claimed`, `applied`, `failed`, `requeued`, `responseLossRecovered`, `expiredLeasesRecovered`를 확인한다. |
13
+ | `typed_sheets_sync_worker_report` | 한 번의 worker(백그라운드 처리기) pass가 로컬 outbox를 어떻게 처리했는지 보여준다. `selected`, `claimed`, `applied`, `failed`, `deferred`, `requeued`, `responseLossRecovered`, `expiredLeasesRecovered`를 확인한다. 모호한 전송은 outbox의 `delivery_uncertain`, `uncertain_since`, `next_probe_at`, `dispatch_id`를 함께 확인한다. |
14
14
  | `typed_sheets_gateway_request` | Node가 Apps Script에 보낸 하나의 요청이다. `requestId`, `operation`, `effectCount`, `effectIds`, `requestBytes`, `durationMs`, `httpStatus`, `clientErrorCode`, `remoteErrorCode`, `effectStatuses`를 확인한다. |
15
15
  | `typed_sheets_sync_worker_error` | worker pass 자체가 예외로 종료된 경우다. |
16
16
 
@@ -25,6 +25,33 @@ Apps Script 실행 기록에는 다음 이벤트가 같은 `requestId`로 남는
25
25
  로그에는 shared secret(공유 비밀키), signature(서명), 셀 값, 전체 payload(요청
26
26
  내용)를 기록하지 않는다. `effectId`는 요청 양 끝을 연결하기 위한 식별자만 기록한다.
27
27
 
28
+ ## 폴링 타이밍 단계 (진단 전용)
29
+
30
+ 인바운드 `User_Input` 폴링은 진단용 타이밍 싱크(`onTiming`, `scope: polling`)를
31
+ 통해 단계별 소요 시간을 내보낸다. 이 값들은 애플리케이션이나 원격 동작에
32
+ 영향을 주지 않는 계측 전용 값이며, 서버나 벤치마크가 이 싱크를 연결한 경우에만
33
+ 관찰된다. 폴링은 append/update/delete 작업을 수반하지 않으므로 모든 단계는 빈
34
+ 작업 종류와 0인 카운트로 보고된다.
35
+
36
+ | 단계 (`scope: polling`) | 의미 |
37
+ | --- | --- |
38
+ | `canonical_state_read` | 비교 기준이 되는 정규 SQLite 상태를 읽는 구간이다. |
39
+ | `values_only_read` | 적응형 preflight가 값 전용(values-only) 원격 읽기로 변경 후보를 찾는 구간이다. |
40
+ | `fast_comparison` | 값 전용 읽기 결과를 정규 상태와 비교해 전체 메타데이터로 올릴 테이블을 가리는 구간이다. |
41
+ | `full_metadata_observation` | 변경/모호/스키마 사례나 주기적 안전 전수 스캔으로 올라간 테이블의 수식/병합/오류 메타데이터를 보존하는 스냅샷 읽기 구간이다. |
42
+ | `persistence` | 수락된 관측과 엔터티 변경, 격리(quarantine) 행을 SQLite에 기록하는 구간이다. |
43
+ | `polling_total` | 폴링 pass 전체 소요 시간이다. |
44
+ | `safety_scan_lag` | 예정된 안전 전수 스캔이 시작 시점에 얼마나 늦었는지(밀리초) 기록한다. 스캔 실패 전에도 기록된다. |
45
+
46
+ 같은 pass의 폴링 보고서는 모드(`mode`), 안전 전수 스캔 여부(`safetyFullScan`),
47
+ 전체 메타데이터로 처리한 테이블 수(`fullMetadataTables`), preflight가 훑은/
48
+ 변경된 행 수(`fastPathRowsScanned`, `fastPathChangedRows`)와 안전 전수 스캔 지연
49
+ (`safetyScanLagMs`)을 함께 보고한다. `safetyScanLagMs`는 적응형 pass에서는 0이고,
50
+ 첫 전수 스캔에는 이전 완료 시점이 없어 0이며, writer lease 경쟁이나 긴 polling
51
+ 간격으로 예정 시각을 넘긴 전수 스캔에서 양수로 기록된다.
52
+ `values_only_read`가 짧고 `full_metadata_observation`이 거의 없으면 적응형
53
+ preflight가 원격 전체 읽기를 건너뛰고 있다는 뜻이다.
54
+
28
55
  ## 판별 순서
29
56
 
30
57
  1. Node 로그에 `typed_sheets_gateway_request` 자체가 없는 effect가 있으면
@@ -40,8 +67,9 @@ Apps Script 실행 기록에는 다음 이벤트가 같은 `requestId`로 남는
40
67
  `postcondition_failed` 중 하나면 Apps Script가 응답한 원격 오류다.
41
68
  이 경우 `requestId`로 Apps Script 실행 기록을 대조한다.
42
69
  4. Apps Script가 `finished`를 `ok: true`로 남겼는데 Node가 timeout이면,
43
- 시트 반영 후 HTTP 응답만 유실됐을 가능성이 있다. 다음 worker pass의
44
- `readEffectPostcondition(원격 반영 확인)` 결과와
70
+ 시트 반영 후 HTTP 응답만 유실됐을 가능성이 있다. outbox가
71
+ `delivery_uncertain`으로 남고 `next_probe_at`이 설정되는지 확인한 뒤,
72
+ due probe의 `readEffectPostcondition(원격 반영 확인)` 결과와
45
73
  `responseLossRecovered`를 확인한다.
46
74
  5. Apps Script와 Node 양쪽에 `finished`/성공 기록이 있고도 outbox가
47
75
  `pending`(대기)으로 남으면, 응답을 로컬 SQLite에 기록하는 단계와
@@ -65,6 +93,14 @@ node --env-file-if-exists=.local/typed-sheets-api-server/.env \
65
93
  시트 처리 비용 또는 요청/응답 크기 제한을 의심할 수 있다. 배치 크기와 무관하게
66
94
  `lock_timeout`이 반복되면 동시 실행 또는 락 점유 시간을 먼저 확인한다.
67
95
 
96
+ 하나의 `applyEffects` 호출이 인정하는 효과 수는 게이트웨이 배치 한계(현재 20)로
97
+ 묶여 있다. 워커가 더 많은 효과를 한 번에 claim해도 각 물리 경로는 이 한계
98
+ 단위로 잘려 별도 요청으로 나가므로 요청당 `effectCount`는 이 한계를 넘지
99
+ 않는다. 응답 손실이나 사후조건 미반영으로 한 pass가 작업을 requeue만
100
+ 반복하면, 즉시 재시도하는 대신 상한이 있는 지터 지연으로 물러나고 정방향
101
+ 진행이 회복되면 곧바로 원래 간격으로 돌아온다. 이 동안 임대 만료와 복구는
102
+ 효과를 계속 살려 둔다.
103
+
68
104
  Apps Script 실행 기록은 해당 프로젝트의 권한이 필요하다. 프로젝트 소유자나
69
105
  편집자가 아니라면 라이브러리가 원격 실행 로그를 자동으로 읽을 수 없으므로,
70
106
  소유자에게 실행 기록 또는 Cloud Logging(보존형 로그) 접근 권한을 요청하거나