@ikon85/agent-workflow-kit 0.32.1 → 0.34.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.
- package/.agents/skills/ask-matt/SKILL.md +4 -3
- package/.agents/skills/audit-skills/SKILL.md +6 -0
- package/.agents/skills/board-to-waves/SKILL.md +6 -2
- package/.agents/skills/code-review/SKILL.md +6 -0
- package/.agents/skills/codebase-design/DESIGN-IT-TWICE.md +11 -1
- package/.agents/skills/codex-adapter-sync/SKILL.md +23 -19
- package/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +6 -0
- package/.agents/skills/improve-codebase-architecture/SKILL.md +10 -1
- package/.agents/skills/kit-update/SKILL.md +13 -0
- package/.agents/skills/orchestrate-wave/SKILL.md +54 -54
- package/.agents/skills/orchestrate-wave/references/builder-contract.md +16 -3
- package/.agents/skills/orchestrate-wave/references/dispatch-subagents.md +91 -0
- package/.agents/skills/orchestrate-wave/references/dispatch-workflow.md +66 -0
- package/.agents/skills/orchestrate-wave/references/report-contracts.md +42 -0
- package/.agents/skills/research/SKILL.md +6 -0
- package/.agents/skills/scale-check/SKILL.md +9 -7
- package/.agents/skills/setup-workflow/SKILL.md +47 -1
- package/.agents/skills/setup-workflow/board-sync.md +7 -2
- package/.agents/skills/setup-workflow/workflow-overview.md +1 -2
- package/.agents/skills/to-issues/SKILL.md +66 -8
- package/.agents/skills/to-waves/SKILL.md +24 -12
- package/.claude/skills/ask-matt/SKILL.md +4 -3
- package/.claude/skills/audit-skills/SKILL.md +6 -0
- package/.claude/skills/board-to-waves/SKILL.md +6 -2
- package/.claude/skills/code-review/SKILL.md +6 -0
- package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +8 -0
- package/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +6 -0
- package/.claude/skills/improve-codebase-architecture/SKILL.md +6 -0
- package/.claude/skills/kit-update/SKILL.md +13 -0
- package/.claude/skills/orchestrate-wave/SKILL.md +54 -54
- package/.claude/skills/orchestrate-wave/references/builder-contract.md +16 -3
- package/.claude/skills/orchestrate-wave/references/dispatch-subagents.md +91 -0
- package/.claude/skills/orchestrate-wave/references/dispatch-workflow.md +66 -0
- package/.claude/skills/orchestrate-wave/references/report-contracts.md +42 -0
- package/.claude/skills/research/SKILL.md +6 -0
- package/.claude/skills/scale-check/SKILL.md +9 -7
- package/.claude/skills/setup-workflow/SKILL.md +47 -1
- package/.claude/skills/setup-workflow/board-sync.md +7 -2
- package/.claude/skills/setup-workflow/workflow-overview.md +1 -2
- package/.claude/skills/to-issues/SKILL.md +66 -8
- package/.claude/skills/to-waves/SKILL.md +24 -12
- package/README.md +126 -11
- package/agent-workflow-kit.package.json +291 -39
- package/docs/adr/0002-capability-gated-orchestration.md +70 -0
- package/docs/adr/0005-to-issues-is-the-planning-facade.md +42 -0
- package/docs/adr/0006-routing-knowledge-access-and-policy-are-separate.md +91 -0
- package/docs/agents/skills/local-ci.md +89 -0
- package/docs/agents/wave-anchor-template.md +2 -2
- package/docs/methodology.html +1 -1
- package/docs/methodology.svg +1 -1
- package/docs/workflow.html +2 -2
- package/docs/workflow.png +0 -0
- package/package.json +1 -1
- package/scripts/codex-exec.sh +47 -8
- package/scripts/codex-exec.test.mjs +56 -0
- package/scripts/test_codex_adapter_sync_contract.py +30 -15
- package/scripts/test_orchestrate_wave_contract.py +209 -0
- package/scripts/test_program_planning_contract.py +70 -0
- package/scripts/test_skill_portability_lint.py +54 -0
- package/scripts/test_worktree_setup_base_guard.py +140 -0
- package/scripts/worktree-lifecycle/setup.py +40 -0
- package/src/cli.mjs +109 -2
- package/src/commands/init.mjs +17 -1
- package/src/commands/routing-policy-update.mjs +204 -0
- package/src/commands/update.mjs +22 -0
- package/src/lib/agentSurfaceRegistry.mjs +60 -0
- package/src/lib/bundle.mjs +34 -0
- package/src/lib/capabilityMatrix.mjs +179 -0
- package/src/lib/dispatchReceipt.mjs +162 -0
- package/src/lib/frontendWorkloads.mjs +170 -0
- package/src/lib/reconcileReconReports.mjs +111 -0
- package/src/lib/reportValidator.mjs +186 -0
- package/src/lib/routeDispatcher.mjs +158 -0
- package/src/lib/routingAccessGraph.mjs +105 -0
- package/src/lib/routingAdapters/claude.mjs +62 -0
- package/src/lib/routingAdapters/codex.mjs +136 -0
- package/src/lib/routingCatalog.mjs +123 -0
- package/src/lib/routingEvidenceCache.mjs +222 -0
- package/src/lib/routingIntent.mjs +93 -0
- package/src/lib/routingPolicy.mjs +67 -0
- package/src/lib/routingProfile.mjs +334 -0
- package/src/lib/routingResolver.mjs +176 -0
- package/src/lib/routingSources/artificialAnalysis.mjs +129 -0
- package/src/lib/routingSources/benchlm.mjs +151 -0
- package/src/lib/routingSources/codeArena.mjs +162 -0
- package/src/lib/routingSources/deepswe.mjs +106 -0
- package/src/lib/routingSources/openhands.mjs +102 -0
- package/src/lib/routingSources/openhandsFrontend.mjs +135 -0
- package/src/lib/waveClaim.mjs +134 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const UNKNOWN = 'unknown';
|
|
2
|
+
|
|
3
|
+
const PATH_A_CAPABILITIES = [
|
|
4
|
+
'namedPhases',
|
|
5
|
+
'runIdentity',
|
|
6
|
+
'runtimeOutputValidation',
|
|
7
|
+
'journal',
|
|
8
|
+
'resume',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const PATH_B_TOOLS = [
|
|
12
|
+
['spawn_agent', 'spawn'],
|
|
13
|
+
['wait_agent', 'wait'],
|
|
14
|
+
['list_agents', 'aggregate'],
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const TARGETS = Object.freeze({
|
|
18
|
+
A: Object.freeze({ path: 'A', kind: 'reference', value: 'references/dispatch-workflow.md' }),
|
|
19
|
+
B: Object.freeze({ path: 'B', kind: 'reference', value: 'references/dispatch-subagents.md' }),
|
|
20
|
+
C: Object.freeze({ path: 'C', kind: 'inline', value: 'path-c' }),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function triState(value) {
|
|
24
|
+
return typeof value === 'boolean' ? value : UNKNOWN;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function capacity(value) {
|
|
28
|
+
return Number.isInteger(value) && value >= 0 ? value : UNKNOWN;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeTool(tool) {
|
|
32
|
+
if (!tool || typeof tool !== 'object' || Array.isArray(tool)) return null;
|
|
33
|
+
return {
|
|
34
|
+
name: typeof tool.name === 'string' ? tool.name : UNKNOWN,
|
|
35
|
+
schema: tool.schema && typeof tool.schema === 'object' && !Array.isArray(tool.schema)
|
|
36
|
+
? tool.schema
|
|
37
|
+
: UNKNOWN,
|
|
38
|
+
callable: triState(tool.callable),
|
|
39
|
+
permitted: triState(tool.permitted),
|
|
40
|
+
capabilities: Array.isArray(tool.capabilities)
|
|
41
|
+
&& tool.capabilities.every((item) => typeof item === 'string')
|
|
42
|
+
? [...new Set(tool.capabilities)]
|
|
43
|
+
: UNKNOWN,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function adaptInventory(inventory) {
|
|
48
|
+
const source = inventory && typeof inventory === 'object' ? inventory : {};
|
|
49
|
+
const validContract = source.contractVersion === 1;
|
|
50
|
+
return {
|
|
51
|
+
contractVersion: 1,
|
|
52
|
+
tools: validContract && Array.isArray(source.tools)
|
|
53
|
+
? source.tools.map(normalizeTool).filter(Boolean)
|
|
54
|
+
: [],
|
|
55
|
+
effectiveConcurrency: validContract ? capacity(source.effectiveConcurrency) : UNKNOWN,
|
|
56
|
+
threadCapacity: validContract ? capacity(source.threadCapacity) : UNKNOWN,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const capabilityAdapter = Object.freeze({
|
|
61
|
+
claude: adaptInventory,
|
|
62
|
+
codex: adaptInventory,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const ROUTE_IDENTITY_FIELDS = [
|
|
66
|
+
'id',
|
|
67
|
+
'surfaceId',
|
|
68
|
+
'providerId',
|
|
69
|
+
'modelId',
|
|
70
|
+
'transportId',
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const ROUTE_ENFORCEMENT_METHODS = [
|
|
74
|
+
'per-spawn',
|
|
75
|
+
'named-agent',
|
|
76
|
+
'session-default',
|
|
77
|
+
'none',
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const ROUTE_PRECEDENCE = [
|
|
81
|
+
'explicit-argument',
|
|
82
|
+
'agent-definition-over-environment',
|
|
83
|
+
'environment-over-agent-definition',
|
|
84
|
+
'session-default',
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
function routingControl(control) {
|
|
88
|
+
const source = control && typeof control === 'object' && !Array.isArray(control)
|
|
89
|
+
? control
|
|
90
|
+
: {};
|
|
91
|
+
return {
|
|
92
|
+
method: ROUTE_ENFORCEMENT_METHODS.includes(source.method) ? source.method : UNKNOWN,
|
|
93
|
+
enforced: triState(source.enforced),
|
|
94
|
+
precedence: ROUTE_PRECEDENCE.includes(source.precedence)
|
|
95
|
+
? source.precedence
|
|
96
|
+
: UNKNOWN,
|
|
97
|
+
applied: typeof source.applied === 'string' && source.applied !== ''
|
|
98
|
+
? source.applied
|
|
99
|
+
: UNKNOWN,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function routingPath(path) {
|
|
104
|
+
const source = path && typeof path === 'object' && !Array.isArray(path) ? path : {};
|
|
105
|
+
const normalized = Object.fromEntries(ROUTE_IDENTITY_FIELDS.map((field) => [
|
|
106
|
+
field,
|
|
107
|
+
typeof source[field] === 'string' && source[field] !== '' ? source[field] : UNKNOWN,
|
|
108
|
+
]));
|
|
109
|
+
normalized.detected = triState(source.detected);
|
|
110
|
+
normalized.callable = triState(source.callable);
|
|
111
|
+
normalized.permitted = triState(source.permitted);
|
|
112
|
+
normalized.model = routingControl(source.model);
|
|
113
|
+
normalized.effort = routingControl(source.effort);
|
|
114
|
+
const failures = [];
|
|
115
|
+
if (ROUTE_IDENTITY_FIELDS.some((field) => normalized[field] === UNKNOWN)) {
|
|
116
|
+
failures.push('route identity is incomplete');
|
|
117
|
+
}
|
|
118
|
+
if (normalized.detected !== true) failures.push('transport is not detected');
|
|
119
|
+
if (normalized.callable !== true) failures.push('transport is not callable');
|
|
120
|
+
if (normalized.permitted !== true) failures.push('transport is not permitted');
|
|
121
|
+
for (const field of ['model', 'effort']) {
|
|
122
|
+
if (normalized[field].enforced !== true || normalized[field].method === 'none'
|
|
123
|
+
|| normalized[field].method === UNKNOWN) {
|
|
124
|
+
failures.push(`${field} control is not enforced`);
|
|
125
|
+
}
|
|
126
|
+
if (normalized[field].precedence === UNKNOWN) {
|
|
127
|
+
failures.push(`${field} environment precedence is unverified`);
|
|
128
|
+
}
|
|
129
|
+
if (normalized[field].applied === UNKNOWN) {
|
|
130
|
+
failures.push(`${field} applied value is unverified`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
normalized.verified = failures.length === 0;
|
|
134
|
+
normalized.verificationFailures = failures;
|
|
135
|
+
return Object.freeze(normalized);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function adaptClaudeRoutingInventory(inventory) {
|
|
139
|
+
const source = inventory && typeof inventory === 'object' && !Array.isArray(inventory)
|
|
140
|
+
? inventory
|
|
141
|
+
: {};
|
|
142
|
+
return Object.freeze({
|
|
143
|
+
contractVersion: 1,
|
|
144
|
+
paths: source.contractVersion === 1 && Array.isArray(source.paths)
|
|
145
|
+
? Object.freeze(source.paths.map(routingPath))
|
|
146
|
+
: Object.freeze([]),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function proves(tool, capability) {
|
|
151
|
+
return tool?.callable === true
|
|
152
|
+
&& tool.permitted === true
|
|
153
|
+
&& tool.schema !== UNKNOWN
|
|
154
|
+
&& Array.isArray(tool.capabilities)
|
|
155
|
+
&& tool.capabilities.includes(capability);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function supportsPathA(inventory) {
|
|
159
|
+
const workflow = inventory.tools.find(({ name }) => name === 'Workflow');
|
|
160
|
+
return PATH_A_CAPABILITIES.every((capability) => proves(workflow, capability));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function supportsPathB(inventory) {
|
|
164
|
+
if (inventory.effectiveConcurrency === UNKNOWN || inventory.effectiveConcurrency < 2) return false;
|
|
165
|
+
if (inventory.threadCapacity === UNKNOWN || inventory.threadCapacity < 2) return false;
|
|
166
|
+
return PATH_B_TOOLS.every(([name, capability]) =>
|
|
167
|
+
inventory.tools.some((tool) => tool.name === name && proves(tool, capability)));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function classifyCapabilities(inventory) {
|
|
171
|
+
const normalized = adaptInventory(inventory);
|
|
172
|
+
if (supportsPathA(normalized)) return 'A';
|
|
173
|
+
if (supportsPathB(normalized)) return 'B';
|
|
174
|
+
return 'C';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function selectOrchestrationReference(inventory) {
|
|
178
|
+
return TARGETS[classifyCapabilities(inventory)];
|
|
179
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export const DISPATCH_RECEIPT_VERSION = 2;
|
|
2
|
+
|
|
3
|
+
const ROUTE_FIELDS = [
|
|
4
|
+
'providerId',
|
|
5
|
+
'modelId',
|
|
6
|
+
'effort',
|
|
7
|
+
'surfaceId',
|
|
8
|
+
'transportId',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const ENFORCEMENT_METHODS = [
|
|
12
|
+
'per-spawn',
|
|
13
|
+
'named-agent',
|
|
14
|
+
'session-default',
|
|
15
|
+
'none',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
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',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
function object(value, field) {
|
|
28
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
29
|
+
throw new TypeError(`${field} must be an object`);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function string(value, field) {
|
|
35
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
36
|
+
throw new TypeError(`${field} must be a non-empty string`);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function timestamp(value, field) {
|
|
42
|
+
string(value, field);
|
|
43
|
+
if (!Number.isFinite(Date.parse(value))) throw new TypeError(`${field} must be an ISO timestamp`);
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
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
|
+
}
|
|
52
|
+
return Object.freeze(Object.fromEntries(
|
|
53
|
+
ROUTE_FIELDS.map((key) => [key, string(input[key], `${field}.${key}`)]),
|
|
54
|
+
));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function enforcement(input) {
|
|
58
|
+
object(input, 'enforcement');
|
|
59
|
+
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(', ')}`);
|
|
63
|
+
}
|
|
64
|
+
result[field] = input[field];
|
|
65
|
+
}
|
|
66
|
+
return Object.freeze(result);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
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);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function revisions(input) {
|
|
87
|
+
object(input, 'revisions');
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
catalog: string(input.catalog, 'revisions.catalog'),
|
|
90
|
+
accessGraph: string(input.accessGraph, 'revisions.accessGraph'),
|
|
91
|
+
policy: string(input.policy, 'revisions.policy'),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
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');
|
|
104
|
+
}
|
|
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');
|
|
109
|
+
}
|
|
110
|
+
return Object.freeze({
|
|
111
|
+
schemaVersion: DISPATCH_RECEIPT_VERSION,
|
|
112
|
+
executionId: string(input.executionId, 'executionId'),
|
|
113
|
+
status: 'blocked',
|
|
114
|
+
afk: input.afk === true,
|
|
115
|
+
requestedRoute,
|
|
116
|
+
appliedRoute,
|
|
117
|
+
enforcement: appliedEnforcement,
|
|
118
|
+
precedence: appliedPrecedence,
|
|
119
|
+
revisions: revisions(input.revisions),
|
|
120
|
+
dispatchedAt: timestamp(input.dispatchedAt, 'dispatchedAt'),
|
|
121
|
+
reason: string(input.reason, 'reason'),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
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');
|
|
130
|
+
}
|
|
131
|
+
const requestedRoute = route(input.requestedRoute, 'requestedRoute');
|
|
132
|
+
const appliedRoute = route(input.appliedRoute, 'appliedRoute');
|
|
133
|
+
for (const field of ROUTE_FIELDS) {
|
|
134
|
+
if (requestedRoute[field] !== appliedRoute[field]) {
|
|
135
|
+
throw new Error(`applied route differs from requested route: ${field}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const appliedEnforcement = enforcement(input.enforcement);
|
|
139
|
+
const appliedPrecedence = precedence(input.precedence);
|
|
140
|
+
if (input.afk === true
|
|
141
|
+
&& (appliedEnforcement.model === 'none' || appliedEnforcement.effort === 'none')) {
|
|
142
|
+
throw new Error('AFK dispatch requires enforced model and effort selection');
|
|
143
|
+
}
|
|
144
|
+
if (input.afk === true
|
|
145
|
+
&& Object.values(appliedPrecedence).some((value) =>
|
|
146
|
+
value === 'uncontrolled' || value === 'unreported')) {
|
|
147
|
+
throw new Error('AFK dispatch requires verified environment precedence');
|
|
148
|
+
}
|
|
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,
|
|
155
|
+
appliedRoute,
|
|
156
|
+
enforcement: appliedEnforcement,
|
|
157
|
+
precedence: appliedPrecedence,
|
|
158
|
+
revisions: revisions(input.revisions),
|
|
159
|
+
dispatchedAt: timestamp(input.dispatchedAt, 'dispatchedAt'),
|
|
160
|
+
reason: null,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
evidenceWorkloadIdentity,
|
|
3
|
+
validateEvidenceSelection,
|
|
4
|
+
} from './routingIntent.mjs';
|
|
5
|
+
|
|
6
|
+
const FRONTEND_DOMAINS = new Set([
|
|
7
|
+
'general',
|
|
8
|
+
'reference-design',
|
|
9
|
+
'marketing',
|
|
10
|
+
'analytics',
|
|
11
|
+
'product',
|
|
12
|
+
'game',
|
|
13
|
+
'simulation',
|
|
14
|
+
'editor',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const QUALITY_AXES = new Set([
|
|
18
|
+
'visual-preference',
|
|
19
|
+
'visual-fidelity',
|
|
20
|
+
'functional',
|
|
21
|
+
'accessibility',
|
|
22
|
+
'responsive',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
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,
|
|
30
|
+
}),
|
|
31
|
+
'openhands-frontend': Object.freeze({
|
|
32
|
+
workloads: Object.freeze(['frontend-repository-repair']),
|
|
33
|
+
axes: Object.freeze(['functional']),
|
|
34
|
+
decisive: true,
|
|
35
|
+
}),
|
|
36
|
+
vision2web: Object.freeze({
|
|
37
|
+
workloads: Object.freeze(['frontend-greenfield']),
|
|
38
|
+
axes: Object.freeze(['visual-fidelity', 'functional', 'responsive']),
|
|
39
|
+
decisive: false,
|
|
40
|
+
}),
|
|
41
|
+
design2code: Object.freeze({
|
|
42
|
+
workloads: Object.freeze(['frontend-greenfield']),
|
|
43
|
+
axes: Object.freeze(['visual-fidelity']),
|
|
44
|
+
decisive: false,
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function nonEmptyString(value, field) {
|
|
49
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
50
|
+
throw new TypeError(`${field} must be a non-empty string`);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function plainObject(value, field) {
|
|
56
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
57
|
+
throw new TypeError(`${field} must be an object`);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function classifyFrontendWorkload(input) {
|
|
63
|
+
plainObject(input, 'frontend workload');
|
|
64
|
+
const lifecycle = nonEmptyString(input.lifecycle, 'frontend workload lifecycle');
|
|
65
|
+
const repositoryContext = nonEmptyString(
|
|
66
|
+
input.repositoryContext,
|
|
67
|
+
'frontend workload repositoryContext',
|
|
68
|
+
);
|
|
69
|
+
if (!Array.isArray(input.qualityAxes) || input.qualityAxes.length === 0) {
|
|
70
|
+
throw new TypeError('frontend workload qualityAxes must be a non-empty array');
|
|
71
|
+
}
|
|
72
|
+
if (new Set(input.qualityAxes).size !== input.qualityAxes.length) {
|
|
73
|
+
throw new TypeError('frontend workload qualityAxes must be unique');
|
|
74
|
+
}
|
|
75
|
+
for (const axis of input.qualityAxes) {
|
|
76
|
+
if (!QUALITY_AXES.has(axis)) throw new TypeError(`unknown frontend quality axis: ${axis}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let workload;
|
|
80
|
+
let supportedAxes;
|
|
81
|
+
if (lifecycle === 'greenfield' && repositoryContext === 'isolated') {
|
|
82
|
+
workload = 'frontend-greenfield';
|
|
83
|
+
supportedAxes = FRONTEND_SOURCE_CLAIMS['code-arena-webdev'].axes;
|
|
84
|
+
} else if (
|
|
85
|
+
['edit', 'repair'].includes(lifecycle)
|
|
86
|
+
&& repositoryContext === 'existing-repository'
|
|
87
|
+
) {
|
|
88
|
+
workload = 'frontend-repository-repair';
|
|
89
|
+
supportedAxes = FRONTEND_SOURCE_CLAIMS['openhands-frontend'].axes;
|
|
90
|
+
} else {
|
|
91
|
+
throw new TypeError(
|
|
92
|
+
`unsupported frontend workload mapping: ${lifecycle}/${repositoryContext}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const frontendDomain = input.frontendDomain ?? 'general';
|
|
97
|
+
if (!FRONTEND_DOMAINS.has(frontendDomain)) {
|
|
98
|
+
throw new TypeError(`unknown frontend domain: ${frontendDomain}`);
|
|
99
|
+
}
|
|
100
|
+
const evidenceAxes = input.qualityAxes.filter((axis) => supportedAxes.includes(axis));
|
|
101
|
+
const unsupportedAxes = input.qualityAxes.filter((axis) => !supportedAxes.includes(axis));
|
|
102
|
+
return Object.freeze({
|
|
103
|
+
evidenceSelection: validateEvidenceSelection({
|
|
104
|
+
workload,
|
|
105
|
+
domain: frontendDomain,
|
|
106
|
+
axes: evidenceAxes,
|
|
107
|
+
}),
|
|
108
|
+
repositoryContext,
|
|
109
|
+
unsupportedAxes: Object.freeze(unsupportedAxes),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function frontendEvidenceWorkload({ workload, frontendDomain = 'general', axis }) {
|
|
114
|
+
if (!['frontend-greenfield', 'frontend-repository-repair'].includes(workload)) {
|
|
115
|
+
throw new TypeError(`unknown frontend evidence workload: ${workload}`);
|
|
116
|
+
}
|
|
117
|
+
if (!FRONTEND_DOMAINS.has(frontendDomain)) {
|
|
118
|
+
throw new TypeError(`unknown frontend domain: ${frontendDomain}`);
|
|
119
|
+
}
|
|
120
|
+
if (!QUALITY_AXES.has(axis)) throw new TypeError(`unknown frontend quality axis: ${axis}`);
|
|
121
|
+
return evidenceWorkloadIdentity({
|
|
122
|
+
workload,
|
|
123
|
+
domain: frontendDomain,
|
|
124
|
+
axes: [axis],
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function createFrontendRouteReason(observation) {
|
|
129
|
+
plainObject(observation, 'frontend observation');
|
|
130
|
+
const [workload, frontendDomain, axis, ...rest] =
|
|
131
|
+
nonEmptyString(observation.workload, 'frontend observation workload').split(':');
|
|
132
|
+
if (rest.length > 0 || !workload || !frontendDomain || !axis) {
|
|
133
|
+
throw new TypeError('frontend observation workload must name workload, domain, and axis');
|
|
134
|
+
}
|
|
135
|
+
const sourceId = nonEmptyString(
|
|
136
|
+
observation.source?.benchmark,
|
|
137
|
+
'frontend observation source benchmark',
|
|
138
|
+
);
|
|
139
|
+
return `${workload} (${frontendDomain}) is supported on ${axis} by ${sourceId}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function evaluateVision2WebReadiness(input) {
|
|
143
|
+
plainObject(input, 'Vision2Web leaderboard');
|
|
144
|
+
const season = nonEmptyString(input.season, 'Vision2Web season');
|
|
145
|
+
const benchmarkVersion = nonEmptyString(
|
|
146
|
+
input.benchmarkVersion,
|
|
147
|
+
'Vision2Web benchmarkVersion',
|
|
148
|
+
);
|
|
149
|
+
if (!Array.isArray(input.results)) {
|
|
150
|
+
throw new TypeError('Vision2Web results must be an array');
|
|
151
|
+
}
|
|
152
|
+
if (input.results.length === 0) {
|
|
153
|
+
return Object.freeze({
|
|
154
|
+
sourceId: 'vision2web',
|
|
155
|
+
status: 'candidate',
|
|
156
|
+
season,
|
|
157
|
+
benchmarkVersion,
|
|
158
|
+
observations: Object.freeze([]),
|
|
159
|
+
reason: 'current-season-leaderboard-empty',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return Object.freeze({
|
|
163
|
+
sourceId: 'vision2web',
|
|
164
|
+
status: 'awaiting-owner-adapter-validation',
|
|
165
|
+
season,
|
|
166
|
+
benchmarkVersion,
|
|
167
|
+
observations: Object.freeze([]),
|
|
168
|
+
reason: 'results-require-owner-adapter',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { validateReconReport } from './reportValidator.mjs';
|
|
2
|
+
|
|
3
|
+
const byPath = (left, right) => left.path.localeCompare(right.path);
|
|
4
|
+
const bySlice = (left, right) => left.localeCompare(right, 'en', { numeric: true });
|
|
5
|
+
const edgeKey = ({ from, to }) => `${from}:${to}`;
|
|
6
|
+
|
|
7
|
+
function validateReports(reports) {
|
|
8
|
+
if (!Array.isArray(reports)) throw new TypeError('recon reports must be an array');
|
|
9
|
+
const sliceIds = new Set();
|
|
10
|
+
for (const report of reports) {
|
|
11
|
+
const validation = validateReconReport(report);
|
|
12
|
+
if (!validation.ok) throw new Error(`invalid recon report: ${validation.errors.join('; ')}`);
|
|
13
|
+
if (sliceIds.has(report.sliceId)) throw new Error(`duplicate recon report for slice ${report.sliceId}`);
|
|
14
|
+
sliceIds.add(report.sliceId);
|
|
15
|
+
}
|
|
16
|
+
return sliceIds;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildGraph(reports, sliceIds) {
|
|
20
|
+
const edges = new Map();
|
|
21
|
+
const successors = new Map([...sliceIds].map((id) => [id, new Set()]));
|
|
22
|
+
const indegree = new Map([...sliceIds].map((id) => [id, 0]));
|
|
23
|
+
for (const { dependencyEdges } of reports) {
|
|
24
|
+
for (const edge of dependencyEdges) {
|
|
25
|
+
if (!sliceIds.has(edge.from) || !sliceIds.has(edge.to)) {
|
|
26
|
+
const unknown = !sliceIds.has(edge.from) ? edge.from : edge.to;
|
|
27
|
+
throw new Error(`dependency edge references unknown slice ${unknown}`);
|
|
28
|
+
}
|
|
29
|
+
if (edge.from === edge.to) throw new Error(`self dependency is not allowed for slice ${edge.from}`);
|
|
30
|
+
if (edges.has(edgeKey(edge))) continue;
|
|
31
|
+
edges.set(edgeKey(edge), edge);
|
|
32
|
+
successors.get(edge.from).add(edge.to);
|
|
33
|
+
indegree.set(edge.to, indegree.get(edge.to) + 1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const ready = [...sliceIds].filter((id) => indegree.get(id) === 0).sort(bySlice);
|
|
37
|
+
const order = [];
|
|
38
|
+
while (ready.length > 0) {
|
|
39
|
+
const current = ready.shift();
|
|
40
|
+
order.push(current);
|
|
41
|
+
for (const next of [...successors.get(current)].sort(bySlice)) {
|
|
42
|
+
indegree.set(next, indegree.get(next) - 1);
|
|
43
|
+
if (indegree.get(next) === 0) ready.push(next);
|
|
44
|
+
}
|
|
45
|
+
ready.sort(bySlice);
|
|
46
|
+
}
|
|
47
|
+
if (order.length !== sliceIds.size) throw new Error('dependency graph contains a cycle');
|
|
48
|
+
return { edges: [...edges.values()].sort((a, b) => edgeKey(a).localeCompare(edgeKey(b))), successors, order };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function collectFiles(reports) {
|
|
52
|
+
const files = new Map();
|
|
53
|
+
for (const { sliceId, plannedFiles } of reports) {
|
|
54
|
+
for (const { path, role } of plannedFiles) {
|
|
55
|
+
const file = files.get(path) ?? { editors: new Set(), shared: false };
|
|
56
|
+
if (role === 'edit') file.editors.add(sliceId);
|
|
57
|
+
if (role === 'sharedMutable') file.shared = true;
|
|
58
|
+
files.set(path, file);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return files;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function canReach(from, to, successors) {
|
|
65
|
+
const pending = [...successors.get(from)];
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
while (pending.length > 0) {
|
|
68
|
+
const current = pending.pop();
|
|
69
|
+
if (current === to) return true;
|
|
70
|
+
if (seen.has(current)) continue;
|
|
71
|
+
seen.add(current);
|
|
72
|
+
pending.push(...successors.get(current));
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function orderedEditors(path, file, graph) {
|
|
78
|
+
const editors = [...file.editors];
|
|
79
|
+
if (file.shared && editors.length !== 1) {
|
|
80
|
+
const found = editors.sort(bySlice);
|
|
81
|
+
throw new Error(`${path} must have exactly one edit owner; found ${found.length ? found.join(', ') : '0'}`);
|
|
82
|
+
}
|
|
83
|
+
const position = new Map(graph.order.map((id, index) => [id, index]));
|
|
84
|
+
editors.sort((left, right) => position.get(left) - position.get(right));
|
|
85
|
+
for (let left = 0; left < editors.length; left += 1) {
|
|
86
|
+
for (let right = left + 1; right < editors.length; right += 1) {
|
|
87
|
+
if (!canReach(editors[left], editors[right], graph.successors)) {
|
|
88
|
+
throw new Error(`${path} edit owners are not totally ordered by dependencies`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return editors;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Reconcile schema-valid per-slice recon reports before any builder dispatch. */
|
|
96
|
+
export function reconcileReconReports(reports) {
|
|
97
|
+
const sliceIds = validateReports(reports);
|
|
98
|
+
const graph = buildGraph(reports, sliceIds);
|
|
99
|
+
const editOwners = [];
|
|
100
|
+
const overlaps = [];
|
|
101
|
+
for (const [path, file] of collectFiles(reports)) {
|
|
102
|
+
const sliceIdsForPath = orderedEditors(path, file, graph);
|
|
103
|
+
if (sliceIdsForPath.length > 0) editOwners.push({ path, sliceIds: sliceIdsForPath });
|
|
104
|
+
if (sliceIdsForPath.length > 1) overlaps.push({ path, editors: sliceIdsForPath });
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
editOwners: editOwners.sort(byPath),
|
|
108
|
+
overlaps: overlaps.sort(byPath),
|
|
109
|
+
dependencyEdges: graph.edges,
|
|
110
|
+
};
|
|
111
|
+
}
|