aios-core 4.2.5 → 4.2.7
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/.aios-core/core/code-intel/code-intel-client.js +280 -0
- package/.aios-core/core/code-intel/code-intel-enricher.js +159 -0
- package/.aios-core/core/code-intel/index.js +137 -0
- package/.aios-core/core/code-intel/providers/code-graph-provider.js +201 -0
- package/.aios-core/core/code-intel/providers/provider-interface.js +108 -0
- package/.aios-core/core/orchestration/context-manager.js +333 -5
- package/.aios-core/core/orchestration/dashboard-integration.js +17 -1
- package/.aios-core/core/orchestration/execution-profile-resolver.js +107 -0
- package/.aios-core/core/orchestration/index.js +3 -0
- package/.aios-core/core/orchestration/skill-dispatcher.js +2 -0
- package/.aios-core/core/orchestration/subagent-prompt-builder.js +2 -0
- package/.aios-core/core/orchestration/workflow-orchestrator.js +113 -5
- package/.aios-core/data/entity-registry.yaml +44 -22
- package/.aios-core/development/agents/ux-design-expert.md +1 -1
- package/.aios-core/development/checklists/brownfield-compatibility-checklist.md +114 -0
- package/.aios-core/development/scripts/workflow-state-manager.js +128 -1
- package/.aios-core/development/tasks/next.md +36 -5
- package/.aios-core/hooks/ids-post-commit.js +29 -1
- package/.aios-core/hooks/ids-pre-push.js +29 -1
- package/.aios-core/infrastructure/contracts/compatibility/aios-4.0.4.yaml +44 -0
- package/.aios-core/infrastructure/scripts/validate-parity.js +238 -2
- package/.aios-core/install-manifest.yaml +63 -27
- package/.aios-core/product/templates/brownfield-risk-report-tmpl.yaml +277 -0
- package/.aios-core/workflow-intelligence/engine/suggestion-engine.js +114 -5
- package/LICENSE +13 -1
- package/README.md +39 -9
- package/package.json +8 -6
- package/packages/installer/src/wizard/ide-config-generator.js +0 -117
- package/packages/installer/src/wizard/index.js +2 -118
- package/packages/installer/src/wizard/pro-setup.js +58 -12
- package/pro/license/license-api.js +9 -9
- package/scripts/semantic-lint.js +190 -0
|
@@ -17,13 +17,15 @@
|
|
|
17
17
|
* - Each session picks up where the last left off via state file
|
|
18
18
|
*
|
|
19
19
|
* @module workflow-state-manager
|
|
20
|
-
* @version
|
|
20
|
+
* @version 2.0.0
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
const fs = require('fs').promises;
|
|
24
24
|
const path = require('path');
|
|
25
25
|
const yaml = require('js-yaml');
|
|
26
26
|
|
|
27
|
+
const WORKFLOW_STATE_VERSION = '2.0';
|
|
28
|
+
|
|
27
29
|
class WorkflowStateManager {
|
|
28
30
|
/**
|
|
29
31
|
* @param {Object} [options={}]
|
|
@@ -64,6 +66,7 @@ class WorkflowStateManager {
|
|
|
64
66
|
const instanceId = this._generateInstanceId(wf.id);
|
|
65
67
|
|
|
66
68
|
const state = {
|
|
69
|
+
state_version: WORKFLOW_STATE_VERSION,
|
|
67
70
|
workflow_id: wf.id,
|
|
68
71
|
workflow_name: wf.name || wf.id,
|
|
69
72
|
instance_id: instanceId,
|
|
@@ -137,6 +140,130 @@ class WorkflowStateManager {
|
|
|
137
140
|
return state;
|
|
138
141
|
}
|
|
139
142
|
|
|
143
|
+
// ============ Runtime-First Next Action ============
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Build normalized execution state from runtime signals.
|
|
147
|
+
* Deterministic priority:
|
|
148
|
+
* blocked > qa_rejected > ci_red > completed > in_development > ready_for_validation > unknown
|
|
149
|
+
*
|
|
150
|
+
* @param {Object} [signals={}]
|
|
151
|
+
* @param {string} [signals.story_status]
|
|
152
|
+
* @param {string} [signals.qa_status]
|
|
153
|
+
* @param {string} [signals.ci_status]
|
|
154
|
+
* @param {boolean} [signals.has_uncommitted_changes]
|
|
155
|
+
* @returns {{ state: string, reasons: string[], normalized: Object }}
|
|
156
|
+
*/
|
|
157
|
+
evaluateExecutionState(signals = {}) {
|
|
158
|
+
const storyStatus = String(signals.story_status || 'unknown').toLowerCase();
|
|
159
|
+
const qaStatus = String(signals.qa_status || 'unknown').toLowerCase();
|
|
160
|
+
const ciStatus = String(signals.ci_status || 'unknown').toLowerCase();
|
|
161
|
+
const hasUncommitted = Boolean(signals.has_uncommitted_changes);
|
|
162
|
+
|
|
163
|
+
const reasons = [];
|
|
164
|
+
let state = 'unknown';
|
|
165
|
+
|
|
166
|
+
if (storyStatus === 'blocked') {
|
|
167
|
+
state = 'blocked';
|
|
168
|
+
reasons.push('story status is blocked');
|
|
169
|
+
} else if (qaStatus === 'rejected' || qaStatus === 'fail' || qaStatus === 'failed') {
|
|
170
|
+
state = 'qa_rejected';
|
|
171
|
+
reasons.push(`qa status is ${qaStatus}`);
|
|
172
|
+
} else if (ciStatus === 'red' || ciStatus === 'failed' || ciStatus === 'error') {
|
|
173
|
+
state = 'ci_red';
|
|
174
|
+
reasons.push(`ci status is ${ciStatus}`);
|
|
175
|
+
} else if (storyStatus === 'done' || storyStatus === 'completed') {
|
|
176
|
+
state = 'completed';
|
|
177
|
+
reasons.push(`story status is ${storyStatus}`);
|
|
178
|
+
} else if (storyStatus === 'in_progress' || storyStatus === 'review') {
|
|
179
|
+
if (hasUncommitted) {
|
|
180
|
+
state = 'in_development';
|
|
181
|
+
reasons.push('story in progress with uncommitted changes');
|
|
182
|
+
} else {
|
|
183
|
+
state = 'ready_for_validation';
|
|
184
|
+
reasons.push('story in progress with clean working tree');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
state,
|
|
190
|
+
reasons,
|
|
191
|
+
normalized: {
|
|
192
|
+
story_status: storyStatus,
|
|
193
|
+
qa_status: qaStatus,
|
|
194
|
+
ci_status: ciStatus,
|
|
195
|
+
has_uncommitted_changes: hasUncommitted,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Deterministic next-action recommendation for runtime-first flows.
|
|
202
|
+
*
|
|
203
|
+
* @param {Object} [signals={}]
|
|
204
|
+
* @param {Object} [options={}]
|
|
205
|
+
* @param {string} [options.story]
|
|
206
|
+
* @returns {{ state: string, command: string, agent: string, rationale: string, confidence: number }}
|
|
207
|
+
*/
|
|
208
|
+
getNextActionRecommendation(signals = {}, options = {}) {
|
|
209
|
+
const result = this.evaluateExecutionState(signals);
|
|
210
|
+
const storyArg = options.story ? ` ${options.story}` : '';
|
|
211
|
+
|
|
212
|
+
const map = {
|
|
213
|
+
blocked: {
|
|
214
|
+
command: `*orchestrate-status${storyArg}`,
|
|
215
|
+
agent: '@po',
|
|
216
|
+
rationale: 'Story is blocked. Surface blockers and unblock path first.',
|
|
217
|
+
confidence: 0.95,
|
|
218
|
+
},
|
|
219
|
+
qa_rejected: {
|
|
220
|
+
command: `*apply-qa-fixes${storyArg}`,
|
|
221
|
+
agent: '@dev',
|
|
222
|
+
rationale: 'QA rejected the story. Apply fixes before progressing.',
|
|
223
|
+
confidence: 0.95,
|
|
224
|
+
},
|
|
225
|
+
ci_red: {
|
|
226
|
+
command: '*run-tests',
|
|
227
|
+
agent: '@dev',
|
|
228
|
+
rationale: 'CI is red. Reproduce and fix failing tests/checks first.',
|
|
229
|
+
confidence: 0.92,
|
|
230
|
+
},
|
|
231
|
+
completed: {
|
|
232
|
+
command: `*close-story${storyArg}`,
|
|
233
|
+
agent: '@po',
|
|
234
|
+
rationale: 'Story is complete. Close lifecycle and move to next item.',
|
|
235
|
+
confidence: 0.9,
|
|
236
|
+
},
|
|
237
|
+
in_development: {
|
|
238
|
+
command: '*run-tests',
|
|
239
|
+
agent: '@dev',
|
|
240
|
+
rationale: 'Code is in progress with local changes. Validate before QA handoff.',
|
|
241
|
+
confidence: 0.85,
|
|
242
|
+
},
|
|
243
|
+
ready_for_validation: {
|
|
244
|
+
command: `*review-build${storyArg}`,
|
|
245
|
+
agent: '@qa',
|
|
246
|
+
rationale: 'Development appears stable. Proceed to QA structured review.',
|
|
247
|
+
confidence: 0.82,
|
|
248
|
+
},
|
|
249
|
+
unknown: {
|
|
250
|
+
command: `*next${storyArg}`,
|
|
251
|
+
agent: '@dev',
|
|
252
|
+
rationale: 'Context is incomplete. Request explicit workflow guidance.',
|
|
253
|
+
confidence: 0.4,
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const recommendation = map[result.state] || map.unknown;
|
|
258
|
+
return {
|
|
259
|
+
state: result.state,
|
|
260
|
+
command: recommendation.command,
|
|
261
|
+
agent: recommendation.agent,
|
|
262
|
+
rationale: recommendation.rationale,
|
|
263
|
+
confidence: recommendation.confidence,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
140
267
|
/**
|
|
141
268
|
* Load state from file
|
|
142
269
|
* @param {string} instanceId
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
Suggest next commands based on current workflow context using the Workflow Intelligence System (WIS). Helps users navigate workflows efficiently without memorizing command sequences.
|
|
6
6
|
|
|
7
|
+
AIOS 4.0.4 runtime-first mode adds deterministic next-step recommendation from
|
|
8
|
+
execution signals (story/qa/ci/diff) via `workflow-state-manager`.
|
|
9
|
+
|
|
7
10
|
## Task Definition (AIOS Task Format V1.0)
|
|
8
11
|
|
|
9
12
|
```yaml
|
|
@@ -88,7 +91,23 @@ const context = await engine.buildContext({
|
|
|
88
91
|
});
|
|
89
92
|
```
|
|
90
93
|
|
|
91
|
-
### Step 3:
|
|
94
|
+
### Step 3: Runtime-First Deterministic Recommendation (Preferred)
|
|
95
|
+
```javascript
|
|
96
|
+
const { WorkflowStateManager } = require('.aios-core/development/scripts/workflow-state-manager');
|
|
97
|
+
const manager = new WorkflowStateManager();
|
|
98
|
+
|
|
99
|
+
const runtimeNext = manager.getNextActionRecommendation(
|
|
100
|
+
{
|
|
101
|
+
story_status: context.projectState?.storyStatus || 'unknown',
|
|
102
|
+
qa_status: context.projectState?.qaStatus || 'unknown',
|
|
103
|
+
ci_status: context.projectState?.ciStatus || 'unknown',
|
|
104
|
+
has_uncommitted_changes: context.projectState?.hasUncommittedChanges || false,
|
|
105
|
+
},
|
|
106
|
+
{ story: args.story || context.storyPath || '' },
|
|
107
|
+
);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Step 4: Get WIS Suggestions (Fallback / enrichment)
|
|
92
111
|
```javascript
|
|
93
112
|
const result = await engine.suggestNext(context);
|
|
94
113
|
|
|
@@ -103,15 +122,27 @@ const result = await engine.suggestNext(context);
|
|
|
103
122
|
// }
|
|
104
123
|
```
|
|
105
124
|
|
|
106
|
-
### Step
|
|
125
|
+
### Step 5: Format Output
|
|
107
126
|
```javascript
|
|
108
127
|
const formatter = require('.aios-core/workflow-intelligence/engine/output-formatter');
|
|
109
128
|
|
|
110
|
-
|
|
111
|
-
|
|
129
|
+
const runtimeSuggestion = {
|
|
130
|
+
command: runtimeNext.command,
|
|
131
|
+
args: '',
|
|
132
|
+
description: runtimeNext.rationale,
|
|
133
|
+
confidence: runtimeNext.confidence,
|
|
134
|
+
priority: 1,
|
|
135
|
+
};
|
|
136
|
+
const mergedSuggestions = [runtimeSuggestion, ...(result.suggestions || [])];
|
|
137
|
+
const displaySuggestions = args.all ? mergedSuggestions : mergedSuggestions.slice(0, 3);
|
|
112
138
|
|
|
113
139
|
// Display formatted output
|
|
114
|
-
formatter.displaySuggestions(
|
|
140
|
+
formatter.displaySuggestions({
|
|
141
|
+
workflow: result.workflow || 'runtime_first',
|
|
142
|
+
currentState: runtimeNext.state,
|
|
143
|
+
confidence: runtimeNext.confidence,
|
|
144
|
+
suggestions: displaySuggestions,
|
|
145
|
+
});
|
|
115
146
|
```
|
|
116
147
|
|
|
117
148
|
---
|
|
@@ -15,6 +15,25 @@ const path = require('path');
|
|
|
15
15
|
|
|
16
16
|
const REPO_ROOT = path.resolve(__dirname, '../..');
|
|
17
17
|
|
|
18
|
+
function isDocsOnlyPath(relativePath) {
|
|
19
|
+
const p = String(relativePath || '').replace(/\\/g, '/');
|
|
20
|
+
if (!p) return false;
|
|
21
|
+
|
|
22
|
+
if (p.startsWith('docs/')) return true;
|
|
23
|
+
if (p === 'README.md') return true;
|
|
24
|
+
if (p === 'CHANGELOG.md') return true;
|
|
25
|
+
if (p === 'CONTRIBUTING.md') return true;
|
|
26
|
+
if (p === 'CODE_OF_CONDUCT.md') return true;
|
|
27
|
+
if (p === 'AGENTS.md') return true;
|
|
28
|
+
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isDocsOnlyCommit(changes) {
|
|
33
|
+
if (!Array.isArray(changes) || changes.length === 0) return false;
|
|
34
|
+
return changes.every((c) => isDocsOnlyPath(c.relativePath));
|
|
35
|
+
}
|
|
36
|
+
|
|
18
37
|
function getChangedFilesFromLastCommit() {
|
|
19
38
|
try {
|
|
20
39
|
const output = execSync('git diff-tree --no-commit-id --name-status -r HEAD', {
|
|
@@ -52,7 +71,11 @@ function getChangedFilesFromLastCommit() {
|
|
|
52
71
|
}
|
|
53
72
|
|
|
54
73
|
if (!filePath) continue;
|
|
55
|
-
changes.push({
|
|
74
|
+
changes.push({
|
|
75
|
+
action,
|
|
76
|
+
relativePath: filePath,
|
|
77
|
+
filePath: path.resolve(REPO_ROOT, filePath),
|
|
78
|
+
});
|
|
56
79
|
}
|
|
57
80
|
|
|
58
81
|
return changes;
|
|
@@ -67,6 +90,11 @@ async function main() {
|
|
|
67
90
|
|
|
68
91
|
if (changes.length === 0) return;
|
|
69
92
|
|
|
93
|
+
if (process.env.AIOS_IDS_FORCE !== '1' && isDocsOnlyCommit(changes)) {
|
|
94
|
+
console.log('[IDS-Hook] Docs-only commit detected, skipping registry update.');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
70
98
|
try {
|
|
71
99
|
const { RegistryUpdater } = require(path.resolve(REPO_ROOT, '.aios-core/core/ids/registry-updater.js'));
|
|
72
100
|
const updater = new RegistryUpdater();
|
|
@@ -16,6 +16,25 @@ const path = require('path');
|
|
|
16
16
|
|
|
17
17
|
const REPO_ROOT = path.resolve(__dirname, '../..');
|
|
18
18
|
|
|
19
|
+
function isDocsOnlyPath(relativePath) {
|
|
20
|
+
const p = String(relativePath || '').replace(/\\/g, '/');
|
|
21
|
+
if (!p) return false;
|
|
22
|
+
|
|
23
|
+
if (p.startsWith('docs/')) return true;
|
|
24
|
+
if (p === 'README.md') return true;
|
|
25
|
+
if (p === 'CHANGELOG.md') return true;
|
|
26
|
+
if (p === 'CONTRIBUTING.md') return true;
|
|
27
|
+
if (p === 'CODE_OF_CONDUCT.md') return true;
|
|
28
|
+
if (p === 'AGENTS.md') return true;
|
|
29
|
+
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isDocsOnlyPush(changes) {
|
|
34
|
+
if (!Array.isArray(changes) || changes.length === 0) return false;
|
|
35
|
+
return changes.every((c) => isDocsOnlyPath(c.relativePath));
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
function getChangedFilesSinceRemote() {
|
|
20
39
|
try {
|
|
21
40
|
const trackingBranch = execSync('git rev-parse --abbrev-ref --symbolic-full-name @{u}', {
|
|
@@ -58,7 +77,11 @@ function getChangedFilesSinceRemote() {
|
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
if (!filePath) continue;
|
|
61
|
-
changes.push({
|
|
80
|
+
changes.push({
|
|
81
|
+
action,
|
|
82
|
+
relativePath: filePath,
|
|
83
|
+
filePath: path.resolve(REPO_ROOT, filePath),
|
|
84
|
+
});
|
|
62
85
|
}
|
|
63
86
|
|
|
64
87
|
return changes;
|
|
@@ -76,6 +99,11 @@ async function main() {
|
|
|
76
99
|
process.exit(0);
|
|
77
100
|
}
|
|
78
101
|
|
|
102
|
+
if (process.env.AIOS_IDS_FORCE !== '1' && isDocsOnlyPush(changes)) {
|
|
103
|
+
console.log('[IDS-Hook] Docs-only push detected, skipping registry update.');
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
79
107
|
try {
|
|
80
108
|
const { RegistryUpdater } = require(path.resolve(REPO_ROOT, '.aios-core/core/ids/registry-updater.js'));
|
|
81
109
|
const updater = new RegistryUpdater();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
release: "AIOS 4.0.4"
|
|
2
|
+
updated_at: "2026-02-16"
|
|
3
|
+
source_of_truth:
|
|
4
|
+
docs_matrix: "docs/ide-integration.md"
|
|
5
|
+
validator: ".aios-core/infrastructure/scripts/validate-parity.js"
|
|
6
|
+
|
|
7
|
+
global_required_checks:
|
|
8
|
+
- paths
|
|
9
|
+
|
|
10
|
+
ide_matrix:
|
|
11
|
+
- ide: "claude-code"
|
|
12
|
+
display_name: "Claude Code"
|
|
13
|
+
expected_status: "Works"
|
|
14
|
+
required_checks:
|
|
15
|
+
- claude-sync
|
|
16
|
+
- claude-integration
|
|
17
|
+
- ide: "gemini"
|
|
18
|
+
display_name: "Gemini CLI"
|
|
19
|
+
expected_status: "Works"
|
|
20
|
+
required_checks:
|
|
21
|
+
- gemini-sync
|
|
22
|
+
- gemini-integration
|
|
23
|
+
- ide: "codex"
|
|
24
|
+
display_name: "Codex CLI"
|
|
25
|
+
expected_status: "Limited"
|
|
26
|
+
required_checks:
|
|
27
|
+
- codex-sync
|
|
28
|
+
- codex-integration
|
|
29
|
+
- codex-skills
|
|
30
|
+
- ide: "cursor"
|
|
31
|
+
display_name: "Cursor"
|
|
32
|
+
expected_status: "Limited"
|
|
33
|
+
required_checks:
|
|
34
|
+
- cursor-sync
|
|
35
|
+
- ide: "github-copilot"
|
|
36
|
+
display_name: "GitHub Copilot"
|
|
37
|
+
expected_status: "Limited"
|
|
38
|
+
required_checks:
|
|
39
|
+
- github-copilot-sync
|
|
40
|
+
- ide: "antigravity"
|
|
41
|
+
display_name: "AntiGravity"
|
|
42
|
+
expected_status: "Limited"
|
|
43
|
+
required_checks:
|
|
44
|
+
- antigravity-sync
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const fs = require('fs');
|
|
4
5
|
const path = require('path');
|
|
6
|
+
const yaml = require('js-yaml');
|
|
5
7
|
const { spawnSync } = require('child_process');
|
|
6
8
|
const { validateClaudeIntegration } = require('./validate-claude-integration');
|
|
7
9
|
const { validateCodexIntegration } = require('./validate-codex-integration');
|
|
@@ -10,10 +12,16 @@ const { validateCodexSkills } = require('./codex-skills-sync/validate');
|
|
|
10
12
|
const { validatePaths } = require('./validate-paths');
|
|
11
13
|
|
|
12
14
|
function parseArgs(argv = process.argv.slice(2)) {
|
|
13
|
-
const args = new Set(
|
|
15
|
+
const args = new Set(
|
|
16
|
+
argv.filter((arg) => !arg.startsWith('--contract=') && !arg.startsWith('--diff=')),
|
|
17
|
+
);
|
|
18
|
+
const contractArg = argv.find((arg) => arg.startsWith('--contract='));
|
|
19
|
+
const diffArg = argv.find((arg) => arg.startsWith('--diff='));
|
|
14
20
|
return {
|
|
15
21
|
quiet: args.has('--quiet') || args.has('-q'),
|
|
16
22
|
json: args.has('--json'),
|
|
23
|
+
contractPath: contractArg ? contractArg.slice('--contract='.length) : null,
|
|
24
|
+
diffPath: diffArg ? diffArg.slice('--diff='.length) : null,
|
|
17
25
|
};
|
|
18
26
|
}
|
|
19
27
|
|
|
@@ -31,6 +39,25 @@ function runSyncValidate(ide, projectRoot) {
|
|
|
31
39
|
};
|
|
32
40
|
}
|
|
33
41
|
|
|
42
|
+
function getDefaultContractPath(projectRoot = process.cwd()) {
|
|
43
|
+
return path.join(
|
|
44
|
+
projectRoot,
|
|
45
|
+
'.aios-core',
|
|
46
|
+
'infrastructure',
|
|
47
|
+
'contracts',
|
|
48
|
+
'compatibility',
|
|
49
|
+
'aios-4.0.4.yaml',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function loadCompatibilityContract(contractPath) {
|
|
54
|
+
if (!contractPath || !fs.existsSync(contractPath)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const raw = fs.readFileSync(contractPath, 'utf8');
|
|
58
|
+
return yaml.load(raw);
|
|
59
|
+
}
|
|
60
|
+
|
|
34
61
|
function normalizeResult(input) {
|
|
35
62
|
if (!input || typeof input !== 'object') {
|
|
36
63
|
return { ok: false, errors: ['Validator returned invalid result'], warnings: [] };
|
|
@@ -43,6 +70,152 @@ function normalizeResult(input) {
|
|
|
43
70
|
};
|
|
44
71
|
}
|
|
45
72
|
|
|
73
|
+
function escapeRegex(value) {
|
|
74
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validateCompatibilityContract(contract, resultById, options = {}) {
|
|
78
|
+
const violations = [];
|
|
79
|
+
|
|
80
|
+
if (!contract || typeof contract !== 'object') {
|
|
81
|
+
return ['Compatibility contract is missing or invalid'];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const matrix = Array.isArray(contract.ide_matrix) ? contract.ide_matrix : [];
|
|
85
|
+
if (matrix.length === 0) {
|
|
86
|
+
return ['Compatibility contract ide_matrix is empty'];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const docsPath = options.docsPath;
|
|
90
|
+
if (!docsPath || !fs.existsSync(docsPath)) {
|
|
91
|
+
violations.push(`Compatibility matrix document not found: ${docsPath || 'undefined'}`);
|
|
92
|
+
return violations;
|
|
93
|
+
}
|
|
94
|
+
const docsContent = fs.readFileSync(docsPath, 'utf8');
|
|
95
|
+
|
|
96
|
+
for (const ide of matrix) {
|
|
97
|
+
const ideName = ide.ide || 'unknown';
|
|
98
|
+
const displayName = ide.display_name || ideName;
|
|
99
|
+
const requiredChecks = Array.isArray(ide.required_checks) ? ide.required_checks : [];
|
|
100
|
+
const expectedStatus = ide.expected_status;
|
|
101
|
+
|
|
102
|
+
if (!expectedStatus) {
|
|
103
|
+
violations.push(`Contract missing expected_status for IDE "${ideName}"`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const rowRegex = new RegExp(
|
|
107
|
+
`\\|\\s*${escapeRegex(displayName)}\\s*\\|\\s*${escapeRegex(expectedStatus || '')}\\s*\\|`,
|
|
108
|
+
'i',
|
|
109
|
+
);
|
|
110
|
+
if (!rowRegex.test(docsContent)) {
|
|
111
|
+
violations.push(
|
|
112
|
+
`Docs matrix mismatch for "${displayName}": expected status "${expectedStatus}" in ${options.docsPathRelative}`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const checkId of requiredChecks) {
|
|
117
|
+
const checkResult = resultById[checkId];
|
|
118
|
+
if (!checkResult) {
|
|
119
|
+
violations.push(`Contract requires unknown check "${checkId}" for IDE "${ideName}"`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!checkResult.ok) {
|
|
123
|
+
violations.push(`Contract violation for "${ideName}": required check "${checkId}" failed`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const globalRequiredChecks = Array.isArray(contract.global_required_checks)
|
|
129
|
+
? contract.global_required_checks
|
|
130
|
+
: [];
|
|
131
|
+
for (const checkId of globalRequiredChecks) {
|
|
132
|
+
const checkResult = resultById[checkId];
|
|
133
|
+
if (!checkResult) {
|
|
134
|
+
violations.push(`Contract requires unknown global check "${checkId}"`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (!checkResult.ok) {
|
|
138
|
+
violations.push(`Contract violation: global required check "${checkId}" failed`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return violations;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sortUnique(values = []) {
|
|
146
|
+
return [...new Set(values)].sort();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function diffCompatibilityContracts(currentContract, previousContract) {
|
|
150
|
+
if (!currentContract || !previousContract) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const currentRelease = currentContract.release || null;
|
|
155
|
+
const previousRelease = previousContract.release || null;
|
|
156
|
+
const releaseChanged = currentRelease !== previousRelease;
|
|
157
|
+
|
|
158
|
+
const currentGlobalChecks = sortUnique(currentContract.global_required_checks || []);
|
|
159
|
+
const previousGlobalChecks = sortUnique(previousContract.global_required_checks || []);
|
|
160
|
+
const globalChecksAdded = currentGlobalChecks.filter((item) => !previousGlobalChecks.includes(item));
|
|
161
|
+
const globalChecksRemoved = previousGlobalChecks.filter((item) => !currentGlobalChecks.includes(item));
|
|
162
|
+
|
|
163
|
+
const currentByIde = Object.fromEntries((currentContract.ide_matrix || []).map((item) => [item.ide, item]));
|
|
164
|
+
const previousByIde = Object.fromEntries((previousContract.ide_matrix || []).map((item) => [item.ide, item]));
|
|
165
|
+
const ideKeys = sortUnique([...Object.keys(currentByIde), ...Object.keys(previousByIde)]);
|
|
166
|
+
|
|
167
|
+
const ideChanges = [];
|
|
168
|
+
for (const ide of ideKeys) {
|
|
169
|
+
const current = currentByIde[ide];
|
|
170
|
+
const previous = previousByIde[ide];
|
|
171
|
+
|
|
172
|
+
if (!previous && current) {
|
|
173
|
+
ideChanges.push({ ide, type: 'added', current });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (previous && !current) {
|
|
177
|
+
ideChanges.push({ ide, type: 'removed', previous });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const currentStatus = current.expected_status || null;
|
|
182
|
+
const previousStatus = previous.expected_status || null;
|
|
183
|
+
const statusChanged = currentStatus !== previousStatus;
|
|
184
|
+
const currentChecks = sortUnique(current.required_checks || []);
|
|
185
|
+
const previousChecks = sortUnique(previous.required_checks || []);
|
|
186
|
+
const checksAdded = currentChecks.filter((item) => !previousChecks.includes(item));
|
|
187
|
+
const checksRemoved = previousChecks.filter((item) => !currentChecks.includes(item));
|
|
188
|
+
|
|
189
|
+
if (statusChanged || checksAdded.length > 0 || checksRemoved.length > 0) {
|
|
190
|
+
ideChanges.push({
|
|
191
|
+
ide,
|
|
192
|
+
type: 'changed',
|
|
193
|
+
status: { previous: previousStatus, current: currentStatus },
|
|
194
|
+
required_checks: {
|
|
195
|
+
added: checksAdded,
|
|
196
|
+
removed: checksRemoved,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
from_release: previousRelease,
|
|
204
|
+
to_release: currentRelease,
|
|
205
|
+
release_changed: releaseChanged,
|
|
206
|
+
global_required_checks: {
|
|
207
|
+
added: globalChecksAdded,
|
|
208
|
+
removed: globalChecksRemoved,
|
|
209
|
+
},
|
|
210
|
+
ide_changes: ideChanges,
|
|
211
|
+
has_changes:
|
|
212
|
+
releaseChanged
|
|
213
|
+
|| globalChecksAdded.length > 0
|
|
214
|
+
|| globalChecksRemoved.length > 0
|
|
215
|
+
|| ideChanges.length > 0,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
46
219
|
function runParityValidation(options = {}, deps = {}) {
|
|
47
220
|
const projectRoot = options.projectRoot || process.cwd();
|
|
48
221
|
const runSync = deps.runSyncValidate || runSyncValidate;
|
|
@@ -51,6 +224,15 @@ function runParityValidation(options = {}, deps = {}) {
|
|
|
51
224
|
const runGeminiIntegration = deps.validateGeminiIntegration || validateGeminiIntegration;
|
|
52
225
|
const runCodexSkills = deps.validateCodexSkills || validateCodexSkills;
|
|
53
226
|
const runPaths = deps.validatePaths || validatePaths;
|
|
227
|
+
const resolvedContractPath = options.contractPath
|
|
228
|
+
? path.resolve(projectRoot, options.contractPath)
|
|
229
|
+
: getDefaultContractPath(projectRoot);
|
|
230
|
+
const loadContract = deps.loadCompatibilityContract || loadCompatibilityContract;
|
|
231
|
+
const contract = loadContract(resolvedContractPath);
|
|
232
|
+
const resolvedDiffPath = options.diffPath ? path.resolve(projectRoot, options.diffPath) : null;
|
|
233
|
+
const previousContract = resolvedDiffPath ? loadContract(resolvedDiffPath) : null;
|
|
234
|
+
const docsPath = path.join(projectRoot, 'docs', 'ide-integration.md');
|
|
235
|
+
const docsPathRelative = path.relative(projectRoot, docsPath);
|
|
54
236
|
const checks = [
|
|
55
237
|
{ id: 'claude-sync', exec: () => runSync('claude-code', projectRoot) },
|
|
56
238
|
{ id: 'claude-integration', exec: () => runClaudeIntegration({ projectRoot }) },
|
|
@@ -58,6 +240,9 @@ function runParityValidation(options = {}, deps = {}) {
|
|
|
58
240
|
{ id: 'codex-integration', exec: () => runCodexIntegration({ projectRoot }) },
|
|
59
241
|
{ id: 'gemini-sync', exec: () => runSync('gemini', projectRoot) },
|
|
60
242
|
{ id: 'gemini-integration', exec: () => runGeminiIntegration({ projectRoot }) },
|
|
243
|
+
{ id: 'cursor-sync', exec: () => runSync('cursor', projectRoot) },
|
|
244
|
+
{ id: 'github-copilot-sync', exec: () => runSync('github-copilot', projectRoot) },
|
|
245
|
+
{ id: 'antigravity-sync', exec: () => runSync('antigravity', projectRoot) },
|
|
61
246
|
{ id: 'codex-skills', exec: () => runCodexSkills({ projectRoot, strict: true, quiet: true }) },
|
|
62
247
|
{ id: 'paths', exec: () => runPaths({ projectRoot }) },
|
|
63
248
|
];
|
|
@@ -66,15 +251,60 @@ function runParityValidation(options = {}, deps = {}) {
|
|
|
66
251
|
const normalized = normalizeResult(check.exec());
|
|
67
252
|
return { id: check.id, ...normalized };
|
|
68
253
|
});
|
|
254
|
+
const resultById = Object.fromEntries(results.map((r) => [r.id, r]));
|
|
255
|
+
const contractViolations = validateCompatibilityContract(contract, resultById, {
|
|
256
|
+
docsPath,
|
|
257
|
+
docsPathRelative,
|
|
258
|
+
});
|
|
259
|
+
const contractSummary = contract
|
|
260
|
+
? {
|
|
261
|
+
release: contract.release || null,
|
|
262
|
+
path: path.relative(projectRoot, resolvedContractPath),
|
|
263
|
+
}
|
|
264
|
+
: {
|
|
265
|
+
release: null,
|
|
266
|
+
path: path.relative(projectRoot, resolvedContractPath),
|
|
267
|
+
};
|
|
69
268
|
|
|
70
269
|
return {
|
|
71
|
-
ok: results.every((r) => r.ok),
|
|
270
|
+
ok: results.every((r) => r.ok) && contractViolations.length === 0,
|
|
72
271
|
checks: results,
|
|
272
|
+
contract: contractSummary,
|
|
273
|
+
contractDiff: diffCompatibilityContracts(contract, previousContract),
|
|
274
|
+
contractViolations,
|
|
73
275
|
};
|
|
74
276
|
}
|
|
75
277
|
|
|
76
278
|
function formatHumanReport(result) {
|
|
77
279
|
const lines = [];
|
|
280
|
+
if (result.contract && result.contract.release) {
|
|
281
|
+
lines.push(`Compatibility Contract: ${result.contract.release} (${result.contract.path})`);
|
|
282
|
+
lines.push('');
|
|
283
|
+
}
|
|
284
|
+
if (result.contractDiff) {
|
|
285
|
+
lines.push(
|
|
286
|
+
`Contract Diff: ${result.contractDiff.from_release || 'unknown'} -> ${result.contractDiff.to_release || 'unknown'}`,
|
|
287
|
+
);
|
|
288
|
+
if (!result.contractDiff.has_changes) {
|
|
289
|
+
lines.push('- no changes');
|
|
290
|
+
} else {
|
|
291
|
+
if (result.contractDiff.release_changed) {
|
|
292
|
+
lines.push('- release changed');
|
|
293
|
+
}
|
|
294
|
+
const globalAdded = result.contractDiff.global_required_checks.added || [];
|
|
295
|
+
const globalRemoved = result.contractDiff.global_required_checks.removed || [];
|
|
296
|
+
if (globalAdded.length > 0) {
|
|
297
|
+
lines.push(`- global checks added: ${globalAdded.join(', ')}`);
|
|
298
|
+
}
|
|
299
|
+
if (globalRemoved.length > 0) {
|
|
300
|
+
lines.push(`- global checks removed: ${globalRemoved.join(', ')}`);
|
|
301
|
+
}
|
|
302
|
+
for (const ideChange of result.contractDiff.ide_changes || []) {
|
|
303
|
+
lines.push(`- ${ideChange.ide}: ${ideChange.type}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
lines.push('');
|
|
307
|
+
}
|
|
78
308
|
for (const check of result.checks) {
|
|
79
309
|
lines.push(`${check.ok ? '✅' : '❌'} ${check.id}`);
|
|
80
310
|
if (check.errors.length > 0) {
|
|
@@ -84,6 +314,11 @@ function formatHumanReport(result) {
|
|
|
84
314
|
lines.push(...check.warnings.map((w) => `⚠️ ${w}`));
|
|
85
315
|
}
|
|
86
316
|
}
|
|
317
|
+
if (Array.isArray(result.contractViolations) && result.contractViolations.length > 0) {
|
|
318
|
+
lines.push('');
|
|
319
|
+
lines.push('❌ Compatibility Contract Violations');
|
|
320
|
+
lines.push(...result.contractViolations.map((v) => `- ${v}`));
|
|
321
|
+
}
|
|
87
322
|
lines.push('');
|
|
88
323
|
lines.push(result.ok ? '✅ Parity validation passed' : '❌ Parity validation failed');
|
|
89
324
|
return lines.join('\n');
|
|
@@ -116,4 +351,5 @@ module.exports = {
|
|
|
116
351
|
runParityValidation,
|
|
117
352
|
normalizeResult,
|
|
118
353
|
formatHumanReport,
|
|
354
|
+
diffCompatibilityContracts,
|
|
119
355
|
};
|