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.
- 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/lib/workspace.js +18 -3
- 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
|
@@ -246,6 +246,81 @@ function addSharedDecision(workspaceRoot, title, content) {
|
|
|
246
246
|
fs.writeFileSync(decisionsPath, existing + entry);
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
// ============================================================
|
|
250
|
+
// S5: Decision Propagation (Active Broadcast)
|
|
251
|
+
// ============================================================
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Add a shared decision AND broadcast it to all workspace members.
|
|
255
|
+
* Extends addSharedDecision with active notification.
|
|
256
|
+
*
|
|
257
|
+
* @param {string} workspaceRoot
|
|
258
|
+
* @param {string} fromRepo — repo that created the decision
|
|
259
|
+
* @param {string} title
|
|
260
|
+
* @param {string} content
|
|
261
|
+
* @param {Object} manifest — workspace manifest for member discovery
|
|
262
|
+
* @returns {{ saved: boolean, broadcastCount: number }}
|
|
263
|
+
*/
|
|
264
|
+
function propagateDecision(workspaceRoot, fromRepo, title, content, manifest) {
|
|
265
|
+
// 1. Save the decision
|
|
266
|
+
addSharedDecision(workspaceRoot, title, content);
|
|
267
|
+
|
|
268
|
+
// 2. Broadcast to all members
|
|
269
|
+
let broadcastCount = 0;
|
|
270
|
+
if (manifest && manifest.members) {
|
|
271
|
+
const { broadcastDecision, saveMessage } = require('./workspace-messages');
|
|
272
|
+
const targetRepos = Object.keys(manifest.members).filter(n => n !== fromRepo);
|
|
273
|
+
const messages = broadcastDecision(fromRepo, title, content, targetRepos);
|
|
274
|
+
|
|
275
|
+
for (const msg of messages) {
|
|
276
|
+
try {
|
|
277
|
+
saveMessage(workspaceRoot, msg);
|
|
278
|
+
broadcastCount++;
|
|
279
|
+
} catch (_err) {
|
|
280
|
+
// Best effort
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return { saved: true, broadcastCount };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Check if there are new shared decisions since a given timestamp.
|
|
290
|
+
* Used by session-start hooks to inject new decisions into worker context.
|
|
291
|
+
*
|
|
292
|
+
* @param {string} workspaceRoot
|
|
293
|
+
* @param {string} sinceDate — ISO date string
|
|
294
|
+
* @returns {Array<{ title: string, content: string, date: string }>}
|
|
295
|
+
*/
|
|
296
|
+
function getNewDecisionsSince(workspaceRoot, sinceDate) {
|
|
297
|
+
const decisions = getSharedDecisions(workspaceRoot);
|
|
298
|
+
if (!decisions) return [];
|
|
299
|
+
|
|
300
|
+
const sinceTime = new Date(sinceDate).getTime();
|
|
301
|
+
const results = [];
|
|
302
|
+
|
|
303
|
+
// Parse decisions.md for entries with *Added: YYYY-MM-DD* markers
|
|
304
|
+
const sections = decisions.split(/^### /m).filter(Boolean);
|
|
305
|
+
for (const section of sections) {
|
|
306
|
+
const lines = section.trim().split('\n');
|
|
307
|
+
const title = lines[0]?.trim() || '';
|
|
308
|
+
const dateMatch = section.match(/\*Added:\s*(\d{4}-\d{2}-\d{2})\*/);
|
|
309
|
+
if (dateMatch) {
|
|
310
|
+
const entryDate = new Date(dateMatch[1]).getTime();
|
|
311
|
+
if (entryDate > sinceTime) {
|
|
312
|
+
results.push({
|
|
313
|
+
title,
|
|
314
|
+
content: lines.slice(1).join('\n').replace(/\*Added:.*\*/, '').trim(),
|
|
315
|
+
date: dateMatch[1]
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return results;
|
|
322
|
+
}
|
|
323
|
+
|
|
249
324
|
// ============================================================
|
|
250
325
|
// S5: API Changelog (Criterion 4) — delegates to workspace-contracts
|
|
251
326
|
// S5: Cross-Repo Ready Queue (Criterion 5) — workspace state/ready.json
|
|
@@ -580,6 +655,402 @@ function getMultiUserSchema() {
|
|
|
580
655
|
// Exports
|
|
581
656
|
// ============================================================
|
|
582
657
|
|
|
658
|
+
// ============================================================
|
|
659
|
+
// Workspace Review Mode (Cross-Repo Contract Impact)
|
|
660
|
+
// ============================================================
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Analyze code review findings for cross-repo impact.
|
|
664
|
+
* Called during /wogi-review when workspace is active.
|
|
665
|
+
*
|
|
666
|
+
* @param {string} workspaceRoot
|
|
667
|
+
* @param {Object} manifest
|
|
668
|
+
* @param {string[]} changedFiles — files changed in the review
|
|
669
|
+
* @param {string} repoName — repo being reviewed
|
|
670
|
+
* @returns {Object} review impact analysis
|
|
671
|
+
*/
|
|
672
|
+
function analyzeReviewForCrossRepoImpact(workspaceRoot, manifest, changedFiles, repoName) {
|
|
673
|
+
const result = {
|
|
674
|
+
hasContractImpact: false,
|
|
675
|
+
endpointChanges: [],
|
|
676
|
+
missingContractUpdates: [],
|
|
677
|
+
affectedConsumers: [],
|
|
678
|
+
recommendations: []
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
const member = manifest.members?.[repoName];
|
|
682
|
+
if (!member) return result;
|
|
683
|
+
|
|
684
|
+
// Check if any changed files are in API/route directories
|
|
685
|
+
const apiPatterns = ['route', 'controller', 'endpoint', 'api', 'handler'];
|
|
686
|
+
const changedApiFiles = changedFiles.filter(f => {
|
|
687
|
+
const lower = f.toLowerCase();
|
|
688
|
+
return apiPatterns.some(p => lower.includes(p));
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
if (changedApiFiles.length > 0) {
|
|
692
|
+
result.hasContractImpact = true;
|
|
693
|
+
result.endpointChanges = changedApiFiles;
|
|
694
|
+
|
|
695
|
+
// Check if contract was also updated
|
|
696
|
+
const contractsDir = path.join(workspaceRoot, '.workspace', 'contracts');
|
|
697
|
+
const contractFiles = changedFiles.filter(f => f.includes('.workspace/contracts'));
|
|
698
|
+
|
|
699
|
+
if (contractFiles.length === 0 && changedApiFiles.length > 0) {
|
|
700
|
+
result.missingContractUpdates.push(
|
|
701
|
+
`API files changed (${changedApiFiles.join(', ')}) but no contract was updated. ` +
|
|
702
|
+
`Run \`flow workspace sync\` and regenerate contracts.`
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Find affected consumers
|
|
707
|
+
const { buildIntegrationMap } = require('./workspace-contracts');
|
|
708
|
+
const integrationMap = buildIntegrationMap(manifest);
|
|
709
|
+
const consumerSet = new Set();
|
|
710
|
+
|
|
711
|
+
for (const matched of integrationMap.matched || []) {
|
|
712
|
+
if ((matched.providers || []).includes(repoName)) {
|
|
713
|
+
for (const consumer of matched.consumers || []) {
|
|
714
|
+
consumerSet.add(consumer);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
result.affectedConsumers = [...consumerSet];
|
|
720
|
+
|
|
721
|
+
if (result.affectedConsumers.length > 0) {
|
|
722
|
+
result.recommendations.push(
|
|
723
|
+
`Notify consumers: ${result.affectedConsumers.join(', ')}`,
|
|
724
|
+
'Update shared contracts if endpoint signatures changed',
|
|
725
|
+
'Consider creating verification tasks in consumer repos'
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return result;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ============================================================
|
|
734
|
+
// Workspace Morning Briefing
|
|
735
|
+
// ============================================================
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Generate a workspace-aware morning briefing.
|
|
739
|
+
*
|
|
740
|
+
* @param {string} workspaceRoot
|
|
741
|
+
* @param {Object} manifest
|
|
742
|
+
* @param {string} [repoName] — current repo (for filtering)
|
|
743
|
+
* @returns {Object} briefing data
|
|
744
|
+
*/
|
|
745
|
+
function generateWorkspaceBriefing(workspaceRoot, manifest, repoName) {
|
|
746
|
+
const briefing = {
|
|
747
|
+
unreadMessages: [],
|
|
748
|
+
contractChanges: [],
|
|
749
|
+
blockedTasks: [],
|
|
750
|
+
healthIssues: [],
|
|
751
|
+
activeLocks: [],
|
|
752
|
+
recentEvents: [],
|
|
753
|
+
summary: ''
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
// 1. Unread messages
|
|
757
|
+
try {
|
|
758
|
+
const { getUnreadMessages } = require('./workspace-messages');
|
|
759
|
+
briefing.unreadMessages = getUnreadMessages(workspaceRoot, repoName || 'all');
|
|
760
|
+
} catch (_err) {
|
|
761
|
+
// Non-critical
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// 2. Contract changes (drift detection)
|
|
765
|
+
try {
|
|
766
|
+
briefing.contractChanges = detectContractDrift(workspaceRoot, manifest);
|
|
767
|
+
} catch (_err) {
|
|
768
|
+
// Non-critical
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// 3. Blocked tasks
|
|
772
|
+
try {
|
|
773
|
+
const { updateCrossRepoBlocking } = require('./workspace-routing');
|
|
774
|
+
const blocking = updateCrossRepoBlocking(workspaceRoot, manifest);
|
|
775
|
+
briefing.blockedTasks = blocking.blockedTasks;
|
|
776
|
+
} catch (_err) {
|
|
777
|
+
// Non-critical
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// 4. Health issues
|
|
781
|
+
try {
|
|
782
|
+
const health = checkWorkspaceHealth(workspaceRoot, manifest);
|
|
783
|
+
briefing.healthIssues = health.issues || [];
|
|
784
|
+
} catch (_err) {
|
|
785
|
+
// Non-critical
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// 5. Active locks
|
|
789
|
+
try {
|
|
790
|
+
const { listActiveLocks } = require('./workspace-locks');
|
|
791
|
+
briefing.activeLocks = listActiveLocks(workspaceRoot);
|
|
792
|
+
} catch (_err) {
|
|
793
|
+
// Non-critical
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// 6. Recent events (last 24h)
|
|
797
|
+
try {
|
|
798
|
+
const { readEvents } = require('./workspace-events');
|
|
799
|
+
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
|
800
|
+
briefing.recentEvents = readEvents(workspaceRoot, { since: yesterday, limit: 20 });
|
|
801
|
+
} catch (_err) {
|
|
802
|
+
// Non-critical
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// 7. Summary
|
|
806
|
+
const parts = [];
|
|
807
|
+
if (briefing.unreadMessages.length > 0) parts.push(`${briefing.unreadMessages.length} unread message(s)`);
|
|
808
|
+
if (briefing.contractChanges.length > 0) parts.push(`${briefing.contractChanges.length} contract drift(s)`);
|
|
809
|
+
if (briefing.blockedTasks.length > 0) parts.push(`${briefing.blockedTasks.length} blocked task(s)`);
|
|
810
|
+
if (briefing.healthIssues.length > 0) parts.push(`${briefing.healthIssues.length} health issue(s)`);
|
|
811
|
+
if (briefing.activeLocks.length > 0) parts.push(`${briefing.activeLocks.length} active lock(s)`);
|
|
812
|
+
briefing.summary = parts.length > 0 ? parts.join(', ') : 'All clear';
|
|
813
|
+
|
|
814
|
+
return briefing;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Format workspace briefing as readable text.
|
|
819
|
+
*
|
|
820
|
+
* @param {Object} briefing — from generateWorkspaceBriefing()
|
|
821
|
+
* @returns {string}
|
|
822
|
+
*/
|
|
823
|
+
function formatWorkspaceBriefing(briefing) {
|
|
824
|
+
const lines = ['Workspace Briefing', '━'.repeat(40), ''];
|
|
825
|
+
|
|
826
|
+
if (briefing.unreadMessages.length > 0) {
|
|
827
|
+
lines.push(`Messages (${briefing.unreadMessages.length} unread):`);
|
|
828
|
+
for (const msg of briefing.unreadMessages.slice(0, 5)) {
|
|
829
|
+
lines.push(` ${msg.from}: ${msg.subject}`);
|
|
830
|
+
}
|
|
831
|
+
lines.push('');
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (briefing.contractChanges.length > 0) {
|
|
835
|
+
lines.push(`Contract Drifts (${briefing.contractChanges.length}):`);
|
|
836
|
+
for (const drift of briefing.contractChanges.slice(0, 5)) {
|
|
837
|
+
lines.push(` ${drift.severity}: ${drift.endpoint || drift.type}`);
|
|
838
|
+
}
|
|
839
|
+
lines.push('');
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (briefing.blockedTasks.length > 0) {
|
|
843
|
+
lines.push(`Blocked Tasks (${briefing.blockedTasks.length}):`);
|
|
844
|
+
for (const bt of briefing.blockedTasks.slice(0, 5)) {
|
|
845
|
+
lines.push(` ${bt.repo}: ${bt.task.title} [blocked by: ${bt.blockedBy?.join(', ')}]`);
|
|
846
|
+
}
|
|
847
|
+
lines.push('');
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (briefing.activeLocks.length > 0) {
|
|
851
|
+
lines.push(`Active Locks (${briefing.activeLocks.length}):`);
|
|
852
|
+
try {
|
|
853
|
+
const { formatLocksForDisplay } = require('./workspace-locks');
|
|
854
|
+
lines.push(formatLocksForDisplay(briefing.activeLocks));
|
|
855
|
+
} catch (_err) {
|
|
856
|
+
for (const lock of briefing.activeLocks) {
|
|
857
|
+
lines.push(` ${lock.interface} — held by ${lock.owner}`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
lines.push('');
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (briefing.healthIssues.length > 0) {
|
|
864
|
+
lines.push(`Health Issues (${briefing.healthIssues.length}):`);
|
|
865
|
+
for (const issue of briefing.healthIssues.slice(0, 5)) {
|
|
866
|
+
lines.push(` ${issue.severity || 'warning'}: ${issue.message || issue}`);
|
|
867
|
+
}
|
|
868
|
+
lines.push('');
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
lines.push(`Summary: ${briefing.summary}`);
|
|
872
|
+
|
|
873
|
+
return lines.join('\n');
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// ============================================================
|
|
877
|
+
// Workspace Audit Dimension
|
|
878
|
+
// ============================================================
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Run workspace-specific audit checks.
|
|
882
|
+
* Returns findings for the workspace dimension of /wogi-audit.
|
|
883
|
+
*
|
|
884
|
+
* @param {string} workspaceRoot
|
|
885
|
+
* @param {Object} manifest
|
|
886
|
+
* @returns {Object} audit results
|
|
887
|
+
*/
|
|
888
|
+
function auditWorkspaceDimension(workspaceRoot, manifest) {
|
|
889
|
+
const audit = {
|
|
890
|
+
dimension: 'workspace',
|
|
891
|
+
score: 100,
|
|
892
|
+
findings: [],
|
|
893
|
+
metrics: {}
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
if (!manifest || !manifest.members) {
|
|
897
|
+
audit.score = 0;
|
|
898
|
+
audit.findings.push({ severity: 'error', message: 'No workspace manifest found' });
|
|
899
|
+
return audit;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const memberCount = Object.keys(manifest.members).length;
|
|
903
|
+
audit.metrics.memberCount = memberCount;
|
|
904
|
+
|
|
905
|
+
// 1. Type consistency — check for type drift
|
|
906
|
+
try {
|
|
907
|
+
const memberMetadata = {};
|
|
908
|
+
const configPath = path.join(workspaceRoot, 'wogi-workspace.json');
|
|
909
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
910
|
+
|
|
911
|
+
for (const [name, memberConfig] of Object.entries(config.members || {})) {
|
|
912
|
+
const memberPath = path.resolve(workspaceRoot, memberConfig.path);
|
|
913
|
+
const workflowPath = path.join(memberPath, '.workflow');
|
|
914
|
+
if (fs.existsSync(workflowPath)) {
|
|
915
|
+
const { readMemberMetadata } = require('./workspace');
|
|
916
|
+
memberMetadata[name] = readMemberMetadata(workflowPath);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const { detectTypeDrift } = require('./workspace-contracts');
|
|
921
|
+
const drifts = detectTypeDrift(manifest, memberMetadata);
|
|
922
|
+
audit.metrics.typeDrifts = drifts.length;
|
|
923
|
+
|
|
924
|
+
if (drifts.length > 0) {
|
|
925
|
+
audit.score -= drifts.length * 5;
|
|
926
|
+
audit.findings.push({
|
|
927
|
+
severity: 'high',
|
|
928
|
+
message: `${drifts.length} type drift(s) detected across repos`,
|
|
929
|
+
details: drifts.map(d => `${d.type}: ${d.entries?.length || 0} conflicting definitions`)
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
} catch (_err) {
|
|
933
|
+
// Non-critical
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// 2. Contract coverage — what % of integrations have contracts
|
|
937
|
+
try {
|
|
938
|
+
const { buildIntegrationMap } = require('./workspace-contracts');
|
|
939
|
+
const map = buildIntegrationMap(manifest);
|
|
940
|
+
const totalIntegrations = (map.matched || []).length;
|
|
941
|
+
const contractsDir = path.join(workspaceRoot, '.workspace', 'contracts');
|
|
942
|
+
const contractCount = fs.existsSync(contractsDir)
|
|
943
|
+
? fs.readdirSync(contractsDir).filter(f => !f.startsWith('.')).length
|
|
944
|
+
: 0;
|
|
945
|
+
|
|
946
|
+
audit.metrics.totalIntegrations = totalIntegrations;
|
|
947
|
+
audit.metrics.contractCount = contractCount;
|
|
948
|
+
audit.metrics.contractCoverage = totalIntegrations > 0
|
|
949
|
+
? Math.round((contractCount / totalIntegrations) * 100)
|
|
950
|
+
: 100;
|
|
951
|
+
|
|
952
|
+
if (audit.metrics.contractCoverage < 50) {
|
|
953
|
+
audit.score -= 15;
|
|
954
|
+
audit.findings.push({
|
|
955
|
+
severity: 'high',
|
|
956
|
+
message: `Contract coverage is ${audit.metrics.contractCoverage}% (${contractCount}/${totalIntegrations})`
|
|
957
|
+
});
|
|
958
|
+
} else if (audit.metrics.contractCoverage < 80) {
|
|
959
|
+
audit.score -= 5;
|
|
960
|
+
audit.findings.push({
|
|
961
|
+
severity: 'medium',
|
|
962
|
+
message: `Contract coverage is ${audit.metrics.contractCoverage}% — consider adding contracts for uncovered integrations`
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Orphaned endpoints
|
|
967
|
+
audit.metrics.orphanedConsumers = (map.orphanedConsumers || []).length;
|
|
968
|
+
audit.metrics.orphanedProviders = (map.orphanedProviders || []).length;
|
|
969
|
+
|
|
970
|
+
if (audit.metrics.orphanedConsumers > 0) {
|
|
971
|
+
audit.score -= audit.metrics.orphanedConsumers * 3;
|
|
972
|
+
audit.findings.push({
|
|
973
|
+
severity: 'high',
|
|
974
|
+
message: `${audit.metrics.orphanedConsumers} consumer(s) calling endpoints with no provider`
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
} catch (_err) {
|
|
978
|
+
// Non-critical
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// 3. Communication health — message acknowledgment rate
|
|
982
|
+
try {
|
|
983
|
+
const { readMessages } = require('./workspace-messages');
|
|
984
|
+
const allMessages = readMessages(workspaceRoot);
|
|
985
|
+
const actionRequired = allMessages.filter(m => m.actionRequired);
|
|
986
|
+
const acknowledged = actionRequired.filter(m => m.status !== 'pending');
|
|
987
|
+
|
|
988
|
+
audit.metrics.totalMessages = allMessages.length;
|
|
989
|
+
audit.metrics.pendingActionRequired = actionRequired.filter(m => m.status === 'pending').length;
|
|
990
|
+
audit.metrics.acknowledgmentRate = actionRequired.length > 0
|
|
991
|
+
? Math.round((acknowledged.length / actionRequired.length) * 100)
|
|
992
|
+
: 100;
|
|
993
|
+
|
|
994
|
+
if (audit.metrics.pendingActionRequired > 5) {
|
|
995
|
+
audit.score -= 10;
|
|
996
|
+
audit.findings.push({
|
|
997
|
+
severity: 'medium',
|
|
998
|
+
message: `${audit.metrics.pendingActionRequired} action-required message(s) still pending`
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
} catch (_err) {
|
|
1002
|
+
// Non-critical
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// 4. Dependency freshness — is the manifest up to date?
|
|
1006
|
+
try {
|
|
1007
|
+
const manifestPath = path.join(workspaceRoot, '.workspace', 'state', 'workspace-manifest.json');
|
|
1008
|
+
if (fs.existsSync(manifestPath)) {
|
|
1009
|
+
const stat = fs.statSync(manifestPath);
|
|
1010
|
+
const ageHours = (Date.now() - stat.mtime.getTime()) / (60 * 60 * 1000);
|
|
1011
|
+
audit.metrics.manifestAgeHours = Math.round(ageHours);
|
|
1012
|
+
|
|
1013
|
+
if (ageHours > 48) {
|
|
1014
|
+
audit.score -= 10;
|
|
1015
|
+
audit.findings.push({
|
|
1016
|
+
severity: 'medium',
|
|
1017
|
+
message: `Workspace manifest is ${Math.round(ageHours)}h old — run \`flow workspace sync\``
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
} catch (_err) {
|
|
1022
|
+
// Non-critical
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// 5. Contract drift
|
|
1026
|
+
try {
|
|
1027
|
+
const contractDrifts = detectContractDrift(workspaceRoot, manifest);
|
|
1028
|
+
audit.metrics.contractDrifts = contractDrifts.length;
|
|
1029
|
+
|
|
1030
|
+
if (contractDrifts.length > 0) {
|
|
1031
|
+
const highSeverity = contractDrifts.filter(d => d.severity === 'high');
|
|
1032
|
+
if (highSeverity.length > 0) {
|
|
1033
|
+
audit.score -= highSeverity.length * 10;
|
|
1034
|
+
audit.findings.push({
|
|
1035
|
+
severity: 'critical',
|
|
1036
|
+
message: `${highSeverity.length} high-severity contract drift(s) — implementation doesn't match spec`
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
} catch (_err) {
|
|
1041
|
+
// Non-critical
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Clamp score
|
|
1045
|
+
audit.score = Math.max(0, Math.min(100, audit.score));
|
|
1046
|
+
|
|
1047
|
+
return audit;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// ============================================================
|
|
1051
|
+
// Exports
|
|
1052
|
+
// ============================================================
|
|
1053
|
+
|
|
583
1054
|
module.exports = {
|
|
584
1055
|
// S5: Cross-Repo Intelligence
|
|
585
1056
|
detectContractDrift,
|
|
@@ -587,6 +1058,10 @@ module.exports = {
|
|
|
587
1058
|
getSharedDecisions,
|
|
588
1059
|
addSharedDecision,
|
|
589
1060
|
|
|
1061
|
+
// Decision propagation (active broadcast)
|
|
1062
|
+
propagateDecision,
|
|
1063
|
+
getNewDecisionsSince,
|
|
1064
|
+
|
|
590
1065
|
// S7: N-Repo Scaling
|
|
591
1066
|
buildDependencyGraph,
|
|
592
1067
|
getLibraryConsumers,
|
|
@@ -596,5 +1071,15 @@ module.exports = {
|
|
|
596
1071
|
// S8: Cloud Preparation
|
|
597
1072
|
exportForDashboard,
|
|
598
1073
|
getMessageTransport,
|
|
599
|
-
getMultiUserSchema
|
|
1074
|
+
getMultiUserSchema,
|
|
1075
|
+
|
|
1076
|
+
// Review mode
|
|
1077
|
+
analyzeReviewForCrossRepoImpact,
|
|
1078
|
+
|
|
1079
|
+
// Morning briefing
|
|
1080
|
+
generateWorkspaceBriefing,
|
|
1081
|
+
formatWorkspaceBriefing,
|
|
1082
|
+
|
|
1083
|
+
// Audit dimension
|
|
1084
|
+
auditWorkspaceDimension
|
|
600
1085
|
};
|