oxe-cc 1.0.0 → 1.2.1
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/.cursor/commands/oxe-ask.md +1 -1
- package/.cursor/commands/oxe-capabilities.md +1 -1
- package/.cursor/commands/oxe-checkpoint.md +1 -1
- package/.cursor/commands/oxe-compact.md +1 -1
- package/.cursor/commands/oxe-dashboard.md +1 -1
- package/.cursor/commands/oxe-debug.md +1 -1
- package/.cursor/commands/oxe-discuss.md +1 -1
- package/.cursor/commands/oxe-execute.md +2 -2
- package/.cursor/commands/oxe-forensics.md +1 -1
- package/.cursor/commands/oxe-help.md +1 -1
- package/.cursor/commands/oxe-loop.md +1 -1
- package/.cursor/commands/oxe-milestone.md +1 -1
- package/.cursor/commands/oxe-next.md +1 -1
- package/.cursor/commands/oxe-obs.md +1 -1
- package/.cursor/commands/oxe-plan-agent.md +1 -1
- package/.cursor/commands/oxe-plan.md +1 -1
- package/.cursor/commands/oxe-project.md +1 -1
- package/.cursor/commands/oxe-quick.md +1 -1
- package/.cursor/commands/oxe-research.md +1 -1
- package/.cursor/commands/oxe-retro.md +1 -1
- package/.cursor/commands/oxe-review-pr.md +1 -1
- package/.cursor/commands/oxe-route.md +1 -1
- package/.cursor/commands/oxe-scan.md +1 -1
- package/.cursor/commands/oxe-security.md +1 -1
- package/.cursor/commands/oxe-session.md +2 -2
- package/.cursor/commands/oxe-ship.md +45 -0
- package/.cursor/commands/oxe-skill.md +1 -1
- package/.cursor/commands/oxe-spec.md +1 -1
- package/.cursor/commands/oxe-ui-review.md +1 -1
- package/.cursor/commands/oxe-ui-spec.md +1 -1
- package/.cursor/commands/oxe-update.md +1 -1
- package/.cursor/commands/oxe-validate-gaps.md +1 -1
- package/.cursor/commands/oxe-verify.md +1 -1
- package/.cursor/commands/oxe-workstream.md +1 -1
- package/.cursor/commands/oxe.md +4 -4
- package/.github/copilot-instructions.md +91 -1
- package/.github/prompts/oxe-ask.prompt.md +1 -1
- package/.github/prompts/oxe-capabilities.prompt.md +1 -1
- package/.github/prompts/oxe-checkpoint.prompt.md +1 -1
- package/.github/prompts/oxe-compact.prompt.md +1 -1
- package/.github/prompts/oxe-dashboard.prompt.md +1 -1
- package/.github/prompts/oxe-debug.prompt.md +1 -1
- package/.github/prompts/oxe-discuss.prompt.md +1 -1
- package/.github/prompts/oxe-execute.prompt.md +2 -2
- package/.github/prompts/oxe-forensics.prompt.md +1 -1
- package/.github/prompts/oxe-help.prompt.md +1 -1
- package/.github/prompts/oxe-loop.prompt.md +1 -1
- package/.github/prompts/oxe-milestone.prompt.md +1 -1
- package/.github/prompts/oxe-next.prompt.md +1 -1
- package/.github/prompts/oxe-obs.prompt.md +1 -1
- package/.github/prompts/oxe-plan-agent.prompt.md +1 -1
- package/.github/prompts/oxe-plan.prompt.md +1 -1
- package/.github/prompts/oxe-project.prompt.md +1 -1
- package/.github/prompts/oxe-quick.prompt.md +1 -1
- package/.github/prompts/oxe-research.prompt.md +1 -1
- package/.github/prompts/oxe-retro.prompt.md +1 -1
- package/.github/prompts/oxe-review-pr.prompt.md +1 -1
- package/.github/prompts/oxe-route.prompt.md +1 -1
- package/.github/prompts/oxe-scan.prompt.md +1 -1
- package/.github/prompts/oxe-security.prompt.md +1 -1
- package/.github/prompts/oxe-session.prompt.md +2 -2
- package/.github/prompts/oxe-ship.prompt.md +45 -0
- package/.github/prompts/oxe-skill.prompt.md +1 -1
- package/.github/prompts/oxe-spec.prompt.md +1 -1
- package/.github/prompts/oxe-ui-review.prompt.md +1 -1
- package/.github/prompts/oxe-ui-spec.prompt.md +1 -1
- package/.github/prompts/oxe-update.prompt.md +1 -1
- package/.github/prompts/oxe-validate-gaps.prompt.md +1 -1
- package/.github/prompts/oxe-verify.prompt.md +1 -1
- package/.github/prompts/oxe-workstream.prompt.md +1 -1
- package/.github/prompts/oxe.prompt.md +3 -3
- package/AGENTS.md +43 -28
- package/CHANGELOG.md +158 -0
- package/README.md +72 -50
- package/bin/banner.txt +1 -1
- package/bin/lib/oxe-project-health.cjs +1 -1
- package/commands/oxe/ask.md +5 -1
- package/commands/oxe/checkpoint.md +1 -1
- package/commands/oxe/compact.md +1 -1
- package/commands/oxe/debug.md +1 -1
- package/commands/oxe/execute.md +2 -2
- package/commands/oxe/forensics.md +1 -1
- package/commands/oxe/loop.md +1 -1
- package/commands/oxe/milestone.md +1 -1
- package/commands/oxe/next.md +1 -1
- package/commands/oxe/obs.md +1 -1
- package/commands/oxe/oxe.md +3 -3
- package/commands/oxe/project.md +1 -1
- package/commands/oxe/research.md +1 -1
- package/commands/oxe/retro.md +1 -1
- package/commands/oxe/review-pr.md +1 -1
- package/commands/oxe/route.md +1 -1
- package/commands/oxe/scan.md +1 -1
- package/commands/oxe/security.md +1 -1
- package/commands/oxe/session.md +2 -2
- package/commands/oxe/ship.md +49 -0
- package/commands/oxe/spec.md +2 -2
- package/commands/oxe/ui-review.md +1 -1
- package/commands/oxe/ui-spec.md +1 -1
- package/commands/oxe/validate-gaps.md +1 -1
- package/commands/oxe/verify.md +2 -2
- package/commands/oxe/workstream.md +1 -1
- package/lib/runtime/audit/audit-trail.d.ts +71 -0
- package/lib/runtime/audit/audit-trail.js +154 -0
- package/lib/runtime/audit/index.d.ts +2 -0
- package/lib/runtime/audit/index.js +18 -0
- package/lib/runtime/audit/policy-pack.d.ts +15 -0
- package/lib/runtime/audit/policy-pack.js +57 -0
- package/lib/runtime/context/context-pack-builder.d.ts +15 -0
- package/lib/runtime/context/context-pack-builder.js +42 -0
- package/lib/runtime/context/context-pack-store.d.ts +38 -0
- package/lib/runtime/context/context-pack-store.js +142 -0
- package/lib/runtime/context/context-profiles.d.ts +11 -0
- package/lib/runtime/context/context-profiles.js +51 -0
- package/lib/runtime/context/index.d.ts +2 -0
- package/lib/runtime/context/index.js +2 -0
- package/lib/runtime/decision/decision-engine.d.ts +43 -0
- package/lib/runtime/decision/decision-engine.js +127 -0
- package/lib/runtime/decision/decision-memo.d.ts +53 -0
- package/lib/runtime/decision/decision-memo.js +173 -0
- package/lib/runtime/decision/index.d.ts +2 -0
- package/lib/runtime/decision/index.js +18 -0
- package/lib/runtime/delivery/index.d.ts +1 -0
- package/lib/runtime/delivery/index.js +1 -0
- package/lib/runtime/delivery/promotion-pipeline.d.ts +39 -0
- package/lib/runtime/delivery/promotion-pipeline.js +127 -0
- package/lib/runtime/index.d.ts +3 -0
- package/lib/runtime/index.js +4 -0
- package/lib/runtime/plugins/capability-matrix.d.ts +20 -0
- package/lib/runtime/plugins/capability-matrix.js +59 -0
- package/lib/runtime/plugins/index.d.ts +2 -0
- package/lib/runtime/plugins/index.js +2 -0
- package/lib/runtime/plugins/plugin-manifest.d.ts +22 -0
- package/lib/runtime/plugins/plugin-manifest.js +91 -0
- package/lib/runtime/plugins/plugin-registry.js +5 -0
- package/lib/runtime/policy/policy-engine.d.ts +28 -1
- package/lib/runtime/policy/policy-engine.js +96 -5
- package/lib/runtime/reducers/run-state-reducer.d.ts +26 -0
- package/lib/runtime/reducers/run-state-reducer.js +117 -1
- package/lib/runtime/scheduler/agent-registry.d.ts +44 -0
- package/lib/runtime/scheduler/agent-registry.js +96 -0
- package/lib/runtime/scheduler/agent-roles.d.ts +54 -0
- package/lib/runtime/scheduler/agent-roles.js +62 -0
- package/lib/runtime/scheduler/index.d.ts +3 -0
- package/lib/runtime/scheduler/index.js +3 -0
- package/lib/runtime/scheduler/multi-agent-coordinator.d.ts +2 -0
- package/lib/runtime/scheduler/multi-agent-coordinator.js +91 -4
- package/lib/runtime/scheduler/run-journal.d.ts +18 -0
- package/lib/runtime/scheduler/run-journal.js +54 -0
- package/lib/runtime/scheduler/scheduler.d.ts +11 -1
- package/lib/runtime/scheduler/scheduler.js +135 -7
- package/lib/runtime/verification/index.d.ts +1 -0
- package/lib/runtime/verification/index.js +1 -0
- package/lib/runtime/verification/verification-manifest.d.ts +58 -0
- package/lib/runtime/verification/verification-manifest.js +129 -0
- package/oxe/workflows/ask.md +4 -0
- package/oxe/workflows/checkpoint.md +14 -10
- package/oxe/workflows/debug.md +19 -15
- package/oxe/workflows/execute.md +30 -2
- package/oxe/workflows/forensics.md +13 -9
- package/oxe/workflows/help.md +97 -49
- package/oxe/workflows/loop.md +17 -13
- package/oxe/workflows/obs.md +4 -0
- package/oxe/workflows/oxe.md +64 -31
- package/oxe/workflows/project.md +6 -1
- package/oxe/workflows/references/workflow-runtime-contracts.json +23 -0
- package/oxe/workflows/research.md +32 -28
- package/oxe/workflows/retro.md +4 -0
- package/oxe/workflows/review-pr.md +15 -11
- package/oxe/workflows/scan.md +4 -0
- package/oxe/workflows/security.md +14 -10
- package/oxe/workflows/session.md +17 -1
- package/oxe/workflows/ship.md +142 -0
- package/oxe/workflows/spec.md +15 -0
- package/oxe/workflows/ui-review.md +20 -16
- package/oxe/workflows/ui-spec.md +7 -3
- package/oxe/workflows/validate-gaps.md +13 -9
- package/oxe/workflows/verify.md +42 -3
- package/package.json +1 -1
- package/packages/runtime/src/audit/audit-trail.ts +243 -0
- package/packages/runtime/src/audit/index.ts +2 -0
- package/packages/runtime/src/audit/policy-pack.ts +62 -0
- package/packages/runtime/src/context/context-pack-builder.ts +66 -0
- package/packages/runtime/src/context/context-pack-store.ts +197 -0
- package/packages/runtime/src/context/context-profiles.ts +60 -0
- package/packages/runtime/src/context/index.ts +2 -0
- package/packages/runtime/src/decision/decision-engine.ts +174 -0
- package/packages/runtime/src/decision/decision-memo.ts +211 -0
- package/packages/runtime/src/decision/index.ts +2 -0
- package/packages/runtime/src/delivery/index.ts +1 -0
- package/packages/runtime/src/delivery/promotion-pipeline.ts +180 -0
- package/packages/runtime/src/index.ts +5 -0
- package/packages/runtime/src/plugins/capability-matrix.ts +83 -0
- package/packages/runtime/src/plugins/index.ts +2 -0
- package/packages/runtime/src/plugins/plugin-manifest.ts +113 -0
- package/packages/runtime/src/plugins/plugin-registry.ts +5 -0
- package/packages/runtime/src/policy/policy-engine.ts +138 -7
- package/packages/runtime/src/reducers/run-state-reducer.ts +143 -1
- package/packages/runtime/src/scheduler/agent-registry.ts +132 -0
- package/packages/runtime/src/scheduler/agent-roles.ts +109 -0
- package/packages/runtime/src/scheduler/index.ts +3 -0
- package/packages/runtime/src/scheduler/multi-agent-coordinator.ts +106 -4
- package/packages/runtime/src/scheduler/run-journal.ts +62 -0
- package/packages/runtime/src/scheduler/scheduler.ts +168 -8
- package/packages/runtime/src/verification/index.ts +1 -0
- package/packages/runtime/src/verification/verification-manifest.ts +192 -0
- package/vscode-extension/oxe-agents-1.0.0.vsix +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import type { DecisionMemo } from './decision-memo';
|
|
5
|
+
|
|
6
|
+
export type DecisionType =
|
|
7
|
+
| 'proceed'
|
|
8
|
+
| 'retry'
|
|
9
|
+
| 'escalate_gate'
|
|
10
|
+
| 'skip'
|
|
11
|
+
| 'abort'
|
|
12
|
+
| 'promote_lesson';
|
|
13
|
+
|
|
14
|
+
export type DecisionSignal =
|
|
15
|
+
| 'policy_allowed'
|
|
16
|
+
| 'policy_denied'
|
|
17
|
+
| 'gate_pending'
|
|
18
|
+
| 'gate_approved'
|
|
19
|
+
| 'evidence_sufficient'
|
|
20
|
+
| 'evidence_missing'
|
|
21
|
+
| 'retry_budget_available'
|
|
22
|
+
| 'retry_budget_exhausted'
|
|
23
|
+
| 'lesson_match'
|
|
24
|
+
| 'risk_high';
|
|
25
|
+
|
|
26
|
+
export type SeniorityLevel = 'junior' | 'standard' | 'senior' | 'expert';
|
|
27
|
+
|
|
28
|
+
export interface DecisionRecord {
|
|
29
|
+
decision_id: string;
|
|
30
|
+
work_item_id: string | null;
|
|
31
|
+
run_id: string;
|
|
32
|
+
type: DecisionType;
|
|
33
|
+
seniority: SeniorityLevel;
|
|
34
|
+
confidence: number;
|
|
35
|
+
signals: DecisionSignal[];
|
|
36
|
+
rationale: string;
|
|
37
|
+
timestamp: string;
|
|
38
|
+
memo?: DecisionMemo;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DecisionLog {
|
|
42
|
+
run_id: string;
|
|
43
|
+
decisions: DecisionRecord[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface DecisionInput {
|
|
47
|
+
work_item_id?: string;
|
|
48
|
+
run_id: string;
|
|
49
|
+
policy_allowed: boolean;
|
|
50
|
+
gate_pending: boolean;
|
|
51
|
+
gate_approved: boolean;
|
|
52
|
+
retry_count: number;
|
|
53
|
+
max_retries: number;
|
|
54
|
+
evidence_count: number;
|
|
55
|
+
risk_level: 'none' | 'low' | 'medium' | 'high' | 'critical';
|
|
56
|
+
lesson_match: boolean;
|
|
57
|
+
memo?: DecisionMemo;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function computeSeniority(confidence: number): SeniorityLevel {
|
|
61
|
+
if (confidence >= 0.9) return 'expert';
|
|
62
|
+
if (confidence >= 0.75) return 'senior';
|
|
63
|
+
if (confidence >= 0.5) return 'standard';
|
|
64
|
+
return 'junior';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class DecisionEngine {
|
|
68
|
+
evaluate(input: DecisionInput): DecisionRecord {
|
|
69
|
+
const signals: DecisionSignal[] = [];
|
|
70
|
+
let type: DecisionType = 'proceed';
|
|
71
|
+
let confidence = 0.8;
|
|
72
|
+
let rationale = '';
|
|
73
|
+
|
|
74
|
+
if (!input.policy_allowed) {
|
|
75
|
+
signals.push('policy_denied');
|
|
76
|
+
type = 'abort';
|
|
77
|
+
confidence = 1.0;
|
|
78
|
+
rationale = 'Policy denied execution — aborting without retry.';
|
|
79
|
+
} else {
|
|
80
|
+
signals.push('policy_allowed');
|
|
81
|
+
|
|
82
|
+
if (input.gate_pending && !input.gate_approved) {
|
|
83
|
+
signals.push('gate_pending');
|
|
84
|
+
type = 'escalate_gate';
|
|
85
|
+
confidence = 0.95;
|
|
86
|
+
rationale = 'Human gate pending — escalating for approval before proceeding.';
|
|
87
|
+
} else {
|
|
88
|
+
if (input.gate_approved) signals.push('gate_approved');
|
|
89
|
+
|
|
90
|
+
if (input.risk_level === 'high' || input.risk_level === 'critical') {
|
|
91
|
+
signals.push('risk_high');
|
|
92
|
+
confidence = Math.max(0.4, confidence - 0.3);
|
|
93
|
+
rationale += 'High residual risk detected. ';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (input.retry_count >= input.max_retries) {
|
|
97
|
+
signals.push('retry_budget_exhausted');
|
|
98
|
+
type = 'abort';
|
|
99
|
+
confidence = 0.9;
|
|
100
|
+
rationale += `Retry budget exhausted (${input.retry_count}/${input.max_retries}).`;
|
|
101
|
+
} else if (input.retry_count > 0) {
|
|
102
|
+
signals.push('retry_budget_available');
|
|
103
|
+
type = 'retry';
|
|
104
|
+
confidence = 0.7;
|
|
105
|
+
rationale += `Retrying (attempt ${input.retry_count + 1}/${input.max_retries + 1}).`;
|
|
106
|
+
} else {
|
|
107
|
+
if (input.evidence_count > 0) {
|
|
108
|
+
signals.push('evidence_sufficient');
|
|
109
|
+
confidence = Math.min(1.0, confidence + 0.1);
|
|
110
|
+
} else {
|
|
111
|
+
signals.push('evidence_missing');
|
|
112
|
+
confidence = Math.max(0.3, confidence - 0.2);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (input.lesson_match) {
|
|
116
|
+
signals.push('lesson_match');
|
|
117
|
+
type = 'promote_lesson';
|
|
118
|
+
confidence = Math.min(1.0, confidence + 0.05);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!rationale) rationale = 'All signals green — proceeding with execution.';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
decision_id: `dec-${crypto.randomBytes(4).toString('hex')}`,
|
|
128
|
+
work_item_id: input.work_item_id ?? null,
|
|
129
|
+
run_id: input.run_id,
|
|
130
|
+
type,
|
|
131
|
+
seniority: computeSeniority(confidence),
|
|
132
|
+
confidence: Math.round(confidence * 100) / 100,
|
|
133
|
+
signals,
|
|
134
|
+
rationale: rationale.trim(),
|
|
135
|
+
timestamp: new Date().toISOString(),
|
|
136
|
+
...(input.memo !== undefined ? { memo: input.memo } : {}),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function appendDecision(projectRoot: string, runId: string, record: DecisionRecord): void {
|
|
142
|
+
const p = logPath(projectRoot, runId);
|
|
143
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
144
|
+
|
|
145
|
+
const log = loadDecisionLog(projectRoot, runId) ?? { run_id: runId, decisions: [] };
|
|
146
|
+
log.decisions.push(record);
|
|
147
|
+
fs.writeFileSync(p, JSON.stringify(log, null, 2), 'utf8');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function loadDecisionLog(projectRoot: string, runId: string): DecisionLog | null {
|
|
151
|
+
const p = logPath(projectRoot, runId);
|
|
152
|
+
if (!fs.existsSync(p)) return null;
|
|
153
|
+
try {
|
|
154
|
+
return JSON.parse(fs.readFileSync(p, 'utf8')) as DecisionLog;
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function queryDecisions(
|
|
161
|
+
log: DecisionLog,
|
|
162
|
+
filter: { type?: DecisionType; workItemId?: string; minConfidence?: number }
|
|
163
|
+
): DecisionRecord[] {
|
|
164
|
+
return log.decisions.filter((d) => {
|
|
165
|
+
if (filter.type && d.type !== filter.type) return false;
|
|
166
|
+
if (filter.workItemId && d.work_item_id !== filter.workItemId) return false;
|
|
167
|
+
if (filter.minConfidence !== undefined && d.confidence < filter.minConfidence) return false;
|
|
168
|
+
return true;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function logPath(projectRoot: string, runId: string): string {
|
|
173
|
+
return path.join(projectRoot, '.oxe', 'runs', runId, 'decisions.json');
|
|
174
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
|
|
5
|
+
export type ChangeStrategy =
|
|
6
|
+
| 'minimal_patch'
|
|
7
|
+
| 'isolated_refactor'
|
|
8
|
+
| 'expand_contract'
|
|
9
|
+
| 'feature_flag'
|
|
10
|
+
| 'no_op';
|
|
11
|
+
|
|
12
|
+
export interface BlastRadiusEstimate {
|
|
13
|
+
estimated_files: number;
|
|
14
|
+
subsystems: string[];
|
|
15
|
+
risk_score: number;
|
|
16
|
+
reversible: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RollbackPlan {
|
|
20
|
+
strategy: 'revert_commit' | 'restore_workspace' | 'undo_patch' | 'no_rollback';
|
|
21
|
+
steps: string[];
|
|
22
|
+
estimated_cost: 'low' | 'medium' | 'high';
|
|
23
|
+
preconditions: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DecisionMemo {
|
|
27
|
+
memo_id: string;
|
|
28
|
+
work_item_id: string;
|
|
29
|
+
run_id: string;
|
|
30
|
+
problem_summary: string;
|
|
31
|
+
chosen_strategy: ChangeStrategy;
|
|
32
|
+
alternatives_rejected: Array<{ strategy: ChangeStrategy; reason: string }>;
|
|
33
|
+
blast_radius: BlastRadiusEstimate;
|
|
34
|
+
rollback_plan: RollbackPlan;
|
|
35
|
+
min_evidence_required: string[];
|
|
36
|
+
confidence: number;
|
|
37
|
+
created_at: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── BlastRadius estimation ───────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function deriveSubsystems(mutationScope: string[]): string[] {
|
|
43
|
+
const seen = new Set<string>();
|
|
44
|
+
for (const p of mutationScope) {
|
|
45
|
+
const parts = p.replace(/\\/g, '/').split('/');
|
|
46
|
+
if (parts.length >= 2) seen.add(parts[0]);
|
|
47
|
+
else seen.add(p);
|
|
48
|
+
}
|
|
49
|
+
return [...seen];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function estimateRiskScore(mutationScope: string[], retryCount: number, riskLevel: string): number {
|
|
53
|
+
let score = 0;
|
|
54
|
+
score += Math.min(0.4, mutationScope.length * 0.05);
|
|
55
|
+
score += retryCount > 0 ? Math.min(0.2, retryCount * 0.05) : 0;
|
|
56
|
+
switch (riskLevel) {
|
|
57
|
+
case 'critical': score += 0.4; break;
|
|
58
|
+
case 'high': score += 0.3; break;
|
|
59
|
+
case 'medium': score += 0.15; break;
|
|
60
|
+
case 'low': score += 0.05; break;
|
|
61
|
+
default: break;
|
|
62
|
+
}
|
|
63
|
+
return Math.min(1, score);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function buildBlastRadius(
|
|
67
|
+
mutationScope: string[],
|
|
68
|
+
retryCount: number,
|
|
69
|
+
riskLevel: string
|
|
70
|
+
): BlastRadiusEstimate {
|
|
71
|
+
const risk_score = estimateRiskScore(mutationScope, retryCount, riskLevel);
|
|
72
|
+
return {
|
|
73
|
+
estimated_files: mutationScope.length,
|
|
74
|
+
subsystems: deriveSubsystems(mutationScope),
|
|
75
|
+
risk_score: Math.round(risk_score * 100) / 100,
|
|
76
|
+
reversible: riskLevel !== 'critical' && mutationScope.length <= 10,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── RollbackPlan ─────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export function buildRollbackPlan(
|
|
83
|
+
blastRadius: BlastRadiusEstimate,
|
|
84
|
+
retryCount: number
|
|
85
|
+
): RollbackPlan {
|
|
86
|
+
if (blastRadius.risk_score >= 0.7 || !blastRadius.reversible) {
|
|
87
|
+
return {
|
|
88
|
+
strategy: 'restore_workspace',
|
|
89
|
+
steps: ['snapshot workspace before mutation', 'restore snapshot on failure', 'verify clean state'],
|
|
90
|
+
estimated_cost: 'high',
|
|
91
|
+
preconditions: ['workspace snapshot available', 'no shared-state mutations'],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (retryCount > 1 || blastRadius.estimated_files > 5) {
|
|
95
|
+
return {
|
|
96
|
+
strategy: 'revert_commit',
|
|
97
|
+
steps: ['record commit SHA before change', 'git revert on failure'],
|
|
98
|
+
estimated_cost: 'medium',
|
|
99
|
+
preconditions: ['git repo initialized', 'changes committed atomically'],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
strategy: 'undo_patch',
|
|
104
|
+
steps: ['apply inverse patch'],
|
|
105
|
+
estimated_cost: 'low',
|
|
106
|
+
preconditions: ['original file state recorded'],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── StrategySelector ────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
export class StrategySelector {
|
|
113
|
+
select(mutationScope: string[], retryCount: number, riskLevel: string): ChangeStrategy {
|
|
114
|
+
if (riskLevel === 'critical') return 'feature_flag';
|
|
115
|
+
if (retryCount > 1) return 'minimal_patch';
|
|
116
|
+
if (mutationScope.length > 8) return 'isolated_refactor';
|
|
117
|
+
if (riskLevel === 'high') return 'feature_flag';
|
|
118
|
+
if (mutationScope.length > 3) return 'expand_contract';
|
|
119
|
+
if (mutationScope.length === 0) return 'no_op';
|
|
120
|
+
return 'minimal_patch';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
alternatives(chosen: ChangeStrategy, mutationScope: string[], riskLevel: string): Array<{ strategy: ChangeStrategy; reason: string }> {
|
|
124
|
+
const all: ChangeStrategy[] = ['minimal_patch', 'isolated_refactor', 'expand_contract', 'feature_flag', 'no_op'];
|
|
125
|
+
return all
|
|
126
|
+
.filter((s) => s !== chosen)
|
|
127
|
+
.map((s) => ({ strategy: s, reason: this.rejectionReason(s, chosen, mutationScope, riskLevel) }));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private rejectionReason(s: ChangeStrategy, chosen: ChangeStrategy, mutationScope: string[], riskLevel: string): string {
|
|
131
|
+
switch (s) {
|
|
132
|
+
case 'no_op': return 'mutation scope is non-empty; no-op would not satisfy requirements';
|
|
133
|
+
case 'feature_flag': return riskLevel !== 'critical' && riskLevel !== 'high' ? 'risk level does not warrant feature flag overhead' : `${chosen} preferred for scope size`;
|
|
134
|
+
case 'isolated_refactor': return mutationScope.length <= 8 ? 'scope is small enough for simpler strategy' : `${chosen} preferred`;
|
|
135
|
+
case 'expand_contract': return 'expand-contract requires coordinated deployment; overhead not justified here';
|
|
136
|
+
case 'minimal_patch': return `${chosen} preferred due to scope or risk level`;
|
|
137
|
+
default: return 'not applicable';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── Memo builder & persistence ──────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
export interface BuildMemoInput {
|
|
145
|
+
work_item_id: string;
|
|
146
|
+
run_id: string;
|
|
147
|
+
problem_summary: string;
|
|
148
|
+
mutation_scope: string[];
|
|
149
|
+
retry_count: number;
|
|
150
|
+
risk_level: string;
|
|
151
|
+
min_evidence_required?: string[];
|
|
152
|
+
confidence?: number;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function buildMemo(input: BuildMemoInput): DecisionMemo {
|
|
156
|
+
const selector = new StrategySelector();
|
|
157
|
+
const chosen = selector.select(input.mutation_scope, input.retry_count, input.risk_level);
|
|
158
|
+
const blast_radius = buildBlastRadius(input.mutation_scope, input.retry_count, input.risk_level);
|
|
159
|
+
const rollback_plan = buildRollbackPlan(blast_radius, input.retry_count);
|
|
160
|
+
const alternatives_rejected = selector.alternatives(chosen, input.mutation_scope, input.risk_level);
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
memo_id: `memo-${crypto.randomBytes(4).toString('hex')}`,
|
|
164
|
+
work_item_id: input.work_item_id,
|
|
165
|
+
run_id: input.run_id,
|
|
166
|
+
problem_summary: input.problem_summary,
|
|
167
|
+
chosen_strategy: chosen,
|
|
168
|
+
alternatives_rejected,
|
|
169
|
+
blast_radius,
|
|
170
|
+
rollback_plan,
|
|
171
|
+
min_evidence_required: input.min_evidence_required ?? [],
|
|
172
|
+
confidence: input.confidence ?? (1 - blast_radius.risk_score),
|
|
173
|
+
created_at: new Date().toISOString(),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function memoDir(projectRoot: string, runId: string): string {
|
|
178
|
+
return path.join(projectRoot, '.oxe', 'runs', runId, 'memos');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function saveMemo(projectRoot: string, memo: DecisionMemo): void {
|
|
182
|
+
const dir = memoDir(projectRoot, memo.run_id);
|
|
183
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
184
|
+
fs.writeFileSync(path.join(dir, `${memo.memo_id}.json`), JSON.stringify(memo, null, 2), 'utf8');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function loadMemo(projectRoot: string, runId: string, memoId: string): DecisionMemo | null {
|
|
188
|
+
const p = path.join(memoDir(projectRoot, runId), `${memoId}.json`);
|
|
189
|
+
if (!fs.existsSync(p)) return null;
|
|
190
|
+
try {
|
|
191
|
+
return JSON.parse(fs.readFileSync(p, 'utf8')) as DecisionMemo;
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function listMemos(projectRoot: string, runId: string): DecisionMemo[] {
|
|
198
|
+
const dir = memoDir(projectRoot, runId);
|
|
199
|
+
if (!fs.existsSync(dir)) return [];
|
|
200
|
+
return fs
|
|
201
|
+
.readdirSync(dir)
|
|
202
|
+
.filter((f) => f.endsWith('.json'))
|
|
203
|
+
.map((f) => {
|
|
204
|
+
try {
|
|
205
|
+
return JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')) as DecisionMemo;
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
.filter((m): m is DecisionMemo => m !== null);
|
|
211
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import type { PRManager } from './pr-manager';
|
|
4
|
+
import type { BranchManager } from './branch-manager';
|
|
5
|
+
import type { VerificationManifest, ResidualRiskLedger } from '../verification/verification-manifest';
|
|
6
|
+
import type { RunResult } from '../scheduler/scheduler';
|
|
7
|
+
|
|
8
|
+
export interface RunPRLink {
|
|
9
|
+
run_id: string;
|
|
10
|
+
branch: string;
|
|
11
|
+
pr_url: string | null;
|
|
12
|
+
pr_number: number | null;
|
|
13
|
+
status: 'pending' | 'open' | 'merged' | 'closed' | 'blocked';
|
|
14
|
+
created_at: string;
|
|
15
|
+
merged_at: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PromotionOptions {
|
|
19
|
+
baseBranch?: string;
|
|
20
|
+
draftPR?: boolean;
|
|
21
|
+
autoMerge?: boolean;
|
|
22
|
+
mergeMethod?: 'merge' | 'squash' | 'rebase';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type MergeGateVerdict = 'approved' | 'blocked' | 'needs_review';
|
|
26
|
+
|
|
27
|
+
export interface MergeGateReport {
|
|
28
|
+
verdict: MergeGateVerdict;
|
|
29
|
+
reasons: string[];
|
|
30
|
+
blocking_risks: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class MergeGateEvaluator {
|
|
34
|
+
evaluate(
|
|
35
|
+
runResult: RunResult,
|
|
36
|
+
manifest: VerificationManifest | null,
|
|
37
|
+
riskLedger: ResidualRiskLedger | null
|
|
38
|
+
): MergeGateReport {
|
|
39
|
+
const reasons: string[] = [];
|
|
40
|
+
const blockingRisks: string[] = [];
|
|
41
|
+
|
|
42
|
+
if (runResult.failed.length > 0) {
|
|
43
|
+
reasons.push(`${runResult.failed.length} task(s) failed: ${runResult.failed.join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (runResult.blocked.length > 0) {
|
|
47
|
+
reasons.push(`${runResult.blocked.length} task(s) blocked: ${runResult.blocked.join(', ')}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (manifest && !manifest.summary.all_passed) {
|
|
51
|
+
reasons.push(`Verification: ${manifest.summary.fail} check(s) failed, ${manifest.summary.error} error(s)`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (riskLedger) {
|
|
55
|
+
const critical = riskLedger.risks.filter((r) => r.severity === 'critical' || r.severity === 'high');
|
|
56
|
+
for (const risk of critical) {
|
|
57
|
+
blockingRisks.push(`[${risk.severity.toUpperCase()}] ${risk.description}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const hasBlockers = reasons.length > 0 || blockingRisks.length > 0;
|
|
62
|
+
const verdict: MergeGateVerdict = hasBlockers ? 'blocked' : 'approved';
|
|
63
|
+
|
|
64
|
+
return { verdict, reasons, blocking_risks: blockingRisks };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class PromotionPipeline {
|
|
69
|
+
constructor(
|
|
70
|
+
private readonly projectRoot: string,
|
|
71
|
+
private readonly branchManager: BranchManager,
|
|
72
|
+
private readonly prManager: PRManager,
|
|
73
|
+
private readonly gateEvaluator: MergeGateEvaluator = new MergeGateEvaluator()
|
|
74
|
+
) {}
|
|
75
|
+
|
|
76
|
+
buildPRBody(
|
|
77
|
+
runResult: RunResult,
|
|
78
|
+
manifest: VerificationManifest | null,
|
|
79
|
+
riskLedger: ResidualRiskLedger | null
|
|
80
|
+
): string {
|
|
81
|
+
const lines: string[] = [];
|
|
82
|
+
lines.push('## OXE Run Summary');
|
|
83
|
+
lines.push('');
|
|
84
|
+
lines.push(`**Run ID:** \`${runResult.run_id}\``);
|
|
85
|
+
lines.push(`**Status:** ${runResult.status}`);
|
|
86
|
+
lines.push(`**Completed:** ${runResult.completed.length} tasks`);
|
|
87
|
+
if (runResult.failed.length > 0) {
|
|
88
|
+
lines.push(`**Failed:** ${runResult.failed.join(', ')}`);
|
|
89
|
+
}
|
|
90
|
+
if (runResult.blocked.length > 0) {
|
|
91
|
+
lines.push(`**Blocked:** ${runResult.blocked.join(', ')}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (manifest) {
|
|
95
|
+
lines.push('');
|
|
96
|
+
lines.push('## Verification');
|
|
97
|
+
lines.push(`- Total: ${manifest.summary.total}`);
|
|
98
|
+
lines.push(`- Pass: ${manifest.summary.pass}`);
|
|
99
|
+
lines.push(`- Fail: ${manifest.summary.fail}`);
|
|
100
|
+
lines.push(`- Skip: ${manifest.summary.skip}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (riskLedger && riskLedger.risks.length > 0) {
|
|
104
|
+
lines.push('');
|
|
105
|
+
lines.push('## Residual Risks');
|
|
106
|
+
for (const risk of riskLedger.risks) {
|
|
107
|
+
lines.push(`- [${risk.severity.toUpperCase()}] ${risk.description}`);
|
|
108
|
+
if (risk.mitigation) lines.push(` - Mitigation: ${risk.mitigation}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
lines.push('');
|
|
113
|
+
lines.push('---');
|
|
114
|
+
lines.push('*Generated by OXE Runtime*');
|
|
115
|
+
|
|
116
|
+
return lines.join('\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async promote(
|
|
120
|
+
runResult: RunResult,
|
|
121
|
+
manifest: VerificationManifest | null,
|
|
122
|
+
riskLedger: ResidualRiskLedger | null,
|
|
123
|
+
opts: PromotionOptions = {}
|
|
124
|
+
): Promise<RunPRLink> {
|
|
125
|
+
const gateReport = this.gateEvaluator.evaluate(runResult, manifest, riskLedger);
|
|
126
|
+
const link: RunPRLink = {
|
|
127
|
+
run_id: runResult.run_id,
|
|
128
|
+
branch: this.branchManager.currentBranch(),
|
|
129
|
+
pr_url: null,
|
|
130
|
+
pr_number: null,
|
|
131
|
+
status: 'pending',
|
|
132
|
+
created_at: new Date().toISOString(),
|
|
133
|
+
merged_at: null,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (gateReport.verdict === 'blocked') {
|
|
137
|
+
link.status = 'blocked';
|
|
138
|
+
this.savePRLink(runResult.run_id, link);
|
|
139
|
+
return link;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const body = this.buildPRBody(runResult, manifest, riskLedger);
|
|
143
|
+
const title = `oxe: run ${runResult.run_id} — ${runResult.completed.length} tasks`;
|
|
144
|
+
|
|
145
|
+
const prResult = this.prManager.createDraft({
|
|
146
|
+
title,
|
|
147
|
+
body,
|
|
148
|
+
base: opts.baseBranch ?? 'main',
|
|
149
|
+
draft: opts.draftPR !== false,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!prResult.success || !prResult.url) {
|
|
153
|
+
link.status = 'blocked';
|
|
154
|
+
this.savePRLink(runResult.run_id, link);
|
|
155
|
+
return link;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
link.pr_url = prResult.url;
|
|
159
|
+
link.status = 'open';
|
|
160
|
+
this.savePRLink(runResult.run_id, link);
|
|
161
|
+
|
|
162
|
+
return link;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
savePRLink(runId: string, link: RunPRLink): void {
|
|
166
|
+
const p = path.join(this.projectRoot, '.oxe', 'runs', runId, 'pr-link.json');
|
|
167
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
168
|
+
fs.writeFileSync(p, JSON.stringify(link, null, 2), 'utf8');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
loadPRLink(runId: string): RunPRLink | null {
|
|
172
|
+
const p = path.join(this.projectRoot, '.oxe', 'runs', runId, 'pr-link.json');
|
|
173
|
+
if (!fs.existsSync(p)) return null;
|
|
174
|
+
try {
|
|
175
|
+
return JSON.parse(fs.readFileSync(p, 'utf8')) as RunPRLink;
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -21,6 +21,7 @@ export type {
|
|
|
21
21
|
AcceptanceCheckSuite,
|
|
22
22
|
CheckResult,
|
|
23
23
|
} from './verification/verification-compiler';
|
|
24
|
+
export * from './verification/verification-manifest';
|
|
24
25
|
export * from './policy/index';
|
|
25
26
|
export * from './gate/index';
|
|
26
27
|
export * from './projection/index';
|
|
@@ -30,3 +31,7 @@ export * from './plugins/index';
|
|
|
30
31
|
export * from './delivery/index';
|
|
31
32
|
export * from './context/index';
|
|
32
33
|
export * from './scheduler/multi-agent-coordinator';
|
|
34
|
+
|
|
35
|
+
// R4 Public ABI — Decision, Audit & Enterprise
|
|
36
|
+
export * from './decision/index';
|
|
37
|
+
export * from './audit/index';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { PluginRegistry } from './plugin-registry';
|
|
2
|
+
import { CURRENT_ABI_VERSION } from './plugin-manifest';
|
|
3
|
+
|
|
4
|
+
export type ApiStability = 'stable' | 'experimental' | 'deprecated';
|
|
5
|
+
|
|
6
|
+
export interface ProviderCapabilityEntry {
|
|
7
|
+
name: string;
|
|
8
|
+
provider_type: 'tool' | 'workspace' | 'verifier' | 'context';
|
|
9
|
+
stability: ApiStability;
|
|
10
|
+
since_abi_version: string;
|
|
11
|
+
deprecated_in?: string;
|
|
12
|
+
replacement?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CapabilityMatrix {
|
|
16
|
+
abi_version: string;
|
|
17
|
+
entries: ProviderCapabilityEntry[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildMatrix(registry: PluginRegistry): CapabilityMatrix {
|
|
21
|
+
const entries: ProviderCapabilityEntry[] = [];
|
|
22
|
+
|
|
23
|
+
for (const plugin of registry.list()) {
|
|
24
|
+
const providers = plugin.providers ?? [];
|
|
25
|
+
|
|
26
|
+
for (const prov of providers) {
|
|
27
|
+
let provider_type: ProviderCapabilityEntry['provider_type'];
|
|
28
|
+
if (prov.startsWith('tool:')) provider_type = 'tool';
|
|
29
|
+
else if (prov.startsWith('workspace:')) provider_type = 'workspace';
|
|
30
|
+
else if (prov.startsWith('verifier:')) provider_type = 'verifier';
|
|
31
|
+
else if (prov.startsWith('context:')) provider_type = 'context';
|
|
32
|
+
else continue;
|
|
33
|
+
|
|
34
|
+
const name = prov.slice(prov.indexOf(':') + 1);
|
|
35
|
+
entries.push({
|
|
36
|
+
name,
|
|
37
|
+
provider_type,
|
|
38
|
+
stability: 'stable',
|
|
39
|
+
since_abi_version: CURRENT_ABI_VERSION,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { abi_version: CURRENT_ABI_VERSION, entries };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getStableEntries(matrix: CapabilityMatrix): ProviderCapabilityEntry[] {
|
|
48
|
+
return matrix.entries.filter((e) => e.stability === 'stable');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getExperimentalEntries(matrix: CapabilityMatrix): ProviderCapabilityEntry[] {
|
|
52
|
+
return matrix.entries.filter((e) => e.stability === 'experimental');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getDeprecatedEntries(matrix: CapabilityMatrix): ProviderCapabilityEntry[] {
|
|
56
|
+
return matrix.entries.filter((e) => e.stability === 'deprecated');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function markDeprecated(
|
|
60
|
+
matrix: CapabilityMatrix,
|
|
61
|
+
name: string,
|
|
62
|
+
deprecatedIn: string,
|
|
63
|
+
replacement?: string
|
|
64
|
+
): CapabilityMatrix {
|
|
65
|
+
return {
|
|
66
|
+
...matrix,
|
|
67
|
+
entries: matrix.entries.map((e) =>
|
|
68
|
+
e.name === name
|
|
69
|
+
? { ...e, stability: 'deprecated', deprecated_in: deprecatedIn, replacement }
|
|
70
|
+
: e
|
|
71
|
+
),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function addEntry(
|
|
76
|
+
matrix: CapabilityMatrix,
|
|
77
|
+
entry: ProviderCapabilityEntry
|
|
78
|
+
): CapabilityMatrix {
|
|
79
|
+
if (matrix.entries.some((e) => e.name === entry.name && e.provider_type === entry.provider_type)) {
|
|
80
|
+
return matrix;
|
|
81
|
+
}
|
|
82
|
+
return { ...matrix, entries: [...matrix.entries, entry] };
|
|
83
|
+
}
|