@seasonkoh/webaz 0.1.23 → 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 +198 -83
  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
@@ -0,0 +1,222 @@
1
+ import { RISK_LEVELS, AUDIENCES, CONTEXT_SIZES, AGENT_BUDGETS, parseJsonList } from './build-task-agent-metadata-store.js';
2
+ import { TASK_STATUS, releaseExpiredClaims } from './build-tasks-engine.js';
3
+ import { getCanonicalContributionTarget } from './canonical-contribution-target.js';
4
+ import { withUncommittedValueBoundary } from './contribution-display-envelope.js';
5
+ /**
6
+ * The single read envelope: stamps the SAME trusted canonical_contribution_target (anti GitHub-target
7
+ * confusion) AND the uncommitted value_boundary onto every task-board read response (public + member), so
8
+ * an agent always gets the identical, config-sourced target — never one derived from task metadata.
9
+ */
10
+ export function withContributionReadEnvelope(payload) {
11
+ return withUncommittedValueBoundary({ ...payload, canonical_contribution_target: getCanonicalContributionTarget() });
12
+ }
13
+ /** Validate raw query filters — fail-closed: an unknown value is rejected, never silently ignored. */
14
+ export function validateTaskFilters(q) {
15
+ const f = {};
16
+ const bad = (code, detail) => ({ ok: false, code, detail });
17
+ if (q.status !== undefined) {
18
+ if (typeof q.status !== 'string' || !TASK_STATUS.has(q.status))
19
+ return bad('INVALID_FILTER_STATUS', 'status must be open|claimed|in_review|done|abandoned');
20
+ f.status = q.status;
21
+ }
22
+ if (q.area !== undefined) {
23
+ if (typeof q.area !== 'string' || q.area.trim().length === 0)
24
+ return bad('INVALID_FILTER_AREA', 'area must be a non-empty string');
25
+ f.area = q.area.slice(0, 64);
26
+ }
27
+ if (q.risk_level !== undefined) {
28
+ if (typeof q.risk_level !== 'string' || !RISK_LEVELS.includes(q.risk_level))
29
+ return bad('INVALID_FILTER_RISK_LEVEL', `risk_level must be ${RISK_LEVELS.join('|')}`);
30
+ f.risk_level = q.risk_level;
31
+ }
32
+ if (q.audience !== undefined) {
33
+ if (typeof q.audience !== 'string' || !AUDIENCES.includes(q.audience))
34
+ return bad('INVALID_FILTER_AUDIENCE', `audience must be ${AUDIENCES.join('|')}`);
35
+ f.audience = q.audience;
36
+ }
37
+ if (q.auto_claimable !== undefined) {
38
+ if (q.auto_claimable !== 'true' && q.auto_claimable !== 'false')
39
+ return bad('INVALID_FILTER_AUTO_CLAIMABLE', 'auto_claimable must be true|false');
40
+ f.auto_claimable = q.auto_claimable === 'true';
41
+ }
42
+ // required_capabilities: comma-separated; a task matches if it requires ALL of them (AND). Capped to keep
43
+ // the WHERE bounded; fail-closed on a non-string / empty list.
44
+ if (q.required_capabilities !== undefined) {
45
+ if (typeof q.required_capabilities !== 'string')
46
+ return bad('INVALID_FILTER_REQUIRED_CAPABILITIES', 'required_capabilities must be a comma-separated string');
47
+ const caps = q.required_capabilities.split(',').map(s => s.trim()).filter(Boolean).slice(0, 10).map(c => c.slice(0, 64));
48
+ if (caps.length === 0)
49
+ return bad('INVALID_FILTER_REQUIRED_CAPABILITIES', 'required_capabilities must list at least one non-empty capability');
50
+ f.requiredCapabilities = caps;
51
+ }
52
+ // agent_capabilities: the agent's OWN capability set — match tasks whose required_capabilities are a
53
+ // SUBSET (tasks the agent can actually do). Distinct from required_capabilities (which is AND/superset:
54
+ // "task requires all listed"). Same 10-item / 64-char caps; fail-closed on non-string / empty.
55
+ if (q.agent_capabilities !== undefined) {
56
+ if (typeof q.agent_capabilities !== 'string')
57
+ return bad('INVALID_FILTER_AGENT_CAPABILITIES', 'agent_capabilities must be a comma-separated string');
58
+ const caps = q.agent_capabilities.split(',').map(s => s.trim()).filter(Boolean).slice(0, 10).map(c => c.slice(0, 64));
59
+ if (caps.length === 0)
60
+ return bad('INVALID_FILTER_AGENT_CAPABILITIES', 'agent_capabilities must list at least one non-empty capability');
61
+ f.agentCapabilities = caps;
62
+ }
63
+ // max_duration_minutes: only tasks whose estimated max duration fits within this many minutes. Fail-closed
64
+ // on non-string / non-positive-integer / out-of-range.
65
+ if (q.max_duration_minutes !== undefined) {
66
+ if (typeof q.max_duration_minutes !== 'string')
67
+ return bad('INVALID_FILTER_MAX_DURATION', 'max_duration_minutes must be a positive integer');
68
+ const n = Number(q.max_duration_minutes);
69
+ if (!Number.isInteger(n) || n <= 0 || n > 100000)
70
+ return bad('INVALID_FILTER_MAX_DURATION', 'max_duration_minutes must be a positive integer (1..100000)');
71
+ f.maxDurationMinutes = n;
72
+ }
73
+ if (q.estimated_context_size !== undefined) {
74
+ if (typeof q.estimated_context_size !== 'string' || !CONTEXT_SIZES.includes(q.estimated_context_size))
75
+ return bad('INVALID_FILTER_CONTEXT_SIZE', `estimated_context_size must be ${CONTEXT_SIZES.join('|')}`);
76
+ f.estimated_context_size = q.estimated_context_size;
77
+ }
78
+ if (q.estimated_agent_budget !== undefined) {
79
+ if (typeof q.estimated_agent_budget !== 'string' || !AGENT_BUDGETS.includes(q.estimated_agent_budget))
80
+ return bad('INVALID_FILTER_AGENT_BUDGET', `estimated_agent_budget must be ${AGENT_BUDGETS.join('|')}`);
81
+ f.estimated_agent_budget = q.estimated_agent_budget;
82
+ }
83
+ return { ok: true, filters: f };
84
+ }
85
+ const LIST_ARRAY_FIELDS = ['required_capabilities', 'dependencies', 'blocking_conditions'];
86
+ /* eslint-disable @typescript-eslint/no-explicit-any */
87
+ function shapeMetadata(row, shape) {
88
+ if (row.task_type == null)
89
+ return null; // no satellite row → old task, compatible
90
+ const m = {
91
+ task_type: row.task_type, risk_level: row.risk_level, audience: row.audience,
92
+ agent_autonomy: row.agent_autonomy, auto_claimable: row.auto_claimable === 1,
93
+ estimated_duration: { min_minutes: row.estimated_duration_min_minutes, max_minutes: row.estimated_duration_max_minutes },
94
+ estimated_context_size: row.estimated_context_size, estimated_agent_budget: row.estimated_agent_budget,
95
+ value_state: row.value_state,
96
+ };
97
+ for (const k of LIST_ARRAY_FIELDS)
98
+ m[k] = parseJsonList(row[k]);
99
+ if (shape === 'detail') {
100
+ m.source_ref = row.source_ref;
101
+ m.version = row.version;
102
+ m.expected_results = row.expected_results;
103
+ m.definition_of_done = row.definition_of_done;
104
+ m.contribution_type = row.contribution_type;
105
+ m.accountable_party_required = row.accountable_party_required === 1;
106
+ for (const k of ['allowed_paths', 'forbidden_paths', 'prohibited_actions', 'human_confirmation_points', 'acceptance_criteria', 'verification_commands', 'deliverables'])
107
+ m[k] = parseJsonList(row[k]);
108
+ }
109
+ return m;
110
+ }
111
+ // FULL legacy build_tasks core — the member (logged-in) endpoint MUST keep every old field for backward
112
+ // compatibility (Codex regression): only agent_metadata / value_boundary / canonical_contribution_target
113
+ // are APPENDED. The public endpoint uses the lighter task_id shape.
114
+ const FULL_CORE = ['id', 'title', 'area', 'description', 'rfc_ref', 'status', 'claimer_id', 'claimer_provenance',
115
+ 'pr_ref', 'claimed_at', 'claim_expires_at', 'created_by', 'resolution', 'resolved_by', 'created_at', 'updated_at'];
116
+ function shapeCoreFull(row) { const o = {}; for (const k of FULL_CORE)
117
+ o[k] = row[k]; return o; }
118
+ function shapeCoreLight(row) {
119
+ return { task_id: row.id, title: row.title, area: row.area, status: row.status, claimer_id: row.claimer_id, created_by: row.created_by, created_at: row.created_at, updated_at: row.updated_at };
120
+ }
121
+ function buildWhere(scope, f) {
122
+ const where = [];
123
+ const params = [];
124
+ const join = scope === 'public' ? 'JOIN' : 'LEFT JOIN';
125
+ if (scope === 'public') {
126
+ where.push("m.audience = 'public'", "t.status = 'open'");
127
+ }
128
+ else {
129
+ where.push("(m.audience IS NULL OR m.audience = 'public')");
130
+ } // member: hide restricted/internal
131
+ if (f.status) {
132
+ where.push('t.status = ?');
133
+ params.push(f.status);
134
+ }
135
+ if (f.area) {
136
+ where.push('t.area = ?');
137
+ params.push(f.area);
138
+ }
139
+ if (f.claimerId) {
140
+ where.push('t.claimer_id = ?');
141
+ params.push(f.claimerId);
142
+ }
143
+ if (f.risk_level) {
144
+ where.push('m.risk_level = ?');
145
+ params.push(f.risk_level);
146
+ }
147
+ if (f.audience) {
148
+ where.push('m.audience = ?');
149
+ params.push(f.audience);
150
+ }
151
+ if (f.auto_claimable !== undefined) {
152
+ where.push('m.auto_claimable = ?');
153
+ params.push(f.auto_claimable ? 1 : 0);
154
+ }
155
+ // required_capabilities (AND): required_capabilities is a JSON array of strings; match an exact element
156
+ // via a quoted LIKE (dialect-agnostic; no json_each). ESCAPE so %/_ in a capability stay literal. This
157
+ // ANDs with the scope clause above, so restricted/internal never leak even when a filter matches.
158
+ if (f.requiredCapabilities)
159
+ for (const cap of f.requiredCapabilities) {
160
+ where.push("m.required_capabilities LIKE ? ESCAPE '\\'");
161
+ params.push('%"' + cap.replace(/[\\%_]/g, c => '\\' + c) + '"%');
162
+ }
163
+ // max_duration_minutes: the task's estimated max duration must be known AND fit within the requested time.
164
+ if (f.maxDurationMinutes !== undefined) {
165
+ where.push('m.estimated_duration_max_minutes IS NOT NULL AND m.estimated_duration_max_minutes <= ?');
166
+ params.push(f.maxDurationMinutes);
167
+ }
168
+ if (f.estimated_context_size) {
169
+ where.push('m.estimated_context_size = ?');
170
+ params.push(f.estimated_context_size);
171
+ }
172
+ if (f.estimated_agent_budget) {
173
+ where.push('m.estimated_agent_budget = ?');
174
+ params.push(f.estimated_agent_budget);
175
+ }
176
+ return { where, params, join };
177
+ }
178
+ // SELECT t.* (every legacy build_tasks column) + the explicit metadata columns (NOT m.created_at, to
179
+ // avoid colliding with t.created_at; m.* names are otherwise disjoint from t.*).
180
+ const META_COLS = `m.task_type, m.source_ref, m.version, m.allowed_paths, m.forbidden_paths, m.prohibited_actions,
181
+ m.risk_level, m.audience, m.agent_autonomy, m.auto_claimable, m.human_confirmation_points,
182
+ m.required_capabilities, m.acceptance_criteria, m.verification_commands, m.expected_results,
183
+ m.deliverables, m.definition_of_done, m.estimated_duration_min_minutes, m.estimated_duration_max_minutes,
184
+ m.estimated_context_size, m.estimated_agent_budget, m.dependencies, m.blocking_conditions, m.value_state,
185
+ m.contribution_type, m.accountable_party_required`;
186
+ /** List tasks visible in `scope` (member = full legacy core; public = light), with parsed agent_metadata or null. */
187
+ export function listBuildTasksWithAgentMetadata(db, filters, scope) {
188
+ releaseExpiredClaims(db); // RFC-006 TTL: recycle expired claims before reading (parity with listBuildTasks)
189
+ const LIST_LIMIT = 200;
190
+ const { where, params, join } = buildWhere(scope, filters);
191
+ // agent_capabilities is a JS subset filter, so it must run BEFORE the cap — applying SQL LIMIT first would
192
+ // drop a doable task that sorted past row 200 (Codex P2: a real false-negative). When it is active we fetch
193
+ // the full SCOPED candidate set (already bounded by the scope/other WHERE clauses) and cap after filtering.
194
+ const limitSql = filters.agentCapabilities ? '' : ` LIMIT ${LIST_LIMIT}`;
195
+ let rows = db.prepare(`SELECT t.*, ${META_COLS} FROM build_tasks t ${join} build_task_agent_metadata m ON m.task_id = t.id
196
+ WHERE ${where.join(' AND ')}
197
+ ORDER BY (t.status='open') DESC, t.updated_at DESC${limitSql}`).all(...params);
198
+ // agent_capabilities (SUBSET): keep tasks whose required_capabilities are all within the agent's set —
199
+ // i.e. tasks the agent can do — then cap. Dialect-agnostic (no json_each). No-leak intact: the scope WHERE
200
+ // already excluded restricted/internal, so this can only narrow. A no-metadata task (member scope) has no
201
+ // required_capabilities → [] → vacuously a subset (no skills required).
202
+ if (filters.agentCapabilities) {
203
+ const have = new Set(filters.agentCapabilities);
204
+ rows = rows.filter(r => parseJsonList(r.required_capabilities).every(c => have.has(c))).slice(0, LIST_LIMIT);
205
+ }
206
+ return rows.map(r => ({ ...(scope === 'public' ? shapeCoreLight(r) : shapeCoreFull(r)), agent_metadata: shapeMetadata(r, 'list') }));
207
+ }
208
+ /** Detail for one task visible in `scope`, else null (no leak). Member keeps the full legacy core + events. */
209
+ export function getBuildTaskWithAgentMetadata(db, id, scope) {
210
+ releaseExpiredClaims(db); // RFC-006 TTL parity with getBuildTask
211
+ const { where, params, join } = buildWhere(scope, {});
212
+ const row = db.prepare(`SELECT t.*, ${META_COLS} FROM build_tasks t ${join} build_task_agent_metadata m ON m.task_id = t.id
213
+ WHERE ${where.join(' AND ')} AND t.id = ?`).get(...params, id);
214
+ if (!row)
215
+ return null;
216
+ const core = scope === 'public' ? shapeCoreLight(row) : shapeCoreFull(row);
217
+ const out = { ...core, agent_metadata: shapeMetadata(row, 'detail') };
218
+ if (scope !== 'public') { // member detail keeps the build_task_events list (old getBuildTask behavior)
219
+ out.events = db.prepare(`SELECT actor_id, from_status, to_status, note, created_at FROM build_task_events WHERE task_id = ? ORDER BY created_at`).all(id);
220
+ }
221
+ return out;
222
+ }
@@ -131,7 +131,10 @@ export function claimBuildTask(db, taskId, userId, provenance) {
131
131
  const row = db.prepare(`SELECT claim_expires_at FROM build_tasks WHERE id = ?`).get(taskId);
132
132
  return { id: taskId, status: 'claimed', claim_expires_at: row.claim_expires_at };
133
133
  }
