@ikon85/agent-workflow-kit 0.44.2 → 0.45.0

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 (64) hide show
  1. package/.agents/skills/audit-skills/SKILL.md +7 -4
  2. package/.agents/skills/code-review/SKILL.md +7 -4
  3. package/.agents/skills/codebase-design/DESIGN-IT-TWICE.md +7 -4
  4. package/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +7 -4
  5. package/.agents/skills/improve-codebase-architecture/SKILL.md +7 -4
  6. package/.agents/skills/orchestrate-wave/SKILL.md +1 -1
  7. package/.agents/skills/orchestrate-wave/references/dispatch-subagents.md +9 -0
  8. package/.agents/skills/orchestrate-wave/references/dispatch-workflow.md +9 -0
  9. package/.agents/skills/research/SKILL.md +7 -4
  10. package/.agents/skills/to-issues/SKILL.md +25 -4
  11. package/.claude/skills/audit-skills/SKILL.md +7 -4
  12. package/.claude/skills/code-review/SKILL.md +7 -4
  13. package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +7 -4
  14. package/.claude/skills/codex-build/SKILL.md +13 -0
  15. package/.claude/skills/codex-review/SKILL.md +13 -0
  16. package/.claude/skills/grill-me-codex/SKILL.md +14 -0
  17. package/.claude/skills/grill-with-docs-codex/SKILL.md +14 -0
  18. package/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +7 -4
  19. package/.claude/skills/improve-codebase-architecture/SKILL.md +7 -4
  20. package/.claude/skills/orchestrate-wave/SKILL.md +1 -1
  21. package/.claude/skills/orchestrate-wave/references/dispatch-subagents.md +9 -0
  22. package/.claude/skills/orchestrate-wave/references/dispatch-workflow.md +9 -0
  23. package/.claude/skills/research/SKILL.md +7 -4
  24. package/.claude/skills/skill-manifest.json +34 -23
  25. package/.claude/skills/to-issues/SKILL.md +25 -4
  26. package/README.md +64 -0
  27. package/agent-workflow-kit.package.json +149 -45
  28. package/package.json +1 -1
  29. package/scripts/doctrine-migration/index.mjs +296 -0
  30. package/scripts/kit-release.mjs +41 -9
  31. package/src/cli.mjs +515 -79
  32. package/src/commands/routing-status.mjs +288 -0
  33. package/src/lib/bundle.mjs +158 -2
  34. package/src/lib/dispatchJournal.mjs +300 -0
  35. package/src/lib/dispatchPlan.mjs +286 -0
  36. package/src/lib/dispatchReceipt.mjs +226 -89
  37. package/src/lib/frontendWorkloads.mjs +35 -33
  38. package/src/lib/routeDispatcher.mjs +367 -70
  39. package/src/lib/routingAccessGraph.mjs +265 -20
  40. package/src/lib/routingAccessGraphStore.mjs +300 -0
  41. package/src/lib/routingAdapters/claude.mjs +104 -4
  42. package/src/lib/routingAdapters/codex.mjs +132 -7
  43. package/src/lib/routingAdapters/hostBridge.mjs +291 -0
  44. package/src/lib/routingCatalog.mjs +201 -24
  45. package/src/lib/routingDispatchLease.mjs +253 -0
  46. package/src/lib/routingEvidenceCache.mjs +78 -0
  47. package/src/lib/routingIntent.mjs +176 -10
  48. package/src/lib/routingIntentClassifier.mjs +137 -0
  49. package/src/lib/routingInventory/snapshots/claude.json +49 -0
  50. package/src/lib/routingInventory/snapshots/codex.json +109 -0
  51. package/src/lib/routingInventory.mjs +182 -0
  52. package/src/lib/routingPolicy.mjs +193 -6
  53. package/src/lib/routingProfile.mjs +1251 -123
  54. package/src/lib/routingProfilePolicy.mjs +156 -0
  55. package/src/lib/routingProfileStorage.mjs +299 -0
  56. package/src/lib/routingResolver.mjs +369 -86
  57. package/src/lib/routingSources/artificialAnalysis.mjs +19 -7
  58. package/src/lib/routingSources/benchlm.mjs +5 -0
  59. package/src/lib/routingSources/codeArena.mjs +13 -3
  60. package/src/lib/routingSources/deepswe.mjs +19 -5
  61. package/src/lib/routingSources/openhands.mjs +17 -4
  62. package/src/lib/routingSources/openhandsFrontend.mjs +13 -3
  63. package/src/lib/safeText.mjs +26 -0
  64. package/src/lib/updateCandidate.mjs +36 -3
