sneakoscope 8.3.2 → 8.4.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/README.md +1 -1
- package/config/architecture-map-policy.v1.json +168 -0
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/commands/bridge.js +19 -1
- package/dist/commands/doctor-profile.js +1 -1
- package/dist/commands/doctor.js +33 -1
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/align/align-route.js +8 -2
- package/dist/core/align/code-navigation-align.js +88 -32
- package/dist/core/architecture-map-pipeline.js +172 -0
- package/dist/core/architecture-map-review.js +180 -0
- package/dist/core/codex-app/codex-app-fast-ui-repair.js +2 -1
- package/dist/core/codex-app/menubar/constants.js +1 -0
- package/dist/core/codex-lb/codex-lb-env.js +25 -3
- package/dist/core/codex-lb/combined-catalog/builder.js +7 -2
- package/dist/core/codex-lb/combined-catalog/model-selection.js +71 -0
- package/dist/core/codex-lb/combined-catalog/normalize.js +20 -3
- package/dist/core/codex-lb/desktop-controller-v3/catalog.js +75 -4
- package/dist/core/codex-lb/desktop-controller-v3.js +5 -1
- package/dist/core/commands/triwiki-atlas-command.js +372 -0
- package/dist/core/commands/triwiki-command.js +12 -1
- package/dist/core/fsx.js +14 -0
- package/dist/core/git-hygiene/git-policy.js +4 -0
- package/dist/core/git-hygiene/gitignore-writer.js +2 -0
- package/dist/core/gx-renderer.js +2 -1
- package/dist/core/managed-paths.js +1 -0
- package/dist/core/perf/release-latency-slo.js +57 -11
- package/dist/core/pipeline-internals/pipeline-stage-builder.js +181 -0
- package/dist/core/pipeline-internals/runtime-core.js +13 -172
- package/dist/core/pipeline-internals/runtime-gates.js +12 -1
- package/dist/core/release/content-digest-cache.js +70 -0
- package/dist/core/release/gate-manifest.js +9 -0
- package/dist/core/release/main-push-guard.js +4 -3
- package/dist/core/release/package-dist-snapshot.js +18 -2
- package/dist/core/release/physical-release-gates.js +26 -3
- package/dist/core/release/release-authorization-snapshot.js +4 -0
- package/dist/core/release/release-gate-cache-v2.js +27 -3
- package/dist/core/release/release-gate-contract.js +2 -0
- package/dist/core/release/release-gate-dag.js +3 -0
- package/dist/core/release/release-inspection-memo.js +69 -0
- package/dist/core/release/release-pack-content-scanner.js +5 -0
- package/dist/core/release/release-pack-receipt.js +28 -18
- package/dist/core/release/release-pack-tarball.js +10 -0
- package/dist/core/release/release-upgrade-baseline.js +4 -0
- package/dist/core/routes.js +1 -1
- package/dist/core/safety/ssot-authority-inventory.js +114 -0
- package/dist/core/safety/ssot-guard.js +2 -44
- package/dist/core/triwiki/context-graph/architecture/baseline.js +81 -0
- package/dist/core/triwiki/context-graph/architecture/capsule.js +34 -0
- package/dist/core/triwiki/context-graph/architecture/contracts.js +67 -0
- package/dist/core/triwiki/context-graph/architecture/delta.js +149 -0
- package/dist/core/triwiki/context-graph/architecture/finding-factory.js +28 -0
- package/dist/core/triwiki/context-graph/architecture/findings.js +173 -0
- package/dist/core/triwiki/context-graph/architecture/fingerprint.js +75 -0
- package/dist/core/triwiki/context-graph/architecture/index.js +15 -0
- package/dist/core/triwiki/context-graph/architecture/input-bundle.js +66 -0
- package/dist/core/triwiki/context-graph/architecture/metrics.js +137 -0
- package/dist/core/triwiki/context-graph/architecture/policy.js +321 -0
- package/dist/core/triwiki/context-graph/architecture/slice.js +100 -0
- package/dist/core/triwiki/context-graph/architecture/ssot-analysis.js +59 -0
- package/dist/core/triwiki/context-graph/architecture/topology-overlay.js +106 -0
- package/dist/core/triwiki/context-graph/architecture/validation.js +145 -0
- package/dist/core/triwiki/context-graph/architecture/voxel-context.js +39 -0
- package/dist/core/triwiki/context-graph/extractors/index.js +4 -0
- package/dist/core/triwiki/context-graph/projections/index.js +2 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/architecture-delta.js +68 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/ast.js +49 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/change-impact.js +43 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/contracts.js +5 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/escape.js +45 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/ids.js +73 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/index.js +113 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/manifest.js +37 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/module-dependency.js +21 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/ownership-workstream.js +21 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/project-topology.js +13 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/public-surface.js +17 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/reduce.js +44 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/risk-domains.js +23 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/runtime-control.js +20 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/selection-ledger.js +94 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/serializer.js +81 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/ssot-provenance.js +17 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/verification-coverage.js +17 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/view-builder-common.js +1 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/view-builder.js +173 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/view-specs.js +33 -0
- package/dist/core/triwiki/context-graph/store/architecture-map-store.js +71 -0
- package/dist/core/triwiki/triwiki-proof-bank-index.js +26 -1
- package/dist/core/triwiki/triwiki-proof-bank.js +82 -2
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/OverviewViewController.swift +2 -2
- package/dist/native/sks-menubar/Sources/ProvidersModelExposure.swift +178 -0
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +10 -1
- package/dist/scripts/architecture-map-check.js +299 -0
- package/dist/scripts/lib/ensure-dist-fresh.js +48 -1
- package/dist/scripts/release-metadata-check.js +2 -0
- package/package.json +6 -3
- package/release-gates.v2.json +204 -0
- package/schemas/architecture-baseline.v1.schema.json +47 -0
- package/schemas/architecture-map-manifest.v1.schema.json +40 -0
- package/schemas/architecture-map-policy.v1.schema.json +147 -0
- package/schemas/architecture-review.v1.schema.json +46 -0
- package/schemas/mermaid-projection.v1.schema.json +47 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { byCodePoint } from './contracts.js';
|
|
2
|
+
import { findingId, makeFinding } from './finding-factory.js';
|
|
3
|
+
import { isAllowedLayerEdge, layerForModule } from './policy.js';
|
|
4
|
+
import { buildModuleDependencyGraph, computeSccs, sccKey } from './metrics.js';
|
|
5
|
+
import { mergeGraphWithTopology } from './topology-overlay.js';
|
|
6
|
+
import { canonicalSsotAuthorityInventory } from '../../../safety/ssot-authority-inventory.js';
|
|
7
|
+
import { analyzeSsotCollisions } from './ssot-analysis.js';
|
|
8
|
+
export { findingId, makeFinding };
|
|
9
|
+
const ANALYSIS_REQUIRED_KINDS = ['module'];
|
|
10
|
+
const ANALYSIS_REQUIRED_EDGES = ['imports'];
|
|
11
|
+
function severityFor(code, policy) {
|
|
12
|
+
return policy.blockingCodes.includes(code) ? 'blocking' : 'warning';
|
|
13
|
+
}
|
|
14
|
+
export function graphSufficiencyIssues(graph) {
|
|
15
|
+
const kinds = new Set(graph.nodes.map((node) => node.kind));
|
|
16
|
+
const edgeTypes = new Set(graph.edges.map((edge) => edge.type));
|
|
17
|
+
const missing = [];
|
|
18
|
+
for (const kind of ANALYSIS_REQUIRED_KINDS) {
|
|
19
|
+
if (!kinds.has(kind))
|
|
20
|
+
missing.push(`kind:${kind}`);
|
|
21
|
+
}
|
|
22
|
+
for (const type of ANALYSIS_REQUIRED_EDGES) {
|
|
23
|
+
if (!edgeTypes.has(type))
|
|
24
|
+
missing.push(`edge:${type}`);
|
|
25
|
+
}
|
|
26
|
+
if (!graph.nodes.length)
|
|
27
|
+
missing.push('nodes:empty');
|
|
28
|
+
return Object.freeze(missing.sort(byCodePoint));
|
|
29
|
+
}
|
|
30
|
+
function insufficientGraphFinding(policy, missing) {
|
|
31
|
+
return makeFinding({
|
|
32
|
+
code: 'insufficient_graph',
|
|
33
|
+
severity: severityFor('insufficient_graph', policy),
|
|
34
|
+
subjectIds: missing,
|
|
35
|
+
evidenceIds: missing,
|
|
36
|
+
ruleId: 'graph-sufficiency',
|
|
37
|
+
message: `Architecture graph lacks required kinds/edges for analysis: ${missing.join(', ')}`
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function detectNewAndExpandedCycles(input) {
|
|
41
|
+
const baselineKeys = new Map(input.baselineSccs.map((members) => [sccKey(members), members]));
|
|
42
|
+
const findings = [];
|
|
43
|
+
for (const members of input.afterSccs) {
|
|
44
|
+
const key = sccKey(members);
|
|
45
|
+
const prior = baselineKeys.get(key);
|
|
46
|
+
if (!prior) {
|
|
47
|
+
let expandedFrom = null;
|
|
48
|
+
for (const baseline of input.baselineSccs) {
|
|
49
|
+
if (baseline.every((id) => members.includes(id)) && members.length > baseline.length) {
|
|
50
|
+
expandedFrom = baseline;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (expandedFrom) {
|
|
55
|
+
findings.push(makeFinding({
|
|
56
|
+
code: 'cycle_expansion',
|
|
57
|
+
severity: severityFor('cycle_expansion', input.policy),
|
|
58
|
+
subjectIds: members,
|
|
59
|
+
evidenceIds: expandedFrom,
|
|
60
|
+
ruleId: 'cycle-expansion',
|
|
61
|
+
message: `Dependency cycle expanded from ${expandedFrom.length} to ${members.length} modules`
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
findings.push(makeFinding({
|
|
66
|
+
code: 'new_cycle',
|
|
67
|
+
severity: severityFor('new_cycle', input.policy),
|
|
68
|
+
subjectIds: members,
|
|
69
|
+
evidenceIds: members,
|
|
70
|
+
ruleId: 'new-cycle',
|
|
71
|
+
message: `New dependency cycle among ${members.length} modules`
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return findings.sort((left, right) => byCodePoint(left.id, right.id));
|
|
77
|
+
}
|
|
78
|
+
export function detectForbiddenDependencies(input) {
|
|
79
|
+
const findings = [];
|
|
80
|
+
for (const key of input.edgeKeys) {
|
|
81
|
+
const [from, to] = key.split('->');
|
|
82
|
+
if (!from || !to)
|
|
83
|
+
continue;
|
|
84
|
+
const fromLayer = layerForModule(input.policy, modulePathFromId(from));
|
|
85
|
+
const toLayer = layerForModule(input.policy, modulePathFromId(to));
|
|
86
|
+
if (!fromLayer || !toLayer)
|
|
87
|
+
continue;
|
|
88
|
+
if (!isAllowedLayerEdge(input.policy, fromLayer, toLayer)) {
|
|
89
|
+
findings.push(makeFinding({
|
|
90
|
+
code: 'forbidden_dependency',
|
|
91
|
+
severity: severityFor('forbidden_dependency', input.policy),
|
|
92
|
+
subjectIds: [from, to],
|
|
93
|
+
evidenceIds: [key],
|
|
94
|
+
ruleId: `${fromLayer}->${toLayer}`,
|
|
95
|
+
message: `Forbidden layer edge ${fromLayer} -> ${toLayer} via ${from} -> ${to}`
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return findings.sort((left, right) => byCodePoint(left.id, right.id));
|
|
100
|
+
}
|
|
101
|
+
function modulePathFromId(moduleId) {
|
|
102
|
+
return moduleId.startsWith('module:') ? moduleId.slice('module:'.length) : moduleId;
|
|
103
|
+
}
|
|
104
|
+
export function detectVerificationGaps(input) {
|
|
105
|
+
const protectedNodes = input.graph.nodes.filter((node) => node.risk === 'protected' || node.risk === 'high');
|
|
106
|
+
if (!protectedNodes.length)
|
|
107
|
+
return [];
|
|
108
|
+
const verifiedTargets = new Set();
|
|
109
|
+
for (const edge of input.graph.edges) {
|
|
110
|
+
if (edge.type === 'tests' || edge.type === 'verified_by' || edge.type === 'gated_by') {
|
|
111
|
+
verifiedTargets.add(edge.to);
|
|
112
|
+
verifiedTargets.add(edge.from);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const kinds = new Set(input.graph.nodes.map((node) => node.kind));
|
|
116
|
+
if (!kinds.has('test') && !kinds.has('gate') && !kinds.has('proof')) {
|
|
117
|
+
return [
|
|
118
|
+
makeFinding({
|
|
119
|
+
code: 'insufficient_graph',
|
|
120
|
+
severity: severityFor('insufficient_graph', input.policy),
|
|
121
|
+
subjectIds: ['kind:test', 'kind:gate', 'kind:proof'],
|
|
122
|
+
evidenceIds: protectedNodes.map((node) => node.id),
|
|
123
|
+
ruleId: 'verification-kinds',
|
|
124
|
+
message: 'Graph lacks test/gate/proof kinds required to evaluate verification gaps'
|
|
125
|
+
})
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
const findings = [];
|
|
129
|
+
for (const node of protectedNodes) {
|
|
130
|
+
if (!verifiedTargets.has(node.id)) {
|
|
131
|
+
findings.push(makeFinding({
|
|
132
|
+
code: 'protected_verification_gap',
|
|
133
|
+
severity: severityFor('protected_verification_gap', input.policy),
|
|
134
|
+
subjectIds: [node.id],
|
|
135
|
+
evidenceIds: [node.id],
|
|
136
|
+
ruleId: 'protected-verification-gap',
|
|
137
|
+
message: `Protected/high-risk node ${node.id} has no tests/gate/evidence path`
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return findings.sort((left, right) => byCodePoint(left.id, right.id));
|
|
142
|
+
}
|
|
143
|
+
export function analyzeArchitectureFindings(input) {
|
|
144
|
+
const missing = graphSufficiencyIssues(input.bundle.graph);
|
|
145
|
+
if (missing.length) {
|
|
146
|
+
return Object.freeze([insufficientGraphFinding(input.policy, missing)]);
|
|
147
|
+
}
|
|
148
|
+
const merged = mergeGraphWithTopology(input.bundle.graph, input.bundle.topology);
|
|
149
|
+
const moduleGraph = buildModuleDependencyGraph(merged.nodes, merged.edges);
|
|
150
|
+
const afterSccs = computeSccs(moduleGraph);
|
|
151
|
+
const baselineSccs = input.baselineSccs ?? [];
|
|
152
|
+
const findings = [
|
|
153
|
+
...detectNewAndExpandedCycles({
|
|
154
|
+
policy: input.policy,
|
|
155
|
+
baselineSccs,
|
|
156
|
+
afterSccs
|
|
157
|
+
}),
|
|
158
|
+
...detectForbiddenDependencies({
|
|
159
|
+
policy: input.policy,
|
|
160
|
+
moduleIds: moduleGraph.moduleIds,
|
|
161
|
+
edgeKeys: moduleGraph.edgeKeys
|
|
162
|
+
}),
|
|
163
|
+
...analyzeSsotCollisions({
|
|
164
|
+
policy: input.policy,
|
|
165
|
+
inventory: canonicalSsotAuthorityInventory()
|
|
166
|
+
}),
|
|
167
|
+
...detectVerificationGaps({
|
|
168
|
+
policy: input.policy,
|
|
169
|
+
graph: input.bundle.graph
|
|
170
|
+
})
|
|
171
|
+
];
|
|
172
|
+
return Object.freeze(findings.sort((left, right) => byCodePoint(left.id, right.id)));
|
|
173
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from '../../../json/canonical.js';
|
|
3
|
+
import { sha256 } from '../../../fsx.js';
|
|
4
|
+
import { byCodePoint } from './contracts.js';
|
|
5
|
+
export function hashCanonical(value) {
|
|
6
|
+
return sha256(canonicalJson(value));
|
|
7
|
+
}
|
|
8
|
+
export function hashWithoutKeys(value, omit) {
|
|
9
|
+
const copy = {};
|
|
10
|
+
for (const key of Object.keys(value).sort(byCodePoint)) {
|
|
11
|
+
if (omit.includes(key))
|
|
12
|
+
continue;
|
|
13
|
+
copy[key] = value[key];
|
|
14
|
+
}
|
|
15
|
+
return hashCanonical(copy);
|
|
16
|
+
}
|
|
17
|
+
export function sealBaselinePayload(canonicalPayloadHash) {
|
|
18
|
+
return sha256(`sks.architecture-baseline.v1\n${canonicalPayloadHash}`);
|
|
19
|
+
}
|
|
20
|
+
export function buildWorktreeFingerprint(input) {
|
|
21
|
+
const paths = [...input.paths].sort((a, b) => byCodePoint(a.path, b.path));
|
|
22
|
+
const fingerprintHash = hashCanonical({
|
|
23
|
+
rootId: input.rootId,
|
|
24
|
+
head: input.head,
|
|
25
|
+
paths
|
|
26
|
+
});
|
|
27
|
+
const dirtyPaths = Object.freeze(paths.filter((entry) => entry.kind !== 'missing').map((entry) => entry.path));
|
|
28
|
+
return Object.freeze({
|
|
29
|
+
rootId: input.rootId,
|
|
30
|
+
head: input.head,
|
|
31
|
+
repositoryHead: input.head,
|
|
32
|
+
paths: Object.freeze(paths),
|
|
33
|
+
fingerprintHash,
|
|
34
|
+
hash: fingerprintHash,
|
|
35
|
+
dirtyPaths
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function emptyWorktreeFingerprint(rootId, head = null) {
|
|
39
|
+
return buildWorktreeFingerprint({ rootId, head, paths: [] });
|
|
40
|
+
}
|
|
41
|
+
export function shortHash(value, length = 12) {
|
|
42
|
+
return createHash('sha256').update(value).digest('hex').slice(0, length);
|
|
43
|
+
}
|
|
44
|
+
export function diffWorktreePaths(before, after) {
|
|
45
|
+
const beforeMap = new Map(before.map((entry) => [entry.path, entry]));
|
|
46
|
+
const afterMap = new Map(after.map((entry) => [entry.path, entry]));
|
|
47
|
+
const paths = new Set([...beforeMap.keys(), ...afterMap.keys()]);
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const path of [...paths].sort(byCodePoint)) {
|
|
50
|
+
const left = beforeMap.get(path);
|
|
51
|
+
const right = afterMap.get(path);
|
|
52
|
+
if (!left && right) {
|
|
53
|
+
out.push({ path, change: 'added' });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (left && !right) {
|
|
57
|
+
out.push({ path, change: 'removed' });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!left || !right)
|
|
61
|
+
continue;
|
|
62
|
+
if (left.kind !== right.kind) {
|
|
63
|
+
out.push({ path, change: 'type' });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (left.sha256 !== right.sha256) {
|
|
67
|
+
out.push({ path, change: 'content' });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (left.executable !== right.executable) {
|
|
71
|
+
out.push({ path, change: 'mode' });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return Object.freeze(out);
|
|
75
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './contracts.js';
|
|
2
|
+
export * from './policy.js';
|
|
3
|
+
export * from './fingerprint.js';
|
|
4
|
+
export * from './input-bundle.js';
|
|
5
|
+
export * from './topology-overlay.js';
|
|
6
|
+
export * from './voxel-context.js';
|
|
7
|
+
export * from './slice.js';
|
|
8
|
+
export * from './metrics.js';
|
|
9
|
+
export * from './finding-factory.js';
|
|
10
|
+
export * from './findings.js';
|
|
11
|
+
export * from './ssot-analysis.js';
|
|
12
|
+
export * from './baseline.js';
|
|
13
|
+
export * from './delta.js';
|
|
14
|
+
export * from './capsule.js';
|
|
15
|
+
export * from './validation.js';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { canonicalSsotAuthorityInventory } from '../../../safety/ssot-authority-inventory.js';
|
|
2
|
+
import { ARCHITECTURE_ANALYZER_VERSION, ARCHITECTURE_INPUT_BUNDLE_SCHEMA, ARCHITECTURE_SERIALIZER_VERSION } from './contracts.js';
|
|
3
|
+
import { emptyWorktreeFingerprint, hashCanonical, hashWithoutKeys } from './fingerprint.js';
|
|
4
|
+
import { buildTopologyOverlayFromSnapshot, emptyTopologyOverlay, topologyOverlayHash } from './topology-overlay.js';
|
|
5
|
+
import { emptyVoxelContext, voxelContextHash } from './voxel-context.js';
|
|
6
|
+
export function policyContentHash(policy) {
|
|
7
|
+
return hashCanonical(policy);
|
|
8
|
+
}
|
|
9
|
+
export function buildArchitectureInputBundle(options) {
|
|
10
|
+
const topology = options.topologyMode === 'empty'
|
|
11
|
+
? emptyTopologyOverlay()
|
|
12
|
+
: buildTopologyOverlayFromSnapshot(options.graph);
|
|
13
|
+
const ssotInventory = canonicalSsotAuthorityInventory().map((domain) => Object.freeze({ id: domain.id }));
|
|
14
|
+
const voxelContext = emptyVoxelContext(options.profile ?? 'global');
|
|
15
|
+
const worktree = options.worktree ?? emptyWorktreeFingerprint(options.rootId);
|
|
16
|
+
const policyHash = options.policyHash ?? policyContentHash(options.policy);
|
|
17
|
+
const generatedAt = options.generatedAt ?? '1970-01-01T00:00:00.000Z';
|
|
18
|
+
const partial = {
|
|
19
|
+
schema: ARCHITECTURE_INPUT_BUNDLE_SCHEMA,
|
|
20
|
+
rootId: options.rootId,
|
|
21
|
+
graph: options.graph,
|
|
22
|
+
graphHash: options.graph.snapshotHash,
|
|
23
|
+
topology,
|
|
24
|
+
topologyHash: topologyOverlayHash(topology),
|
|
25
|
+
ssotInventory: Object.freeze(ssotInventory),
|
|
26
|
+
ssotInventoryHash: hashCanonical(ssotInventory),
|
|
27
|
+
voxelContext,
|
|
28
|
+
voxelContextHash: voxelContextHash(voxelContext),
|
|
29
|
+
worktree,
|
|
30
|
+
policyHash,
|
|
31
|
+
analyzerVersion: ARCHITECTURE_ANALYZER_VERSION,
|
|
32
|
+
serializerVersion: ARCHITECTURE_SERIALIZER_VERSION,
|
|
33
|
+
generatedAt
|
|
34
|
+
};
|
|
35
|
+
const canonicalHash = hashWithoutKeys({
|
|
36
|
+
schema: partial.schema,
|
|
37
|
+
rootId: partial.rootId,
|
|
38
|
+
graphHash: partial.graphHash,
|
|
39
|
+
topologyHash: partial.topologyHash,
|
|
40
|
+
ssotInventoryHash: partial.ssotInventoryHash,
|
|
41
|
+
voxelContextHash: partial.voxelContextHash,
|
|
42
|
+
worktree: partial.worktree.fingerprintHash,
|
|
43
|
+
policyHash: partial.policyHash,
|
|
44
|
+
analyzerVersion: partial.analyzerVersion,
|
|
45
|
+
serializerVersion: partial.serializerVersion
|
|
46
|
+
}, []);
|
|
47
|
+
const bundle = Object.freeze({
|
|
48
|
+
...partial,
|
|
49
|
+
canonicalHash
|
|
50
|
+
});
|
|
51
|
+
return bundle;
|
|
52
|
+
}
|
|
53
|
+
export function architectureInputBundleHash(bundle) {
|
|
54
|
+
return hashWithoutKeys({
|
|
55
|
+
schema: bundle.schema,
|
|
56
|
+
rootId: bundle.rootId,
|
|
57
|
+
graphHash: bundle.graphHash,
|
|
58
|
+
topologyHash: bundle.topologyHash,
|
|
59
|
+
ssotInventoryHash: bundle.ssotInventoryHash,
|
|
60
|
+
voxelContextHash: bundle.voxelContextHash,
|
|
61
|
+
worktree: bundle.worktree.fingerprintHash,
|
|
62
|
+
policyHash: bundle.policyHash,
|
|
63
|
+
analyzerVersion: bundle.analyzerVersion,
|
|
64
|
+
serializerVersion: bundle.serializerVersion
|
|
65
|
+
}, []);
|
|
66
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { byCodePoint } from './contracts.js';
|
|
2
|
+
import { mergeGraphWithTopology } from './topology-overlay.js';
|
|
3
|
+
export function buildModuleDependencyGraph(nodes, edges) {
|
|
4
|
+
const modules = nodes.filter((node) => node.kind === 'module').map((node) => node.id).sort(byCodePoint);
|
|
5
|
+
const moduleSet = new Set(modules);
|
|
6
|
+
const fileToModule = new Map();
|
|
7
|
+
for (const edge of edges) {
|
|
8
|
+
if (edge.type !== 'contains')
|
|
9
|
+
continue;
|
|
10
|
+
if (moduleSet.has(edge.from))
|
|
11
|
+
fileToModule.set(edge.to, edge.from);
|
|
12
|
+
}
|
|
13
|
+
const resolve = (id) => {
|
|
14
|
+
if (moduleSet.has(id))
|
|
15
|
+
return id;
|
|
16
|
+
return fileToModule.get(id) ?? null;
|
|
17
|
+
};
|
|
18
|
+
const adj = new Map();
|
|
19
|
+
const rev = new Map();
|
|
20
|
+
for (const id of modules) {
|
|
21
|
+
adj.set(id, new Set());
|
|
22
|
+
rev.set(id, new Set());
|
|
23
|
+
}
|
|
24
|
+
const edgeKeys = new Set();
|
|
25
|
+
for (const edge of edges) {
|
|
26
|
+
if (edge.type !== 'imports' && edge.type !== 'reexports' && edge.type !== 'depends_on')
|
|
27
|
+
continue;
|
|
28
|
+
const from = resolve(edge.from);
|
|
29
|
+
const to = resolve(edge.to);
|
|
30
|
+
if (!from || !to || from === to)
|
|
31
|
+
continue;
|
|
32
|
+
adj.get(from)?.add(to);
|
|
33
|
+
rev.get(to)?.add(from);
|
|
34
|
+
edgeKeys.add(`${from}->${to}`);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
moduleIds: Object.freeze(modules),
|
|
38
|
+
adjacency: new Map([...adj.entries()].map(([key, value]) => [key, Object.freeze([...value].sort(byCodePoint))])),
|
|
39
|
+
reverse: new Map([...rev.entries()].map(([key, value]) => [key, Object.freeze([...value].sort(byCodePoint))])),
|
|
40
|
+
edgeKeys: Object.freeze([...edgeKeys].sort(byCodePoint))
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function computeSccs(graph) {
|
|
44
|
+
let index = 0;
|
|
45
|
+
const indices = new Map();
|
|
46
|
+
const lowlink = new Map();
|
|
47
|
+
const onStack = new Set();
|
|
48
|
+
const stack = [];
|
|
49
|
+
const sccs = [];
|
|
50
|
+
const strongConnect = (node) => {
|
|
51
|
+
indices.set(node, index);
|
|
52
|
+
lowlink.set(node, index);
|
|
53
|
+
index += 1;
|
|
54
|
+
stack.push(node);
|
|
55
|
+
onStack.add(node);
|
|
56
|
+
for (const next of graph.adjacency.get(node) ?? []) {
|
|
57
|
+
if (!indices.has(next)) {
|
|
58
|
+
strongConnect(next);
|
|
59
|
+
lowlink.set(node, Math.min(lowlink.get(node) ?? 0, lowlink.get(next) ?? 0));
|
|
60
|
+
}
|
|
61
|
+
else if (onStack.has(next)) {
|
|
62
|
+
lowlink.set(node, Math.min(lowlink.get(node) ?? 0, indices.get(next) ?? 0));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (lowlink.get(node) === indices.get(node)) {
|
|
66
|
+
const component = [];
|
|
67
|
+
while (true) {
|
|
68
|
+
const item = stack.pop();
|
|
69
|
+
if (item === undefined)
|
|
70
|
+
break;
|
|
71
|
+
onStack.delete(item);
|
|
72
|
+
component.push(item);
|
|
73
|
+
if (item === node)
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
component.sort(byCodePoint);
|
|
77
|
+
if (component.length >= 2)
|
|
78
|
+
sccs.push(component);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
for (const node of graph.moduleIds) {
|
|
82
|
+
if (!indices.has(node))
|
|
83
|
+
strongConnect(node);
|
|
84
|
+
}
|
|
85
|
+
sccs.sort((left, right) => byCodePoint(left.join('\0'), right.join('\0')));
|
|
86
|
+
return Object.freeze(sccs.map((component) => Object.freeze(component)));
|
|
87
|
+
}
|
|
88
|
+
function percentile(sorted, p) {
|
|
89
|
+
if (!sorted.length)
|
|
90
|
+
return 0;
|
|
91
|
+
const rank = Math.min(sorted.length - 1, Math.max(0, Math.ceil(p * sorted.length) - 1));
|
|
92
|
+
return sorted[rank] ?? 0;
|
|
93
|
+
}
|
|
94
|
+
export function computeArchitectureMetrics(input) {
|
|
95
|
+
const merged = mergeGraphWithTopology(input.graph, input.topology ?? {
|
|
96
|
+
nodes: [],
|
|
97
|
+
edges: [],
|
|
98
|
+
sourceManifestHashes: {},
|
|
99
|
+
extractionErrors: []
|
|
100
|
+
});
|
|
101
|
+
const moduleGraph = buildModuleDependencyGraph(merged.nodes, merged.edges);
|
|
102
|
+
const sccs = computeSccs(moduleGraph);
|
|
103
|
+
const cyclic = new Set(sccs.flatMap((component) => component));
|
|
104
|
+
const fanOut = moduleGraph.moduleIds.map((id) => (moduleGraph.adjacency.get(id) ?? []).length).sort((a, b) => a - b);
|
|
105
|
+
const fanIn = moduleGraph.moduleIds.map((id) => (moduleGraph.reverse.get(id) ?? []).length).sort((a, b) => a - b);
|
|
106
|
+
const publicSurfaceCount = merged.nodes.filter((node) => node.kind === 'command' || node.kind === 'route' || node.kind === 'schema' ||
|
|
107
|
+
(typeof node.metadata.public === 'boolean' && node.metadata.public === true)).length;
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
nodeCount: merged.nodes.length,
|
|
110
|
+
edgeCount: merged.edges.length,
|
|
111
|
+
moduleCount: moduleGraph.moduleIds.length,
|
|
112
|
+
publicSurfaceCount,
|
|
113
|
+
sccCount: sccs.length,
|
|
114
|
+
cyclicNodeCount: cyclic.size,
|
|
115
|
+
largestSccSize: sccs.reduce((max, component) => Math.max(max, component.length), 0),
|
|
116
|
+
maxFanIn: fanIn.length ? fanIn[fanIn.length - 1] : 0,
|
|
117
|
+
maxFanOut: fanOut.length ? fanOut[fanOut.length - 1] : 0,
|
|
118
|
+
p95FanIn: percentile(fanIn, 0.95),
|
|
119
|
+
p95FanOut: percentile(fanOut, 0.95),
|
|
120
|
+
layerViolationCount: input.layerViolationCount ?? 0,
|
|
121
|
+
ssotCollisionCount: input.ssotCollisionCount ?? 0,
|
|
122
|
+
authorityBypassCount: input.authorityBypassCount ?? 0,
|
|
123
|
+
protectedVerificationGapCount: input.protectedVerificationGapCount ?? 0,
|
|
124
|
+
passthroughChainCount: input.passthroughChainCount ?? 0,
|
|
125
|
+
duplicateFacadeCount: input.duplicateFacadeCount ?? 0,
|
|
126
|
+
orphanEntrypointCount: input.orphanEntrypointCount ?? 0,
|
|
127
|
+
averageArchitectureDepth: null,
|
|
128
|
+
maxArchitectureDepth: null,
|
|
129
|
+
changedPathAccounting: null,
|
|
130
|
+
projectionAccounting: null,
|
|
131
|
+
eligibleExtractionSuccess: null,
|
|
132
|
+
unknownDynamicRelationCount: input.unknownDynamicRelationCount ?? 0
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
export function sccKey(members) {
|
|
136
|
+
return [...members].sort(byCodePoint).join('|');
|
|
137
|
+
}
|