@seasonkoh/webaz 0.1.24 → 0.1.25

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 (187) hide show
  1. package/README.md +2 -0
  2. package/dist/layer0-foundation/L0-1-database/db-backends/pg-backend.js +51 -0
  3. package/dist/layer0-foundation/L0-1-database/db-backends/sql-dialect-datetime.js +437 -0
  4. package/dist/layer0-foundation/L0-1-database/db-backends/sql-placeholders.js +98 -0
  5. package/dist/layer0-foundation/L0-1-database/db.js +65 -0
  6. package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +13 -11
  7. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +1 -1
  8. package/dist/layer0-foundation/L0-5-manifest/manifest.js +13 -11
  9. package/dist/layer1-agent/L1-1-mcp-server/server.js +165 -64
  10. package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +14 -12
  11. package/dist/layer2-business/L2-6-notifications/notification-engine.js +8 -5
  12. package/dist/layer2-business/L2-7-snf/snf-engine.js +16 -14
  13. package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +18 -10
  14. package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +37 -23
  15. package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +173 -0
  16. package/dist/layer2-business/L2-9-contribution/build-task-participation.js +47 -0
  17. package/dist/layer2-business/L2-9-contribution/build-task-read.js +222 -0
  18. package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +10 -2
  19. package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +16 -0
  20. package/dist/layer2-business/L2-9-contribution/contribution-display-envelope.js +40 -0
  21. package/dist/layer2-business/L2-9-contribution/contribution-score-contract.js +36 -0
  22. package/dist/layer2-business/L2-9-contribution/contribution-score-evidence.js +61 -0
  23. package/dist/layer2-business/L2-9-contribution/github-credential/canonical.js +60 -0
  24. package/dist/layer2-business/L2-9-contribution/github-credential/github-credential.schema.js +140 -0
  25. package/dist/layer2-business/L2-9-contribution/github-credential/github-fetch-adapter.js +437 -0
  26. package/dist/layer2-business/L2-9-contribution/github-credential/self-consistency.js +38 -0
  27. package/dist/layer2-business/L2-9-contribution/github-credential/verifier.js +231 -0
  28. package/dist/layer2-business/L2-9-contribution/github-credential-ingestion-engine.js +145 -0
  29. package/dist/layer2-business/L2-9-contribution/github-credential-store.js +115 -0
  30. package/dist/layer2-business/L2-9-contribution/identity-binding-engine.js +134 -0
  31. package/dist/layer2-business/L2-9-contribution/identity-binding-store.js +101 -0
  32. package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-engine.js +126 -0
  33. package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-store.js +30 -0
  34. package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
  35. package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
  36. package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
  37. package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
  38. package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
  39. package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
  40. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
  41. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
  42. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
  43. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
  44. package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
  45. package/dist/pwa/acp-feed.js +13 -1
  46. package/dist/pwa/contract-fingerprint.js +2 -0
  47. package/dist/pwa/endpoint-actions.js +5 -1
  48. package/dist/pwa/goal-index.js +8 -8
  49. package/dist/pwa/human-presence.js +62 -0
  50. package/dist/pwa/public/app.js +575 -68
  51. package/dist/pwa/public/i18n.js +29 -20
  52. package/dist/pwa/public/index.html +1 -0
  53. package/dist/pwa/public/openapi.json +2 -2
  54. package/dist/pwa/rate-limit.js +22 -0
  55. package/dist/pwa/routes/account-deletion.js +15 -13
  56. package/dist/pwa/routes/addresses.js +10 -9
  57. package/dist/pwa/routes/admin-admins.js +13 -14
  58. package/dist/pwa/routes/admin-analytics.js +109 -69
  59. package/dist/pwa/routes/admin-catalog.js +13 -11
  60. package/dist/pwa/routes/admin-editor-picks.js +15 -10
  61. package/dist/pwa/routes/admin-events.js +5 -3
  62. package/dist/pwa/routes/admin-health.js +2 -1
  63. package/dist/pwa/routes/admin-moderation.js +26 -29
  64. package/dist/pwa/routes/admin-ops.js +22 -21
  65. package/dist/pwa/routes/admin-protocol-params.js +16 -19
  66. package/dist/pwa/routes/admin-reports.js +23 -21
  67. package/dist/pwa/routes/admin-tokenomics.js +26 -25
  68. package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
  69. package/dist/pwa/routes/admin-users-query.js +54 -53
  70. package/dist/pwa/routes/admin-verifier-flow.js +82 -41
  71. package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
  72. package/dist/pwa/routes/admin-wallet-ops.js +7 -5
  73. package/dist/pwa/routes/agent-buy.js +46 -22
  74. package/dist/pwa/routes/agent-governance.js +52 -56
  75. package/dist/pwa/routes/ai.js +7 -5
  76. package/dist/pwa/routes/analytics.js +43 -41
  77. package/dist/pwa/routes/anchors.js +19 -20
  78. package/dist/pwa/routes/announcements.js +13 -13
  79. package/dist/pwa/routes/arbitrator.js +97 -31
  80. package/dist/pwa/routes/auction.js +153 -114
  81. package/dist/pwa/routes/auth-login.js +6 -4
  82. package/dist/pwa/routes/auth-read.js +11 -9
  83. package/dist/pwa/routes/auth-register.js +35 -20
  84. package/dist/pwa/routes/auth-sessions.js +12 -11
  85. package/dist/pwa/routes/blocklist.js +16 -15
  86. package/dist/pwa/routes/build-feedback.js +10 -9
  87. package/dist/pwa/routes/build-reputation.js +6 -2
  88. package/dist/pwa/routes/build-tasks.js +45 -13
  89. package/dist/pwa/routes/buyer-feeds.js +27 -25
  90. package/dist/pwa/routes/cart.js +16 -15
  91. package/dist/pwa/routes/charity.js +212 -150
  92. package/dist/pwa/routes/chat.js +42 -43
  93. package/dist/pwa/routes/checkin-tasks.js +10 -9
  94. package/dist/pwa/routes/checkout-helpers.js +12 -10
  95. package/dist/pwa/routes/claim-initiators.js +34 -14
  96. package/dist/pwa/routes/claim-verify.js +86 -53
  97. package/dist/pwa/routes/claim-voting.js +43 -18
  98. package/dist/pwa/routes/contribution-identity.js +147 -0
  99. package/dist/pwa/routes/contribution-score.js +19 -0
  100. package/dist/pwa/routes/coupons.js +19 -16
  101. package/dist/pwa/routes/dashboards.js +18 -16
  102. package/dist/pwa/routes/dispute-cases.js +25 -24
  103. package/dist/pwa/routes/disputes-read.js +45 -51
  104. package/dist/pwa/routes/disputes-write.js +124 -61
  105. package/dist/pwa/routes/evidence.js +9 -9
  106. package/dist/pwa/routes/external-anchors.js +13 -12
  107. package/dist/pwa/routes/feedback.js +29 -33
  108. package/dist/pwa/routes/flash-sales.js +18 -16
  109. package/dist/pwa/routes/follows.js +25 -24
  110. package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
  111. package/dist/pwa/routes/governance-onboarding.js +70 -59
  112. package/dist/pwa/routes/group-buys.js +22 -22
  113. package/dist/pwa/routes/growth.js +33 -30
  114. package/dist/pwa/routes/import-product.js +12 -10
  115. package/dist/pwa/routes/kyc.js +9 -8
  116. package/dist/pwa/routes/leaderboard.js +20 -18
  117. package/dist/pwa/routes/listings.js +23 -22
  118. package/dist/pwa/routes/logistics.js +10 -8
  119. package/dist/pwa/routes/manifests.js +27 -27
  120. package/dist/pwa/routes/me-data.js +23 -21
  121. package/dist/pwa/routes/notifications.js +7 -6
  122. package/dist/pwa/routes/offers.js +30 -12
  123. package/dist/pwa/routes/orders-action.js +33 -17
  124. package/dist/pwa/routes/orders-create.js +75 -20
  125. package/dist/pwa/routes/orders-read.js +21 -20
  126. package/dist/pwa/routes/p2p-products.js +30 -18
  127. package/dist/pwa/routes/payments-governance.js +61 -56
  128. package/dist/pwa/routes/peers.js +9 -8
  129. package/dist/pwa/routes/pin-receipts.js +13 -13
  130. package/dist/pwa/routes/products-aliases.js +12 -10
  131. package/dist/pwa/routes/products-claims.js +36 -17
  132. package/dist/pwa/routes/products-create.js +53 -38
  133. package/dist/pwa/routes/products-crud.js +17 -16
  134. package/dist/pwa/routes/products-links.js +49 -26
  135. package/dist/pwa/routes/products-list.js +6 -4
  136. package/dist/pwa/routes/products-meta.js +40 -39
  137. package/dist/pwa/routes/products-update.js +19 -5
  138. package/dist/pwa/routes/profile-credentials.js +14 -16
  139. package/dist/pwa/routes/profile-identity.js +14 -13
  140. package/dist/pwa/routes/profile-location.js +7 -6
  141. package/dist/pwa/routes/profile-placement.js +19 -17
  142. package/dist/pwa/routes/profile-prefs.js +11 -11
  143. package/dist/pwa/routes/promoter.js +55 -49
  144. package/dist/pwa/routes/public-build-tasks.js +19 -0
  145. package/dist/pwa/routes/public-utils.js +108 -46
  146. package/dist/pwa/routes/push.js +16 -15
  147. package/dist/pwa/routes/ratings.js +30 -30
  148. package/dist/pwa/routes/recover-key.js +13 -12
  149. package/dist/pwa/routes/referral.js +37 -32
  150. package/dist/pwa/routes/reputation.js +3 -2
  151. package/dist/pwa/routes/returns.js +76 -73
  152. package/dist/pwa/routes/reviews.js +41 -18
  153. package/dist/pwa/routes/rewards-apply.js +16 -15
  154. package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
  155. package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
  156. package/dist/pwa/routes/rfqs.js +163 -85
  157. package/dist/pwa/routes/search.js +16 -14
  158. package/dist/pwa/routes/secondhand.js +25 -22
  159. package/dist/pwa/routes/seller-quota.js +24 -26
  160. package/dist/pwa/routes/share-redirects.js +59 -55
  161. package/dist/pwa/routes/shareables-interactions.js +34 -35
  162. package/dist/pwa/routes/shareables.js +55 -51
  163. package/dist/pwa/routes/shop-referral.js +57 -0
  164. package/dist/pwa/routes/shops.js +20 -18
  165. package/dist/pwa/routes/signaling.js +10 -9
  166. package/dist/pwa/routes/skill-market.js +16 -16
  167. package/dist/pwa/routes/skills.js +15 -14
  168. package/dist/pwa/routes/snf.js +14 -13
  169. package/dist/pwa/routes/tags.js +10 -9
  170. package/dist/pwa/routes/task-proposals.js +45 -0
  171. package/dist/pwa/routes/trial.js +69 -51
  172. package/dist/pwa/routes/trusted-kpi.js +20 -18
  173. package/dist/pwa/routes/url-claim.js +67 -28
  174. package/dist/pwa/routes/users-public.js +62 -60
  175. package/dist/pwa/routes/variants.js +12 -13
  176. package/dist/pwa/routes/verifier-user.js +61 -21
  177. package/dist/pwa/routes/verify-tasks.js +49 -25
  178. package/dist/pwa/routes/waitlist.js +16 -15
  179. package/dist/pwa/routes/wallet-read.js +74 -36
  180. package/dist/pwa/routes/wallet-write.js +12 -9
  181. package/dist/pwa/routes/webauthn.js +25 -26
  182. package/dist/pwa/routes/webhooks.js +26 -26
  183. package/dist/pwa/routes/welcome.js +45 -50
  184. package/dist/pwa/routes/wishlist-qa.js +29 -32
  185. package/dist/pwa/server.js +237 -81
  186. package/dist/version.js +1 -1
  187. package/package.json +47 -2