@@ -1,29 +1,77 @@
1
+ /**
2
+ * The Dispatch receipt — the runtime record proving which requested Route
3
+ * decision was actually applied, by which enforcement mechanism, under which
4
+ * policy and evidence revisions.
5
+ *
6
+ * A receipt has one of four kinds, and each validates its own field set rather
7
+ * than a union of everything any dispatch could ever carry. `routed-dispatch`
8
+ * ran the resolved route, applied matching requested field for field, and
9
+ * `blocked` did not run at all. `inherited-dispatch` ran the session default: it
10
+ * is a receipt only when it carries the *attested* applied pair and the
11
+ * session-default enforcement method, because the inherited pair may
12
+ * legitimately differ from the route that was requested. `handoff` dispatched
13
+ * nothing and handed the work on, so it records no applied route and no
14
+ * enforcement and can never read as proof that something ran.
15
+ *
16
+ * Every kind names the revisions it decided under (`policyRevision`,
17
+ * `catalogRevision`, `decisionAccessRevision`) plus the plan-authorization id
18
+ * it dispatched against; a blocked receipt additionally names
19
+ * `resultingAccessRevision` when the failure itself mutated the Access graph.
20
+ */
1
21
  export const DISPATCH_RECEIPT_VERSION = 2;
2
22
 
3
- const ROUTE_FIELDS = [
4
- 'providerId',
5
- 'modelId',
6
- 'effort',
7
- 'surfaceId',
8
- 'transportId',
9
- ];
23
+ export const DISPATCH_RECEIPT_KINDS = Object.freeze([
24
+ 'routed-dispatch', 'inherited-dispatch', 'handoff', 'blocked',
25
+ ]);
10
26
 
11
- const ENFORCEMENT_METHODS = [
12
- 'per-spawn',
13
- 'named-agent',
14
- 'session-default',
15
- 'none',
16
- ];
27
+ const KIND_STATUS = Object.freeze({
28
+ 'routed-dispatch': 'dispatched',
29
+ 'inherited-dispatch': 'dispatched',
30
+ handoff: 'handoff',
31
+ blocked: 'blocked',
32
+ });
33
+
34
+ /** What a caller that only knows the legacy outcome axis means by its status. */
35
+ const STATUS_KIND = Object.freeze({
36
+ dispatched: 'routed-dispatch', handoff: 'handoff', blocked: 'blocked',
37
+ });
38
+
39
+ const ROUTE_FIELDS = ['providerId', 'modelId', 'effort', 'surfaceId', 'transportId'];
40
+
41
+ const ENFORCEMENT_METHODS = ['per-spawn', 'named-agent', 'session-default', 'none'];
17
42
 
18
43
  export const DISPATCH_PRECEDENCE = Object.freeze([
19
- 'explicit-argument',
20
- 'agent-definition-over-environment',
21
- 'environment-over-agent-definition',
22
- 'session-default',
23
- 'uncontrolled',
24
- 'unreported',
44
+ 'explicit-argument', 'agent-definition-over-environment',
45
+ 'environment-over-agent-definition', 'session-default', 'uncontrolled', 'unreported',
25
46
  ]);
26
47
 
