@seasonkoh/webaz 0.1.26 → 0.1.27

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 (81) hide show
  1. package/LICENSE +2 -2
  2. package/NOTICE +24 -3
  3. package/README.md +74 -330
  4. package/README.zh-CN.md +419 -0
  5. package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +21 -0
  6. package/dist/layer0-foundation/L0-5-manifest/manifest.js +8 -3
  7. package/dist/layer1-agent/L1-1-mcp-server/auth.js +13 -1
  8. package/dist/layer1-agent/L1-1-mcp-server/server.js +36 -28
  9. package/dist/layer2-business/L2-9-contribution/admin-coordination-ingestion-engine.js +181 -0
  10. package/dist/layer2-business/L2-9-contribution/admin-coordination-resolver.js +114 -0
  11. package/dist/layer2-business/L2-9-contribution/admin-coordination-store.js +251 -0
  12. package/dist/layer2-business/L2-9-contribution/admin-operator-claim-workflow.js +390 -0
  13. package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +24 -0
  14. package/dist/layer2-business/L2-9-contribution/build-task-participation.js +6 -2
  15. package/dist/layer2-business/L2-9-contribution/build-task-quota.js +337 -0
  16. package/dist/layer2-business/L2-9-contribution/build-task-read.js +25 -2
  17. package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +57 -7
  18. package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +1 -1
  19. package/dist/layer2-business/L2-9-contribution/contribution-facts-read.js +66 -0
  20. package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +187 -18
  21. package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +29 -4
  22. package/dist/ledger.js +1 -1
  23. package/dist/pwa/admin-audit.js +38 -0
  24. package/dist/pwa/anti-abuse-thresholds.js +135 -0
  25. package/dist/pwa/cf-origin-guard.js +33 -0
  26. package/dist/pwa/contract-fingerprint.js +1 -0
  27. package/dist/pwa/data/onboarding-cases.js +2 -2
  28. package/dist/pwa/data/onboarding-quiz.js +1 -1
  29. package/dist/pwa/economic-participation.js +2 -2
  30. package/dist/pwa/integration-contract.js +46 -4
  31. package/dist/pwa/internal/pv-settlement.js +12 -0
  32. package/dist/pwa/internal/wallet-signer.js +26 -0
  33. package/dist/pwa/public/app.js +679 -679
  34. package/dist/pwa/public/i18n.js +15 -28
  35. package/dist/pwa/public/index.html +1 -1
  36. package/dist/pwa/public/openapi.json +4760 -2769
  37. package/dist/pwa/pv-kill-switch.js +31 -0
  38. package/dist/pwa/routes/admin-admins.js +48 -1
  39. package/dist/pwa/routes/admin-analytics.js +1 -10
  40. package/dist/pwa/routes/admin-atomic.js +4 -17
  41. package/dist/pwa/routes/admin-operator-claims.js +280 -0
  42. package/dist/pwa/routes/admin-reports.js +4 -26
  43. package/dist/pwa/routes/admin-tokenomics.js +2 -76
  44. package/dist/pwa/routes/admin-users-lifecycle.js +1 -14
  45. package/dist/pwa/routes/admin-users-query.js +23 -1
  46. package/dist/pwa/routes/admin-wallet-ops.js +1 -1
  47. package/dist/pwa/routes/auth-read.js +1 -5
  48. package/dist/pwa/routes/auth-register.js +3 -13
  49. package/dist/pwa/routes/build-task-quota.js +113 -0
  50. package/dist/pwa/routes/claim-verify.js +15 -11
  51. package/dist/pwa/routes/contribution-facts.js +18 -0
  52. package/dist/pwa/routes/dispute-cases.js +5 -4
  53. package/dist/pwa/routes/growth.js +3 -3
  54. package/dist/pwa/routes/orders-action.js +27 -10
  55. package/dist/pwa/routes/orders-create.js +1 -1
  56. package/dist/pwa/routes/products-meta.js +19 -6
  57. package/dist/pwa/routes/profile-placement.js +1 -1
  58. package/dist/pwa/routes/promoter.js +10 -29
  59. package/dist/pwa/routes/public-build-tasks.js +5 -1
  60. package/dist/pwa/routes/public-utils.js +9 -12
  61. package/dist/pwa/routes/referral.js +5 -26
  62. package/dist/pwa/routes/rewards-apply.js +3 -2
  63. package/dist/pwa/routes/share-redirects.js +1 -1
  64. package/dist/pwa/routes/shareables-interactions.js +2 -1
  65. package/dist/pwa/routes/task-proposals.js +85 -9
  66. package/dist/pwa/routes/users-public.js +1 -4
  67. package/dist/pwa/routes/wallet-read.js +2 -14
  68. package/dist/pwa/routes/webauthn.js +1 -1
  69. package/dist/pwa/server.js +156 -469
  70. package/dist/settlement-math.js +3 -3
  71. package/dist/version.js +6 -4
  72. package/package.json +33 -7
  73. package/dist/index.js +0 -182
  74. package/dist/pwa/public/docs/ECONOMIC-MODEL.md +0 -287
  75. package/dist/pwa/public/docs/INTEGRATOR.md +0 -67
  76. package/dist/pwa/public/docs/META-RULES-FULL.md +0 -543
  77. package/dist/test-dispute.js +0 -153
  78. package/dist/test-manifest.js +0 -61
  79. package/dist/test-mcp-tools.js +0 -135
  80. package/dist/test-reputation.js +0 -116
  81. package/dist/test-skill-market.js +0 -101
@@ -521,7 +521,7 @@ Options:
521
521
  },
