@ikon85/agent-workflow-kit 0.33.0 → 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 +1 -1
- package/.agents/skills/orchestrate-wave/references/dispatch-subagents.md +23 -11
- package/.agents/skills/orchestrate-wave/references/dispatch-workflow.md +8 -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 +1 -1
- package/.claude/skills/orchestrate-wave/references/dispatch-subagents.md +23 -11
- package/.claude/skills/orchestrate-wave/references/dispatch-workflow.md +8 -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 +79 -11
- package/agent-workflow-kit.package.json +201 -41
- 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 +73 -0
- package/scripts/test_program_planning_contract.py +70 -0
- package/scripts/test_skill_portability_lint.py +54 -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 +24 -0
- package/src/lib/capabilityMatrix.mjs +85 -0
- package/src/lib/dispatchReceipt.mjs +162 -0
- package/src/lib/frontendWorkloads.mjs +170 -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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { adaptClaudeRoutingInventory } from '../capabilityMatrix.mjs';
|
|
2
|
+
|
|
3
|
+
const MODEL_SELECTORS = ['model'];
|
|
4
|
+
const EFFORT_SELECTORS = ['effort', 'reasoning_effort', 'model_reasoning_effort'];
|
|
5
|
+
|
|
6
|
+
function object(value, field) {
|
|
7
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
8
|
+
throw new TypeError(`${field} must be an object`);
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function string(value, field) {
|
|
14
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
15
|
+
throw new TypeError(`${field} must be a non-empty string`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function timestamp(value, field) {
|
|
21
|
+
string(value, field);
|
|
22
|
+
if (!Number.isFinite(Date.parse(value))) {
|
|
23
|
+
throw new TypeError(`${field} must be an ISO timestamp`);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function schemaProperties(schema) {
|
|
29
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return {};
|
|
30
|
+
if (!schema.properties || typeof schema.properties !== 'object'
|
|
31
|
+
|| Array.isArray(schema.properties)) return {};
|
|
32
|
+
return schema.properties;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function hasSelector(properties, selectors) {
|
|
36
|
+
return selectors.some((selector) =>
|
|
37
|
+
Object.prototype.hasOwnProperty.call(properties, selector));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function uncontrolled(control) {
|
|
41
|
+
return {
|
|
42
|
+
...control,
|
|
43
|
+
method: 'none',
|
|
44
|
+
enforced: false,
|
|
45
|
+
precedence: 'uncontrolled',
|
|
46
|
+
applied: undefined,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function applySpawnSchemaEvidence(path, properties) {
|
|
51
|
+
const candidate = { ...path };
|
|
52
|
+
if (path?.model?.method === 'per-spawn'
|
|
53
|
+
&& !hasSelector(properties, MODEL_SELECTORS)) {
|
|
54
|
+
candidate.model = uncontrolled(path.model);
|
|
55
|
+
}
|
|
56
|
+
if (path?.effort?.method === 'per-spawn'
|
|
57
|
+
&& !hasSelector(properties, EFFORT_SELECTORS)) {
|
|
58
|
+
candidate.effort = uncontrolled(path.effort);
|
|
59
|
+
}
|
|
60
|
+
return candidate;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function matchesRoute(path, route) {
|
|
64
|
+
return ['surfaceId', 'providerId', 'modelId', 'transportId']
|
|
65
|
+
.every((field) => path[field] === route[field]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function appliedRoute(path, requestedRoute) {
|
|
69
|
+
return Object.freeze({
|
|
70
|
+
...requestedRoute,
|
|
71
|
+
modelId: path.model.applied,
|
|
72
|
+
effort: path.effort.applied,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function mismatchReason(path, requested, applied) {
|
|
77
|
+
for (const field of ['modelId', 'effort']) {
|
|
78
|
+
if (requested[field] === applied[field]) continue;
|
|
79
|
+
const control = field === 'modelId' ? path.model : path.effort;
|
|
80
|
+
if (control.precedence === 'environment-over-agent-definition') {
|
|
81
|
+
return `environment precedence mismatch: ${field === 'modelId' ? 'model' : field}`;
|
|
82
|
+
}
|
|
83
|
+
return `applied route mismatch: ${field}`;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function adaptCodexRoutingInventory(inventory) {
|
|
89
|
+
const source = object(inventory, 'Codex host attestation');
|
|
90
|
+
timestamp(source.observedAt, 'Codex host attestation observedAt');
|
|
91
|
+
const host = object(source.host, 'Codex host attestation host');
|
|
92
|
+
string(host.id, 'Codex host attestation host.id');
|
|
93
|
+
string(host.version, 'Codex host attestation host.version');
|
|
94
|
+
const properties = schemaProperties(source.spawnSchema);
|
|
95
|
+
const paths = Array.isArray(source.paths)
|
|
96
|
+
? source.paths
|
|
97
|
+
.filter((path) => path?.surfaceId === 'codex')
|
|
98
|
+
.map((path) => applySpawnSchemaEvidence(path, properties))
|
|
99
|
+
: [];
|
|
100
|
+
return adaptClaudeRoutingInventory({
|
|
101
|
+
contractVersion: source.contractVersion,
|
|
102
|
+
paths,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function createCodexRoutingAdapter({ inventory, dispatchers = {} }) {
|
|
107
|
+
const capabilities = adaptCodexRoutingInventory(inventory);
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
async prepare(requestedRoute) {
|
|
110
|
+
const path = capabilities.paths.find((candidate) =>
|
|
111
|
+
matchesRoute(candidate, requestedRoute));
|
|
112
|
+
if (!path) throw new Error('Codex route capability is not attested');
|
|
113
|
+
if (!path.verified) throw new Error(path.verificationFailures.join('; '));
|
|
114
|
+
const invoke = dispatchers[path.transportId];
|
|
115
|
+
if (typeof invoke !== 'function') {
|
|
116
|
+
throw new Error(`transport has no approved dispatcher: ${path.transportId}`);
|
|
117
|
+
}
|
|
118
|
+
const applied = appliedRoute(path, requestedRoute);
|
|
119
|
+
const mismatch = mismatchReason(path, requestedRoute, applied);
|
|
120
|
+
const enforcement = Object.freeze({
|
|
121
|
+
model: path.model.method,
|
|
122
|
+
effort: path.effort.method,
|
|
123
|
+
});
|
|
124
|
+
return Object.freeze({
|
|
125
|
+
appliedRoute: applied,
|
|
126
|
+
enforcement,
|
|
127
|
+
precedence: Object.freeze({
|
|
128
|
+
model: path.model.precedence,
|
|
129
|
+
effort: path.effort.precedence,
|
|
130
|
+
}),
|
|
131
|
+
mismatchReason: mismatch,
|
|
132
|
+
dispatch: () => invoke(Object.freeze({ route: applied, enforcement })),
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export const EVIDENCE_CATALOG_VERSION = 1;
|
|
2
|
+
|
|
3
|
+
function object(value, field) {
|
|
4
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
5
|
+
throw new TypeError(`${field} must be an object`);
|
|
6
|
+
}
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function string(value, field) {
|
|
11
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
12
|
+
throw new TypeError(`${field} must be a non-empty string`);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function number(value, field) {
|
|
18
|
+
if (!Number.isFinite(value)) throw new TypeError(`${field} must be a finite number`);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function timestamp(value, field) {
|
|
23
|
+
string(value, field);
|
|
24
|
+
const parsed = Date.parse(value);
|
|
25
|
+
if (!Number.isFinite(parsed)) throw new TypeError(`${field} must be an ISO timestamp`);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function clone(value) {
|
|
30
|
+
return structuredClone(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function deepFreeze(value) {
|
|
34
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
35
|
+
Object.freeze(value);
|
|
36
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const identity = ({ providerId, modelId }) => `${providerId}:${modelId}`;
|
|
41
|
+
|
|
42
|
+
function validateModel(model, index) {
|
|
43
|
+
object(model, `models[${index}]`);
|
|
44
|
+
return {
|
|
45
|
+
providerId: string(model.providerId, `models[${index}].providerId`),
|
|
46
|
+
modelId: string(model.modelId, `models[${index}].modelId`),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validateObservation(entry, index, knownModels) {
|
|
51
|
+
const field = `observations[${index}]`;
|
|
52
|
+
object(entry, field);
|
|
53
|
+
const observation = {
|
|
54
|
+
id: string(entry.id, `${field}.id`),
|
|
55
|
+
providerId: string(entry.providerId, `${field}.providerId`),
|
|
56
|
+
modelId: string(entry.modelId, `${field}.modelId`),
|
|
57
|
+
effort: string(entry.effort, `${field}.effort`),
|
|
58
|
+
workload: string(entry.workload, `${field}.workload`),
|
|
59
|
+
harness: clone(object(entry.harness, `${field}.harness`)),
|
|
60
|
+
score: number(entry.score, `${field}.score`),
|
|
61
|
+
source: clone(object(entry.source, `${field}.source`)),
|
|
62
|
+
uncertainty: clone(object(entry.uncertainty, `${field}.uncertainty`)),
|
|
63
|
+
freshness: clone(object(entry.freshness, `${field}.freshness`)),
|
|
64
|
+
cost: clone(object(entry.cost, `${field}.cost`)),
|
|
65
|
+
};
|
|
66
|
+
string(observation.harness.id, `${field}.harness.id`);
|
|
67
|
+
string(observation.harness.version, `${field}.harness.version`);
|
|
68
|
+
string(observation.source.owner, `${field}.source.owner`);
|
|
69
|
+
string(observation.source.id, `${field}.source.id`);
|
|
70
|
+
string(observation.source.url, `${field}.source.url`);
|
|
71
|
+
string(observation.source.benchmark, `${field}.source.benchmark`);
|
|
72
|
+
string(observation.source.version, `${field}.source.version`);
|
|
73
|
+
string(observation.source.snapshotHash, `${field}.source.snapshotHash`);
|
|
74
|
+
string(observation.uncertainty.kind, `${field}.uncertainty.kind`);
|
|
75
|
+
number(observation.uncertainty.value, `${field}.uncertainty.value`);
|
|
76
|
+
const observedAt = timestamp(observation.freshness.observedAt, `${field}.freshness.observedAt`);
|
|
77
|
+
const expiresAt = timestamp(observation.freshness.expiresAt, `${field}.freshness.expiresAt`);
|
|
78
|
+
if (Date.parse(expiresAt) <= Date.parse(observedAt)) {
|
|
79
|
+
throw new TypeError(`${field}.freshness.expiresAt must follow observedAt`);
|
|
80
|
+
}
|
|
81
|
+
const amount = number(observation.cost.amount, `${field}.cost.amount`);
|
|
82
|
+
if (amount < 0) throw new TypeError(`${field}.cost.amount must be non-negative`);
|
|
83
|
+
string(observation.cost.currency, `${field}.cost.currency`);
|
|
84
|
+
string(observation.cost.unit, `${field}.cost.unit`);
|
|
85
|
+
if (!knownModels.has(identity(observation))) {
|
|
86
|
+
throw new TypeError(`${field} references an unknown model`);
|
|
87
|
+
}
|
|
88
|
+
return observation;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function validateEvidenceCatalog(input) {
|
|
92
|
+
object(input, 'evidence catalog');
|
|
93
|
+
if (input.schemaVersion !== EVIDENCE_CATALOG_VERSION) {
|
|
94
|
+
throw new TypeError(`evidence catalog schemaVersion must be ${EVIDENCE_CATALOG_VERSION}`);
|
|
95
|
+
}
|
|
96
|
+
const revision = string(input.revision, 'evidence catalog revision');
|
|
97
|
+
if (!Array.isArray(input.models)) throw new TypeError('evidence catalog models must be an array');
|
|
98
|
+
if (!Array.isArray(input.observations)) {
|
|
99
|
+
throw new TypeError('evidence catalog observations must be an array');
|
|
100
|
+
}
|
|
101
|
+
const models = input.models.map(validateModel);
|
|
102
|
+
const modelIds = new Set();
|
|
103
|
+
for (const model of models) {
|
|
104
|
+
const key = identity(model);
|
|
105
|
+
if (modelIds.has(key)) throw new TypeError(`duplicate evidence model: ${key}`);
|
|
106
|
+
modelIds.add(key);
|
|
107
|
+
}
|
|
108
|
+
const observations = input.observations.map((entry, index) =>
|
|
109
|
+
validateObservation(entry, index, modelIds));
|
|
110
|
+
const observationIds = new Set();
|
|
111
|
+
for (const entry of observations) {
|
|
112
|
+
if (observationIds.has(entry.id)) {
|
|
113
|
+
throw new TypeError(`duplicate evidence observation: ${entry.id}`);
|
|
114
|
+
}
|
|
115
|
+
observationIds.add(entry.id);
|
|
116
|
+
}
|
|
117
|
+
return deepFreeze({
|
|
118
|
+
schemaVersion: EVIDENCE_CATALOG_VERSION,
|
|
119
|
+
revision,
|
|
120
|
+
models,
|
|
121
|
+
observations,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { validateEvidenceCatalog } from './routingCatalog.mjs';
|
|
2
|
+
import { validateAccessGraph } from './routingAccessGraph.mjs';
|
|
3
|
+
import { validateRoutingPolicy } from './routingPolicy.mjs';
|
|
4
|
+
|
|
5
|
+
export const ROUTING_EVIDENCE_CACHE_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
function object(value, field) {
|
|
8
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
9
|
+
throw new TypeError(`${field} must be an object`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function timestamp(value, field) {
|
|
15
|
+
if (typeof value !== 'string' || !Number.isFinite(Date.parse(value))) {
|
|
16
|
+
throw new TypeError(`${field} must be an ISO timestamp`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function string(value, field) {
|
|
22
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
23
|
+
throw new TypeError(`${field} must be a non-empty string`);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function deepFreeze(value) {
|
|
29
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
30
|
+
Object.freeze(value);
|
|
31
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function validateRoutingEvidenceSourceSnapshot(input, index = 0) {
|
|
36
|
+
const field = `sources[${index}]`;
|
|
37
|
+
object(input, field);
|
|
38
|
+
const sourceId = string(input.sourceId, `${field}.sourceId`);
|
|
39
|
+
const owner = string(input.owner, `${field}.owner`);
|
|
40
|
+
const artifactUrl = string(input.artifactUrl, `${field}.artifactUrl`);
|
|
41
|
+
const snapshotHash = string(input.snapshotHash, `${field}.snapshotHash`);
|
|
42
|
+
const observedAt = timestamp(input.observedAt, `${field}.observedAt`);
|
|
43
|
+
const expiresAt = timestamp(input.expiresAt, `${field}.expiresAt`);
|
|
44
|
+
if (Date.parse(expiresAt) <= Date.parse(observedAt)) {
|
|
45
|
+
throw new TypeError(`${field}.expiresAt must follow observedAt`);
|
|
46
|
+
}
|
|
47
|
+
if (!Array.isArray(input.models)) throw new TypeError(`${field}.models must be an array`);
|
|
48
|
+
if (!Array.isArray(input.observations)) {
|
|
49
|
+
throw new TypeError(`${field}.observations must be an array`);
|
|
50
|
+
}
|
|
51
|
+
const catalog = validateEvidenceCatalog({
|
|
52
|
+
schemaVersion: 1,
|
|
53
|
+
revision: `source:${sourceId}`,
|
|
54
|
+
models: input.models,
|
|
55
|
+
observations: input.observations,
|
|
56
|
+
});
|
|
57
|
+
for (const [observationIndex, observation] of catalog.observations.entries()) {
|
|
58
|
+
const observationField = `${field}.observations[${observationIndex}].source`;
|
|
59
|
+
if (observation.source.id !== sourceId) {
|
|
60
|
+
throw new TypeError(`${observationField}.id must equal ${sourceId}`);
|
|
61
|
+
}
|
|
62
|
+
if (observation.source.snapshotHash !== snapshotHash) {
|
|
63
|
+
throw new TypeError(`${observationField}.snapshotHash must equal loaded snapshotHash`);
|
|
64
|
+
}
|
|
65
|
+
if (observation.source.owner !== owner) {
|
|
66
|
+
throw new TypeError(`${observationField}.owner must equal ${owner}`);
|
|
67
|
+
}
|
|
68
|
+
if (observation.source.url !== artifactUrl) {
|
|
69
|
+
throw new TypeError(`${observationField}.url must equal ${artifactUrl}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const rawSignals = input.signals ?? [];
|
|
73
|
+
if (!Array.isArray(rawSignals)) throw new TypeError(`${field}.signals must be an array`);
|
|
74
|
+
const signals = rawSignals.map((signal, signalIndex) => {
|
|
75
|
+
object(signal, `${field}.signals[${signalIndex}]`);
|
|
76
|
+
return structuredClone(signal);
|
|
77
|
+
});
|
|
78
|
+
return deepFreeze({
|
|
79
|
+
sourceId,
|
|
80
|
+
owner,
|
|
81
|
+
artifactUrl,
|
|
82
|
+
snapshotHash,
|
|
83
|
+
observedAt,
|
|
84
|
+
expiresAt,
|
|
85
|
+
models: structuredClone(catalog.models),
|
|
86
|
+
observations: structuredClone(catalog.observations),
|
|
87
|
+
signals,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function validateSourceSnapshots(input) {
|
|
92
|
+
if (input === undefined) return [];
|
|
93
|
+
if (!Array.isArray(input)) throw new TypeError('routing evidence cache sources must be an array');
|
|
94
|
+
const sources = input.map(validateRoutingEvidenceSourceSnapshot);
|
|
95
|
+
const sourceIds = new Set();
|
|
96
|
+
const observationIds = new Set();
|
|
97
|
+
for (const source of sources) {
|
|
98
|
+
if (sourceIds.has(source.sourceId)) {
|
|
99
|
+
throw new TypeError(`duplicate routing evidence source: ${source.sourceId}`);
|
|
100
|
+
}
|
|
101
|
+
sourceIds.add(source.sourceId);
|
|
102
|
+
for (const observation of source.observations) {
|
|
103
|
+
if (observationIds.has(observation.id)) {
|
|
104
|
+
throw new TypeError(`duplicate evidence observation across sources: ${observation.id}`);
|
|
105
|
+
}
|
|
106
|
+
observationIds.add(observation.id);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return sources;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function buildEvidenceCatalogFromSources(sources, revision) {
|
|
113
|
+
const validated = validateSourceSnapshots(sources);
|
|
114
|
+
const modelMap = new Map();
|
|
115
|
+
const observations = [];
|
|
116
|
+
const observationIds = new Set();
|
|
117
|
+
for (const source of validated) {
|
|
118
|
+
for (const model of source.models) {
|
|
119
|
+
modelMap.set(`${model.providerId}:${model.modelId}`, model);
|
|
120
|
+
}
|
|
121
|
+
for (const observation of source.observations) {
|
|
122
|
+
if (observationIds.has(observation.id)) {
|
|
123
|
+
throw new TypeError(`duplicate evidence observation across sources: ${observation.id}`);
|
|
124
|
+
}
|
|
125
|
+
observationIds.add(observation.id);
|
|
126
|
+
observations.push(observation);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return validateEvidenceCatalog({
|
|
130
|
+
schemaVersion: 1,
|
|
131
|
+
revision,
|
|
132
|
+
models: [...modelMap.values()],
|
|
133
|
+
observations,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validateCacheShape(input) {
|
|
138
|
+
object(input, 'routing evidence cache');
|
|
139
|
+
if (input.schemaVersion !== ROUTING_EVIDENCE_CACHE_VERSION) {
|
|
140
|
+
throw new TypeError(
|
|
141
|
+
`routing evidence cache schemaVersion must be ${ROUTING_EVIDENCE_CACHE_VERSION}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (!Number.isInteger(input.revision) || input.revision < 0) {
|
|
145
|
+
throw new TypeError('routing evidence cache revision must be a non-negative integer');
|
|
146
|
+
}
|
|
147
|
+
const refreshedAt = timestamp(input.refreshedAt, 'routing evidence cache refreshedAt');
|
|
148
|
+
const expiresAt = timestamp(input.expiresAt, 'routing evidence cache expiresAt');
|
|
149
|
+
if (Date.parse(expiresAt) <= Date.parse(refreshedAt)) {
|
|
150
|
+
throw new TypeError('routing evidence cache expiresAt must follow refreshedAt');
|
|
151
|
+
}
|
|
152
|
+
return deepFreeze({
|
|
153
|
+
schemaVersion: ROUTING_EVIDENCE_CACHE_VERSION,
|
|
154
|
+
revision: input.revision,
|
|
155
|
+
refreshedAt,
|
|
156
|
+
expiresAt,
|
|
157
|
+
catalog: validateEvidenceCatalog(input.catalog),
|
|
158
|
+
sources: validateSourceSnapshots(input.sources),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function routingEvidenceSourcesFromCache(input) {
|
|
163
|
+
return validateCacheShape(input).sources;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function validateRoutingEvidenceCache(input, { now } = {}) {
|
|
167
|
+
const cache = validateCacheShape(input);
|
|
168
|
+
const checkedAt = Date.parse(now);
|
|
169
|
+
if (!Number.isFinite(checkedAt)) throw new TypeError('now must be an ISO timestamp');
|
|
170
|
+
if (Date.parse(cache.expiresAt) <= checkedAt) {
|
|
171
|
+
throw new Error(`stale routing evidence cache: expired at ${cache.expiresAt}`);
|
|
172
|
+
}
|
|
173
|
+
return cache;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function commitRoutingEvidenceCache({
|
|
177
|
+
current,
|
|
178
|
+
expectedRevision,
|
|
179
|
+
nextCatalog,
|
|
180
|
+
nextSources,
|
|
181
|
+
refreshedAt,
|
|
182
|
+
expiresAt,
|
|
183
|
+
}) {
|
|
184
|
+
const cache = validateCacheShape(current);
|
|
185
|
+
if (cache.revision !== expectedRevision) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`concurrent evidence cache mutation: expected revision ${expectedRevision}, `
|
|
188
|
+
+ `found ${cache.revision}`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return validateRoutingEvidenceCache({
|
|
192
|
+
schemaVersion: ROUTING_EVIDENCE_CACHE_VERSION,
|
|
193
|
+
revision: cache.revision + 1,
|
|
194
|
+
refreshedAt,
|
|
195
|
+
expiresAt,
|
|
196
|
+
catalog: nextCatalog,
|
|
197
|
+
sources: nextSources ?? cache.sources,
|
|
198
|
+
}, { now: refreshedAt });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function captureRoutingProfileSnapshot({ accessGraph, policy }) {
|
|
202
|
+
const access = validateAccessGraph(accessGraph);
|
|
203
|
+
const routingPolicy = validateRoutingPolicy(policy);
|
|
204
|
+
return Object.freeze({
|
|
205
|
+
accessGraph: access.revision,
|
|
206
|
+
policy: routingPolicy.revision,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function assertRoutingProfileUnchanged(snapshot, { accessGraph, policy }) {
|
|
211
|
+
object(snapshot, 'routing profile snapshot');
|
|
212
|
+
const current = captureRoutingProfileSnapshot({ accessGraph, policy });
|
|
213
|
+
for (const field of ['accessGraph', 'policy']) {
|
|
214
|
+
if (snapshot[field] !== current[field]) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`concurrent routing profile mutation: ${field} changed from `
|
|
217
|
+
+ `${snapshot[field]} to ${current[field]}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export const ROUTING_INTENT_VERSION = 1;
|
|
2
|
+
|
|
3
|
+
export const ROUTING_WORKLOADS = Object.freeze([
|
|
4
|
+
'judgment',
|
|
5
|
+
'development',
|
|
6
|
+
'mechanical',
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
export const ROUTING_REASONING = Object.freeze([
|
|
10
|
+
'deep',
|
|
11
|
+
'balanced',
|
|
12
|
+
'light',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const FIELDS = new Set(['version', 'workload', 'reasoning', 'evidenceSelection']);
|
|
16
|
+
const EVIDENCE_SELECTION_FIELDS = new Set(['workload', 'domain', 'axes']);
|
|
17
|
+
|
|
18
|
+
function requireEnum(value, allowed, field) {
|
|
19
|
+
if (!allowed.includes(value)) {
|
|
20
|
+
throw new TypeError(`${field} must be one of: ${allowed.join(', ')}`);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function requireIdentitySegment(value, field) {
|
|
26
|
+
if (typeof value !== 'string' || value.trim() === '' || value.includes(':')) {
|
|
27
|
+
throw new TypeError(`${field} must be a non-empty string without colons`);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function validateEvidenceSelection(input) {
|
|
33
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
34
|
+
throw new TypeError('evidence selection must be an object');
|
|
35
|
+
}
|
|
36
|
+
for (const field of Object.keys(input)) {
|
|
37
|
+
if (!EVIDENCE_SELECTION_FIELDS.has(field)) {
|
|
38
|
+
throw new TypeError(`unknown evidence selection field: ${field}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!Array.isArray(input.axes) || input.axes.length === 0) {
|
|
42
|
+
throw new TypeError('evidence selection axes must be a non-empty array');
|
|
43
|
+
}
|
|
44
|
+
const axes = input.axes.map((axis) =>
|
|
45
|
+
requireIdentitySegment(axis, 'evidence selection axis'));
|
|
46
|
+
if (new Set(axes).size !== axes.length) {
|
|
47
|
+
throw new TypeError('evidence selection axes must be unique');
|
|
48
|
+
}
|
|
49
|
+
return Object.freeze({
|
|
50
|
+
workload: requireIdentitySegment(input.workload, 'evidence selection workload'),
|
|
51
|
+
domain: requireIdentitySegment(input.domain, 'evidence selection domain'),
|
|
52
|
+
axes: Object.freeze(axes),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function evidenceWorkloadIdentity(input) {
|
|
57
|
+
const selection = validateEvidenceSelection(input);
|
|
58
|
+
if (selection.axes.length !== 1) {
|
|
59
|
+
throw new TypeError('evidence workload identity requires exactly one axis');
|
|
60
|
+
}
|
|
61
|
+
return `${selection.workload}:${selection.domain}:${selection.axes[0]}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function evidenceSelectionMatchesObservation(input, observationWorkload) {
|
|
65
|
+
const selection = validateEvidenceSelection(input);
|
|
66
|
+
if (typeof observationWorkload !== 'string') return false;
|
|
67
|
+
const [workload, domain, axis, ...rest] = observationWorkload.split(':');
|
|
68
|
+
if (rest.length > 0 || !workload || !domain || !axis) return false;
|
|
69
|
+
return selection.workload === workload
|
|
70
|
+
&& selection.domain === domain
|
|
71
|
+
&& selection.axes.includes(axis);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function validateRoutingIntent(input) {
|
|
75
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
76
|
+
throw new TypeError('routing intent must be an object');
|
|
77
|
+
}
|
|
78
|
+
for (const field of Object.keys(input)) {
|
|
79
|
+
if (!FIELDS.has(field)) throw new TypeError(`unknown routing intent field: ${field}`);
|
|
80
|
+
}
|
|
81
|
+
if (input.version !== ROUTING_INTENT_VERSION) {
|
|
82
|
+
throw new TypeError(`routing intent version must be ${ROUTING_INTENT_VERSION}`);
|
|
83
|
+
}
|
|
84
|
+
const intent = {
|
|
85
|
+
version: ROUTING_INTENT_VERSION,
|
|
86
|
+
workload: requireEnum(input.workload, ROUTING_WORKLOADS, 'workload'),
|
|
87
|
+
reasoning: requireEnum(input.reasoning, ROUTING_REASONING, 'reasoning'),
|
|
88
|
+
};
|
|
89
|
+
if (input.evidenceSelection !== undefined) {
|
|
90
|
+
intent.evidenceSelection = validateEvidenceSelection(input.evidenceSelection);
|
|
91
|
+
}
|
|
92
|
+
return Object.freeze(intent);
|
|
93
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export const ROUTING_POLICY_VERSION = 1;
|
|
2
|
+
|
|
3
|
+
const POLICY_FIELDS = new Set([
|
|
4
|
+
'schemaVersion',
|
|
5
|
+
'revision',
|
|
6
|
+
'allowedSurfaces',
|
|
7
|
+
'allowedTransports',
|
|
8
|
+
'switching',
|
|
9
|
+
'optimization',
|
|
10
|
+
'unreachable',
|
|
11
|
+
'missingInfrastructure',
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const SWITCHING = ['automatic', 'ask', 'current-surface-only'];
|
|
15
|
+
const OPTIMIZATION = ['quality', 'balanced', 'cost'];
|
|
16
|
+
const UNREACHABLE = ['handoff', 'inherit', 'block'];
|
|
17
|
+
const MISSING_INFRASTRUCTURE = ['inherit', 'block'];
|
|
18
|
+
|
|
19
|
+
function object(value, field) {
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
21
|
+
throw new TypeError(`${field} must be an object`);
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function string(value, field) {
|
|
27
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
28
|
+
throw new TypeError(`${field} must be a non-empty string`);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function stringList(value, field) {
|
|
34
|
+
if (!Array.isArray(value) || !value.every((entry) => typeof entry === 'string' && entry !== '')) {
|
|
35
|
+
throw new TypeError(`${field} must be an array of non-empty strings`);
|
|
36
|
+
}
|
|
37
|
+
return [...new Set(value)];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function oneOf(value, values, field) {
|
|
41
|
+
if (!values.includes(value)) throw new TypeError(`${field} must be one of: ${values.join(', ')}`);
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function validateRoutingPolicy(input) {
|
|
46
|
+
object(input, 'routing policy');
|
|
47
|
+
for (const key of Object.keys(input)) {
|
|
48
|
+
if (!POLICY_FIELDS.has(key)) throw new TypeError(`unknown routing policy field: ${key}`);
|
|
49
|
+
}
|
|
50
|
+
if (input.schemaVersion !== ROUTING_POLICY_VERSION) {
|
|
51
|
+
throw new TypeError(`routing policy schemaVersion must be ${ROUTING_POLICY_VERSION}`);
|
|
52
|
+
}
|
|
53
|
+
return Object.freeze({
|
|
54
|
+
schemaVersion: ROUTING_POLICY_VERSION,
|
|
55
|
+
revision: string(input.revision, 'routing policy revision'),
|
|
56
|
+
allowedSurfaces: Object.freeze(stringList(input.allowedSurfaces, 'allowedSurfaces')),
|
|
57
|
+
allowedTransports: Object.freeze(stringList(input.allowedTransports, 'allowedTransports')),
|
|
58
|
+
switching: oneOf(input.switching, SWITCHING, 'switching'),
|
|
59
|
+
optimization: oneOf(input.optimization, OPTIMIZATION, 'optimization'),
|
|
60
|
+
unreachable: oneOf(input.unreachable, UNREACHABLE, 'unreachable'),
|
|
61
|
+
missingInfrastructure: oneOf(
|
|
62
|
+
input.missingInfrastructure,
|
|
63
|
+
MISSING_INFRASTRUCTURE,
|
|
64
|
+
'missingInfrastructure',
|
|
65
|
+
),
|
|
66
|
+
});
|
|
67
|
+
}
|