134
- export function submitBuildTask(db, taskId, userId, prRef, note) {
134
+ // `verificationSummary` (what the contributor ran/verified) is the submit evidence — stored in the
135
+ // existing build_task_events.note (no schema churn). The ROUTE requires it; the engine stores it if
136
+ // present, staying backward-compatible for any direct caller.
137
+ export function submitBuildTask(db, taskId, userId, prRef, note, verificationSummary) {
135
138
  const t = db.prepare(`SELECT status, claimer_id FROM build_tasks WHERE id = ?`).get(taskId);
136
139
  if (!t)
137
140
  return { error: '任务不存在', error_code: 'NOT_FOUND' };
@@ -141,7 +144,12 @@ export function submitBuildTask(db, taskId, userId, prRef, note) {
141
144
  return { error: `任务状态为 ${t.status},仅 claimed 可提交进 in_review`, error_code: 'BAD_STATE' };
142
145
  const pr = prRef ? String(prRef).slice(0, 300) : null;
143
146
  db.prepare(`UPDATE build_tasks SET status='in_review', pr_ref=?, updated_at=datetime('now') WHERE id = ? AND status='claimed'`).run(pr, taskId);
144
- logTaskEvent(db, taskId, userId, 'claimed', 'in_review', note ? `pr=${pr || '?'} note=${String(note).slice(0, 200)}` : `pr=${pr || '?'}`);
147
+ const parts = [`pr=${pr || '?'}`];
148
+ if (verificationSummary)
149
+ parts.push(`verify=${String(verificationSummary).slice(0, 500)}`);
150
+ if (note)
151
+ parts.push(`note=${String(note).slice(0, 200)}`);
152
+ logTaskEvent(db, taskId, userId, 'claimed', 'in_review', parts.join(' '));
145
153
  return { id: taskId, status: 'in_review' };
146
154
  }
147
155
  // 认领者主动放弃 → 回 open(让别人接)
@@ -0,0 +1,16 @@
1
+ const DEFAULT_FULL_NAME = 'seasonsagents-art/webaz';
2
+ const DEFAULT_BASE_BRANCH = 'main';
3
+ /** The frozen canonical target from trusted config. Identical for every read response (public + member). */
4
+ export function getCanonicalContributionTarget() {
5
+ const fullName = (process.env.CANONICAL_GITHUB_REPO || DEFAULT_FULL_NAME).trim();
6
+ const baseBranch = (process.env.CANONICAL_GITHUB_BASE_BRANCH || DEFAULT_BASE_BRANCH).trim();
7
+ const repoId = (process.env.CANONICAL_GITHUB_REPOSITORY_ID || '').trim() || null;
8
+ return Object.freeze({
9
+ canonical_repository_id: repoId,
10
+ canonical_repository_full_name: fullName,
11
+ canonical_github_url: `https://github.com/${fullName}`,
12
+ base_branch: baseBranch,
13
+ expected_pr_base_repo: fullName,
14
+ note: 'Trusted constant — NOT derived from task metadata or source_ref. Only a merged PR whose base repo is this canonical repository can become a WebAZ contribution fact; a task source_ref is a reference only. If a target repo differs from this canonical repo, STOP and ask the user to confirm — do not contribute to a non-canonical repository.',
15
+ });
16
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * PR-5A — Contribution display: the uncommitted-value boundary (RFC-017 I-12 / §7).
3
+ *
4
+ * This is the SAFETY CONTRACT that must wrap every contribution metering/display surface BEFORE any
5
+ * valuation/scoring is ever built. RFC-017 separates three layers — fact · valuation · redemption — and
6
+ * locks the boundary: today the protocol grants NO economic value. So any surface that shows facts /
7
+ * attribution must carry an explicit, machine-readable boundary saying so, so the act of *measuring and
8
+ * displaying* contribution can never read as a payout promise (the legal/trust firewall, RFC-017 §7 R1).
9
+ *
10
+ * This module is PURELY a display contract: it computes/scores NOTHING, stores NOTHING, and imports NO
11
+ * reward / KYC / wallet / valuation module (enforced by the §8 iron-rule guard, rule5). It only stamps a
12
+ * constant boundary onto a payload:
13
+ * - value_state = 'uncommitted' (RFC-017 I-12 — the whole-protocol stance)
14
+ * - valuation_state = 'not_defined' (the valuation layer is deferred to a future DAO + team)
15
+ * - redemption_state = 'not_defined' (the redemption layer is explicitly uncommitted in full)
16
+ * - economic_rights = false (grants no security / equity / debt / redemption right)
17
+ * and NO amount / currency / yield / payout field is ever added (the notice deliberately does not even
18
+ * name those words, so a display can carry the boundary without listing a "value").
19
+ *
20
+ * spec: docs/rfcs/RFC-017-contribution-protocol-v1.md §I-12/§7 · docs/IDENTITY-CLAIM-DESIGN.md §8.8.
21
+ */
22
+ // Frozen constant — there is exactly ONE boundary stance pre-launch; callers must not vary it. The notice
23
+ // is an informational disclaimer ONLY; it intentionally avoids the words amount/currency/yield/payout/
24
+ // reward so a display never restates a "value", and it promises nothing.
25
+ export const UNCOMMITTED_VALUE_BOUNDARY = Object.freeze({
26
+ value_state: 'uncommitted',
27
+ valuation_state: 'not_defined',
28
+ redemption_state: 'not_defined',
29
+ economic_rights: false,
30
+ boundary_ref: 'RFC-017 I-12',
31
+ notice_en: 'Informational record of contribution facts and attribution only. It is not a financial instrument and confers no economic or redemption right; nothing here is promised or guaranteed (RFC-017 I-12 / §7).',
32
+ notice_zh: '仅为贡献事实与归属的信息性记录,不是金融工具,不授予任何经济或兑现权利,此处不作任何承诺或保证(RFC-017 I-12 / §7)。',
33
+ });
34
+ /**
35
+ * Stamp the uncommitted-value boundary onto a contribution display payload, under a single top-level
36
+ * `value_boundary` key. Pure: returns a new object, never mutates the input, adds no economic field.
37
+ */
38
+ export function withUncommittedValueBoundary(payload) {
39
+ return { ...payload, value_boundary: UNCOMMITTED_VALUE_BOUNDARY };
40
+ }
@@ -0,0 +1,36 @@
1
+ // Frozen metadata the static guard asserts against — makes the boundary a CODE contract, not just prose.
2
+ export const CONTRIBUTION_SCORE_V1 = Object.freeze({
3
+ score_version: 'v1',
4
+ // user-facing field names of a displayed score (guard: none may be an economic-promise term)
5
+ display_fields: ['score_version', 'contribution_score', 'components', 'value_boundary'],
6
+ // evidence component keys (weights/formula DEFERRED to governance — invariant 4/7)
7
+ component_keys: ['accepted_contributions', 'reviews_provided', 'maintenance_actions', 'impact_observed', 'reverted_penalty'],
8
+ // READ-ONLY inputs — all pre-existing models; v1 adds NO table and NO write path (§3)
9
+ input_sources: [
10
+ 'contribution_facts (RFC-017 fact layer)',
11
+ 'github_contribution_credentials + github_fact_credentials',
12
+ 'identity_bindings_active accountable overlay (/github/me, PR-F4)',
13
+ 'build_reputation read model (RFC-006, PR5B)',
14
+ ],
15
+ // hard boundary flags (the whole point of this PR)
16
+ display_requires_value_boundary: true,
17
+ decides_money_or_rights: false,
18
+ is_redeemable: false,
19
+ defines_reward_formula: false,
20
+ requires_or_unlocks_kyc: false,
21
+ affects_wallet_escrow_commission: false,
22
+ affects_binary_tree_position: false,
23
+ gates_verifier_or_arbitrator: false,
24
+ revisable_by_governance: true,
25
+ // the 8 locked invariants (full text: docs/CONTRIBUTION-SCORE-V1-DESIGN.md §2)
26
+ invariants: [
27
+ 'uncommitted only',
28
+ 'no economic rights',
29
+ 'no redemption',
30
+ 'no reward formula (deferred)',
31
+ 'no KYC / fulfillment',
32
+ 'explainable by evidence_refs',
33
+ 'revisable by governance',
34
+ 'every displayed score carries value_boundary',
35
+ ],
36
+ });
@@ -0,0 +1,61 @@
1
+ /**
2
+ * PR5E — Contribution Score v1 read-only EVIDENCE COLLECTOR. Aggregates component-level evidence ONLY;
3
+ * computes NO score: no `contribution_score`, no total, no weights / formula / curve / tier / reward /
4
+ * eligibility (all deferred — RFC-017 + the #318 contract). Read-only: no DB write, no new table, no
5
+ * schema change; attribution is the read overlay, so `contribution_facts.accountable_ref` stays NULL.
6
+ * Any future display of this evidence inherits the PR5A uncommitted-value boundary.
7
+ *
8
+ * For a logged-in account it returns one ScoreComponentV1 ({ key, raw_count, evidence_refs[] }) per fixed
9
+ * contract component key (#318), sourced ONLY from existing models:
10
+ * - accepted_contributions: ACTIVE GitHub credential-BACKED facts attributable to the account — the
11
+ * /github/me overlay trust root (contribution_facts ⋈ github_fact_credentials ⋈
12
+ * github_contribution_credentials, executor = a currently-bound actor).
13
+ * - reviews_provided / maintenance_actions: the active attributable facts of type 'audit' / 'maintenance'.
14
+ * - impact_observed: NO impact-observation source exists in the current models → 0 / [] (NOT fabricated
15
+ * to look complete; a future PR wires a real source).
16
+ * - reverted_penalty: NO source yet → 0 / []. Lifecycle status changes (revert/supersede/void) belong to
17
+ * a FUTURE append-only status-events overlay; `contribution_facts.status` is as-ingested 'active' and
18
+ * is NEVER updated in place (GITHUB-CREDENTIAL-INGESTION-DESIGN.md; github-credential-ingestion-engine.ts).
19
+ * So we deliberately do NOT read `status='reverted'` here — that would both stay perpetually 0 under the
20
+ * current ingestion AND tempt future code into an in-place status mutation that violates append-only.
21
+ * reverted_penalty is wired to the real status-events overlay only once that overlay PR lands.
22
+ * `evidence_refs` are real `contribution_facts.fact_id` values (invariant 6 — explainable by evidence).
23
+ *
24
+ * spec: docs/CONTRIBUTION-SCORE-V1-DESIGN.md · contribution-score-contract.ts · docs/IDENTITY-CLAIM-DESIGN.md §8.7.
25
+ */
26
+ import { dbAll } from '../../layer0-foundation/L0-1-database/db.js';
27
+ // Active attributable facts: the SAME credential-backed + executor-bound-to-me overlay as /github/me
28
+ // (PR-F4), anchored on b.account_id = the caller, so only the account's own facts are seen. status='active'
29
+ // is the as-ingested value (never updated in place); a future status-events overlay will derive lifecycle.
30
+ const ATTRIBUTABLE_ACTIVE_FACTS_SQL = `
31
+ SELECT DISTINCT f.fact_id, f.type
32
+ FROM identity_bindings_active b
33
+ JOIN github_contribution_credentials c
34
+ ON c.github_actor_id = b.github_actor_id
35
+ JOIN github_fact_credentials l
36
+ ON l.credential_id = c.credential_id AND l.source_event_key = c.source_event_key
37
+ JOIN contribution_facts f
38
+ ON f.fact_id = l.fact_id AND f.source_event_key = l.source_event_key
39
+ WHERE b.account_id = ? AND f.source = 'github' AND f.status = 'active'
40
+ AND f.executor_ref = 'github:' || b.github_actor_id
41
+ ORDER BY f.fact_id`;
42
+ const component = (key, refs) => ({ key, raw_count: refs.length, evidence_refs: refs });
43
+ /**
44
+ * Collect the five fixed-contract evidence components for `accountId`. Returns counts + evidence_refs
45
+ * only — never a `contribution_score`. Order matches CONTRIBUTION_SCORE_V1.component_keys (#318).
46
+ */
47
+ export async function collectContributionScoreEvidence(accountId) {
48
+ if (!accountId) {
49
+ return ['accepted_contributions', 'reviews_provided', 'maintenance_actions', 'impact_observed', 'reverted_penalty']
50
+ .map(k => component(k, []));
51
+ }
52
+ const active = await dbAll(ATTRIBUTABLE_ACTIVE_FACTS_SQL, [accountId]);
53
+ const ofType = (rows, t) => rows.filter(r => r.type === t).map(r => r.fact_id);
54
+ return [
55
+ component('accepted_contributions', active.map(r => r.fact_id)),
56
+ component('reviews_provided', ofType(active, 'audit')),
57
+ component('maintenance_actions', ofType(active, 'maintenance')),
58
+ component('impact_observed', []), // no evidence source in v1 models (not fabricated)
59
+ component('reverted_penalty', []), // no status-events overlay source yet — NOT read from fact.status (append-only)
60
+ ];
61
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Canonical serialization + SHA-256 digest for GitHub Contribution Credentials (PR 3A).
3
+ *
4
+ * Reuse note: `canonicalSerialize` below is **byte-identical** to
5
+ * `src/layer0-foundation/L0-2-state-machine/order-chain.ts` canonicalSerialize (the
6
+ * repo's established canonical-JSON idiom). It is inlined here so the credential verifier
7
+ * stays a self-contained pure module (no coupling to the order state machine); the static
8
+ * test asserts equivalence with the src version on samples (no-drift guard).
9
+ *
10
+ * No new dependency — SHA-256 via Node built-in `node:crypto`.
11
+ */
12
+ import { createHash } from 'node:crypto';
13
+ /** Deterministic canonical JSON: recursively sort object keys; arrays keep order. */
14
+ export function canonicalSerialize(obj) {
15
+ if (obj === null || obj === undefined)
16
+ return JSON.stringify(obj);
17
+ if (Array.isArray(obj))
18
+ return '[' + obj.map(canonicalSerialize).join(',') + ']';
19
+ if (typeof obj === 'object') {
20
+ const keys = Object.keys(obj).sort();
21
+ return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalSerialize(obj[k])).join(',') + '}';
22
+ }
23
+ return JSON.stringify(obj);
24
+ }
25
+ export const sha256hex = (s) => createHash('sha256').update(s).digest('hex');
26
+ /**
27
+ * The `core` object = the immutable, GitHub-authoritative fact. These are the ONLY fields
28
+ * that the `core_digest` (and thus `credential_id`) authenticate. The observation envelope
29
+ * (display names, observed_at, self-reported, evidence summaries) is explicitly NOT part of
30
+ * this digest — it is mutable / non-authoritative and carries its own `observation_digest`.
31
+ */
32
+ export const DIGEST_CORE_FIELDS = [
33
+ 'credential_type', // fixed protocol domain — isolates this credential family in the digest
34
+ 'credential_version', // version domain — a future v2 of the SAME GitHub fact gets a DIFFERENT id
35
+ 'repository_id',
36
+ 'pr_node_id',
37
+ 'pr_number',
38
+ 'base_ref',
39
+ 'head_sha',
40
+ 'merge_commit_sha',
41
+ 'merged_at',
42
+ 'github_actor_id',
43
+ 'lifecycle_event',
44
+ 'supersedes_credential_id', // lifecycle parent link is bound into the immutable fact
45
+ ];
46
+ /** Canonical SHA-256 over the exact core-field set (key-order independent). */
47
+ export function digestCore(source) {
48
+ const picked = {};
49
+ for (const k of DIGEST_CORE_FIELDS)
50
+ picked[k] = source[k] === undefined ? null : source[k];
51
+ return sha256hex(canonicalSerialize(picked));
52
+ }
53
+ /** Canonical SHA-256 over an arbitrary (observation) object — key-order independent. */
54
+ export function digestObject(obj) {
55
+ return sha256hex(canonicalSerialize(obj));
56
+ }
57
+ /** Deterministic credential id derived from the CORE digest (idempotent for the same fact). */
58
+ export function credentialIdFromDigest(coreDigest) {
59
+ return `ghc_${coreDigest.slice(0, 40)}`;
60
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * GitHub Immutable Contribution Credential v2 — machine-readable contract (canonical).
3
+ *
4
+ * A credential proves a GitHub FACT (a merged PR) observed via a GitHub API response. It is a *candidate* for RFC-017's contribution fact layer — NOT a
5
+ * Passkey/KYC claim, scoring, or reward. Distinct from: Contribution Fact (RFC-017
6
+ * authoritative, produced later), Identity Claim (future Passkey), Valuation/Reward (never here).
7
+ *
8
+ * Immutability model (Codex #294 audit):
9
+ * - `core` = the immutable GitHub fact. `core_digest` (and `credential_id` derived from it)
10
+ * authenticate ONLY this. Lifecycle parent link `supersedes_credential_id` is in core.
11
+ * - `observation` = a NON-authoritative, MUTABLE envelope (display names, observed_at,
12
+ * self-reported task/provenance, evidence summaries). It carries its OWN
13
+ * `observation_digest`. The same fact re-observed → same credential_id/core_digest,
14
+ * possibly different observation_digest. credential_id does NOT authenticate the
15
+ * observation envelope.
16
+ *
17
+ * Trust boundary: this is a PURE verifier over a CALLER-SUPPLIED response. It validates
18
+ * STRUCTURE + repository anchoring; it does NOT prove the response authentically came from
19
+ * GitHub. `verification_state='verified'` = structural + repo-anchor verification only.
20
+ * Source-authenticity (authenticated fetch) is deferred to PR 3B.
21
+ *
22
+ * Canonical = zod (no new dependency). The committed JSON Schema is generated by
23
+ * `toJSONSchema()` (drift-guarded) and carries the cross-field rules as `allOf` if/then.
24
+ */
25
+ import { z } from 'zod';
26
+ export const CONTRIBUTION_TYPES = ['code', 'tests', 'audit', 'maintenance', 'governance', 'usage', 'transaction', 'referral']; // RFC-017 §5
27
+ // 'unknown' included so a missing/invalid self-report is recorded honestly, NOT guessed as human.
28
+ export const PROVENANCE = ['human', 'ai_assisted', 'ai_authored', 'unknown'];
29
+ // 'unknown' included so a missing GitHub visibility is recorded honestly, NOT assumed public.
30
+ export const VISIBILITY = ['public', 'private', 'internal', 'unknown'];
31
+ export const VERIFICATION_STATE = ['verified', 'unverified', 'insufficient_evidence'];
32
+ export const EVIDENCE_SCOPE = ['public_metadata', 'repo_collaborator_metadata'];
33
+ export const DCO_STATE = ['present', 'absent', 'unknown'];
34
+ // per-evidence-stream coverage — machine-readable so a consumer can tell "observed zero" from
35
+ // "not observed". 'partial' = fetched but truncated at the page cap (more may exist).
36
+ export const EVIDENCE_COVERAGE = ['observed', 'unobserved', 'partial'];
37
+ // The PURE PR verifier mints ONLY the `merged` lifecycle (merged-only profile). A pure PR response
38
+ // cannot independently prove reverted / superseded / void (it only proves THIS PR merged, not that
39
+ // it rolled back a target credential) — ALL of them are DEFERRED to a separate lifecycle-event
40
+ // verifier (PR 3B) with their own trusted evidence + reason rules. The field/enum keeps the slot
41
+ // for forward compatibility; the verifier forces `merged` + `supersedes_credential_id = null`.
42
+ export const LIFECYCLE_EVENT = ['merged'];
43
+ const SHA256_HEX = /^[0-9a-f]{64}$/;
44
+ const nonNegInt = z.number().int().min(0);
45
+ export const CREDENTIAL_TYPE = 'github_contribution_credential';
46
+ // v2 (PR 3B-2): adds observation.evidence_coverage. A strict schema rejects unknown fields in BOTH
47
+ // directions, so an additive field is NOT v1-compatible — this is a formal version bump. There are
48
+ // no persisted v1 credentials (no storage yet), so v2 cleanly supersedes. credential_version is in
49
+ // the digest, so v2 of the same GitHub fact gets a different credential_id (domain/version isolation).
50
+ export const CREDENTIAL_VERSION = '2';
51
+ // ── immutable GitHub fact core ──────────────────────────────────────────────
52
+ // strictObject: reject unknown fields so the immutable core cannot carry un-digested claims,
53
+ // and so zod matches the JSON Schema's additionalProperties:false (consumer parity).
54
+ export const CoreObject = z.strictObject({
55
+ credential_type: z.literal(CREDENTIAL_TYPE), // fixed protocol domain (digest isolation)
56
+ credential_version: z.literal(CREDENTIAL_VERSION), // version domain (v2 of same fact ⇒ different id)
57
+ repository_id: z.string().min(1), // stable GitHub node/database id
58
+ pr_node_id: z.string().min(1), // stable
59
+ pr_number: z.number().int().positive(),
60
+ base_ref: z.string().min(1),
61
+ head_sha: z.string().min(1), // actual observed head sha (force-push aware)
62
+ merge_commit_sha: z.string().min(1).nullable(),
63
+ merged_at: z.string().min(1).nullable(),
64
+ github_actor_id: z.string().min(1), // stable
65
+ lifecycle_event: z.enum(LIFECYCLE_EVENT),
66
+ supersedes_credential_id: z.string().min(1).nullable(), // merged-only profile ⇒ always null (reverted/etc. deferred)
67
+ });
68
+ // ── non-authoritative, mutable observation envelope (strictObject — reject unknown fields) ──
69
+ export const ObservationObject = z.strictObject({
70
+ observed_at: z.string().min(1),
71
+ repository_owner: z.string().min(1), // display (mutable)
72
+ repository_name: z.string().min(1), // display (mutable)
73
+ repository_visibility_at_observation: z.enum(VISIBILITY), // 'unknown' when GitHub didn't say (never guessed public)
74
+ head_ref: z.string().min(1), // display (branch may be renamed/deleted)
75
+ github_login: z.string().min(1), // display (mutable)
76
+ commit_authors: z.array(z.strictObject({
77
+ author_id: z.string().nullable(), login: z.string().nullable(), name: z.string().nullable(), is_coauthor: z.boolean(),
78
+ })), // NO emails (rule 10)
79
+ agent_provenance: z.enum(PROVENANCE), // self-declared; 'unknown' when not validly self-reported (never guessed human)
80
+ claimed_task_id: z.string().nullable(), // self-reported, NON-authoritative (rule 1/9)
81
+ source_ref: z.string().nullable(), // self-reported, NON-authoritative
82
+ contribution_type: z.enum(CONTRIBUTION_TYPES).nullable(), // candidate classification; null when not self-reported (never guessed 'code')
83
+ verification_state: z.enum(VERIFICATION_STATE), // structural + repo-anchor only (see trust boundary)
84
+ evidence_scope: z.enum(EVIDENCE_SCOPE),
85
+ checks_summary: z.strictObject({ total: nonNegInt, success: nonNegInt, failure: nonNegInt, neutral: nonNegInt, other: nonNegInt }),
86
+ reviews_summary: z.strictObject({ approved: nonNegInt, changes_requested: nonNegInt, commented: nonNegInt, reviewer_ids: z.array(z.string()) }),
87
+ dco_state: z.enum(DCO_STATE), // independent of co-authors (rule 8)
88
+ // machine-readable per-stream coverage: distinguishes "observed zero" from "not observed".
89
+ // A summary's zeros/unknown are only meaningful when its coverage is 'observed'.
90
+ // required in v2 — every minted credential reports per-stream coverage.
91
+ evidence_coverage: z.strictObject({
92
+ checks: z.enum(EVIDENCE_COVERAGE),
93
+ reviews: z.enum(EVIDENCE_COVERAGE),
94
+ commit_authors: z.enum(EVIDENCE_COVERAGE),
95
+ dco: z.enum(['observed', 'unobserved']), // single check, no pagination → no 'partial'
96
+ }),
97
+ merged_by_actor_id: z.string().nullable(),
98
+ evidence_refs: z.array(z.string()), // stable refs (no tokens/PII)
99
+ known_limitations: z.array(z.string()),
100
+ });
101
+ export const GithubCredentialObject = z.strictObject({
102
+ credential_id: z.string().min(1), // = ghc_<core_digest[0:40]>; authenticates ONLY core
103
+ event_source: z.literal('github_api'), // CLAIMED source; not proof of source authenticity (see trust boundary)
104
+ accountable_party_ref: z.null(), // rule 7: reserved, set later on the FACT (not here)
105
+ core: CoreObject,
106
+ core_digest: z.string().regex(SHA256_HEX), // SHA-256 over core fields — the immutable fact identity
107
+ observation: ObservationObject,
108
+ observation_digest: z.string().regex(SHA256_HEX), // SHA-256 over the observation envelope (a specific observation)
109
+ });
110
+ export const GithubCredentialSchema = GithubCredentialObject.superRefine((c, ctx) => {
111
+ // merged-only profile: lifecycle is `merged` only ⇒ a merge happened, verified, and NO parent link.
112
+ if (c.core.lifecycle_event === 'merged') {
113
+ if (c.core.merge_commit_sha === null)
114
+ ctx.addIssue({ code: 'custom', path: ['core', 'merge_commit_sha'], message: 'merged requires merge_commit_sha' });
115
+ if (c.core.merged_at === null)
116
+ ctx.addIssue({ code: 'custom', path: ['core', 'merged_at'], message: 'merged requires merged_at' });
117
+ if (c.observation.verification_state !== 'verified')
118
+ ctx.addIssue({ code: 'custom', path: ['observation', 'verification_state'], message: 'minted credential requires verification_state=verified' });
119
+ if (c.core.supersedes_credential_id !== null)
120
+ ctx.addIssue({ code: 'custom', path: ['core', 'supersedes_credential_id'], message: 'merged must NOT supersede (null parent link)' });
121
+ }
122
+ });
123
+ /** Structural JSON Schema (Draft 2020-12) + cross-field if/then (parity with superRefine). */
124
+ export function toJSONSchema() {
125
+ const base = z.toJSONSchema(GithubCredentialObject);
126
+ base.allOf = [
127
+ // merged ⇒ merge anchors present + verified + NO parent link
128
+ {
129
+ if: { properties: { core: { properties: { lifecycle_event: { const: 'merged' } }, required: ['lifecycle_event'] } }, required: ['core'] },
130
+ then: {
131
+ properties: {
132
+ core: { properties: { merge_commit_sha: { type: 'string' }, merged_at: { type: 'string' }, supersedes_credential_id: { type: 'null' } }, required: ['merge_commit_sha', 'merged_at', 'supersedes_credential_id'] },
133
+ observation: { properties: { verification_state: { const: 'verified' } }, required: ['verification_state'] },
134
+ },
135
+ required: ['core', 'observation'],
136
+ },
137
+ },
138
+ ];
139
+ return base;
140
+ }