522
522
  promoter_api_key: {
523
523
  type: 'string',
524
- description: "Referrer api_key (optional). ⚠️ Only L1 recorded (direct referrer, 70% commission); L2/L3 can't be inferred via MCP, redirects per region rule (singapore-like high max_levels charity chain_gap; global max_levels=1 → global_fund region cap). Full 7:2:1 three-tier chain requires buyer clicking ?ref= URL from webaz_share_link (creates product_share_attribution).",
524
+ description: "Referrer api_key (optional). ⚠️ Only L1 recorded (direct referrer, 70% commission); L2/L3 can't be inferred via MCP, so the undelivered L2/L3 portions go to commission_reserve (protocol reserve, in-only). Full 7:2:1 three-tier chain requires buyer clicking ?ref= URL from webaz_share_link (creates product_share_attribution).",
525
525
  },
526
526
  // B2 隐私购物
527
527
  anonymous_recipient: {
@@ -1444,7 +1444,8 @@ Discovery + suggesting need NO api_key (anyone / any agent can browse and propos
1444
1444
  Actions:
1445
1445
  - list_open (default): open public tasks (opt. filters: area / risk_level / auto_claimable / required_capabilities / agent_capabilities / max_duration_minutes / estimated_context_size / estimated_agent_budget — estimated_agent_budget is a resource/effort estimate, NOT a payment). Each task carries its execution boundary + the trusted canonical contribution target. NO api_key needed.
1446
1446
  - detail: one task's full execution boundary (allowed/forbidden paths, prohibited actions, acceptance criteria, verification commands, deliverables, definition_of_done) + the canonical repo to PR to + a copy-ready agent_handoff. NO api_key needed.
1447
- - suggest: propose a NEW task (title + summary/reason; opt. area/expected_outcome/source_ref/github_login). It enters the maintainer inbox — it is a suggestion, NOT a contribution fact / reward / participation, and never auto-becomes a task. NO api_key needed.
1447
+ - suggest: propose a NEW task (title + summary/reason; opt. area/expected_outcome/source_ref/github_login). It enters the maintainer inbox — it is a suggestion, NOT a contribution fact / reward / participation, and never auto-becomes a task. NO api_key needed (but pass your key to LINK it to your account so you can track it via my_suggestions).
1448
+ - my_suggestions: your OWN past proposals + their review status / public_reply / next_action (api_key). Agent-readable 回执 so a proposer-agent can act on the maintainer's decision (needs_info → resubmit; converted → see converted_ref).
1448
1449
  - claim: take an open task (api_key); provenance=human|ai_assisted|ai_authored (self-declared, not detected); auto-expires ~7d if not submitted. Returns a handoff — point a coding agent at it; the human needn't know git but stays accountable (Passkey).
1449
1450
  - submit: mark in_review with pr_ref + verification_summary (api_key). The PR's base repo MUST be the canonical WebAZ repo, and a verification_summary (what you ran/verified) is REQUIRED — both server-enforced. A human maintainer reviews next; done ≠ merge.
1450
1451
  - status: tasks you hold (api_key).
@@ -1454,12 +1455,12 @@ Coordinates + records only — NO merge/reward; acceptance (done) = human mainta
1454
1455
  inputSchema: {
1455
1456
  type: 'object',
1456
1457
  properties: {
1457
- action: { type: 'string', enum: ['list_open', 'detail', 'suggest', 'claim', 'submit', 'status', 'profile'], description: 'list_open (default) | detail | suggest | claim | submit | status | profile' },
1458
+ action: { type: 'string', enum: ['list_open', 'detail', 'suggest', 'my_suggestions', 'claim', 'submit', 'status', 'profile'], description: 'list_open (default) | detail | suggest | my_suggestions | claim | submit | status | profile' },
1458
1459
  api_key: { type: 'string', description: 'claim/submit/status/profile: your api_key (accountable identity). NOT needed for list_open/detail/suggest. (or set the WEBAZ_API_KEY env var)' },
1459
1460
  task_id: { type: 'string', description: 'detail / claim / submit: the task id' },
1460
1461
  area: { type: 'string', description: 'list_open: area filter / suggest: suggested area (e.g. search / docs / mcp)' },
1461
1462
  risk_level: { type: 'string', enum: ['low', 'medium', 'high', 'critical'], description: 'list_open: optional risk filter' },
1462
- auto_claimable: { type: 'boolean', description: 'list_open: optional filter — only auto-claimable (true) or manual-claim (false) tasks' },
1463
+ auto_claimable: { type: 'boolean', description: 'list_open: optional filter — true returns tasks an agent can just do (auto-claimable AND with a real effort estimate; matches the derived claimability=auto_claimable), false returns manual-claim tasks. A task with a placeholder (unknown) estimate is treated as manual_review even if its raw auto_claimable flag is true, so it is excluded from true.' },
1463
1464
  required_capabilities: { type: 'string', description: 'list_open: optional filter — comma-separated; matches tasks that REQUIRE ALL of the listed capabilities (superset/AND match on the task requirement). For "tasks my agent can do", use agent_capabilities instead.' },
1464
1465
  agent_capabilities: { type: 'string', description: 'list_open: optional filter — capabilities your agent HAS (comma-separated); matches tasks whose required_capabilities are a SUBSET of these, i.e. tasks your agent can actually do' },
1465
1466
  max_duration_minutes: { type: 'number', description: 'list_open: optional filter — only tasks whose estimated max duration fits within this many minutes (your idle time)' },
@@ -1518,18 +1519,22 @@ async function handleFeedback(args) {
1518
1519
  }
1519
1520
  // RFC-006 断点1(b)交接:从【可信】canonical 目标(API 响应里,绝不硬编码/不取自 task metadata)构造"怎么真正
1520
1521
  // 动手"。人的编码 agent 做 git/PR;Passkey 真人担责。sandbox 运行 / 本地草稿不算正式参与。
1521
- function buildContributeHandoff(cct, taskId) {
1522
+ function buildContributeHandoff(cct, taskId, caseId) {
1522
1523
  const c = (cct ?? {});
1523
- const repoUrl = c.canonical_github_url || 'https://github.com/seasonsagents-art/webaz';
1524
- const baseRepo = c.expected_pr_base_repo || c.canonical_repository_full_name || 'seasonsagents-art/webaz';
1524
+ const repoUrl = c.canonical_github_url || 'https://github.com/webaz-protocol/webaz';
1525
+ const baseRepo = c.expected_pr_base_repo || c.canonical_repository_full_name || 'webaz-protocol/webaz';
1525
1526
  const baseBranch = c.base_branch || 'main';
1527
+ // case_id threads proposal → task → PR. = the source proposal id when this task came from a proposal,
1528
+ // else the task id itself. Quote it in the PR so the whole case stays traceable end to end.
1529
+ const cid = caseId || taskId;
1526
1530
  return {
1531
+ case_id: cid,
1527
1532
  canonical_repo: baseRepo,
1528
1533
  repo: repoUrl,
1529
1534
  base_branch: baseBranch,
1530
1535
  start_here: 'Read AGENTS.md (project map + before-you-code + PR flow), then CONTRIBUTING.md.',
1531
1536
  do_the_work: 'Point a coding agent (e.g. Claude Code) at the repo on a single-topic branch. The buyer/shopping agent is not the coding agent — hand off to one.',
1532
- submit_pr: `Open a PR whose BASE repo is ${baseRepo} (${repoUrl}), base branch ${baseBranch}. If any target repo differs from this canonical repo, STOP and ask the human — never contribute to a non-canonical repository.`,
1537
+ submit_pr: `Open a PR whose BASE repo is ${baseRepo} (${repoUrl}), base branch ${baseBranch}. Reference case ${cid} in the PR title/body so the proposal → task → PR chain stays traceable. If any target repo differs from this canonical repo, STOP and ask the human — never contribute to a non-canonical repository.`,
1533
1538
  pr_flow: 'Commit with DCO sign-off (git commit -s). If AI-authored, mark the PR per the meta-rule. Humans merge — no auto-merge.',
1534
1539
  then: `When the PR is open, report it back: webaz_contribute action=submit task_id=${taskId} pr_ref=#<N> verification_summary="<the verification_commands you ran + their results>". Both pr_ref and verification_summary are required.`,
1535
1540
  not_participation: 'A sandbox run or a local-only draft is NOT participation and is NOT a contribution; only a merged PR (or recognized issue/task/RFC) on the canonical repo enters the contribution record.',
@@ -1580,7 +1585,7 @@ export async function handleContribute(args) {
1580
1585
  return { error: 'task_id required for action=detail' };
1581
1586
  const r = await apiCall('/api/public/build-tasks/' + encodeURIComponent(tid));
1582
1587
  if (!r.error && r.task)
1583
- r.agent_handoff = buildContributeHandoff(r.canonical_contribution_target, tid);
1588
+ r.agent_handoff = buildContributeHandoff(r.canonical_contribution_target, tid, r.task.case_id);
1584
1589
  return r;
1585
1590
  }
1586
1591
  if (action === 'suggest') {
@@ -1592,6 +1597,7 @@ export async function handleContribute(args) {
1592
1597
  return { error: 'summary (the reason) required for action=suggest' };
1593
1598
  const r = await apiCall('/api/public/task-proposals', {
1594
1599
  method: 'POST',
1600
+ apiKey, // optional — when present, links the proposal to the submitter so it shows up in action=my_suggestions (still works anonymously)
1595
1601
  body: {
1596
1602
  title, summary,
1597
1603
  suggested_area: args.area ?? args.suggested_area,
@@ -1600,6 +1606,8 @@ export async function handleContribute(args) {
1600
1606
  proposer_github_login: args.proposer_github_login,
1601
1607
  },
1602
1608
  });
1609
+ if (!r.error && r.linked_to_account)
1610
+ r._next = 'Track this proposal\'s review status + reply: webaz_contribute action=my_suggestions api_key=<key>.';
1603
1611
  // typed errors (RATE_LIMITED / DUPLICATE_PROPOSAL / validation) are already mapped by apiCall; the
1604
1612
  // success response already carries the route-level `proposal_notice` (suggestion ≠ contribution/reward).
1605
1613
  return r;
@@ -1612,6 +1620,13 @@ export async function handleContribute(args) {
1612
1620
  };
1613
1621
  if (action === 'status')
1614
1622
  return apiCall('/api/build-tasks?mine=1', { apiKey });
1623
+ if (action === 'my_suggestions') {
1624
+ // your OWN past proposals + review status/public_reply/next_action (agent-readable 回执). Own rows only (server-enforced).
1625
+ const r = await apiCall('/api/me/task-proposals', { apiKey });
1626
+ if (!r.error)
1627
+ r._next = 'Each item carries status + public_reply + next_action. needs_info → resubmit via action=suggest referencing the id; converted → see converted_ref.';
1628
+ return r;
1629
+ }
1615
1630
  if (action === 'profile')
1616
1631
  return apiCall('/api/build-reputation/me', { apiKey });
1617
1632
  if (action === 'claim') {
@@ -1730,8 +1745,11 @@ async function handleInfo() {
1730
1745
  // 连接两个场景:用协议(本工具) ↔ 改协议(开发协作)。想改 WebAZ 本身的 agent 从这里进。
1731
1746
  for_contributors: {
1732
1747
  note: 'Want to change WebAZ itself (not just use it)? This is an open, agent-native protocol — AI-authored PRs are welcome, with accountability. / 想改 WebAZ 本身(不只是用)?这是开放的 agent 原生协议,欢迎 AI 提 PR,但需问责。',
1733
- repo: 'https://github.com/seasonsagents-art/webaz',
1748
+ repo: 'https://github.com/webaz-protocol/webaz',
1734
1749
  start_here: 'AGENTS.md (project map + before-you-code + PR flow) → CONTRIBUTING.md (full guide)',
1750
+ // 低门槛路径:无需 api_key、无需 clone 仓库,直接经协议发现任务 / 提建议(对外 well-known 入口也有,见 agent_quickstart)。
1751
+ no_key_path: 'No api_key needed to START contributing: discover open tasks and submit a suggestion via webaz_contribute action=list_open / action=suggest (mirrors GET /api/public/build-tasks + POST /api/public/task-proposals). / 无需 key 即可起步:webaz_contribute action=list_open 发现开放任务、action=suggest 提建议。',
1752
+ contribution_boundary: 'A suggestion is a proposal in the maintainer review inbox — NOT a contribution fact, NOT formal participation, and NOT any economic or redemption right; recorded contribution is facts / evidence / attribution only (RFC-017). / 建议只是进入维护者审阅箱的提议,不是贡献事实、不是正式参与、不构成任何经济或兑现权利;记录的贡献只是事实/证据/归属(RFC-017)。',
1735
1753
  ai_accountability: 'AI-authored PRs: add 🤖🤖🤖 to the PR title; the agent must be triggered by a Passkey-bound human (webazer) who is accountable. / AI 提 PR:标题加 🤖🤖🤖,且须由已绑 Passkey 的真人(webazer)触发并担责。',
1736
1754
  },
1737
1755
  // NETWORK 模式:真网络 live 状态(best-effort 拉自 webaz.xyz);SANDBOX 模式为 null。
@@ -1741,10 +1759,10 @@ async function handleInfo() {
1741
1759
  // 佣金机制 —— 纯功能性描述(怎么运作),不做"自证清白"式辩护。
1742
1760
  commission_model: {
1743
1761
  split: '7:2:1 — L1 70% / L2 20% / L3 10% of an order\'s commission_pool',
1744
- jurisdiction_tiers: 'Tiers are graded by the order region\'s max_levels — NOT a uniform 3 tiers everywhere. e.g. global region max_levels=1 → L1 only; singapore (etc.) max_levels=3 → up to L3. A region may also be 0 (no commission tiers; pool → community fund).',
1762
+ jurisdiction_tiers: 'Tiers are graded by the order region\'s max_levels — NOT a uniform 3 tiers everywhere. e.g. global region max_levels=1 → L1 only; singapore (etc.) max_levels=3 → up to L3. A region may also be 0 (no commission tiers; pool → commission_reserve / protocol reserve).',
1745
1763
  attribution: 'EXPLICIT per-order — commission goes to the promoter attributed at purchase time, not derived from the buyer\'s sponsor chain.',
1746
1764
  how_to_attribute: 'L1: webaz_place_order(promoter_api_key) records the direct promoter. Full L2/L3 chain requires the buyer to arrive via a webaz_share_link /i/<permanent_code> (?ref=<permanent_code>) URL clicked in a browser (builds product_share_attribution).',
1747
- redirect_rules: 'chain_gap (no L / invalid sponsor) charity_fund; level beyond the region cap → global_fund.',
1765
+ redirect_rules: 'all undelivered commission (chain_gap / no L / invalid sponsor / level beyond the region cap / max_levels=0 / opt-out / escrow expiry) commission_reserve (protocol reserve, in-only; use decided by governance).',
1748
1766
  l1_gate: 'the promoter must be a verified buyer (≥1 completed order) to receive commission, otherwise that share redirects.',
1749
1767
  opt_in: 'Participation is opt-in (RFC-002): default = off. A user applies (Passkey + ≥1 completed order); attribution is always recorded. Commission destination is state-dependent: never_activated / auto_downgraded → held in pending_commission_escrow (30d grace), recoverable by (re-)activating within the window, else swept to commission_reserve; deactivated (active opt-out) → future commission goes directly to commission_reserve, NOT escrow and NOT recoverable. Never to charity_fund. See docs/rfcs/RFC-002-rewards-opt-in.md.',
1750
1768
  },
@@ -3438,7 +3456,7 @@ function handleRotateKey(args) {
3438
3456
  },
3439
3457
  };
3440
3458
  }
3441
- // ─── 推广 / 双轨 (Tokenomics) ───────────────────────────────────
3459
+ // ─── 推广 / 推荐网络 (Tokenomics) ───────────────────────────────────
3442
3460
  async function handleReferral(args) {
3443
3461
  // RFC-003 Batch 2:NETWORK 模式 → webaz.xyz 真网络聚合(Bearer api_key);SANDBOX 走本地。
3444
3462
  if (toolBackend('webaz_referral') === 'network') {
@@ -3465,16 +3483,9 @@ async function handleReferral(args) {
3465
3483
  const completed = db.prepare("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'").get(userId).n;
3466
3484
  const override = db.prepare("SELECT l1_share_override FROM users WHERE id = ?").get(userId)?.l1_share_override ?? 0;
3467
3485
  const canL1 = override === 1 || (override === 0 && completed > 0);
3468
- // 原子能双轨
3486
+ // Neutral participation record only — placement position + per-leg PV. Matching-rewards engine excised (#401):
3487
+ // no Score / tier / pair-volume / payout is read or exposed.
3469
3488
  const me = db.prepare("SELECT total_left_pv, total_right_pv, left_child_id, right_child_id, placement_id, placement_side FROM users WHERE id = ?").get(userId);
3470
- const score = db.prepare(`
3471
- SELECT COALESCE(SUM(CASE WHEN settled_at IS NULL THEN score ELSE 0 END),0) as pending,
3472
- COALESCE(SUM(CASE WHEN settled_at IS NOT NULL THEN waz_amount ELSE 0 END),0) as settled_waz
3473
- FROM binary_score_records WHERE user_id = ?
3474
- `).get(userId);
3475
- const tiers = db.prepare("SELECT tier, pv_threshold, score_per_hit FROM binary_tier_config WHERE active=1 ORDER BY tier ASC").all();
3476
- const pair = Math.min(Number(me?.total_left_pv ?? 0), Number(me?.total_right_pv ?? 0));
3477
- const nextTier = tiers.find(t => t.pv_threshold > pair);
3478
3489
  // invite / share links use permanent_code ONLY — never usr_xxx. (sandbox users have one from register.)
3479
3490
  const permaCode = db.prepare("SELECT permanent_code FROM users WHERE id = ?").get(userId)?.permanent_code || null;
3480
3491
  return {
@@ -3496,15 +3507,12 @@ async function handleReferral(args) {
3496
3507
  l1: byLevel[1], l2: byLevel[2], l3: byLevel[3],
3497
3508
  grand_total: byLevel[1].total + byLevel[2].total + byLevel[3].total,
3498
3509
  },
3499
- binary: {
3500
- // pre-public 去左右码:只暴露唯一的推荐码;放置侧别由系统自动决定(无 left/right 选择)
3510
+ placement: {
3511
+ // Neutral participation/attribution record: a single referral code + per-leg PV. No matching rewards.
3501
3512
  referral_link: permaCode ? `/i/${permaCode}` : null,
3502
3513
  total_left_pv: Number(me?.total_left_pv ?? 0),
3503
3514
  total_right_pv: Number(me?.total_right_pv ?? 0),
3504
- pair_volume: pair,
3505
- next_tier: nextTier ? { tier: nextTier.tier, pv_threshold: nextTier.pv_threshold, score_per_hit: nextTier.score_per_hit, pv_needed: nextTier.pv_threshold - pair } : null,
3506
- score_pending: score.pending,
3507
- waz_total_earned: score.settled_waz,
3515
+ note: 'total_left_pv / total_right_pv are a participation / attribution record only — not income, not redeemable, no entitlement.',
3508
3516
  },
3509
3517
  rewards_status: (() => {
3510
3518
  // RFC-002 §3.5 — 4 states + pending escrow visibility (PR-4)
@@ -0,0 +1,181 @@
1
+ import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
2
+ import { coordinationActionSpec, LIVE_ADMIN_COORDINATION_AUDIT_ACTIONS } from './admin-coordination-store.js';
3
+ import { resolveOperatorClaimAsOf, resolveAgentMandateAsOf } from './admin-coordination-resolver.js';
4
+ import { readAdminActionContext } from '../../pwa/admin-audit.js';
5
+ /* eslint-disable @typescript-eslint/no-explicit-any */
6
+ function deriveProvenance(ctx) {
7
+ const p = ctx.provenance;
8
+ if (p === 'human' || p === 'ai_assisted' || p === 'ai_authored' || p === 'unknown')
9
+ return p;
10
+ if (ctx.actor_type === 'agent')
11
+ return ctx.agent_mode === 'agent_assisted' ? 'ai_assisted' : 'ai_authored';
12
+ if (ctx.actor_type === 'human' || ctx.actor_type === 'admin_account')
13
+ return ctx.agent_mode === 'human_direct' ? 'human' : 'unknown';
14
+ return 'unknown';
15
+ }
16
+ export function ingestAdminCoordinationFact(db, input) {
17
+ const { auditId, redactionSummary } = input;
18
+ if (!auditId)
19
+ return { ok: false, reason: 'invalid_input', detail: 'auditId is required' };
20
+ // ── anchor: the audit row is the evidence truth; read EVERYTHING from it (incl. the coordinated
21
+ // object target_id) so the fact can NEVER point at a different object than the audit row records ──
22
+ const row = db.prepare('SELECT id, admin_id, action, detail, created_at, target_type, target_id FROM admin_audit_log WHERE id = ?').get(auditId);
23
+ if (!row)
24
+ return { ok: false, reason: 'audit_row_not_found', detail: auditId };
25
+ const action = row.action;
26
+ const spec = coordinationActionSpec(action);
27
+ if (!spec)
28
+ return { ok: false, reason: 'unknown_action', detail: action }; // allowlist fail-closed
29
+ const ctx = readAdminActionContext(row.detail);
30
+ const actorType = ctx.actor_type;
31
+ const adminAccountId = row.admin_id;
32
+ const occurredAt = row.created_at;
33
+ // legacy / context-less / system rows are NOT contribution-eligible.
34
+ if (actorType !== 'human' && actorType !== 'admin_account' && actorType !== 'agent') {
35
+ return { ok: false, reason: 'not_eligible_context', detail: `actor_type=${String(actorType)}` };
36
+ }
37
+ // ── attribution gate (as-of) — NOT written onto the fact; only proves the work is attributable ──
38
+ let contributorAccountId;
39
+ let via;
40
+ let executorRef;
41
+ if (actorType === 'agent') {
42
+ const actorRef = String(ctx.actor_ref || '');
43
+ const agentRef = actorRef.startsWith('agent:') ? actorRef.slice('agent:'.length) : actorRef;
44
+ const mandateId = ctx.mandate_id ? String(ctx.mandate_id) : '';
45
+ // The audit row's mandate_id is REQUIRED and DECIDES attribution — resolve by (agent_ref, mandate_id),
46
+ // never by agent_ref alone (one agent_ref may hold several mandates → wrong-account错账 otherwise).
47
+ if (!agentRef || !mandateId)
48
+ return { ok: false, reason: 'not_eligible_context', detail: 'agent action requires _ctx.actor_ref + _ctx.mandate_id' };
49
+ const m = resolveAgentMandateAsOf(db, agentRef, mandateId, occurredAt);
50
+ if (!m)
51
+ return { ok: false, reason: 'no_attribution', detail: `no valid mandate (agent_ref=${agentRef}, mandate_id=${mandateId}) as-of occurred_at` };
52
+ if (!m.allowed_actions.includes(action))
53
+ return { ok: false, reason: 'agent_action_not_in_mandate', detail: action };
54
+ contributorAccountId = m.owner_contributor_account_id;
55
+ via = 'agent_mandate';
56
+ executorRef = `agent:${agentRef}#${mandateId}`; // mandate encoded → deterministic read-time resolution
57
+ }
58
+ else {
59
+ const c = resolveOperatorClaimAsOf(db, adminAccountId, occurredAt);
60
+ if (!c)
61
+ return { ok: false, reason: 'no_attribution', detail: 'no approved operator claim as-of occurred_at' };
62
+ // A self/related (root/founder bootstrap) approval that is NOT honestly disclosed must NOT enter
63
+ // production evidence — fail closed until an append-only marking correction discloses self_or_related.
64
+ if (c.self_related && !c.honestly_disclosed) {
65
+ return { ok: false, reason: 'self_related_not_disclosed', detail: `claim ${c.claim_event_id} is self/related but marked ${c.approval_kind}/${c.conflict_disclosure}; needs a governance-marking correction` };
66
+ }
67
+ contributorAccountId = c.contributor_account_id;
68
+ via = 'operator_claim';
69
+ executorRef = `admin:${adminAccountId}`;
70
+ }
71
+ const provenance = deriveProvenance(ctx);
72
+ const visibility = input.visibility ?? 'governance_only';
73
+ const sourceEventKey = `admin:${auditId}:coordination`;
74
+ // The coordinated object is the audit row's target_id (NOT a caller-supplied value). artifact_ref is
75
+ // NOT NULL → fall back to the action when the row has no target_id; source_id mirrors it (or NULL).
76
+ const targetId = row.target_id ?? null;
77
+ const artifactRef = targetId || action;
78
+ // dry-run: all gates passed; report what WOULD happen without writing (read-only — no insert, no tx).
79
+ if (input.dryRun) {
80
+ const existing = db.prepare('SELECT fact_id FROM contribution_facts WHERE source_event_key = ?').get(sourceEventKey);
81
+ return { ok: true, status: existing ? 'already_present' : 'would_ingest', factId: existing?.fact_id ?? '(dry-run)', sourceEventKey, executorRef, contributorAccountId, via };
82
+ }
83
+ const run = db.transaction(() => {
84
+ const existing = db.prepare('SELECT fact_id FROM contribution_facts WHERE source_event_key = ?').get(sourceEventKey);
85
+ if (existing)
86
+ return { factId: existing.fact_id, status: 'already_present' };
87
+ const factId = generateId('cfact');
88
+ db.prepare(`INSERT INTO contribution_facts (fact_id, source_event_key, source, type, artifact_ref, occurred_at, executor_ref, accountable_ref, provenance, status, immutable)
89
+ VALUES (?,?,?,?,?,?,?, NULL, ?, 'active', 1)`).run(factId, sourceEventKey, spec.factSource, spec.factType, artifactRef, occurredAt, executorRef, provenance);
90
+ db.prepare(`INSERT INTO admin_coordination_fact_sources (fact_id, admin_audit_log_id, source_type, source_id, visibility, redaction_summary)
91
+ VALUES (?,?,?,?,?,?)`).run(factId, auditId, action, targetId, visibility, redactionSummary ?? null);
92
+ return { factId, status: 'ingested' };
93
+ });
94
+ const out = run.immediate();
95
+ return { ok: true, status: out.status, factId: out.factId, sourceEventKey, executorRef, contributorAccountId, via };
96
+ }
97
+ // ───────────────────────────── batch / operator entry ─────────────────────────────
98
+ // Small, bounded, manual-run batch over ALLOWLISTED audit rows. NOT a historical backfill: the operator
99
+ // scopes it with sinceTime / sinceId + a hard limit, and it is DRY-RUN unless `commit` is set. Each row
100
+ // goes through the SAME single-row engine above (same fail-closed gates, same idempotency), so the batch
101
+ // adds no new attribution logic — it only selects candidates and aggregates the per-row outcomes.
102
+ export const DEFAULT_INGEST_LIMIT = 50;
103
+ export const MAX_INGEST_LIMIT = 500;
104
+ /**
105
+ * Parse the `--commit` switch at the production write entry. Accept ONLY a bare flag (`raw === ''`) or
106
+ * `--commit=true`. Anything else (`false`, `0`, `no`, …) THROWS — an explicit dry-run intent must never
107
+ * be misread as a write. `undefined` (flag absent) → false (dry-run).
108
+ */
109
+ export function parseCommitSwitch(raw) {
110
+ if (raw === undefined)
111
+ return false;
112
+ if (raw === '' || raw === 'true')
113
+ return true;
114
+ throw new Error(`invalid_commit_flag: --commit takes no value (or =true); got ${JSON.stringify(raw)}`);
115
+ }
116
+ /**
117
+ * Select rows whose action is in the LIVE production set (only the real `operator_claim.*` actions —
118
+ * NOT the reserved concept names), run each through the single-row engine, and aggregate. Dry-run by
119
+ * default (NOTHING written unless `commit: true`). The candidate query filters to the live set so
120
+ * non-coordination AND reserved-concept rows are never even scanned; unknown/uneligible/no-claim rows
121
+ * that DO match still fail closed per-row and are reported as `skipped` with a reason.
122
+ *
123
+ * THROWS `invalid_cursor` when `sinceId` is supplied but matches no audit row — a fail-closed guard so a
124
+ * typo'd cursor can NEVER silently degrade into a from-the-beginning (backfill) scan.
125
+ *
126
+ * THROWS `commit_requires_cursor` when `commit` is true but NEITHER `sinceTime` nor `sinceId` is given —
127
+ * a no-cursor commit would write from the earliest live row, i.e. a small historical backfill. This
128
+ * pipeline is "from the present, cursor + limit scoped", so a write MUST be cursor-bounded. A no-cursor
129
+ * DRY-RUN is still allowed (preview from the earliest row writes nothing).
130
+ */
131
+ export function ingestAdminCoordinationSince(db, options = {}) {
132
+ const commit = options.commit === true;
133
+ if (commit && !options.sinceTime && !options.sinceId) {
134
+ throw new Error('commit_requires_cursor: --commit requires --since-time or --since-id (no-cursor commit would backfill history); run a dry-run first to find a cursor');
135
+ }
136
+ const limit = Math.min(Math.max(1, options.limit ?? DEFAULT_INGEST_LIMIT), MAX_INGEST_LIMIT);
137
+ const actions = [...LIVE_ADMIN_COORDINATION_AUDIT_ACTIONS];
138
+ const placeholders = actions.map(() => '?').join(',');
139
+ const where = [`action IN (${placeholders})`];
140
+ const params = [...actions];
141
+ // resume cursor: rows strictly after (cursorTime, cursorId) in (created_at ASC, id ASC) order.
142
+ let cursorTime = options.sinceTime;
143
+ let cursorId;
144
+ if (options.sinceId) {
145
+ const c = db.prepare('SELECT id, created_at FROM admin_audit_log WHERE id = ?').get(options.sinceId);
146
+ // fail-closed: an explicit-but-unknown cursor must NOT degrade into a full from-earliest scan.
147
+ if (!c)
148
+ throw new Error(`invalid_cursor: no admin_audit_log row with id=${options.sinceId}`);
149
+ cursorTime = c.created_at;
150
+ cursorId = c.id;
151
+ }
152
+ if (cursorTime && cursorId) {
153
+ where.push('(created_at > ? OR (created_at = ? AND id > ?))');
154
+ params.push(cursorTime, cursorTime, cursorId);
155
+ }
156
+ else if (cursorTime) {
157
+ where.push('created_at > ?');
158
+ params.push(cursorTime);
159
+ }
160
+ params.push(limit);
161
+ const candidates = db.prepare(`SELECT id, action, admin_id, created_at FROM admin_audit_log WHERE ${where.join(' AND ')} ORDER BY created_at ASC, id ASC LIMIT ?`).all(...params);
162
+ const rows = [];
163
+ let ingested = 0, wouldIngest = 0, alreadyPresent = 0, skipped = 0;
164
+ for (const cand of candidates) {
165
+ const r = ingestAdminCoordinationFact(db, { auditId: cand.id, visibility: options.visibility, dryRun: !commit });
166
+ const base = { auditId: cand.id, action: cand.action, adminId: cand.admin_id, occurredAt: cand.created_at };
167
+ if (!r.ok) {
168
+ skipped++;
169
+ rows.push({ ...base, outcome: 'skipped', reason: r.reason + (r.detail ? `: ${r.detail}` : '') });
170
+ continue;
171
+ }
172
+ if (r.status === 'ingested')
173
+ ingested++;
174
+ else if (r.status === 'would_ingest')
175
+ wouldIngest++;
176
+ else
177
+ alreadyPresent++;
178
+ rows.push({ ...base, outcome: r.status, contributorAccountId: r.contributorAccountId, via: r.via, factId: r.factId });
179
+ }
180
+ return { committed: commit, scanned: candidates.length, ingested, wouldIngest, alreadyPresent, skipped, limit, rows };
181
+ }
@@ -0,0 +1,114 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ /** As-of resolution of which contributor a non-root admin SEAT was attributed to at `asOf`. */
3
+ export function resolveOperatorClaimAsOf(db, adminAccountId, asOf) {
4
+ const events = db.prepare(`SELECT event_type, contributor_account_id, approval_kind, approved_by, conflict_disclosure, event_id, effective_from, supersedes_event_id, created_at
5
+ FROM admin_operator_claim_events WHERE admin_account_id = ? ORDER BY effective_from ASC, created_at ASC, rowid ASC`).all(adminAccountId);
6
+ // An approval is terminated as-of `asOf` iff a revoked/superseded event that is EFFECTIVE by then
7
+ // LINKS to it (supersedes_event_id). Link-based (not a timestamp window) so a same-second
8
+ // approve→revoke and a same-instant rotation both resolve deterministically.
9
+ const terminatedIds = new Set(events
10
+ .filter(e => (e.event_type === 'revoked' || e.event_type === 'superseded') && e.effective_from && e.effective_from <= asOf && e.supersedes_event_id)
11
+ .map(e => e.supersedes_event_id));
12
+ const active = events.filter(e => e.event_type === 'approved' && e.effective_from && e.effective_from <= asOf && !terminatedIds.has(e.event_id));
13
+ if (active.length === 0)
14
+ return null;
15
+ const latest = active[active.length - 1]; // latest effective approval still active as-of asOf
16
+ // Overlay the LATEST append-only marking correction (if any). The correction never changes the
17
+ // contributor or the effective interval — only the disclosure marking — so it applies regardless of
18
+ // asOf (the approval was always self/related; we are only recording that honestly now).
19
+ const correction = db.prepare(`SELECT approval_kind, conflict_disclosure FROM admin_operator_claim_marking_corrections
20
+ WHERE approved_event_id = ? ORDER BY corrected_at DESC, rowid DESC LIMIT 1`).get(latest.event_id);
21
+ const approvalKind = (correction?.approval_kind ?? latest.approval_kind) ?? null;
22
+ const conflictDisclosure = correction?.conflict_disclosure ?? latest.conflict_disclosure;
23
+ const approvedBy = latest.approved_by ?? null;
24
+ // self/related = the approver was itself a party to the claim (admin seat or contributor) — a
25
+ // root/founder bootstrap self-approval. Such an approval MUST disclose self_or_related + a
26
+ // non-independent_governance kind; otherwise it is dishonestly marked (gated downstream).
27
+ const selfRelated = !!approvedBy && (approvedBy === adminAccountId || approvedBy === latest.contributor_account_id);
28
+ const honestlyDisclosed = !selfRelated || (conflictDisclosure === 'self_or_related' && approvalKind !== 'independent_governance');
29
+ return {
30
+ contributor_account_id: latest.contributor_account_id,
31
+ approval_kind: approvalKind,
32
+ conflict_disclosure: conflictDisclosure,
33
+ claim_event_id: latest.event_id,
34
+ approved_by: approvedBy,
35
+ self_related: selfRelated,
36
+ corrected: !!correction,
37
+ honestly_disclosed: honestlyDisclosed,
38
+ };
39
+ }
40
+ /**
41
+ * As-of resolution of a SPECIFIC agent mandate (agent_ref + mandate_id) effective at `asOf` (→ owner
42
+ * contributor). Keyed on BOTH agent_ref AND mandate_id: when one agent_ref has several mandates, the
43
+ * audit row's mandate_id decides attribution — never "whichever mandate is latest" (which would
44
+ * mis-credit). A mandate_id belonging to a different agent_ref does not match.
45
+ */
46
+ export function resolveAgentMandateAsOf(db, agentRef, mandateId, asOf) {
47
+ if (!agentRef || !mandateId)
48
+ return null;
49
+ const events = db.prepare(`SELECT event_type, mandate_id, owner_contributor_account_id, allowed_actions, effective_from, expires_at, revoked_at, event_id, created_at
50
+ FROM agent_execution_mandate_events WHERE agent_ref = ? AND mandate_id = ? ORDER BY effective_from ASC, created_at ASC`).all(agentRef, mandateId);
51
+ const grants = events.filter(e => e.event_type === 'granted' &&
52
+ e.effective_from && e.effective_from <= asOf &&
53
+ (!e.expires_at || e.expires_at >= asOf) &&
54
+ (!e.revoked_at || e.revoked_at > asOf));
55
+ if (grants.length === 0)
56
+ return null;
57
+ const latest = grants[grants.length - 1];
58
+ const terminated = events.some(e => (e.event_type === 'revoked' || e.event_type === 'superseded') &&
59
+ e.effective_from && e.effective_from > latest.effective_from && e.effective_from <= asOf);
60
+ if (terminated)
61
+ return null;
62
+ let allowed = [];
63
+ try {
64
+ const p = JSON.parse(latest.allowed_actions);
65
+ if (Array.isArray(p))
66
+ allowed = p.map(String);
67
+ }
68
+ catch {
69
+ allowed = [];
70
+ }
71
+ return { owner_contributor_account_id: latest.owner_contributor_account_id, mandate_id: latest.mandate_id, allowed_actions: allowed, grant_event_id: latest.event_id };
72
+ }
73
+ /**
74
+ * Unified resolver. `github:<id>` → current identity binding; `admin:<id>` / `agent:<ref>` → as-of
75
+ * claim/mandate at `occurredAt`. Returns null when no valid attribution exists (the fact then has no
76
+ * resolvable contributor — it stays evidence only).
77
+ */
78
+ export function resolveCoordinationContributor(db, executorRef, occurredAt) {
79
+ if (executorRef.startsWith('github:')) {
80
+ const githubActorId = executorRef.slice('github:'.length);
81
+ if (!githubActorId)
82
+ return null;
83
+ const row = db.prepare('SELECT account_id FROM identity_bindings_active WHERE github_actor_id = ?').get(githubActorId);
84
+ if (!row)
85
+ return null;
86
+ return { contributor_account_id: row.account_id, via: 'github_binding', detail: {} };
87
+ }
88
+ if (executorRef.startsWith('admin:')) {
89
+ const adminAccountId = executorRef.slice('admin:'.length);
90
+ if (!adminAccountId)
91
+ return null;
92
+ const r = resolveOperatorClaimAsOf(db, adminAccountId, occurredAt);
93
+ if (!r)
94
+ return null;
95
+ return { contributor_account_id: r.contributor_account_id, via: 'operator_claim', detail: { approval_kind: r.approval_kind, conflict_disclosure: r.conflict_disclosure, claim_event_id: r.claim_event_id } };
96
+ }
97
+ if (executorRef.startsWith('agent:')) {
98
+ // executor_ref for agents is `agent:<agent_ref>#<mandate_id>` — the mandate is encoded so read-time
99
+ // resolution is as deterministic as ingest-time (no "latest mandate wins" ambiguity).
100
+ const rest = executorRef.slice('agent:'.length);
101
+ const hash = rest.indexOf('#');
102
+ if (hash < 0)
103
+ return null; // no mandate encoded → not resolvable (refuse rather than guess)
104
+ const agentRef = rest.slice(0, hash);
105
+ const mandateId = rest.slice(hash + 1);
106
+ if (!agentRef || !mandateId)
107
+ return null;
108
+ const r = resolveAgentMandateAsOf(db, agentRef, mandateId, occurredAt);
109
+ if (!r)
110
+ return null;
111
+ return { contributor_account_id: r.owner_contributor_account_id, via: 'agent_mandate', detail: { mandate_id: r.mandate_id } };
112
+ }
113
+ return null;
114
+ }