Skip to content

Commit 972f332

Browse files
committed
refactor(algorithms): split graph-gen analyzer.ts into modular structure
Break monolithic analyzer.ts (2,201 lines) into 12 focused modules: - core-props.ts: Vertex/edge cardinality, identity, ordering, data - connectivity.ts: Connected, acyclic, bipartite, degree constraints - structure.ts: Completeness, partiteness, density - advanced-props.ts: Embedding, rooting, temporal, layering, ports, semantics - spectral.ts: Scale-free, small-world, community structure - path-props.ts: Hamiltonian, traceable properties - advanced-structures.ts: Perfect, split, cograph, threshold, regular graphs - predicates.ts: Runtime type guard functions (isTree, isBipartite, etc.) - main.ts: Main API and computeGraphSpecFromGraph - types.ts: Core types (AnalyzerGraph, ComputePolicy, etc.) - helpers.ts: Helper functions (buildAdjUndirectedBinary, etc.) - index.ts: Barrel file with clean re-exports Rename files for better naming: - analyzer-types.ts → types.ts (redundant prefix removed) - analyzer-helpers.ts → helpers.ts (redundant prefix removed) Improves maintainability and reduces cognitive load. All typecheck, test, and build validation passing.
1 parent daca795 commit 972f332

33 files changed

+2736
-2352
lines changed

packages/graph-gen/src/analyzer-helpers.ts

Lines changed: 0 additions & 29 deletions
This file was deleted.

packages/graph-gen/src/analyzer.ts

