wogiflow 2.6.4 → 2.7.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/.claude/settings.json +1 -11
- 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 +147 -2
- package/lib/workspace.js +8 -0
- package/package.json +1 -1
- package/scripts/flow-done-gates.js +70 -0
- package/scripts/hooks/entry/claude-code/permission-denied.js +111 -0
- package/scripts/postinstall.js +64 -2
- 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
package/.claude/settings.json
CHANGED
|
@@ -133,19 +133,9 @@
|
|
|
133
133
|
}
|
|
134
134
|
]
|
|
135
135
|
}
|
|
136
|
-
],
|
|
137
|
-
"TaskCreated": [
|
|
138
|
-
{
|
|
139
|
-
"hooks": [
|
|
140
|
-
{
|
|
141
|
-
"type": "command",
|
|
142
|
-
"command": "node scripts/hooks/entry/claude-code/task-created.js",
|
|
143
|
-
"timeout": 5
|
|
144
|
-
}
|
|
145
|
-
]
|
|
146
|
-
}
|
|
147
136
|
]
|
|
148
137
|
},
|
|
138
|
+
"_comment_dynamicHooks": "TaskCreated (2.1.84+) and PermissionDenied (2.1.88+) are added by postinstall.js when the CC version supports them. They must NOT be committed statically — CC rejects the entire settings file if it encounters an unknown hook event name.",
|
|
149
139
|
"_wogiFlowManaged": true,
|
|
150
140
|
"_wogiFlowVersion": "2.4.2",
|
|
151
141
|
"_comment": "Shared WogiFlow hook configuration. Committed to repo for team use. User-specific overrides go in settings.local.json."
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Workspace — Cross-Repo Changelog Aggregation
|
|
5
|
+
*
|
|
6
|
+
* Aggregates request-log entries from all member repos into a unified
|
|
7
|
+
* timeline showing the full story of cross-repo changes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
|
|
15
|
+
const { WORKSPACE_CONFIG_FILE } = require('./workspace');
|
|
16
|
+
|
|
17
|
+
// ============================================================
|
|
18
|
+
// Request Log Parsing
|
|
19
|
+
// ============================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse a member's request-log.md into structured entries.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} logContent — raw request-log.md content
|
|
25
|
+
* @param {string} repoName — name of the repo
|
|
26
|
+
* @returns {Array<Object>} parsed entries
|
|
27
|
+
*/
|
|
28
|
+
function parseRequestLog(logContent, repoName) {
|
|
29
|
+
const entries = [];
|
|
30
|
+
const sections = logContent.split(/^### /m).filter(Boolean);
|
|
31
|
+
|
|
32
|
+
for (const section of sections) {
|
|
33
|
+
const lines = section.trim().split('\n');
|
|
34
|
+
const header = lines[0] || '';
|
|
35
|
+
|
|
36
|
+
// Parse header: R-XXX | YYYY-MM-DD HH:MM
|
|
37
|
+
const headerMatch = header.match(/R-(\d+)\s*\|\s*(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
|
|
38
|
+
if (!headerMatch) continue;
|
|
39
|
+
|
|
40
|
+
const entry = {
|
|
41
|
+
repo: repoName,
|
|
42
|
+
id: `R-${headerMatch[1]}`,
|
|
43
|
+
date: headerMatch[2],
|
|
44
|
+
time: headerMatch[3],
|
|
45
|
+
// Note: request-log times are treated as local time (no timezone in format)
|
|
46
|
+
timestamp: new Date(`${headerMatch[2]}T${headerMatch[3]}:00`).toISOString(),
|
|
47
|
+
type: '',
|
|
48
|
+
tags: [],
|
|
49
|
+
request: '',
|
|
50
|
+
result: '',
|
|
51
|
+
files: []
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Parse fields
|
|
55
|
+
for (const line of lines.slice(1)) {
|
|
56
|
+
const typeMatch = line.match(/\*\*Type\*\*:\s*(.+)/);
|
|
57
|
+
if (typeMatch) entry.type = typeMatch[1].trim();
|
|
58
|
+
|
|
59
|
+
const tagsMatch = line.match(/\*\*Tags\*\*:\s*(.+)/);
|
|
60
|
+
if (tagsMatch) entry.tags = tagsMatch[1].trim().split(/\s+/);
|
|
61
|
+
|
|
62
|
+
const reqMatch = line.match(/\*\*Request\*\*:\s*"?(.+?)"?\s*$/);
|
|
63
|
+
if (reqMatch) entry.request = reqMatch[1].trim();
|
|
64
|
+
|
|
65
|
+
const resMatch = line.match(/\*\*Result\*\*:\s*(.+)/);
|
|
66
|
+
if (resMatch) entry.result = resMatch[1].trim();
|
|
67
|
+
|
|
68
|
+
const filesMatch = line.match(/\*\*Files\*\*:\s*(.+)/);
|
|
69
|
+
if (filesMatch) entry.files = filesMatch[1].trim().split(/[,\s]+/).filter(Boolean);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
entries.push(entry);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return entries;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ============================================================
|
|
79
|
+
// Changelog Aggregation
|
|
80
|
+
// ============================================================
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Aggregate request logs from all workspace members into a unified timeline.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} workspaceRoot
|
|
86
|
+
* @param {Object} [options]
|
|
87
|
+
* @param {string} [options.since] — only entries after this date (YYYY-MM-DD)
|
|
88
|
+
* @param {number} [options.limit] — max entries (default: 50)
|
|
89
|
+
* @returns {{ entries: Array<Object>, memberCount: number, totalEntries: number }}
|
|
90
|
+
*/
|
|
91
|
+
function aggregateChangelogs(workspaceRoot, options = {}) {
|
|
92
|
+
const { since = '', limit = 50 } = options;
|
|
93
|
+
const configPath = path.join(workspaceRoot, WORKSPACE_CONFIG_FILE);
|
|
94
|
+
|
|
95
|
+
let config;
|
|
96
|
+
try {
|
|
97
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
98
|
+
} catch (_err) {
|
|
99
|
+
return { entries: [], memberCount: 0, totalEntries: 0 };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let allEntries = [];
|
|
103
|
+
let memberCount = 0;
|
|
104
|
+
|
|
105
|
+
for (const [name, memberConfig] of Object.entries(config.members || {})) {
|
|
106
|
+
const memberPath = path.resolve(workspaceRoot, memberConfig.path);
|
|
107
|
+
const logPath = path.join(memberPath, '.workflow', 'state', 'request-log.md');
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
if (!fs.existsSync(logPath)) continue;
|
|
111
|
+
const content = fs.readFileSync(logPath, 'utf-8');
|
|
112
|
+
const entries = parseRequestLog(content, name);
|
|
113
|
+
allEntries = allEntries.concat(entries);
|
|
114
|
+
memberCount++;
|
|
115
|
+
} catch (_err) {
|
|
116
|
+
// Skip unreadable logs
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Filter by date if specified
|
|
121
|
+
if (since) {
|
|
122
|
+
const sinceTime = new Date(since).getTime();
|
|
123
|
+
allEntries = allEntries.filter(e => new Date(e.timestamp).getTime() >= sinceTime);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Sort by timestamp (newest first)
|
|
127
|
+
allEntries.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
128
|
+
|
|
129
|
+
const totalEntries = allEntries.length;
|
|
130
|
+
|
|
131
|
+
// Apply limit
|
|
132
|
+
if (limit > 0) {
|
|
133
|
+
allEntries = allEntries.slice(0, limit);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { entries: allEntries, memberCount, totalEntries };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Format aggregated changelog as markdown.
|
|
141
|
+
*
|
|
142
|
+
* @param {Object} result — from aggregateChangelogs()
|
|
143
|
+
* @returns {string} formatted markdown
|
|
144
|
+
*/
|
|
145
|
+
function formatAggregatedChangelog(result) {
|
|
146
|
+
const lines = [
|
|
147
|
+
'# Workspace Changelog',
|
|
148
|
+
'',
|
|
149
|
+
`*${result.memberCount} repos, ${result.totalEntries} entries*`,
|
|
150
|
+
''
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
let currentDate = '';
|
|
154
|
+
for (const entry of result.entries) {
|
|
155
|
+
if (entry.date !== currentDate) {
|
|
156
|
+
currentDate = entry.date;
|
|
157
|
+
lines.push(`## ${currentDate}`);
|
|
158
|
+
lines.push('');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const typeIcon = {
|
|
162
|
+
new: '+',
|
|
163
|
+
fix: '!',
|
|
164
|
+
change: '~',
|
|
165
|
+
refactor: '>'
|
|
166
|
+
}[entry.type] || '*';
|
|
167
|
+
|
|
168
|
+
lines.push(`- \`${entry.time}\` **${entry.repo}** [${typeIcon}${entry.type}] ${entry.request || entry.result}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return lines.join('\n');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ============================================================
|
|
175
|
+
// Exports
|
|
176
|
+
// ============================================================
|
|
177
|
+
|
|
178
|
+
module.exports = {
|
|
179
|
+
parseRequestLog,
|
|
180
|
+
aggregateChangelogs,
|
|
181
|
+
formatAggregatedChangelog
|
|
182
|
+
};
|
|
@@ -384,6 +384,59 @@ rl.on('line', (line) => {
|
|
|
384
384
|
// HTTP Webhook Server (receives dispatches from manager/peers)
|
|
385
385
|
// ============================================================
|
|
386
386
|
|
|
387
|
+
// ============================================================
|
|
388
|
+
// SSE Client Management (Event Bus)
|
|
389
|
+
// ============================================================
|
|
390
|
+
|
|
391
|
+
const sseClients = new Set();
|
|
392
|
+
|
|
393
|
+
function addSSEClient(res, lastEventId) {
|
|
394
|
+
res.writeHead(200, {
|
|
395
|
+
'Content-Type': 'text/event-stream',
|
|
396
|
+
'Cache-Control': 'no-cache',
|
|
397
|
+
Connection: 'keep-alive',
|
|
398
|
+
'X-Accel-Buffering': 'no'
|
|
399
|
+
});
|
|
400
|
+
res.write(':ok\n\n');
|
|
401
|
+
|
|
402
|
+
// If workspace root is available, send missed events
|
|
403
|
+
if (WORKSPACE_ROOT && lastEventId) {
|
|
404
|
+
try {
|
|
405
|
+
const events = require('./workspace-events');
|
|
406
|
+
const missed = events.getEventsSince(WORKSPACE_ROOT, lastEventId);
|
|
407
|
+
for (const evt of missed) {
|
|
408
|
+
res.write(events.formatAsSSE(evt));
|
|
409
|
+
}
|
|
410
|
+
} catch (_err) {
|
|
411
|
+
// Non-critical
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
sseClients.add(res);
|
|
416
|
+
res.on('close', () => sseClients.delete(res));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function broadcastSSE(event) {
|
|
420
|
+
let formatted;
|
|
421
|
+
try {
|
|
422
|
+
const events = require('./workspace-events');
|
|
423
|
+
formatted = events.formatAsSSE(event);
|
|
424
|
+
} catch (_err) {
|
|
425
|
+
formatted = `data: ${JSON.stringify(event)}\n\n`;
|
|
426
|
+
}
|
|
427
|
+
for (const client of sseClients) {
|
|
428
|
+
try {
|
|
429
|
+
client.write(formatted);
|
|
430
|
+
} catch (_err) {
|
|
431
|
+
sseClients.delete(client);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ============================================================
|
|
437
|
+
// HTTP Server
|
|
438
|
+
// ============================================================
|
|
439
|
+
|
|
387
440
|
const server = http.createServer(async (req, res) => {
|
|
388
441
|
// Health check — minimal info, no topology exposure
|
|
389
442
|
if (req.method === 'GET' && req.url === '/health') {
|
|
@@ -392,6 +445,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
392
445
|
return;
|
|
393
446
|
}
|
|
394
447
|
|
|
448
|
+
// SSE endpoint for event subscriptions
|
|
449
|
+
if (req.method === 'GET' && req.url?.startsWith('/events')) {
|
|
450
|
+
const lastEventId = req.headers['last-event-id'] || '';
|
|
451
|
+
addSSEClient(res, lastEventId);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
395
455
|
// Receive webhook (POST)
|
|
396
456
|
if (req.method === 'POST') {
|
|
397
457
|
const { body, truncated } = await collectBody(req, MAX_BODY_BYTES);
|
|
@@ -406,12 +466,25 @@ const server = http.createServer(async (req, res) => {
|
|
|
406
466
|
const from = req.headers['x-wogi-from'] || 'workspace-manager';
|
|
407
467
|
|
|
408
468
|
// Forward as channel notification to Claude Code
|
|
409
|
-
|
|
469
|
+
const meta = {
|
|
410
470
|
from,
|
|
411
471
|
port: String(PORT),
|
|
412
472
|
repo: REPO_NAME,
|
|
413
473
|
receivedAt: new Date().toISOString()
|
|
414
|
-
}
|
|
474
|
+
};
|
|
475
|
+
sendChannelNotification(body, meta);
|
|
476
|
+
|
|
477
|
+
// Also broadcast to SSE subscribers
|
|
478
|
+
if (sseClients.size > 0) {
|
|
479
|
+
const crypto = require('node:crypto');
|
|
480
|
+
broadcastSSE({
|
|
481
|
+
id: 'evt-' + crypto.randomBytes(4).toString('hex'),
|
|
482
|
+
type: 'webhook-received',
|
|
483
|
+
source: from,
|
|
484
|
+
data: { body: body.substring(0, 500) },
|
|
485
|
+
timestamp: meta.receivedAt
|
|
486
|
+
});
|
|
487
|
+
}
|
|
415
488
|
|
|
416
489
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
417
490
|
res.end('ok');
|
|
@@ -571,6 +571,152 @@ function writeContract(workspaceRoot, contractName, format, content, changedBy,
|
|
|
571
571
|
return trackContractVersion(workspaceRoot, contractName, serialized, changedBy, reason);
|
|
572
572
|
}
|
|
573
573
|
|
|
574
|
+
// ============================================================
|
|
575
|
+
// Schema/Type Sync Enforcement
|
|
576
|
+
// ============================================================
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Auto-generate shared TypeScript interfaces from all providers' schemas.
|
|
580
|
+
* Writes to .workspace/contracts/shared-types.d.ts so consumers can import.
|
|
581
|
+
*
|
|
582
|
+
* @param {string} workspaceRoot
|
|
583
|
+
* @param {Object} manifest — workspace manifest
|
|
584
|
+
* @returns {{ typesGenerated: number, filePath: string }}
|
|
585
|
+
*/
|
|
586
|
+
function generateSharedTypes(workspaceRoot, manifest) {
|
|
587
|
+
const contractsDir = path.join(workspaceRoot, '.workspace', 'contracts');
|
|
588
|
+
fs.mkdirSync(contractsDir, { recursive: true });
|
|
589
|
+
|
|
590
|
+
const lines = [
|
|
591
|
+
'/**',
|
|
592
|
+
' * Auto-generated shared types from workspace providers.',
|
|
593
|
+
` * Generated: ${new Date().toISOString()}`,
|
|
594
|
+
' * DO NOT EDIT — regenerated by `flow workspace sync`',
|
|
595
|
+
' */',
|
|
596
|
+
''
|
|
597
|
+
];
|
|
598
|
+
|
|
599
|
+
let typesGenerated = 0;
|
|
600
|
+
const seenTypes = new Map(); // Track types across repos to detect conflicts
|
|
601
|
+
|
|
602
|
+
// Strict identifier validation to prevent code injection via manifest data
|
|
603
|
+
const VALID_TS_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
604
|
+
const VALID_REPO_NAME = /^[a-zA-Z0-9_-]+$/;
|
|
605
|
+
|
|
606
|
+
for (const [name, member] of Object.entries(manifest.members || {})) {
|
|
607
|
+
if (member.role !== 'provider' && member.role !== 'both' && member.role !== 'library') continue;
|
|
608
|
+
|
|
609
|
+
const schemas = member.schemas || [];
|
|
610
|
+
if (schemas.length === 0) continue;
|
|
611
|
+
|
|
612
|
+
const safeName = VALID_REPO_NAME.test(name) ? name : 'unknown';
|
|
613
|
+
const safeRole = VALID_REPO_NAME.test(member.role) ? member.role : 'unknown';
|
|
614
|
+
lines.push(`// ── From ${safeName} (${safeRole}) ${'─'.repeat(Math.max(1, 50 - safeName.length))}`);
|
|
615
|
+
lines.push('');
|
|
616
|
+
|
|
617
|
+
for (const schema of schemas) {
|
|
618
|
+
const typeName = schema.name || schema;
|
|
619
|
+
const fields = schema.fields || [];
|
|
620
|
+
|
|
621
|
+
// Validate type name is a safe TypeScript identifier
|
|
622
|
+
if (!VALID_TS_IDENTIFIER.test(typeName)) {
|
|
623
|
+
lines.push(`// SKIPPED: Invalid type name "${String(typeName).replace(/[^a-zA-Z0-9_$ ]/g, '')}"`);
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Track for conflict detection
|
|
628
|
+
if (seenTypes.has(typeName)) {
|
|
629
|
+
const prevDef = VALID_REPO_NAME.test(seenTypes.get(typeName)) ? seenTypes.get(typeName) : 'unknown';
|
|
630
|
+
lines.push(`// WARNING: Type "${typeName}" also defined by ${prevDef}`);
|
|
631
|
+
}
|
|
632
|
+
seenTypes.set(typeName, name);
|
|
633
|
+
|
|
634
|
+
lines.push(`export interface ${typeName} {`);
|
|
635
|
+
for (const field of fields) {
|
|
636
|
+
const fieldName = field.name || field;
|
|
637
|
+
// Validate field name is a safe identifier
|
|
638
|
+
if (!VALID_TS_IDENTIFIER.test(fieldName)) continue;
|
|
639
|
+
const fieldType = mapToTsType(field.type || 'string');
|
|
640
|
+
const optional = field.nullable || field.optional ? '?' : '';
|
|
641
|
+
lines.push(` ${fieldName}${optional}: ${fieldType};`);
|
|
642
|
+
}
|
|
643
|
+
lines.push('}');
|
|
644
|
+
lines.push('');
|
|
645
|
+
typesGenerated++;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (typesGenerated === 0) {
|
|
650
|
+
lines.push('// No provider schemas found in workspace manifest.');
|
|
651
|
+
lines.push('// Run `flow workspace sync` after adding provider repos.');
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const filePath = path.join(contractsDir, 'shared-types.d.ts');
|
|
655
|
+
fs.writeFileSync(filePath, lines.join('\n'));
|
|
656
|
+
|
|
657
|
+
// Track version
|
|
658
|
+
const content = lines.join('\n');
|
|
659
|
+
try {
|
|
660
|
+
trackContractVersion(workspaceRoot, 'shared-types', content, 'workspace-sync', 'Auto-generated from provider schemas');
|
|
661
|
+
} catch (_err) {
|
|
662
|
+
// Non-critical
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return { typesGenerated, filePath };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Verify that consumer repos reference shared types correctly.
|
|
670
|
+
* Checks if consumers have local type definitions that duplicate shared types.
|
|
671
|
+
*
|
|
672
|
+
* @param {string} workspaceRoot
|
|
673
|
+
* @param {Object} manifest
|
|
674
|
+
* @returns {Array<{ consumer: string, type: string, issue: string }>}
|
|
675
|
+
*/
|
|
676
|
+
function checkTypeSyncCompliance(workspaceRoot, manifest) {
|
|
677
|
+
const issues = [];
|
|
678
|
+
const sharedTypesPath = path.join(workspaceRoot, '.workspace', 'contracts', 'shared-types.d.ts');
|
|
679
|
+
|
|
680
|
+
if (!fs.existsSync(sharedTypesPath)) return issues;
|
|
681
|
+
|
|
682
|
+
// Extract type names from shared types
|
|
683
|
+
const sharedContent = fs.readFileSync(sharedTypesPath, 'utf-8');
|
|
684
|
+
const typeNames = [];
|
|
685
|
+
const typeRegex = /export interface (\w+)/g;
|
|
686
|
+
let match;
|
|
687
|
+
while ((match = typeRegex.exec(sharedContent)) !== null) {
|
|
688
|
+
typeNames.push(match[1]);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (typeNames.length === 0) return issues;
|
|
692
|
+
|
|
693
|
+
// Check each consumer's schema-map for duplicates
|
|
694
|
+
const configPath = path.join(workspaceRoot, 'wogi-workspace.json');
|
|
695
|
+
try {
|
|
696
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
697
|
+
|
|
698
|
+
for (const [name, memberConfig] of Object.entries(config.members || {})) {
|
|
699
|
+
const member = manifest.members?.[name];
|
|
700
|
+
if (!member || member.role === 'provider') continue; // Only check consumers
|
|
701
|
+
|
|
702
|
+
const memberSchemas = (member.schemas || []).map(s => s.name || s);
|
|
703
|
+
for (const typeName of typeNames) {
|
|
704
|
+
if (memberSchemas.includes(typeName)) {
|
|
705
|
+
issues.push({
|
|
706
|
+
consumer: name,
|
|
707
|
+
type: typeName,
|
|
708
|
+
issue: `Consumer "${name}" defines "${typeName}" locally — should import from .workspace/contracts/shared-types.d.ts`
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
} catch (_err) {
|
|
714
|
+
// Non-critical
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return issues;
|
|
718
|
+
}
|
|
719
|
+
|
|
574
720
|
// ============================================================
|
|
575
721
|
// Exports
|
|
576
722
|
// ============================================================
|
|
@@ -595,5 +741,9 @@ module.exports = {
|
|
|
595
741
|
|
|
596
742
|
// Multi-format
|
|
597
743
|
detectContractFormat,
|
|
598
|
-
writeContract
|
|
744
|
+
writeContract,
|
|
745
|
+
|
|
746
|
+
// Schema/type sync
|
|
747
|
+
generateSharedTypes,
|
|
748
|
+
checkTypeSyncCompliance
|
|
599
749
|
};
|