48
+ /**
49
+ * The readback channels that may attest an applied pair, and what each proves
50
+ * per axis. A channel absent from this table is not an attestation:
51
+ * `~/.claude/settings.json` reported `medium` for a session that ran at `xhigh`,
52
+ * so a configured value never attests an applied one. Claude's transcript
53
+ * returns `message.model` from the server while its top-level `effort` is the
54
+ * CLI's own record; Codex's `turn_context` is client-written throughout, so a
55
+ * Codex receipt cannot claim provider attestation.
56
+ */
57
+ export const APPLIED_PAIR_ATTESTATION_SOURCES = Object.freeze({
58
+ 'session-transcript': Object.freeze({ model: true, effort: false }),
59
+ 'rollout-turn-context': Object.freeze({ model: false, effort: false }),
60
+ });
61
+
62
+ const COMMON_FIELDS = [
63
+ 'kind', 'status', 'executionId', 'afk', 'revisions', 'authorizationId', 'dispatchedAt',
64
+ ];
65
+ const APPLIED_FIELDS = ['requestedRoute', 'appliedRoute', 'enforcement', 'precedence'];
66
+
67
+ /** The closed field set of each kind — what it may carry, and nothing else. */
68
+ const KIND_FIELDS = Object.freeze({
69
+ 'routed-dispatch': new Set([...COMMON_FIELDS, ...APPLIED_FIELDS]),
70
+ 'inherited-dispatch': new Set([...COMMON_FIELDS, ...APPLIED_FIELDS, 'attestation']),
71
+ handoff: new Set([...COMMON_FIELDS, 'requestedRoute', 'handoff', 'reason']),
72
+ blocked: new Set([...COMMON_FIELDS, ...APPLIED_FIELDS, 'reason', 'resultingAccessRevision']),
73
+ });
74
+
27
75
  function object(value, field) {
28
76
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
29
77
  throw new TypeError(`${field} must be an object`);
@@ -31,6 +79,13 @@ function object(value, field) {
31
79
  return value;
32
80
  }
33
81
 
82
+ function closedFields(value, allowed, message) {
83
+ for (const key of Object.keys(value)) {
84
+ if (!allowed.includes(key)) throw new TypeError(`${message}: ${key}`);
85
+ }
86
+ return value;
87
+ }
88
+
34
89
  function string(value, field) {
35
90
  if (typeof value !== 'string' || value.trim() === '') {
36
91
  throw new TypeError(`${field} must be a non-empty string`);
@@ -38,6 +93,8 @@ function string(value, field) {
38
93
  return value;
39
94
  }
40
95
 
96
+ const optionalString = (value, field) => (value == null ? null : string(value, field));
97
+
41
98
  function timestamp(value, field) {
42
99
  string(value, field);
43
100
  if (!Number.isFinite(Date.parse(value))) throw new TypeError(`${field} must be an ISO timestamp`);
@@ -45,42 +102,31 @@ function timestamp(value, field) {
45
102
  }
46
103
 
47
104
  function route(input, field) {
48
- object(input, field);
49
- for (const key of Object.keys(input)) {
50
- if (!ROUTE_FIELDS.includes(key)) throw new TypeError(`unknown ${field} field: ${key}`);
51
- }
105
+ closedFields(object(input, field), ROUTE_FIELDS, `unknown ${field} field`);
52
106
  return Object.freeze(Object.fromEntries(
53
107
  ROUTE_FIELDS.map((key) => [key, string(input[key], `${field}.${key}`)]),
54
108
  ));
55
109
  }
56
110
 
57
- function enforcement(input) {
58
- object(input, 'enforcement');
111
+ const optionalRoute = (input, field) => (input == null ? null : route(input, field));
112
+
113
+ function pair(input, field, allowed) {
114
+ object(input, field);
59
115
  const result = {};
60
- for (const field of ['model', 'effort']) {
61
- if (!ENFORCEMENT_METHODS.includes(input[field])) {
62
- throw new TypeError(`${field} enforcement must be one of: ${ENFORCEMENT_METHODS.join(', ')}`);
116
+ for (const axis of ['model', 'effort']) {
117
+ if (!allowed.includes(input[axis])) {
118
+ throw new TypeError(`${axis} ${field} must be one of: ${allowed.join(', ')}`);
63
119
  }
64
- result[field] = input[field];
120
+ result[axis] = input[axis];
65
121
  }
66
122
  return Object.freeze(result);
67
123
  }
68
124
 
125
+ const enforcement = (input) => pair(input, 'enforcement', ENFORCEMENT_METHODS);
126
+
69
127
  function precedence(input) {
70
- if (input == null) {
71
- return Object.freeze({ model: 'unreported', effort: 'unreported' });
72
- }
73
- object(input, 'precedence');
74
- const result = {};
75
- for (const field of ['model', 'effort']) {
76
- if (!DISPATCH_PRECEDENCE.includes(input[field])) {
77
- throw new TypeError(
78
- `${field} precedence must be one of: ${DISPATCH_PRECEDENCE.join(', ')}`,
79
- );
80
- }
81
- result[field] = input[field];
82
- }
83
- return Object.freeze(result);
128
+ if (input == null) return Object.freeze({ model: 'unreported', effort: 'unreported' });
129
+ return pair(input, 'precedence', DISPATCH_PRECEDENCE);
84
130
  }
85
131
 
86
132
  function revisions(input) {
@@ -92,42 +138,84 @@ function revisions(input) {
92
138
  });
93
139
  }
94
140
 
95
- function blockedReceipt(input) {
96
- const requestedRoute = input.requestedRoute == null
97
- ? null
98
- : route(input.requestedRoute, 'requestedRoute');
99
- const appliedRoute = input.appliedRoute == null
100
- ? null
101
- : route(input.appliedRoute, 'appliedRoute');
102
- if (appliedRoute !== null && requestedRoute === null) {
103
- throw new TypeError('blocked applied route requires a requested route');
141
+ /** The kind a caller asked for, or the one its legacy status still implies. */
142
+ function receiptKind(input) {
143
+ if (input.kind == null) {
144
+ const derived = STATUS_KIND[input.status];
145
+ if (derived) return derived;
146
+ throw new TypeError(
147
+ `dispatch receipt needs a kind, or a status of: ${Object.keys(STATUS_KIND).join(', ')}`,
148
+ );
104
149
  }
105
- const appliedEnforcement = input.enforcement == null ? null : enforcement(input.enforcement);
106
- const appliedPrecedence = input.precedence == null ? null : precedence(input.precedence);
107
- if (appliedRoute !== null && (appliedEnforcement === null || appliedPrecedence === null)) {
108
- throw new TypeError('blocked applied route requires enforcement and precedence');
150
+ if (!DISPATCH_RECEIPT_KINDS.includes(input.kind)) {
151
+ throw new TypeError(
152
+ `dispatch receipt kind must be one of: ${DISPATCH_RECEIPT_KINDS.join(', ')}`,
153
+ );
109
154
  }
110
- return Object.freeze({
155
+ if (input.status != null && input.status !== KIND_STATUS[input.kind]) {
156
+ throw new TypeError(`${input.kind} receipt status must be ${KIND_STATUS[input.kind]}`);
157
+ }
158
+ return input.kind;
159
+ }
160
+
161
+ /**
162
+ * `resultingAccessRevision` is the Access-graph revision *after* a failure
163
+ * mutated the graph, so it belongs to a blocked receipt and must differ from the
164
+ * decision's own — repeating that one names a mutation that never happened.
165
+ */
166
+ function namedRevisions(input, kind, decided) {
167
+ const resulting = optionalString(input.resultingAccessRevision, 'resultingAccessRevision');
168
+ if (resulting !== null && resulting === decided.accessGraph) {
169
+ throw new TypeError('resultingAccessRevision names no access graph mutation');
170
+ }
171
+ return {
172
+ catalogRevision: decided.catalog,
173
+ decisionAccessRevision: decided.accessGraph,
174
+ policyRevision: decided.policy,
175
+ resultingAccessRevision: kind === 'blocked' ? resulting : null,
176
+ };
177
+ }
178
+
179
+ function baseReceipt(input, kind) {
180
+ const decided = revisions(input.revisions);
181
+ return {
111
182
  schemaVersion: DISPATCH_RECEIPT_VERSION,
183
+ kind,
112
184
  executionId: string(input.executionId, 'executionId'),
113
- status: 'blocked',
185
+ status: KIND_STATUS[kind],
114
186
  afk: input.afk === true,
115
- requestedRoute,
116
- appliedRoute,
117
- enforcement: appliedEnforcement,
118
- precedence: appliedPrecedence,
119
- revisions: revisions(input.revisions),
187
+ requestedRoute: null, appliedRoute: null, enforcement: null, precedence: null,
188
+ attestation: null, handoff: null,
189
+ revisions: decided,
190
+ ...namedRevisions(input, kind, decided),
191
+ authorizationId: optionalString(input.authorizationId, 'authorizationId'),
120
192
  dispatchedAt: timestamp(input.dispatchedAt, 'dispatchedAt'),
121
- reason: string(input.reason, 'reason'),
122
- });
193
+ reason: null,
194
+ };
123
195
  }
124
196
 
125
- export function createDispatchReceipt(input) {
126
- object(input, 'dispatch receipt');
127
- if (input.status === 'blocked') return blockedReceipt(input);
128
- if (input.status !== 'dispatched') {
129
- throw new TypeError('dispatch receipt status must be dispatched or blocked');
197
+ /** The attested applied pair: which channel read it back, and what that proves. */
198
+ function appliedPairAttestation(input, appliedRoute) {
199
+ if (input == null) throw new TypeError('inherited dispatch requires an attested applied pair');
200
+ const sources = Object.keys(APPLIED_PAIR_ATTESTATION_SOURCES);
201
+ closedFields(object(input, 'attestation'),
202
+ ['source', 'model', 'effort', 'observedAt'], 'unknown attestation field');
203
+ if (!sources.includes(input.source)) {
204
+ throw new TypeError(`attestation source must be one of: ${sources.join(', ')}`);
205
+ }
206
+ const model = string(input.model, 'attestation.model');
207
+ const effort = string(input.effort, 'attestation.effort');
208
+ if (model !== appliedRoute.modelId || effort !== appliedRoute.effort) {
209
+ throw new Error('attested applied pair differs from the applied route');
130
210
  }
211
+ return Object.freeze({
212
+ source: input.source, model, effort,
213
+ observedAt: timestamp(input.observedAt, 'attestation.observedAt'),
214
+ providerAttested: APPLIED_PAIR_ATTESTATION_SOURCES[input.source],
215
+ });
216
+ }
217
+
218
+ function routedDispatch(input, base) {
131
219
  const requestedRoute = route(input.requestedRoute, 'requestedRoute');
132
220
  const appliedRoute = route(input.appliedRoute, 'appliedRoute');
133
221
  for (const field of ROUTE_FIELDS) {
@@ -135,28 +223,77 @@ export function createDispatchReceipt(input) {
135
223
  throw new Error(`applied route differs from requested route: ${field}`);
136
224
  }
137
225
  }
138
- const appliedEnforcement = enforcement(input.enforcement);
139
- const appliedPrecedence = precedence(input.precedence);
140
- if (input.afk === true
141
- && (appliedEnforcement.model === 'none' || appliedEnforcement.effort === 'none')) {
226
+ const applied = enforcement(input.enforcement);
227
+ const order = precedence(input.precedence);
228
+ if (base.afk && (applied.model === 'none' || applied.effort === 'none')) {
142
229
  throw new Error('AFK dispatch requires enforced model and effort selection');
143
230
  }
144
- if (input.afk === true
145
- && Object.values(appliedPrecedence).some((value) =>
146
- value === 'uncontrolled' || value === 'unreported')) {
231
+ if (base.afk && Object.values(order).some((value) =>
232
+ value === 'uncontrolled' || value === 'unreported')) {
147
233
  throw new Error('AFK dispatch requires verified environment precedence');
148
234
  }
149
- return Object.freeze({
150
- schemaVersion: DISPATCH_RECEIPT_VERSION,
151
- executionId: string(input.executionId, 'executionId'),
152
- status: 'dispatched',
153
- afk: input.afk === true,
154
- requestedRoute,
235
+ return { ...base, requestedRoute, appliedRoute, enforcement: applied, precedence: order };
236
+ }
237
+
238
+ /**
239
+ * Inheritance is the constrained non-AFK path: the applied pair is whatever the
240
+ * session already runs, so it proves nothing without its attestation.
241
+ */
242
+ function inheritedDispatch(input, base) {
243
+ const appliedRoute = route(input.appliedRoute, 'appliedRoute');
244
+ const applied = enforcement(input.enforcement);
245
+ if (applied.model !== 'session-default' || applied.effort !== 'session-default') {
246
+ throw new TypeError('inherited dispatch requires the session-default enforcement method');
247
+ }
248
+ if (base.afk) throw new Error('AFK dispatch cannot inherit a session default');
249
+ return {
250
+ ...base,
251
+ requestedRoute: optionalRoute(input.requestedRoute, 'requestedRoute'),
155
252
  appliedRoute,
156
- enforcement: appliedEnforcement,
157
- precedence: appliedPrecedence,
158
- revisions: revisions(input.revisions),
159
- dispatchedAt: timestamp(input.dispatchedAt, 'dispatchedAt'),
160
- reason: null,
161
- });
253
+ enforcement: applied,
254
+ precedence: precedence(input.precedence),
255
+ attestation: appliedPairAttestation(input.attestation, appliedRoute),
256
+ };
257
+ }
258
+
259
+ function handoffReceipt(input, base) {
260
+ const target = closedFields(object(input.handoff, 'handoff'), ['to'], 'unknown handoff field');
261
+ return {
262
+ ...base,
263
+ requestedRoute: optionalRoute(input.requestedRoute, 'requestedRoute'),
264
+ handoff: Object.freeze({ to: string(target.to, 'handoff.to') }),
265
+ reason: string(input.reason, 'reason'),
266
+ };
267
+ }
268
+
269
+ function blockedReceipt(input, base) {
270
+ const requestedRoute = optionalRoute(input.requestedRoute, 'requestedRoute');
271
+ const appliedRoute = optionalRoute(input.appliedRoute, 'appliedRoute');
272
+ if (appliedRoute !== null && requestedRoute === null) {
273
+ throw new TypeError('blocked applied route requires a requested route');
274
+ }
275
+ const applied = input.enforcement == null ? null : enforcement(input.enforcement);
276
+ const order = input.precedence == null ? null : precedence(input.precedence);
277
+ if (appliedRoute !== null && (applied === null || order === null)) {
278
+ throw new TypeError('blocked applied route requires enforcement and precedence');
279
+ }
280
+ return {
281
+ ...base,
282
+ requestedRoute, appliedRoute, enforcement: applied, precedence: order,
283
+ reason: string(input.reason, 'reason'),
284
+ };
285
+ }
286
+
287
+ const BUILDERS = Object.freeze({
288
+ 'routed-dispatch': routedDispatch,
289
+ 'inherited-dispatch': inheritedDispatch,
290
+ handoff: handoffReceipt,
291
+ blocked: blockedReceipt,
292
+ });
293
+
294
+ export function createDispatchReceipt(input) {
295
+ object(input, 'dispatch receipt');
296
+ const kind = receiptKind(input);
297
+ closedFields(input, [...KIND_FIELDS[kind]], `unknown ${kind} receipt field`);
298
+ return Object.freeze(BUILDERS[kind](input, baseReceipt(input, kind)));
162
299
  }
@@ -2,46 +2,47 @@ import {
2
2
  evidenceWorkloadIdentity,
3
3
  validateEvidenceSelection,
4
4
  } from './routingIntent.mjs';
5
+ import {
6
+ FRONTEND_QUALITY_AXES,
7
+ evidenceDomainsFor,
8
+ evidenceIdentity,
9
+ evidenceSourceClaim,
10
+ } from './routingCatalog.mjs';
5
11
 
6
- const FRONTEND_DOMAINS = new Set([
7
- 'general',
8
- 'reference-design',
9
- 'marketing',
10
- 'analytics',
11
- 'product',
12
- 'game',
13
- 'simulation',
14
- 'editor',
12
+ // The frontend vocabulary is a strict subset of the researched taxonomy
13
+ // (docs/research/agent-task-taxonomy-benchmark-coverage.md §1.1): it reads its
14
+ // domains and quality axes from the catalog rather than restating them.
15
+ const FRONTEND_WORKLOADS = Object.freeze([
16
+ 'frontend-greenfield',
17
+ 'frontend-repository-repair',
15
18
  ]);
19
+ const FRONTEND_DOMAINS = new Set(evidenceDomainsFor(FRONTEND_WORKLOADS[0]));
20
+ const QUALITY_AXES = new Set(FRONTEND_QUALITY_AXES);
16
21
 
17
- const QUALITY_AXES = new Set([
18
- 'visual-preference',
19
- 'visual-fidelity',
20
- 'functional',
21
- 'accessibility',
22
- 'responsive',
23
- ]);
22
+ // Each source carries the three-part decisiveness test instead of a boolean, so
23
+ // a collapsed dimension is named rather than hidden (§4).
24
+ const frontendClaim = (sourceId, { workloads, axes }) => Object.freeze({
25
+ workloads: Object.freeze(workloads),
26
+ axes: Object.freeze(axes),
27
+ ...evidenceSourceClaim(sourceId),
28
+ });
24
29
 
25
30
  export const FRONTEND_SOURCE_CLAIMS = Object.freeze({
26
- 'code-arena-webdev': Object.freeze({
27
- workloads: Object.freeze(['frontend-greenfield']),
28
- axes: Object.freeze(['visual-preference']),
29
- decisive: true,
31
+ 'code-arena-webdev': frontendClaim('code-arena-webdev', {
32
+ workloads: ['frontend-greenfield'],
33
+ axes: ['visual-preference'],
30
34
  }),
31
- 'openhands-frontend': Object.freeze({
32
- workloads: Object.freeze(['frontend-repository-repair']),
33
- axes: Object.freeze(['functional']),
34
- decisive: true,
35
+ 'openhands-frontend': frontendClaim('openhands-frontend', {
36
+ workloads: ['frontend-repository-repair'],
37
+ axes: ['functional'],
35
38
  }),
36
- vision2web: Object.freeze({
37
- workloads: Object.freeze(['frontend-greenfield']),
38
- axes: Object.freeze(['visual-fidelity', 'functional', 'responsive']),
39
- decisive: false,
39
+ vision2web: frontendClaim('vision2web', {
40
+ workloads: ['frontend-greenfield'],
41
+ axes: ['visual-fidelity', 'functional', 'responsive'],
40
42
  }),
41
- design2code: Object.freeze({
42
- workloads: Object.freeze(['frontend-greenfield']),
43
- axes: Object.freeze(['visual-fidelity']),
44
- decisive: false,
43
+ design2code: frontendClaim('design2code', {
44
+ workloads: ['frontend-greenfield'],
45
+ axes: ['visual-fidelity'],
45
46
  }),
46
47
  });
47
48
 
@@ -111,13 +112,14 @@ export function classifyFrontendWorkload(input) {
111
112
  }
112
113
 
113
114
  export function frontendEvidenceWorkload({ workload, frontendDomain = 'general', axis }) {
114
- if (!['frontend-greenfield', 'frontend-repository-repair'].includes(workload)) {
115
+ if (!FRONTEND_WORKLOADS.includes(workload)) {
115
116
  throw new TypeError(`unknown frontend evidence workload: ${workload}`);
116
117
  }
117
118
  if (!FRONTEND_DOMAINS.has(frontendDomain)) {
118
119
  throw new TypeError(`unknown frontend domain: ${frontendDomain}`);
119
120
  }
120
121
  if (!QUALITY_AXES.has(axis)) throw new TypeError(`unknown frontend quality axis: ${axis}`);
122
+ evidenceIdentity({ workload, domain: frontendDomain, axis });
121
123
  return evidenceWorkloadIdentity({
122
124
  workload,
123
125
  domain: frontendDomain,