@seasonkoh/webaz 0.1.24 → 0.1.26
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.
- package/README.md +5 -1
- package/dist/layer0-foundation/L0-1-database/db-backends/pg-backend.js +51 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-dialect-datetime.js +437 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-placeholders.js +98 -0
- package/dist/layer0-foundation/L0-1-database/db.js +65 -0
- package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +13 -11
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +1 -1
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +13 -11
- package/dist/layer1-agent/L1-1-mcp-server/server.js +288 -208
- package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +14 -12
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +8 -5
- package/dist/layer2-business/L2-7-snf/snf-engine.js +16 -14
- package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +18 -10
- package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +37 -23
- package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +182 -0
- package/dist/layer2-business/L2-9-contribution/build-task-participation.js +47 -0
- package/dist/layer2-business/L2-9-contribution/build-task-read.js +222 -0
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +11 -3
- package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +16 -0
- package/dist/layer2-business/L2-9-contribution/contribution-display-envelope.js +40 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-contract.js +36 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-evidence.js +61 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/canonical.js +60 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-credential.schema.js +140 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-fetch-adapter.js +437 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/self-consistency.js +38 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/verifier.js +231 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-ingestion-engine.js +145 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-store.js +115 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-engine.js +134 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-store.js +101 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-engine.js +126 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-store.js +30 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-discovery.js +55 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-ai-store.js +99 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +191 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
- package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
- package/dist/pwa/acp-feed.js +13 -1
- package/dist/pwa/admin-bearer-auth.js +21 -0
- package/dist/pwa/contract-fingerprint.js +2 -0
- package/dist/pwa/email-delivery.js +127 -0
- package/dist/pwa/endpoint-actions.js +5 -1
- package/dist/pwa/goal-index.js +8 -8
- package/dist/pwa/human-presence.js +62 -0
- package/dist/pwa/public/app.js +1485 -283
- package/dist/pwa/public/i18n.js +297 -59
- package/dist/pwa/public/index.html +1 -0
- package/dist/pwa/public/openapi.json +5 -5
- package/dist/pwa/public/whitepaper/en/index.html +153 -0
- package/dist/pwa/public/whitepaper/zh-CN/index.html +153 -0
- package/dist/pwa/rate-limit.js +22 -0
- package/dist/pwa/routes/account-deletion.js +15 -13
- package/dist/pwa/routes/addresses.js +10 -9
- package/dist/pwa/routes/admin-admins.js +13 -14
- package/dist/pwa/routes/admin-analytics.js +109 -69
- package/dist/pwa/routes/admin-atomic.js +10 -4
- package/dist/pwa/routes/admin-catalog.js +13 -11
- package/dist/pwa/routes/admin-editor-picks.js +15 -10
- package/dist/pwa/routes/admin-events.js +5 -3
- package/dist/pwa/routes/admin-health.js +2 -1
- package/dist/pwa/routes/admin-moderation.js +50 -29
- package/dist/pwa/routes/admin-ops.js +35 -23
- package/dist/pwa/routes/admin-protocol-params.js +16 -19
- package/dist/pwa/routes/admin-reports.js +23 -21
- package/dist/pwa/routes/admin-tokenomics.js +26 -25
- package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
- package/dist/pwa/routes/admin-users-query.js +65 -53
- package/dist/pwa/routes/admin-verifier-flow.js +82 -41
- package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
- package/dist/pwa/routes/admin-wallet-ops.js +32 -7
- package/dist/pwa/routes/agent-buy.js +46 -22
- package/dist/pwa/routes/agent-governance.js +52 -56
- package/dist/pwa/routes/ai.js +7 -5
- package/dist/pwa/routes/analytics.js +43 -41
- package/dist/pwa/routes/anchors.js +19 -20
- package/dist/pwa/routes/announcements.js +13 -13
- package/dist/pwa/routes/arbitrator.js +97 -31
- package/dist/pwa/routes/auction.js +157 -116
- package/dist/pwa/routes/auth-login.js +6 -4
- package/dist/pwa/routes/auth-read.js +21 -10
- package/dist/pwa/routes/auth-register.js +111 -26
- package/dist/pwa/routes/auth-sessions.js +12 -11
- package/dist/pwa/routes/blocklist.js +16 -15
- package/dist/pwa/routes/build-feedback.js +10 -9
- package/dist/pwa/routes/build-reputation.js +6 -2
- package/dist/pwa/routes/build-tasks.js +45 -13
- package/dist/pwa/routes/buyer-feeds.js +27 -25
- package/dist/pwa/routes/cart.js +16 -15
- package/dist/pwa/routes/charity.js +212 -150
- package/dist/pwa/routes/chat.js +42 -43
- package/dist/pwa/routes/checkin-tasks.js +10 -9
- package/dist/pwa/routes/checkout-helpers.js +12 -10
- package/dist/pwa/routes/claim-initiators.js +34 -14
- package/dist/pwa/routes/claim-verify.js +86 -53
- package/dist/pwa/routes/claim-voting.js +43 -18
- package/dist/pwa/routes/contribution-identity.js +164 -0
- package/dist/pwa/routes/contribution-score.js +19 -0
- package/dist/pwa/routes/coupons.js +19 -16
- package/dist/pwa/routes/dashboards.js +18 -16
- package/dist/pwa/routes/dispute-cases.js +25 -24
- package/dist/pwa/routes/disputes-read.js +45 -51
- package/dist/pwa/routes/disputes-write.js +124 -61
- package/dist/pwa/routes/evidence.js +9 -9
- package/dist/pwa/routes/external-anchors.js +13 -12
- package/dist/pwa/routes/feedback.js +29 -33
- package/dist/pwa/routes/flash-sales.js +18 -16
- package/dist/pwa/routes/follows.js +25 -24
- package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
- package/dist/pwa/routes/governance-onboarding.js +70 -59
- package/dist/pwa/routes/group-buys.js +22 -22
- package/dist/pwa/routes/growth.js +34 -31
- package/dist/pwa/routes/import-product.js +12 -10
- package/dist/pwa/routes/kyc.js +9 -8
- package/dist/pwa/routes/leaderboard.js +20 -18
- package/dist/pwa/routes/listings.js +23 -22
- package/dist/pwa/routes/logistics.js +10 -8
- package/dist/pwa/routes/manifests.js +27 -27
- package/dist/pwa/routes/me-data.js +23 -21
- package/dist/pwa/routes/notifications.js +7 -6
- package/dist/pwa/routes/offers.js +30 -12
- package/dist/pwa/routes/orders-action.js +51 -29
- package/dist/pwa/routes/orders-create.js +75 -20
- package/dist/pwa/routes/orders-read.js +21 -20
- package/dist/pwa/routes/p2p-products.js +30 -18
- package/dist/pwa/routes/payments-governance.js +61 -56
- package/dist/pwa/routes/peers.js +9 -8
- package/dist/pwa/routes/pin-receipts.js +13 -13
- package/dist/pwa/routes/products-aliases.js +12 -10
- package/dist/pwa/routes/products-claims.js +36 -17
- package/dist/pwa/routes/products-create.js +53 -38
- package/dist/pwa/routes/products-crud.js +17 -16
- package/dist/pwa/routes/products-links.js +49 -26
- package/dist/pwa/routes/products-list.js +6 -4
- package/dist/pwa/routes/products-meta.js +40 -39
- package/dist/pwa/routes/products-update.js +19 -5
- package/dist/pwa/routes/profile-credentials.js +20 -19
- package/dist/pwa/routes/profile-identity.js +14 -13
- package/dist/pwa/routes/profile-location.js +7 -6
- package/dist/pwa/routes/profile-placement.js +20 -19
- package/dist/pwa/routes/profile-prefs.js +11 -11
- package/dist/pwa/routes/promoter.js +58 -66
- package/dist/pwa/routes/public-build-tasks.js +19 -0
- package/dist/pwa/routes/public-utils.js +108 -46
- package/dist/pwa/routes/push.js +16 -15
- package/dist/pwa/routes/ratings.js +92 -32
- package/dist/pwa/routes/recover-key.js +66 -26
- package/dist/pwa/routes/referral.js +37 -52
- package/dist/pwa/routes/reputation.js +3 -2
- package/dist/pwa/routes/returns.js +76 -73
- package/dist/pwa/routes/reviews.js +41 -18
- package/dist/pwa/routes/rewards-apply.js +16 -15
- package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
- package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
- package/dist/pwa/routes/rfqs.js +163 -85
- package/dist/pwa/routes/search.js +16 -14
- package/dist/pwa/routes/secondhand.js +25 -22
- package/dist/pwa/routes/seller-quota.js +24 -26
- package/dist/pwa/routes/share-redirects.js +60 -55
- package/dist/pwa/routes/shareables-interactions.js +34 -35
- package/dist/pwa/routes/shareables.js +55 -51
- package/dist/pwa/routes/shop-referral.js +58 -0
- package/dist/pwa/routes/shops.js +25 -20
- package/dist/pwa/routes/signaling.js +10 -9
- package/dist/pwa/routes/skill-market.js +16 -16
- package/dist/pwa/routes/skills.js +15 -14
- package/dist/pwa/routes/snf.js +14 -13
- package/dist/pwa/routes/tags.js +10 -9
- package/dist/pwa/routes/task-proposals.js +121 -0
- package/dist/pwa/routes/trial.js +72 -52
- package/dist/pwa/routes/trusted-kpi.js +20 -18
- package/dist/pwa/routes/url-claim.js +67 -28
- package/dist/pwa/routes/users-public.js +62 -70
- package/dist/pwa/routes/variants.js +12 -13
- package/dist/pwa/routes/verifier-user.js +61 -21
- package/dist/pwa/routes/verify-tasks.js +49 -25
- package/dist/pwa/routes/waitlist.js +16 -15
- package/dist/pwa/routes/wallet-read.js +75 -37
- package/dist/pwa/routes/wallet-write.js +12 -9
- package/dist/pwa/routes/webauthn.js +25 -26
- package/dist/pwa/routes/webhooks.js +26 -26
- package/dist/pwa/routes/welcome.js +45 -50
- package/dist/pwa/routes/wishlist-qa.js +29 -32
- package/dist/pwa/server.js +304 -90
- package/dist/version.js +1 -1
- package/package.json +76 -3
|
@@ -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
|
+
}
|
|
@@ -45,7 +45,7 @@ export function initBuildTasksSchema(db) {
|
|
|
45
45
|
`);
|
|
46
46
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_build_task_events ON build_task_events(task_id, created_at)`);
|
|
47
47
|
}
|
|
48
|
-
function logTaskEvent(db, taskId, actorId, from, to, note) {
|
|
48
|
+
export function logTaskEvent(db, taskId, actorId, from, to, note) {
|
|
49
49
|
db.prepare(`INSERT INTO build_task_events (id, task_id, actor_id, from_status, to_status, note) VALUES (?,?,?,?,?,?)`)
|
|
50
50
|
.run(generateId('btev'), taskId, actorId, from, to, note);
|
|
51
51
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|