package/README.md CHANGED
@@ -393,3 +393,5 @@ One codebase, two runtimes: the **MCP server** (for AI agents) and the **PWA** (
393
393
  **Bug 报告 / 功能想法 / RFC**:走 [GitHub Issues](https://github.com/seasonsagents-art/webaz/issues) 或 [Discussions](https://github.com/seasonsagents-art/webaz/discussions);PR 流程见 [CONTRIBUTING.md](CONTRIBUTING.md)。
394
394
 
395
395
  **Bug reports / feature ideas / RFCs**: please use [GitHub Issues](https://github.com/seasonsagents-art/webaz/issues) or [Discussions](https://github.com/seasonsagents-art/webaz/discussions); PR workflow per [CONTRIBUTING.md](CONTRIBUTING.md).
396
+
397
+ **新贡献者 / GitHub-first / 带 agent 的入口**:[`docs/PUBLIC-CONTRIBUTOR-ENTRY.md`](docs/PUBLIC-CONTRIBUTOR-ENTRY.md) — *contribute first, bind later*;贡献被记录但一切 `uncommitted`(不承诺奖励)。 / **New / GitHub-first / agent contributor entry**: [`docs/PUBLIC-CONTRIBUTOR-ENTRY.md`](docs/PUBLIC-CONTRIBUTOR-ENTRY.md).
@@ -0,0 +1,51 @@
1
+ import { toPgPlaceholders } from './sql-placeholders.js';
2
+ import { translateDatetimeNow, translateDatetimeInterval, translateDatetimeExprInterval } from './sql-dialect-datetime.js';
3
+ export function createPgBackend(connectionString = process.env.DATABASE_URL) {
4
+ if (!connectionString) {
5
+ throw new Error('createPgBackend: 缺少 DATABASE_URL —— pg 后端需连接串');
6
+ }
7
+ let pool = null;
8
+ // lazy 连接池 —— 首次查询时建池(pg 纯 JS,仅本文件加载)。
9
+ async function getPool() {
10
+ if (pool)
11
+ return pool;
12
+ const pg = await import('pg');
13
+ pool = new pg.Pool({ connectionString });
14
+ return pool;
15
+ }
16
+ // SQL 文本归一化:先方言译(③a,逐块叠加),再 ?→$n 占位符。
17
+ // 方言译器只动 SQL 文本、不引入/消除 `?`,故不影响占位符对账。
18
+ // ③a-1 datetime('now') · ③a-2 datetime('now',±N) · ③a-2b datetime(<列>,±N) 已接入;
19
+ // ③a-3 ON CONFLICT 后续追加。
20
+ // 三个 datetime 译器形态互斥(now-单参 / now-两参 / 非now-两参),顺序无关;
21
+ // 排此序仅为可读。方言译器只动 SQL 文本、不增删 `?`,故占位符对账不受影响。
22
+ function toPg(sql, params) {
23
+ let s = translateDatetimeInterval(sql).text;
24
+ s = translateDatetimeExprInterval(s).text;
25
+ s = translateDatetimeNow(s).text;
26
+ const { text, count } = toPgPlaceholders(s);
27
+ if (count !== params.length) {
28
+ throw new Error(`pg-backend: 占位符个数(${count})与参数个数(${params.length})不符`);
29
+ }
30
+ return text;
31
+ }
32
+ return {
33
+ kind: 'pg',
34
+ async one(sql, params) {
35
+ const p = await getPool();
36
+ const r = await p.query(toPg(sql, params), params);
37
+ return r.rows[0];
38
+ },
39
+ async all(sql, params) {
40
+ const p = await getPool();
41
+ const r = await p.query(toPg(sql, params), params);
42
+ return r.rows;
43
+ },
44
+ async run(sql, params) {
45
+ const p = await getPool();
46
+ const r = await p.query(toPg(sql, params), params);
47
+ // pg 无隐式 rowid;lastInsertRowid 的 2 个用点 Phase 3 改 RETURNING。changes = 受影响行数。
48
+ return { changes: r.rowCount ?? 0, lastInsertRowid: 0 };
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,437 @@
1
+ /**
2
+ * RFC-016 Phase 3 ③a-1 — `datetime('now')` → PostgreSQL 方言译器。
3
+ *
4
+ * 中心化方言转换的第一块(延续 sql-placeholders 的设计):转换【只在 pg 后端内部】跑,
5
+ * call site 全不动、SQLite 路径不经过这里 = 对 SQLite 行为零影响。
6
+ *
7
+ * 仅匹配【裸】`datetime('now')`(单参)。带修饰参的 `datetime('now', '+7 days')` 留给
8
+ * ③a-2(interval 译器),本译器明确放过(`'now'` 后紧跟 `)`,跟逗号的不匹配)。
9
+ *
10
+ * 产出表达式与 scripts/gen-pg-schema.ts 的列默认值【逐字一致】——
11
+ * `to_char((now() AT TIME ZONE 'UTC'), 'YYYY-MM-DD HH24:MI:SS')`
12
+ * (UTC、秒级、'YYYY-MM-DD HH:MM:SS' 字符串),保证运行时写入的时间戳与 schema
13
+ * 默认值同格式,全仓 `datetime('now')` 比较语义在 pg 上不变。
14
+ *
15
+ * 字符串/标识符/注释感知(同 sql-placeholders):`'...'` / `"..."` / `-- …` / `/* … *​/`
16
+ * 里的 `datetime('now')` 不动;`mydatetime('now')`(标识符一部分)不动(词界检查)。
17
+ */
18
+ /** 与 gen-pg-schema.ts 的 PG_NOW 逐字一致 —— 改一处必须同改另一处。 */
19
+ export const PG_NOW = `to_char((now() AT TIME ZONE 'UTC'), 'YYYY-MM-DD HH24:MI:SS')`;
20
+ // 锚定匹配:datetime ( 'now' ) —— 函数名大小写不敏感,'now' 小写(全仓用法),单参(后跟 `)`)。
21
+ const DT_NOW = /datetime\s*\(\s*'now'\s*\)/iy;
22
+ // ③a-2:datetime('now', <modifier>) 前缀 —— 'now' 后跟逗号(与 ③a-1 的单参形态互斥)。
23
+ const DT_NOW_INTERVAL_PREFIX = /datetime\s*\(\s*'now'\s*,\s*/iy;
24
+ // ③a-2b:datetime( 通用开括号锚 —— 首参为列/表达式(非 'now')的两参 interval 形态入口。
25
+ const DT_OPEN = /datetime\s*\(/iy;
26
+ function isIdentChar(ch) {
27
+ return ch !== undefined && /[A-Za-z0-9_]/.test(ch);
28
+ }
29
+ export function translateDatetimeNow(sql) {
30
+ let out = '';
31
+ let count = 0;
32
+ let i = 0;
33
+ const len = sql.length;
34
+ while (i < len) {
35
+ const c = sql[i];
36
+ // 单引号字符串字面量
37
+ if (c === "'") {
38
+ out += c;
39
+ i++;
40
+ while (i < len) {
41
+ if (sql[i] === "'") {
42
+ if (sql[i + 1] === "'") {
43
+ out += "''";
44
+ i += 2;
45
+ continue;
46
+ }
47
+ out += "'";
48
+ i++;
49
+ break;
50
+ }
51
+ out += sql[i];
52
+ i++;
53
+ }
54
+ continue;
55
+ }
56
+ // 双引号标识符
57
+ if (c === '"') {
58
+ out += c;
59
+ i++;
60
+ while (i < len) {
61
+ if (sql[i] === '"') {
62
+ if (sql[i + 1] === '"') {
63
+ out += '""';
64
+ i += 2;
65
+ continue;
66
+ }
67
+ out += '"';
68
+ i++;
69
+ break;
70
+ }
71
+ out += sql[i];
72
+ i++;
73
+ }
74
+ continue;
75
+ }
76
+ // 行注释 -- … 到行尾
77
+ if (c === '-' && sql[i + 1] === '-') {
78
+ while (i < len && sql[i] !== '\n') {
79
+ out += sql[i];
80
+ i++;
81
+ }
82
+ continue;
83
+ }
84
+ // 块注释 /* … */
85
+ if (c === '/' && sql[i + 1] === '*') {
86
+ out += '/*';
87
+ i += 2;
88
+ while (i < len && !(sql[i] === '*' && sql[i + 1] === '/')) {
89
+ out += sql[i];
90
+ i++;
91
+ }
92
+ if (i < len) {
93
+ out += '*/';
94
+ i += 2;
95
+ }
96
+ continue;
97
+ }
98
+ // datetime('now') —— 仅在词界处尝试:
99
+ // 前置:前一字符非标识符,避免 mydatetime('now');
100
+ // 后置:闭括号后一字符非标识符,避免 datetime('now')x / datetime('now')_x(贴标识符后缀)。
101
+ // `)::text`(`:`)、`))`、行尾等非标识符后缀仍正常转换。
102
+ if ((c === 'd' || c === 'D') && !isIdentChar(sql[i - 1])) {
103
+ DT_NOW.lastIndex = i;
104
+ const m = DT_NOW.exec(sql);
105
+ if (m && m.index === i && !isIdentChar(sql[i + m[0].length])) {
106
+ out += PG_NOW;
107
+ i += m[0].length;
108
+ count++;
109
+ continue;
110
+ }
111
+ }
112
+ out += c;
113
+ i++;
114
+ }
115
+ return { text: out, count };
116
+ }
117
+ /**
118
+ * ③a-2:`datetime('now', <modifier>)` → pg interval 算术。
119
+ *
120
+ * 统一变换(不解析符号/数值/单位):
121
+ * datetime('now', X)
122
+ * → to_char((now() AT TIME ZONE 'UTC') + (X)::interval, 'YYYY-MM-DD HH24:MI:SS')
123
+ *
124
+ * 为什么恒用 `+`:SQLite 修饰串自带符号('-30 days' / '+7 days'),pg `(X)::interval`
125
+ * 解析同样的串,符号在 interval 内部 ——`now() + '-30 days'::interval` = now-30天。
126
+ * 故对所有形态一致成立:
127
+ * - 字面量:datetime('now', '-30 days') → (… + ('-30 days')::interval …)
128
+ * - 拼接参数:datetime('now', '-' || ? || ' days') → (… + ('-' || ? || ' days')::interval …)
129
+ * (pg 原生支持 `||`;其中 `?` 由后续占位符阶段统一转 `$n`,本译器原样保留)
130
+ * - 裸参数:datetime('now', ?) → (… + (?)::interval …)
131
+ *
132
+ * ⚠️ 仅处理第一参为 `'now'` 的形态。`datetime(<列>, <修饰>)`(如 datetime(expires_at,…))
133
+ * 的 pg 译法不同(列::timestamp + interval),【不在本译器范围】,见 ③a-2b
134
+ * `translateDatetimeExprInterval`。
135
+ * ⚠️ 全仓审计确认无多修饰(datetime('now', A, B))、无非 interval 修饰
136
+ * (start of / weekday / localtime / unixepoch),故 `::interval` 强转安全。
137
+ *
138
+ * 字符串/标识符/注释感知 + 前后词界守卫(同 ③a-1)。
139
+ */
140
+ export function translateDatetimeInterval(sql) {
141
+ let out = '';
142
+ let count = 0;
143
+ let i = 0;
144
+ const len = sql.length;
145
+ while (i < len) {
146
+ const c = sql[i];
147
+ // 单引号字符串
148
+ if (c === "'") {
149
+ out += c;
150
+ i++;
151
+ while (i < len) {
152
+ if (sql[i] === "'") {
153
+ if (sql[i + 1] === "'") {
154
+ out += "''";
155
+ i += 2;
156
+ continue;
157
+ }
158
+ out += "'";
159
+ i++;
160
+ break;
161
+ }
162
+ out += sql[i];
163
+ i++;
164
+ }
165
+ continue;
166
+ }
167
+ // 双引号标识符
168
+ if (c === '"') {
169
+ out += c;
170
+ i++;
171
+ while (i < len) {
172
+ if (sql[i] === '"') {
173
+ if (sql[i + 1] === '"') {
174
+ out += '""';
175
+ i += 2;
176
+ continue;
177
+ }
178
+ out += '"';
179
+ i++;
180
+ break;
181
+ }
182
+ out += sql[i];
183
+ i++;
184
+ }
185
+ continue;
186
+ }
187
+ // 行注释
188
+ if (c === '-' && sql[i + 1] === '-') {
189
+ while (i < len && sql[i] !== '\n') {
190
+ out += sql[i];
191
+ i++;
192
+ }
193
+ continue;
194
+ }
195
+ // 块注释
196
+ if (c === '/' && sql[i + 1] === '*') {
197
+ out += '/*';
198
+ i += 2;
199
+ while (i < len && !(sql[i] === '*' && sql[i + 1] === '/')) {
200
+ out += sql[i];
201
+ i++;
202
+ }
203
+ if (i < len) {
204
+ out += '*/';
205
+ i += 2;
206
+ }
207
+ continue;
208
+ }
209
+ // datetime('now', <modifier>) —— 前置词界 + 前缀匹配
210
+ if ((c === 'd' || c === 'D') && !isIdentChar(sql[i - 1])) {
211
+ DT_NOW_INTERVAL_PREFIX.lastIndex = i;
212
+ const pm = DT_NOW_INTERVAL_PREFIX.exec(sql);
213
+ if (pm && pm.index === i) {
214
+ // 从前缀(含逗号后空白)之后,扫描修饰表达式到 datetime 的配对 `)`(尊重字符串/嵌套括号)。
215
+ const mod = scanToMatchingParen(sql, i + pm[0].length);
216
+ // 后置词界:闭括号后紧贴标识符字符(datetime('now','-1 day')x)→ 不转换(同 ③a-1 P3 守卫)。
217
+ if (mod && !isIdentChar(sql[mod.endIndex + 1])) {
218
+ const expr = mod.text.trim();
219
+ out += `to_char((now() AT TIME ZONE 'UTC') + (${expr})::interval, 'YYYY-MM-DD HH24:MI:SS')`;
220
+ i = mod.endIndex + 1; // 跳过闭括号
221
+ count++;
222
+ continue;
223
+ }
224
+ }
225
+ }
226
+ out += c;
227
+ i++;
228
+ }
229
+ return { text: out, count };
230
+ }
231
+ /**
232
+ * ③a-2b:`datetime(<列|表达式>, <interval 修饰>)` → pg 列时间戳 + interval 算术。
233
+ *
234
+ * 与 ③a-2(首参 'now')互补 —— 首参是【已存储的时间戳文本列/表达式】(如 deadline_at、
235
+ * p.created_at),pg 译法是把该文本 cast 成 timestamp 再加 interval:
236
+ * datetime(EXPR, MOD)
237
+ * → to_char((EXPR)::timestamp + (MOD)::interval, 'YYYY-MM-DD HH24:MI:SS')
238
+ *
239
+ * 与 ③a-2 一致:恒用 `+` —— SQLite 修饰串自带符号('+1 days' / '-' || ? || ' minutes'),
240
+ * pg `(MOD)::interval` 解析同串、符号在 interval 内部;`||` 拼接与 `?` 占位原样保留。
241
+ * 输出 to_char 回 'YYYY-MM-DD HH:MM:SS' 字符串(同 PG_NOW 格式)→ 时间戳列在 pg 上仍存
242
+ * 文本,全仓字符串比较语义(`>` / `=`)不变。
243
+ *
244
+ * 严格只转【确认的两参 interval 形态】:
245
+ * • 首参为 'now' 字面量 → 跳过(归 ③a-2,与本译器互斥);
246
+ * • 单参 datetime(<expr>)(无顶层逗号,如 datetime(col) 归一化比较)→ 跳过(不在本子块,
247
+ * 由后续单参归一化子项处理);
248
+ * • 闭括号后紧贴标识符 → 跳过(词界守卫,同 ③a-1/③a-2)。
249
+ * 全仓盘点(RFC-016 §5)确认仅 2 处命中:auction.deadline_at 反狙击续期、follows restock
250
+ * 窗口(updated_at > created_at + 1 day),均为干净两参 interval。
251
+ *
252
+ * 字符串/标识符/注释感知 + 前后词界守卫(同 ③a-1/③a-2)。
253
+ */
254
+ export function translateDatetimeExprInterval(sql) {
255
+ let out = '';
256
+ let count = 0;
257
+ let i = 0;
258
+ const len = sql.length;
259
+ while (i < len) {
260
+ const c = sql[i];
261
+ // 单引号字符串
262
+ if (c === "'") {
263
+ out += c;
264
+ i++;
265
+ while (i < len) {
266
+ if (sql[i] === "'") {
267
+ if (sql[i + 1] === "'") {
268
+ out += "''";
269
+ i += 2;
270
+ continue;
271
+ }
272
+ out += "'";
273
+ i++;
274
+ break;
275
+ }
276
+ out += sql[i];
277
+ i++;
278
+ }
279
+ continue;
280
+ }
281
+ // 双引号标识符
282
+ if (c === '"') {
283
+ out += c;
284
+ i++;
285
+ while (i < len) {
286
+ if (sql[i] === '"') {
287
+ if (sql[i + 1] === '"') {
288
+ out += '""';
289
+ i += 2;
290
+ continue;
291
+ }
292
+ out += '"';
293
+ i++;
294
+ break;
295
+ }
296
+ out += sql[i];
297
+ i++;
298
+ }
299
+ continue;
300
+ }
301
+ // 行注释
302
+ if (c === '-' && sql[i + 1] === '-') {
303
+ while (i < len && sql[i] !== '\n') {
304
+ out += sql[i];
305
+ i++;
306
+ }
307
+ continue;
308
+ }
309
+ // 块注释
310
+ if (c === '/' && sql[i + 1] === '*') {
311
+ out += '/*';
312
+ i += 2;
313
+ while (i < len && !(sql[i] === '*' && sql[i + 1] === '/')) {
314
+ out += sql[i];
315
+ i++;
316
+ }
317
+ if (i < len) {
318
+ out += '*/';
319
+ i += 2;
320
+ }
321
+ continue;
322
+ }
323
+ // datetime( —— 前置词界 + 通用开括号匹配,再扫两参
324
+ if ((c === 'd' || c === 'D') && !isIdentChar(sql[i - 1])) {
325
+ DT_OPEN.lastIndex = i;
326
+ const om = DT_OPEN.exec(sql);
327
+ if (om && om.index === i) {
328
+ const argsStart = i + om[0].length; // datetime( 之后(首参起点)
329
+ const comma = scanToTopLevelComma(sql, argsStart);
330
+ if (comma !== null) { // 有顶层逗号 = 两参形态
331
+ const arg1 = sql.slice(argsStart, comma).trim();
332
+ // 首参 'now' 字面量归 ③a-2;单参(无逗号)上面已 null 跳过。
333
+ if (arg1.toLowerCase() !== "'now'") {
334
+ const mod = scanToMatchingParen(sql, comma + 1);
335
+ // 后置词界:闭括号后紧贴标识符 → 不转换(同 ③a-1/③a-2 守卫)。
336
+ if (mod && !isIdentChar(sql[mod.endIndex + 1])) {
337
+ const modExpr = mod.text.trim();
338
+ out += `to_char((${arg1})::timestamp + (${modExpr})::interval, 'YYYY-MM-DD HH24:MI:SS')`;
339
+ i = mod.endIndex + 1; // 跳过闭括号
340
+ count++;
341
+ continue;
342
+ }
343
+ }
344
+ }
345
+ }
346
+ }
347
+ out += c;
348
+ i++;
349
+ }
350
+ return { text: out, count };
351
+ }
352
+ /**
353
+ * 从 `start`(datetime 开括号之内)扫描首参,直到 datetime 内的【顶层逗号】(深度=1)。
354
+ * 尊重单引号字符串('' 转义)与嵌套括号;若先遇配对 `)`(深度归 0,单参形态)返回 null。
355
+ * 返回顶层逗号下标。
356
+ */
357
+ function scanToTopLevelComma(sql, start) {
358
+ let depth = 1;
359
+ let j = start;
360
+ const len = sql.length;
361
+ while (j < len) {
362
+ const ch = sql[j];
363
+ if (ch === "'") {
364
+ j++;
365
+ while (j < len) {
366
+ if (sql[j] === "'") {
367
+ if (sql[j + 1] === "'") {
368
+ j += 2;
369
+ continue;
370
+ }
371
+ j++;
372
+ break;
373
+ }
374
+ j++;
375
+ }
376
+ continue;
377
+ }
378
+ if (ch === '(') {
379
+ depth++;
380
+ j++;
381
+ continue;
382
+ }
383
+ if (ch === ')') {
384
+ depth--;
385
+ if (depth === 0)
386
+ return null; // 单参:配对 ) 先于顶层逗号
387
+ j++;
388
+ continue;
389
+ }
390
+ if (ch === ',' && depth === 1)
391
+ return j; // 顶层逗号 = 两参分隔
392
+ j++;
393
+ }
394
+ return null;
395
+ }
396
+ /**
397
+ * 从 `start`(datetime 开括号之内、第一参逗号之后)扫描修饰表达式,直到 datetime 的配对 `)`。
398
+ * 已在 datetime 括号内,深度从 1 起;尊重单引号字符串('' 转义),计配对括号。
399
+ * 返回修饰原文 + 闭括号下标;不配对返回 null(留原文不动)。
400
+ */
401
+ function scanToMatchingParen(sql, start) {
402
+ let depth = 1;
403
+ let j = start;
404
+ const len = sql.length;
405
+ while (j < len) {
406
+ const ch = sql[j];
407
+ if (ch === "'") {
408
+ j++;
409
+ while (j < len) {
410
+ if (sql[j] === "'") {
411
+ if (sql[j + 1] === "'") {
412
+ j += 2;
413
+ continue;
414
+ }
415
+ j++;
416
+ break;
417
+ }
418
+ j++;
419
+ }
420
+ continue;
421
+ }
422
+ if (ch === '(') {
423
+ depth++;
424
+ j++;
425
+ continue;
426
+ }
427
+ if (ch === ')') {
428
+ depth--;
429
+ if (depth === 0)
430
+ return { text: sql.slice(start, j), endIndex: j };
431
+ j++;
432
+ continue;
433
+ }
434
+ j++;
435
+ }
436
+ return null;
437
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * RFC-016 — `?` → `$1..$n` 占位符转换(Phase 2 PR2)。
3
+ *
4
+ * 全仓 SQL 用 SQLite 风格匿名 `?` 占位符 + 数组传参(seam 约定)。pg 用 `$1..$n`。
5
+ * 本转换【只在 pg 后端内部】跑(见 pg-backend.ts);SQLite 路径不经过这里,仍原样收 `?`。
6
+ * 故对 SQLite 行为零影响 —— 纯增量,集中一处。
7
+ *
8
+ * 必须感知字符串/标识符/注释,不能把字面量里的 `?` 误当占位符:
9
+ * • 单引号字符串 `'...'`(SQLite/pg 同用 `''` 转义内嵌单引号,无反斜杠转义)
10
+ * • 双引号标识符 `"..."`(`""` 转义内嵌双引号)
11
+ * • 行注释 `-- ... \n`、块注释 `/* ... *​/`
12
+ * 遇到编号占位符 `?1`(better-sqlite3 支持但本协议不用)直接抛错,绝不静默错配。
13
+ */
14
+ export function toPgPlaceholders(sql) {
15
+ let out = '';
16
+ let n = 0;
17
+ let i = 0;
18
+ const len = sql.length;
19
+ while (i < len) {
20
+ const c = sql[i];
21
+ // 单引号字符串字面量
22
+ if (c === "'") {
23
+ out += c;
24
+ i++;
25
+ while (i < len) {
26
+ if (sql[i] === "'") {
27
+ if (sql[i + 1] === "'") {
28
+ out += "''";
29
+ i += 2;
30
+ continue;
31
+ } // 转义的内嵌单引号
32
+ out += "'";
33
+ i++;
34
+ break;
35
+ }
36
+ out += sql[i];
37
+ i++;
38
+ }
39
+ continue;
40
+ }
41
+ // 双引号标识符
42
+ if (c === '"') {
43
+ out += c;
44
+ i++;
45
+ while (i < len) {
46
+ if (sql[i] === '"') {
47
+ if (sql[i + 1] === '"') {
48
+ out += '""';
49
+ i += 2;
50
+ continue;
51
+ } // 转义的内嵌双引号
52
+ out += '"';
53
+ i++;
54
+ break;
55
+ }
56
+ out += sql[i];
57
+ i++;
58
+ }
59
+ continue;
60
+ }
61
+ // 行注释 -- ... 到行尾
62
+ if (c === '-' && sql[i + 1] === '-') {
63
+ while (i < len && sql[i] !== '\n') {
64
+ out += sql[i];
65
+ i++;
66
+ }
67
+ continue;
68
+ }
69
+ // 块注释 /* ... */
70
+ if (c === '/' && sql[i + 1] === '*') {
71
+ out += '/*';
72
+ i += 2;
73
+ while (i < len && !(sql[i] === '*' && sql[i + 1] === '/')) {
74
+ out += sql[i];
75
+ i++;
76
+ }
77
+ if (i < len) {
78
+ out += '*/';
79
+ i += 2;
80
+ }
81
+ continue;
82
+ }
83
+ // 占位符
84
+ if (c === '?') {
85
+ const next = sql[i + 1];
86
+ if (next >= '0' && next <= '9') {
87
+ throw new Error(`toPgPlaceholders: 不支持编号占位符 "?${next}…" —— 本协议仅用匿名 "?" 定位参数`);
88
+ }
89
+ n++;
90
+ out += '$' + n;
91
+ i++;
92
+ continue;
93
+ }
94
+ out += c;
95
+ i++;
96
+ }
97
+ return { text: out, count: n };
98
+ }
@@ -0,0 +1,65 @@
1
+ let _backend = null;
2
+ // 原始 better-sqlite3 句柄,仅在 sqlite 后端下持有 —— 供【同步事务】逃生口用(见下 seamSqliteHandle)。
3
+ let _sqliteHandle = null;
4
+ /** better-sqlite3 后端 —— 等价于迁移前的 `db.prepare().get/all/run`,逐字保行为。 */
5
+ function createSqliteBackend(db) {
6
+ return {
7
+ kind: 'sqlite',
8
+ async one(sql, params) {
9
+ return db.prepare(sql).get(...params);
10
+ },
11
+ async all(sql, params) {
12
+ return db.prepare(sql).all(...params);
13
+ },
14
+ async run(sql, params) {
15
+ const r = db.prepare(sql).run(...params);
16
+ return { changes: r.changes, lastInsertRowid: r.lastInsertRowid };
17
+ },
18
+ };
19
+ }
20
+ /**
21
+ * 启动时注入共享 SQLite 连接(PWA 与 MCP 各自进程各注入一次)。
22
+ * 内部包成 sqlite 后端 —— 默认且当前唯一接入启动的后端,行为与迁移前一致。
23
+ */
24
+ export function setSeamDb(db) {
25
+ _backend = createSqliteBackend(db);
26
+ _sqliteHandle = db;
27
+ }
28
+ /**
29
+ * 直接替换 seam 后端(Phase 3 pg 切换的接入钩子)。
30
+ * Phase 2 不在启动路径调用本函数;保留以便 Phase 3 用 `setSeamBackend(createPgBackend())` 切换。
31
+ */
32
+ export function setSeamBackend(backend) {
33
+ _backend = backend;
34
+ _sqliteHandle = null; // 非 sqlite 后端无同步句柄 → 调用方 fail-closed
35
+ }
36
+ /** 当前后端种类(诊断/守卫用);未初始化返回 null。 */
37
+ export function seamBackendKind() {
38
+ return _backend ? _backend.kind : null;
39
+ }
40
+ /**
41
+ * 原始 better-sqlite3 句柄 —— 仅供【同步事务】逃生口(seam 三方法是 async,无法承载
42
+ * better-sqlite3 必须同步的 `db.transaction`,见本文件顶部事务说明)。仅当当前后端是 sqlite
43
+ * 时返回句柄;pg 后端 / 未初始化返回 null,调用方据此 fail-closed(PG 同步事务在 RFC-016
44
+ * Phase 3 才接入)。不是给非事务读写用的 —— 那些必须走 dbOne/dbAll/dbRun。
45
+ */
46
+ export function seamSqliteHandle() {
47
+ return _backend?.kind === 'sqlite' ? _sqliteHandle : null;
48
+ }
49
+ function backend() {
50
+ if (!_backend)
51
+ throw new Error('DB seam 未初始化 —— 启动时需先调用 setSeamDb(db)');
52
+ return _backend;
53
+ }
54
+ /** 单行读;无则 undefined。等价 `db.prepare(sql).get(...params)`。 */
55
+ export async function dbOne(sql, params = []) {
56
+ return backend().one(sql, params);
57
+ }
58
+ /** 多行读。等价 `db.prepare(sql).all(...params)`。 */
59
+ export async function dbAll(sql, params = []) {
60
+ return backend().all(sql, params);
61
+ }
62
+ /** 写(INSERT/UPDATE/DELETE)。等价 `db.prepare(sql).run(...params)`。 */
63
+ export async function dbRun(sql, params = []) {
64
+ return backend().run(sql, params);
65
+ }