Lines changed: 0 additions & 2201 deletions
This file was deleted.
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/**
2+
* Graph Spec Analyzer - Advanced Properties
3+
*
4+
* Compute advanced graph properties including embedding, rooting, temporal,
5+
* layering, edge ordering, ports, observability, operational semantics,
6+
* and measure semantics.
7+
*/
8+
9+
import type {
10+
AnalyzerGraph,
11+
ComputePolicy
12+
} from './types';
13+
import {
14+
unique
15+
} from './types';
16+
17+
export const computeEmbedding = (g: AnalyzerGraph, policy: ComputePolicy): { kind: "abstract" } | { kind: "geometric_metric_space" } | { kind: "spatial_coordinates"; dims: 2 | 3 } => {
18+
// Convention: if every vertex has pos {x,y} or {x,y,z}, treat as spatial_coordinates.
19+
const poss = g.vertices.map(v => v.attrs?.[policy.posKey]);
20+
const allHavePos = poss.length > 0 && poss.every(p => typeof p === "object" && p != null);
21+
if (!allHavePos) return { kind: "abstract" };
22+
23+
const dims = poss.map(p => {
24+
const o = p as Record<string, unknown>;
25+
const hasX = typeof o.x === "number";
26+
const hasY = typeof o.y === "number";
27+
const hasZ = typeof o.z === "number";
28+
if (hasX && hasY && hasZ) return 3 as const;
29+
if (hasX && hasY) return 2 as const;
30+
return null;
31+
});
32+
33+
if (dims.some(d => d == null)) return { kind: "abstract" };
34+
const uniq = unique(dims as Array<2 | 3>);
35+
if (uniq.length === 1) return { kind: "spatial_coordinates", dims: uniq[0] };
36+
// mixed dims -> fall back
37+
return { kind: "abstract" };
38+
};
39+
40+
export const computeRooting = (g: AnalyzerGraph, policy: ComputePolicy): { kind: "unrooted" } | { kind: "rooted" } | { kind: "multi_rooted" } => {
41+
// Convention:
42+
// - rooted if exactly one vertex has attrs[rootKey] === true
43+
// - multi_rooted if >1
44+
// - unrooted otherwise
45+
const roots = g.vertices.filter(v => v.attrs?.[policy.rootKey] === true);
46+
if (roots.length === 1) return { kind: "rooted" };
47+
if (roots.length > 1) return { kind: "multi_rooted" };
48+
return { kind: "unrooted" };
49+
};
50+
51+
export const computeTemporal = (g: AnalyzerGraph, policy: ComputePolicy):
52+
| { kind: "static" }
53+
| { kind: "dynamic_structure" }
54+
| { kind: "temporal_edges" }
55+
| { kind: "temporal_vertices" }
56+
| { kind: "time_ordered" } => {
57+
// Convention:
58+
// - temporal_vertices if any vertex has attrs[timeKey]
59+
// - temporal_edges if any edge has attrs[timeKey]
60+
// - time_ordered if both and values look ordered (numbers)
61+
// - static otherwise
62+
const vTimes = g.vertices.map(v => v.attrs?.[policy.timeKey]);
63+
const eTimes = g.edges.map(e => e.attrs?.[policy.timeKey]);
64+
const anyV = vTimes.some(t => t != null);
65+
const anyE = eTimes.some(t => t != null);
66+
67+
const allNumericV = anyV && vTimes.every(t => t == null || typeof t === "number");
68+
const allNumericE = anyE && eTimes.every(t => t == null || typeof t === "number");
69+
70+
if (anyV && anyE && allNumericV && allNumericE) return { kind: "time_ordered" };
71+
if (anyV) return { kind: "temporal_vertices" };
72+
if (anyE) return { kind: "temporal_edges" };
73+
return { kind: "static" };
74+
};
75+
76+
export const computeLayering = (g: AnalyzerGraph, policy: ComputePolicy):
77+
| { kind: "single_layer" }
78+
| { kind: "multi_layer" }
79+
| { kind: "multiplex" }
80+
| { kind: "interdependent" } => {
81+
// Convention:
82+
// - multi_layer if vertices or edges have a layer label and >1 unique layers
83+
const layers: Array<string | number> = [];
84+
for (const v of g.vertices) {
85+
const lv = v.attrs?.[policy.layerKey];
86+
if (typeof lv === "string" || typeof lv === "number") layers.push(lv);
87+
}
88+
for (const e of g.edges) {
89+
const le = e.attrs?.[policy.layerKey];
90+
if (typeof le === "string" || typeof le === "number") layers.push(le);
91+
}
92+
const uniq = unique(layers.map(String));
93+
return uniq.length > 1 ? { kind: "multi_layer" } : { kind: "single_layer" };
94+
};
95+
96+
export const computeEdgeOrdering = (g: AnalyzerGraph, policy: ComputePolicy): { kind: "unordered" } | { kind: "ordered" } => {
97+
const orders = g.edges.map(e => e.attrs?.[policy.edgeOrderKey]);
98+
if (!orders.every(x => typeof x === "number")) return { kind: "unordered" };
99+
return { kind: "ordered" };
100+
};
101+
102+
export const computePorts = (g: AnalyzerGraph, policy: ComputePolicy): { kind: "none" } | { kind: "port_labelled_vertices" } => {
103+
// Convention: vertex.attrs[portKey] exists => ports
104+
const anyPorts = g.vertices.some(v => v.attrs?.[policy.portKey] != null);
105+
return anyPorts ? { kind: "port_labelled_vertices" } : { kind: "none" };
106+
};
107+
108+
export const computeObservability = (g: AnalyzerGraph): { kind: "fully_specified" } | { kind: "partially_observed" } | { kind: "latent_or_inferred" } => {
109+
// Convention: if any vertex/edge has attrs.latent===true => latent_or_inferred
110+
// else if any has attrs.observed===false => partially_observed
111+
// else fully_specified
112+
const anyLatent =
113+
g.vertices.some(v => v.attrs?.["latent"] === true) ||
114+
g.edges.some(e => e.attrs?.["latent"] === true);
115+
116+
if (anyLatent) return { kind: "latent_or_inferred" };
117+
118+
const anyUnobserved =
119+
g.vertices.some(v => v.attrs?.["observed"] === false) ||
120+
g.edges.some(e => e.attrs?.["observed"] === false);
121+
122+
if (anyUnobserved) return { kind: "partially_observed" };
123+
124+
return { kind: "fully_specified" };
125+
};
126+
127+
export const computeOperationalSemantics = (g: AnalyzerGraph):
128+
| { kind: "structural_only" }
129+
| { kind: "annotated_with_functions" }
130+
| { kind: "executable" } => {
131+
// Convention: if any vertex/edge has attrs.exec===true => executable
132+
// else if any has attrs.fn present => annotated_with_functions
133+
// else structural_only
134+
const anyExec =
135+
g.vertices.some(v => v.attrs?.["exec"] === true) ||
136+
g.edges.some(e => e.attrs?.["exec"] === true);
137+
if (anyExec) return { kind: "executable" };
138+
139+
const anyFn =
140+
g.vertices.some(v => typeof v.attrs?.["fn"] === "string") ||
141+
g.edges.some(e => typeof e.attrs?.["fn"] === "string");
142+
if (anyFn) return { kind: "annotated_with_functions" };
143+
144+
return { kind: "structural_only" };
145+
};
146+
147+
export const computeMeasureSemantics = (g: AnalyzerGraph): { kind: "none" } | { kind: "metric" } | { kind: "cost" } | { kind: "utility" } => {
148+
// Convention: if weights exist => metric/cost/utility is unknown; choose "metric"
149+
// If attrs.cost exists => cost; attrs.utility exists => utility
150+
const anyCost =
151+
g.edges.some(e => typeof e.attrs?.["cost"] === "number") ||
152+
g.vertices.some(v => typeof v.attrs?.["cost"] === "number");
153+
if (anyCost) return { kind: "cost" };
154+
155+
const anyUtility =
156+
g.edges.some(e => typeof e.attrs?.["utility"] === "number") ||
157+
g.vertices.some(v => typeof v.attrs?.["utility"] === "number");
158+
if (anyUtility) return { kind: "utility" };
159+
160+
const anyWeight =
161+
g.edges.some(e => typeof e.weight === "number") ||
162+
g.edges.some(e => Array.isArray(e.attrs?.["weightVector"]));
163+
if (anyWeight) return { kind: "metric" };
164+
165+
return { kind: "none" };
166+
};

0 commit comments

Comments
 (0)