wogiflow 2.6.4 → 2.7.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/.claude/settings.json +0 -1
- package/lib/workspace-changelog.js +182 -0
- package/lib/workspace-channel-server.js +75 -2
- package/lib/workspace-contracts.js +151 -1
- package/lib/workspace-events.js +383 -0
- package/lib/workspace-gates.js +740 -0
- package/lib/workspace-integration-tests.js +299 -0
- package/lib/workspace-intelligence.js +486 -1
- package/lib/workspace-locks.js +371 -0
- package/lib/workspace-messages.js +203 -3
- package/lib/workspace-routing.js +144 -0
- package/package.json +1 -1
- package/scripts/flow-done-gates.js +70 -0
- package/.claude/rules/_internal/README.md +0 -64
- package/.claude/rules/_internal/document-structure.md +0 -77
- package/.claude/rules/_internal/dual-repo-management.md +0 -174
- package/.claude/rules/_internal/feature-refactoring-cleanup.md +0 -87
- package/.claude/rules/_internal/github-releases.md +0 -71
- package/.claude/rules/_internal/model-management.md +0 -35
- package/.claude/rules/_internal/self-maintenance.md +0 -87
- package/.claude/rules/architecture/component-reuse.md +0 -38
- package/.claude/rules/code-style/naming-conventions.md +0 -107
- package/.claude/rules/operations/git-workflows.md +0 -92
- package/.claude/rules/operations/scratch-directory.md +0 -54
- package/.claude/rules/security/security-patterns.md +0 -176
- package/.claude/skills/figma-analyzer/knowledge/learnings.md +0 -11
- package/.workflow/specs/architecture.md.template +0 -24
- package/.workflow/specs/stack.md.template +0 -33
- package/.workflow/specs/testing.md.template +0 -36
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Workspace — Integration Test Triggers
|
|
5
|
+
*
|
|
6
|
+
* When a provider changes an endpoint, auto-generate integration test specs
|
|
7
|
+
* for consumer repos and verify consumer API calls match the new provider
|
|
8
|
+
* signature via static analysis.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
|
|
16
|
+
const { buildIntegrationMap } = require('./workspace-contracts');
|
|
17
|
+
|
|
18
|
+
// ============================================================
|
|
19
|
+
// Endpoint Change Detection
|
|
20
|
+
// ============================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Detect which endpoints changed by comparing old and new manifests.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} oldManifest — previous workspace manifest
|
|
26
|
+
* @param {Object} newManifest — current workspace manifest
|
|
27
|
+
* @returns {Array<{ repo: string, endpoint: string, change: 'added'|'removed'|'modified' }>}
|
|
28
|
+
*/
|
|
29
|
+
function detectEndpointChanges(oldManifest, newManifest) {
|
|
30
|
+
const changes = [];
|
|
31
|
+
|
|
32
|
+
for (const [name, member] of Object.entries(newManifest.members || {})) {
|
|
33
|
+
const oldMember = oldManifest?.members?.[name];
|
|
34
|
+
const newProvides = new Set(member.provides || []);
|
|
35
|
+
const oldProvides = new Set(oldMember?.provides || []);
|
|
36
|
+
|
|
37
|
+
// New endpoints
|
|
38
|
+
for (const ep of newProvides) {
|
|
39
|
+
if (!oldProvides.has(ep)) {
|
|
40
|
+
changes.push({ repo: name, endpoint: ep, change: 'added' });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Removed endpoints
|
|
45
|
+
for (const ep of oldProvides) {
|
|
46
|
+
if (!newProvides.has(ep)) {
|
|
47
|
+
changes.push({ repo: name, endpoint: ep, change: 'removed' });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return changes;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================================
|
|
56
|
+
// Consumer Impact Analysis
|
|
57
|
+
// ============================================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* For a list of changed endpoints, find which consumers are affected
|
|
61
|
+
* and what their API calls look like.
|
|
62
|
+
*
|
|
63
|
+
* @param {Array<Object>} endpointChanges — from detectEndpointChanges()
|
|
64
|
+
* @param {Object} manifest
|
|
65
|
+
* @returns {Array<Object>} consumer impacts
|
|
66
|
+
*/
|
|
67
|
+
function analyzeConsumerImpact(endpointChanges, manifest) {
|
|
68
|
+
const integrationMap = buildIntegrationMap(manifest);
|
|
69
|
+
const impacts = [];
|
|
70
|
+
|
|
71
|
+
for (const change of endpointChanges) {
|
|
72
|
+
// Find consumers of this endpoint
|
|
73
|
+
const match = integrationMap.matched?.find(m =>
|
|
74
|
+
m.endpoint === change.endpoint ||
|
|
75
|
+
m.providers?.includes(change.repo)
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (match) {
|
|
79
|
+
for (const consumer of match.consumers || []) {
|
|
80
|
+
impacts.push({
|
|
81
|
+
consumer,
|
|
82
|
+
provider: change.repo,
|
|
83
|
+
endpoint: change.endpoint,
|
|
84
|
+
change: change.change,
|
|
85
|
+
matchScore: match.matchScore,
|
|
86
|
+
severity: change.change === 'removed' ? 'critical' : 'high'
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return impacts;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================================
|
|
96
|
+
// Integration Test Spec Generation
|
|
97
|
+
// ============================================================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generate an integration test specification for a consumer repo
|
|
101
|
+
* to verify its usage of a changed endpoint.
|
|
102
|
+
*
|
|
103
|
+
* @param {Object} impact — single impact from analyzeConsumerImpact()
|
|
104
|
+
* @param {Object} [providerContract] — OpenAPI spec if available
|
|
105
|
+
* @returns {Object} test spec
|
|
106
|
+
*/
|
|
107
|
+
function generateIntegrationTestSpec(impact, providerContract) {
|
|
108
|
+
const spec = {
|
|
109
|
+
consumer: impact.consumer,
|
|
110
|
+
provider: impact.provider,
|
|
111
|
+
endpoint: impact.endpoint,
|
|
112
|
+
changeType: impact.change,
|
|
113
|
+
severity: impact.severity,
|
|
114
|
+
generatedAt: new Date().toISOString(),
|
|
115
|
+
testCases: []
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Parse endpoint for method and path
|
|
119
|
+
const parts = impact.endpoint.split(' ');
|
|
120
|
+
const method = parts[0] || 'GET';
|
|
121
|
+
const routePath = parts.slice(1).join(' ') || '/unknown';
|
|
122
|
+
|
|
123
|
+
// Basic connectivity test
|
|
124
|
+
spec.testCases.push({
|
|
125
|
+
name: `${method} ${routePath} — endpoint exists`,
|
|
126
|
+
type: 'connectivity',
|
|
127
|
+
description: `Verify that ${impact.provider} still serves ${method} ${routePath}`,
|
|
128
|
+
method,
|
|
129
|
+
path: routePath,
|
|
130
|
+
expectedStatus: impact.change === 'removed' ? 404 : 200
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Response shape test (if contract available)
|
|
134
|
+
if (providerContract?.paths) {
|
|
135
|
+
const pathKey = routePath.replace(/:([^/]+)/g, '{$1}');
|
|
136
|
+
const pathSpec = providerContract.paths[pathKey];
|
|
137
|
+
const methodSpec = pathSpec?.[method.toLowerCase()];
|
|
138
|
+
|
|
139
|
+
if (methodSpec?.responses?.['200']?.content?.['application/json']?.schema) {
|
|
140
|
+
const schema = methodSpec.responses['200'].content['application/json'].schema;
|
|
141
|
+
spec.testCases.push({
|
|
142
|
+
name: `${method} ${routePath} — response shape matches contract`,
|
|
143
|
+
type: 'schema-validation',
|
|
144
|
+
description: 'Verify response body matches the OpenAPI schema',
|
|
145
|
+
method,
|
|
146
|
+
path: routePath,
|
|
147
|
+
expectedSchema: schema
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Breaking change test
|
|
153
|
+
if (impact.change === 'removed') {
|
|
154
|
+
spec.testCases.push({
|
|
155
|
+
name: `${method} ${routePath} — handle removed endpoint gracefully`,
|
|
156
|
+
type: 'error-handling',
|
|
157
|
+
description: `Endpoint was REMOVED by ${impact.provider}. Consumer must handle 404/connection errors.`,
|
|
158
|
+
method,
|
|
159
|
+
path: routePath,
|
|
160
|
+
expectedBehavior: 'Consumer should degrade gracefully when endpoint is unavailable'
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return spec;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Generate integration test specs for ALL affected consumers
|
|
169
|
+
* after a provider changes endpoints.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} workspaceRoot
|
|
172
|
+
* @param {Object} oldManifest
|
|
173
|
+
* @param {Object} newManifest
|
|
174
|
+
* @returns {{ changes: Array, impacts: Array, specs: Array }}
|
|
175
|
+
*/
|
|
176
|
+
function generateAllIntegrationSpecs(workspaceRoot, oldManifest, newManifest) {
|
|
177
|
+
const changes = detectEndpointChanges(oldManifest, newManifest);
|
|
178
|
+
if (changes.length === 0) {
|
|
179
|
+
return { changes: [], impacts: [], specs: [] };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const impacts = analyzeConsumerImpact(changes, newManifest);
|
|
183
|
+
const specs = [];
|
|
184
|
+
|
|
185
|
+
// Try to load provider contracts for richer test specs
|
|
186
|
+
const contractsDir = path.join(workspaceRoot, '.workspace', 'contracts');
|
|
187
|
+
|
|
188
|
+
for (const impact of impacts) {
|
|
189
|
+
let contract = null;
|
|
190
|
+
try {
|
|
191
|
+
const contractPath = path.join(contractsDir, `${impact.provider}.json`);
|
|
192
|
+
if (fs.existsSync(contractPath)) {
|
|
193
|
+
contract = JSON.parse(fs.readFileSync(contractPath, 'utf-8'));
|
|
194
|
+
}
|
|
195
|
+
} catch (_err) {
|
|
196
|
+
// No contract available — generate basic tests
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
specs.push(generateIntegrationTestSpec(impact, contract));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { changes, impacts, specs };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Write integration test specs to workspace for tracking.
|
|
207
|
+
*
|
|
208
|
+
* @param {string} workspaceRoot
|
|
209
|
+
* @param {Array<Object>} specs — from generateAllIntegrationSpecs()
|
|
210
|
+
* @returns {Array<string>} file paths written
|
|
211
|
+
*/
|
|
212
|
+
function writeTestSpecs(workspaceRoot, specs) {
|
|
213
|
+
const specsDir = path.join(workspaceRoot, '.workspace', 'specs', 'integration-tests');
|
|
214
|
+
fs.mkdirSync(specsDir, { recursive: true });
|
|
215
|
+
|
|
216
|
+
// Use deterministic file names (consumer-provider pair) to prevent unbounded accumulation
|
|
217
|
+
const paths = [];
|
|
218
|
+
for (const spec of specs) {
|
|
219
|
+
const fileName = `${spec.consumer}-${spec.provider}.json`;
|
|
220
|
+
const filePath = path.join(specsDir, fileName);
|
|
221
|
+
fs.writeFileSync(filePath, JSON.stringify(spec, null, 2));
|
|
222
|
+
paths.push(filePath);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return paths;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============================================================
|
|
229
|
+
// Consumer Call Verification (Static Analysis)
|
|
230
|
+
// ============================================================
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Verify that consumer API calls match the provider's current signature.
|
|
234
|
+
* Reads consumer's api-map and compares against provider's api-index.
|
|
235
|
+
*
|
|
236
|
+
* @param {Object} manifest — workspace manifest
|
|
237
|
+
* @param {string} consumerName — consumer repo
|
|
238
|
+
* @param {string} providerName — provider repo
|
|
239
|
+
* @returns {{ matching: Array, mismatched: Array, orphaned: Array }}
|
|
240
|
+
*/
|
|
241
|
+
function verifyConsumerCalls(manifest, consumerName, providerName) {
|
|
242
|
+
const consumer = manifest.members[consumerName];
|
|
243
|
+
const provider = manifest.members[providerName];
|
|
244
|
+
|
|
245
|
+
if (!consumer || !provider) {
|
|
246
|
+
return { matching: [], mismatched: [], orphaned: [] };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const providerEndpoints = new Set((provider.provides || []).map(e => e.toLowerCase()));
|
|
250
|
+
const matching = [];
|
|
251
|
+
const mismatched = [];
|
|
252
|
+
const orphaned = [];
|
|
253
|
+
|
|
254
|
+
for (const consumed of consumer.consumes || []) {
|
|
255
|
+
const consumedLower = consumed.toLowerCase();
|
|
256
|
+
if (providerEndpoints.has(consumedLower)) {
|
|
257
|
+
matching.push(consumed);
|
|
258
|
+
} else {
|
|
259
|
+
// Check for partial matches (path matches but method differs, etc.)
|
|
260
|
+
let partialMatch = false;
|
|
261
|
+
const consumedPath = consumed.split(' ').slice(1).join(' ').toLowerCase();
|
|
262
|
+
|
|
263
|
+
for (const provided of providerEndpoints) {
|
|
264
|
+
const providedPath = provided.split(' ').slice(1).join(' ');
|
|
265
|
+
if (consumedPath === providedPath) {
|
|
266
|
+
mismatched.push({ consumed, reason: 'Method mismatch' });
|
|
267
|
+
partialMatch = true;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!partialMatch) {
|
|
273
|
+
orphaned.push(consumed);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return { matching, mismatched, orphaned };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ============================================================
|
|
282
|
+
// Exports
|
|
283
|
+
// ============================================================
|
|
284
|
+
|
|
285
|
+
module.exports = {
|
|
286
|
+
// Change detection
|
|
287
|
+
detectEndpointChanges,
|
|
288
|
+
|
|
289
|
+
// Consumer impact
|
|
290
|
+
analyzeConsumerImpact,
|
|
291
|
+
|
|
292
|
+
// Test spec generation
|
|
293
|
+
generateIntegrationTestSpec,
|
|
294
|
+
generateAllIntegrationSpecs,
|
|
295
|
+
writeTestSpecs,
|
|
296
|
+
|
|
297
|
+
// Call verification
|
|
298
|
+
verifyConsumerCalls
|
|
299
|
+
};
|