wogiflow 2.6.3 → 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.
Files changed (30) hide show
  1. package/.claude/settings.json +0 -1
  2. package/lib/workspace-changelog.js +182 -0
  3. package/lib/workspace-channel-server.js +75 -2
  4. package/lib/workspace-contracts.js +151 -1
  5. package/lib/workspace-events.js +383 -0
  6. package/lib/workspace-gates.js +740 -0
  7. package/lib/workspace-integration-tests.js +299 -0
  8. package/lib/workspace-intelligence.js +486 -1
  9. package/lib/workspace-locks.js +371 -0
  10. package/lib/workspace-messages.js +203 -3
  11. package/lib/workspace-routing.js +144 -0
  12. package/lib/workspace.js +18 -3
  13. package/package.json +1 -1
  14. package/scripts/flow-done-gates.js +70 -0
  15. package/.claude/rules/_internal/README.md +0 -64
  16. package/.claude/rules/_internal/document-structure.md +0 -77
  17. package/.claude/rules/_internal/dual-repo-management.md +0 -174
  18. package/.claude/rules/_internal/feature-refactoring-cleanup.md +0 -87
  19. package/.claude/rules/_internal/github-releases.md +0 -71
  20. package/.claude/rules/_internal/model-management.md +0 -35
  21. package/.claude/rules/_internal/self-maintenance.md +0 -87
  22. package/.claude/rules/architecture/component-reuse.md +0 -38
  23. package/.claude/rules/code-style/naming-conventions.md +0 -107
  24. package/.claude/rules/operations/git-workflows.md +0 -92
  25. package/.claude/rules/operations/scratch-directory.md +0 -54
  26. package/.claude/rules/security/security-patterns.md +0 -176
  27. package/.claude/skills/figma-analyzer/knowledge/learnings.md +0 -11
  28. package/.workflow/specs/architecture.md.template +0 -24
  29. package/.workflow/specs/stack.md.template +0 -33
  30. 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
+ };