okstra 0.130.4 → 0.131.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 +0 -1
- package/docs/architecture.md +2 -2
- package/docs/cli.md +1 -1
- package/docs/for-ai/README.md +0 -2
- package/docs/for-ai/skills/okstra-run.md +13 -2
- package/docs/for-ai/skills/okstra-setup.md +4 -1
- package/docs/project-structure-overview.md +5 -4
- package/docs/task-process/implementation-planning.md +13 -0
- package/docs/task-process/implementation.md +17 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +7 -0
- package/runtime/bin/lib/okstra/usage.sh +4 -4
- package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
- package/runtime/prompts/lead/plan-body-verification.md +26 -5
- package/runtime/prompts/lead/report-writer.md +6 -2
- package/runtime/prompts/profiles/_common-contract.md +2 -1
- package/runtime/prompts/profiles/_implementation-deliverable.md +6 -1
- package/runtime/prompts/profiles/_implementation-executor.md +2 -2
- package/runtime/prompts/profiles/_implementation-verifier.md +33 -10
- package/runtime/prompts/profiles/final-verification.md +21 -6
- package/runtime/prompts/profiles/implementation-planning.md +16 -4
- package/runtime/python/okstra_ctl/codex_dispatch.py +31 -0
- package/runtime/python/okstra_ctl/conformance.py +49 -11
- package/runtime/python/okstra_ctl/report_finalize.py +52 -1
- package/runtime/python/okstra_ctl/run.py +15 -3
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +14 -6
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +19 -1
- package/runtime/python/okstra_vendor/__init__.py +0 -44
- package/runtime/schemas/final-report-v1.0.schema.json +99 -5
- package/runtime/skills/okstra-run/SKILL.md +11 -4
- package/runtime/skills/okstra-setup/SKILL.md +3 -2
- package/runtime/skills/okstra-setup/references/project-config.md +16 -7
- package/runtime/templates/reports/final-report.template.md +15 -9
- package/runtime/templates/reports/i18n/en.json +1 -0
- package/runtime/templates/reports/i18n/ko.json +1 -0
- package/runtime/validators/lib/fixtures.sh +6 -0
- package/runtime/validators/validate-run.py +1722 -98
- package/src/cli-registry.mjs +0 -7
- package/src/commands/lifecycle/doctor.mjs +0 -9
- package/src/lib/skill-catalog.mjs +0 -1
- package/docs/for-ai/skills/okstra-graphify.md +0 -64
- package/runtime/python/okstra_ctl/graphify_cmd.py +0 -225
- package/runtime/python/okstra_vendor/graphify/.vendored-version +0 -1
- package/runtime/python/okstra_vendor/graphify/__init__.py +0 -28
- package/runtime/python/okstra_vendor/graphify/__main__.py +0 -1371
- package/runtime/python/okstra_vendor/graphify/analyze.py +0 -540
- package/runtime/python/okstra_vendor/graphify/benchmark.py +0 -129
- package/runtime/python/okstra_vendor/graphify/build.py +0 -107
- package/runtime/python/okstra_vendor/graphify/cache.py +0 -169
- package/runtime/python/okstra_vendor/graphify/cluster.py +0 -137
- package/runtime/python/okstra_vendor/graphify/detect.py +0 -510
- package/runtime/python/okstra_vendor/graphify/export.py +0 -1014
- package/runtime/python/okstra_vendor/graphify/extract.py +0 -3277
- package/runtime/python/okstra_vendor/graphify/hooks.py +0 -220
- package/runtime/python/okstra_vendor/graphify/ingest.py +0 -297
- package/runtime/python/okstra_vendor/graphify/manifest.py +0 -4
- package/runtime/python/okstra_vendor/graphify/report.py +0 -175
- package/runtime/python/okstra_vendor/graphify/security.py +0 -203
- package/runtime/python/okstra_vendor/graphify/serve.py +0 -373
- package/runtime/python/okstra_vendor/graphify/skill-aider.md +0 -1184
- package/runtime/python/okstra_vendor/graphify/skill-claw.md +0 -1184
- package/runtime/python/okstra_vendor/graphify/skill-codex.md +0 -1242
- package/runtime/python/okstra_vendor/graphify/skill-copilot.md +0 -1268
- package/runtime/python/okstra_vendor/graphify/skill-droid.md +0 -1239
- package/runtime/python/okstra_vendor/graphify/skill-kiro.md +0 -1183
- package/runtime/python/okstra_vendor/graphify/skill-opencode.md +0 -1238
- package/runtime/python/okstra_vendor/graphify/skill-trae.md +0 -1208
- package/runtime/python/okstra_vendor/graphify/skill-vscode.md +0 -253
- package/runtime/python/okstra_vendor/graphify/skill-windows.md +0 -1245
- package/runtime/python/okstra_vendor/graphify/skill.md +0 -1319
- package/runtime/python/okstra_vendor/graphify/transcribe.py +0 -182
- package/runtime/python/okstra_vendor/graphify/validate.py +0 -72
- package/runtime/python/okstra_vendor/graphify/watch.py +0 -188
- package/runtime/python/okstra_vendor/graphify/wiki.py +0 -214
- package/runtime/python/okstra_vendor/networkx/__init__.py +0 -62
- package/runtime/python/okstra_vendor/networkx/algorithms/__init__.py +0 -134
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/__init__.py +0 -26
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/clique.py +0 -259
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/clustering_coefficient.py +0 -71
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/connectivity.py +0 -412
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/density.py +0 -396
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/distance_measures.py +0 -150
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/dominating_set.py +0 -149
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/kcomponents.py +0 -369
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/matching.py +0 -44
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/maxcut.py +0 -143
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/ramsey.py +0 -53
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/steinertree.py +0 -265
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/traveling_salesman.py +0 -1508
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/treewidth.py +0 -255
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/vertex_cover.py +0 -83
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/__init__.py +0 -5
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/connectivity.py +0 -122
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/correlation.py +0 -302
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/mixing.py +0 -255
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/neighbor_degree.py +0 -160
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/pairs.py +0 -127
- package/runtime/python/okstra_vendor/networkx/algorithms/asteroidal.py +0 -164
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/__init__.py +0 -88
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/basic.py +0 -322
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/centrality.py +0 -290
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/cluster.py +0 -289
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/covering.py +0 -57
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/edgelist.py +0 -360
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/extendability.py +0 -105
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/generators.py +0 -603
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/link_analysis.py +0 -316
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/matching.py +0 -590
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/matrix.py +0 -232
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/projection.py +0 -526
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/redundancy.py +0 -112
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/spectral.py +0 -69
- package/runtime/python/okstra_vendor/networkx/algorithms/boundary.py +0 -168
- package/runtime/python/okstra_vendor/networkx/algorithms/bridges.py +0 -205
- package/runtime/python/okstra_vendor/networkx/algorithms/broadcasting.py +0 -164
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/__init__.py +0 -20
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/betweenness.py +0 -591
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/betweenness_subset.py +0 -236
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/closeness.py +0 -282
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/current_flow_betweenness.py +0 -364
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/current_flow_betweenness_subset.py +0 -227
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/current_flow_closeness.py +0 -96
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/degree_alg.py +0 -150
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/dispersion.py +0 -107
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/eigenvector.py +0 -357
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/flow_matrix.py +0 -130
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/group.py +0 -787
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/harmonic.py +0 -88
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/katz.py +0 -331
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/laplacian.py +0 -150
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/load.py +0 -200
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/percolation.py +0 -128
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/reaching.py +0 -209
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/second_order.py +0 -141
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/subgraph_alg.py +0 -361
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/trophic.py +0 -181
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/voterank_alg.py +0 -95
- package/runtime/python/okstra_vendor/networkx/algorithms/chains.py +0 -172
- package/runtime/python/okstra_vendor/networkx/algorithms/chordal.py +0 -443
- package/runtime/python/okstra_vendor/networkx/algorithms/clique.py +0 -818
- package/runtime/python/okstra_vendor/networkx/algorithms/cluster.py +0 -732
- package/runtime/python/okstra_vendor/networkx/algorithms/coloring/__init__.py +0 -4
- package/runtime/python/okstra_vendor/networkx/algorithms/coloring/equitable_coloring.py +0 -505
- package/runtime/python/okstra_vendor/networkx/algorithms/coloring/greedy_coloring.py +0 -565
- package/runtime/python/okstra_vendor/networkx/algorithms/communicability_alg.py +0 -163
- package/runtime/python/okstra_vendor/networkx/algorithms/community/__init__.py +0 -28
- package/runtime/python/okstra_vendor/networkx/algorithms/community/asyn_fluid.py +0 -153
- package/runtime/python/okstra_vendor/networkx/algorithms/community/bipartitions.py +0 -354
- package/runtime/python/okstra_vendor/networkx/algorithms/community/centrality.py +0 -171
- package/runtime/python/okstra_vendor/networkx/algorithms/community/community_utils.py +0 -30
- package/runtime/python/okstra_vendor/networkx/algorithms/community/divisive.py +0 -216
- package/runtime/python/okstra_vendor/networkx/algorithms/community/kclique.py +0 -79
- package/runtime/python/okstra_vendor/networkx/algorithms/community/label_propagation.py +0 -338
- package/runtime/python/okstra_vendor/networkx/algorithms/community/leiden.py +0 -162
- package/runtime/python/okstra_vendor/networkx/algorithms/community/local.py +0 -220
- package/runtime/python/okstra_vendor/networkx/algorithms/community/louvain.py +0 -384
- package/runtime/python/okstra_vendor/networkx/algorithms/community/lukes.py +0 -227
- package/runtime/python/okstra_vendor/networkx/algorithms/community/modularity_max.py +0 -452
- package/runtime/python/okstra_vendor/networkx/algorithms/community/quality.py +0 -347
- package/runtime/python/okstra_vendor/networkx/algorithms/components/__init__.py +0 -6
- package/runtime/python/okstra_vendor/networkx/algorithms/components/attracting.py +0 -115
- package/runtime/python/okstra_vendor/networkx/algorithms/components/biconnected.py +0 -394
- package/runtime/python/okstra_vendor/networkx/algorithms/components/connected.py +0 -282
- package/runtime/python/okstra_vendor/networkx/algorithms/components/semiconnected.py +0 -71
- package/runtime/python/okstra_vendor/networkx/algorithms/components/strongly_connected.py +0 -359
- package/runtime/python/okstra_vendor/networkx/algorithms/components/weakly_connected.py +0 -196
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/__init__.py +0 -11
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/connectivity.py +0 -811
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/cuts.py +0 -616
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/disjoint_paths.py +0 -408
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/edge_augmentation.py +0 -1270
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/edge_kcomponents.py +0 -592
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/kcomponents.py +0 -220
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/kcutsets.py +0 -235
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/stoerwagner.py +0 -152
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/utils.py +0 -88
- package/runtime/python/okstra_vendor/networkx/algorithms/core.py +0 -588
- package/runtime/python/okstra_vendor/networkx/algorithms/covering.py +0 -142
- package/runtime/python/okstra_vendor/networkx/algorithms/cuts.py +0 -416
- package/runtime/python/okstra_vendor/networkx/algorithms/cycles.py +0 -1234
- package/runtime/python/okstra_vendor/networkx/algorithms/d_separation.py +0 -677
- package/runtime/python/okstra_vendor/networkx/algorithms/dag.py +0 -1392
- package/runtime/python/okstra_vendor/networkx/algorithms/distance_measures.py +0 -1095
- package/runtime/python/okstra_vendor/networkx/algorithms/distance_regular.py +0 -272
- package/runtime/python/okstra_vendor/networkx/algorithms/dominance.py +0 -142
- package/runtime/python/okstra_vendor/networkx/algorithms/dominating.py +0 -268
- package/runtime/python/okstra_vendor/networkx/algorithms/efficiency_measures.py +0 -167
- package/runtime/python/okstra_vendor/networkx/algorithms/euler.py +0 -470
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/__init__.py +0 -11
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/boykovkolmogorov.py +0 -370
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/capacityscaling.py +0 -407
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/dinitz_alg.py +0 -238
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/edmondskarp.py +0 -241
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/gomory_hu.py +0 -178
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/maxflow.py +0 -611
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/mincost.py +0 -356
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/networksimplex.py +0 -662
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/preflowpush.py +0 -425
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/shortestaugmentingpath.py +0 -300
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/utils.py +0 -194
- package/runtime/python/okstra_vendor/networkx/algorithms/graph_hashing.py +0 -435
- package/runtime/python/okstra_vendor/networkx/algorithms/graphical.py +0 -483
- package/runtime/python/okstra_vendor/networkx/algorithms/hierarchy.py +0 -57
- package/runtime/python/okstra_vendor/networkx/algorithms/hybrid.py +0 -196
- package/runtime/python/okstra_vendor/networkx/algorithms/isolate.py +0 -107
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/__init__.py +0 -7
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/ismags.py +0 -1306
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/isomorph.py +0 -336
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/isomorphvf2.py +0 -1262
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/matchhelpers.py +0 -352
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/temporalisomorphvf2.py +0 -308
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tree_isomorphism.py +0 -264
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/vf2pp.py +0 -1102
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/vf2userfunc.py +0 -192
- package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/__init__.py +0 -2
- package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/hits_alg.py +0 -337
- package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/pagerank_alg.py +0 -498
- package/runtime/python/okstra_vendor/networkx/algorithms/link_prediction.py +0 -687
- package/runtime/python/okstra_vendor/networkx/algorithms/lowest_common_ancestors.py +0 -280
- package/runtime/python/okstra_vendor/networkx/algorithms/matching.py +0 -1148
- package/runtime/python/okstra_vendor/networkx/algorithms/minors/__init__.py +0 -27
- package/runtime/python/okstra_vendor/networkx/algorithms/minors/contraction.py +0 -738
- package/runtime/python/okstra_vendor/networkx/algorithms/mis.py +0 -78
- package/runtime/python/okstra_vendor/networkx/algorithms/moral.py +0 -59
- package/runtime/python/okstra_vendor/networkx/algorithms/node_classification.py +0 -219
- package/runtime/python/okstra_vendor/networkx/algorithms/non_randomness.py +0 -155
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/__init__.py +0 -4
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/all.py +0 -324
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/binary.py +0 -468
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/product.py +0 -633
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/unary.py +0 -77
- package/runtime/python/okstra_vendor/networkx/algorithms/perfect_graph.py +0 -73
- package/runtime/python/okstra_vendor/networkx/algorithms/planar_drawing.py +0 -464
- package/runtime/python/okstra_vendor/networkx/algorithms/planarity.py +0 -1463
- package/runtime/python/okstra_vendor/networkx/algorithms/polynomials.py +0 -306
- package/runtime/python/okstra_vendor/networkx/algorithms/reciprocity.py +0 -98
- package/runtime/python/okstra_vendor/networkx/algorithms/regular.py +0 -167
- package/runtime/python/okstra_vendor/networkx/algorithms/richclub.py +0 -138
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/__init__.py +0 -5
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/astar.py +0 -239
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/dense.py +0 -264
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/generic.py +0 -716
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/unweighted.py +0 -625
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/weighted.py +0 -2542
- package/runtime/python/okstra_vendor/networkx/algorithms/similarity.py +0 -2107
- package/runtime/python/okstra_vendor/networkx/algorithms/simple_paths.py +0 -966
- package/runtime/python/okstra_vendor/networkx/algorithms/smallworld.py +0 -404
- package/runtime/python/okstra_vendor/networkx/algorithms/smetric.py +0 -30
- package/runtime/python/okstra_vendor/networkx/algorithms/sparsifiers.py +0 -296
- package/runtime/python/okstra_vendor/networkx/algorithms/structuralholes.py +0 -374
- package/runtime/python/okstra_vendor/networkx/algorithms/summarization.py +0 -564
- package/runtime/python/okstra_vendor/networkx/algorithms/swap.py +0 -406
- package/runtime/python/okstra_vendor/networkx/algorithms/threshold.py +0 -981
- package/runtime/python/okstra_vendor/networkx/algorithms/time_dependent.py +0 -142
- package/runtime/python/okstra_vendor/networkx/algorithms/tournament.py +0 -406
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/__init__.py +0 -5
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/beamsearch.py +0 -90
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/breadth_first_search.py +0 -576
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/depth_first_search.py +0 -529
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/edgebfs.py +0 -185
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/edgedfs.py +0 -182
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/__init__.py +0 -7
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/branchings.py +0 -1042
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/coding.py +0 -413
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/decomposition.py +0 -88
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/distance_measures.py +0 -219
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/mst.py +0 -1281
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/operations.py +0 -106
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/recognition.py +0 -273
- package/runtime/python/okstra_vendor/networkx/algorithms/triads.py +0 -500
- package/runtime/python/okstra_vendor/networkx/algorithms/vitality.py +0 -76
- package/runtime/python/okstra_vendor/networkx/algorithms/voronoi.py +0 -86
- package/runtime/python/okstra_vendor/networkx/algorithms/walks.py +0 -77
- package/runtime/python/okstra_vendor/networkx/algorithms/wiener.py +0 -278
- package/runtime/python/okstra_vendor/networkx/classes/__init__.py +0 -13
- package/runtime/python/okstra_vendor/networkx/classes/coreviews.py +0 -435
- package/runtime/python/okstra_vendor/networkx/classes/digraph.py +0 -1363
- package/runtime/python/okstra_vendor/networkx/classes/filters.py +0 -95
- package/runtime/python/okstra_vendor/networkx/classes/function.py +0 -1549
- package/runtime/python/okstra_vendor/networkx/classes/graph.py +0 -2082
- package/runtime/python/okstra_vendor/networkx/classes/graphviews.py +0 -269
- package/runtime/python/okstra_vendor/networkx/classes/multidigraph.py +0 -977
- package/runtime/python/okstra_vendor/networkx/classes/multigraph.py +0 -1294
- package/runtime/python/okstra_vendor/networkx/classes/reportviews.py +0 -1447
- package/runtime/python/okstra_vendor/networkx/convert.py +0 -502
- package/runtime/python/okstra_vendor/networkx/convert_matrix.py +0 -1314
- package/runtime/python/okstra_vendor/networkx/drawing/__init__.py +0 -7
- package/runtime/python/okstra_vendor/networkx/drawing/layout.py +0 -2036
- package/runtime/python/okstra_vendor/networkx/drawing/nx_agraph.py +0 -470
- package/runtime/python/okstra_vendor/networkx/drawing/nx_latex.py +0 -570
- package/runtime/python/okstra_vendor/networkx/drawing/nx_pydot.py +0 -361
- package/runtime/python/okstra_vendor/networkx/drawing/nx_pylab.py +0 -2978
- package/runtime/python/okstra_vendor/networkx/exception.py +0 -131
- package/runtime/python/okstra_vendor/networkx/generators/__init__.py +0 -34
- package/runtime/python/okstra_vendor/networkx/generators/atlas.dat.gz +0 -0
- package/runtime/python/okstra_vendor/networkx/generators/atlas.py +0 -227
- package/runtime/python/okstra_vendor/networkx/generators/classic.py +0 -1091
- package/runtime/python/okstra_vendor/networkx/generators/cographs.py +0 -68
- package/runtime/python/okstra_vendor/networkx/generators/community.py +0 -1070
- package/runtime/python/okstra_vendor/networkx/generators/degree_seq.py +0 -886
- package/runtime/python/okstra_vendor/networkx/generators/directed.py +0 -572
- package/runtime/python/okstra_vendor/networkx/generators/duplication.py +0 -174
- package/runtime/python/okstra_vendor/networkx/generators/ego.py +0 -66
- package/runtime/python/okstra_vendor/networkx/generators/expanders.py +0 -499
- package/runtime/python/okstra_vendor/networkx/generators/geometric.py +0 -1037
- package/runtime/python/okstra_vendor/networkx/generators/harary_graph.py +0 -163
- package/runtime/python/okstra_vendor/networkx/generators/internet_as_graphs.py +0 -443
- package/runtime/python/okstra_vendor/networkx/generators/intersection.py +0 -125
- package/runtime/python/okstra_vendor/networkx/generators/interval_graph.py +0 -70
- package/runtime/python/okstra_vendor/networkx/generators/joint_degree_seq.py +0 -664
- package/runtime/python/okstra_vendor/networkx/generators/lattice.py +0 -405
- package/runtime/python/okstra_vendor/networkx/generators/line.py +0 -501
- package/runtime/python/okstra_vendor/networkx/generators/mycielski.py +0 -110
- package/runtime/python/okstra_vendor/networkx/generators/nonisomorphic_trees.py +0 -259
- package/runtime/python/okstra_vendor/networkx/generators/random_clustered.py +0 -117
- package/runtime/python/okstra_vendor/networkx/generators/random_graphs.py +0 -1416
- package/runtime/python/okstra_vendor/networkx/generators/small.py +0 -1070
- package/runtime/python/okstra_vendor/networkx/generators/social.py +0 -554
- package/runtime/python/okstra_vendor/networkx/generators/spectral_graph_forge.py +0 -120
- package/runtime/python/okstra_vendor/networkx/generators/stochastic.py +0 -54
- package/runtime/python/okstra_vendor/networkx/generators/sudoku.py +0 -131
- package/runtime/python/okstra_vendor/networkx/generators/time_series.py +0 -74
- package/runtime/python/okstra_vendor/networkx/generators/trees.py +0 -1070
- package/runtime/python/okstra_vendor/networkx/generators/triads.py +0 -94
- package/runtime/python/okstra_vendor/networkx/lazy_imports.py +0 -188
- package/runtime/python/okstra_vendor/networkx/linalg/__init__.py +0 -13
- package/runtime/python/okstra_vendor/networkx/linalg/algebraicconnectivity.py +0 -650
- package/runtime/python/okstra_vendor/networkx/linalg/attrmatrix.py +0 -466
- package/runtime/python/okstra_vendor/networkx/linalg/bethehessianmatrix.py +0 -77
- package/runtime/python/okstra_vendor/networkx/linalg/graphmatrix.py +0 -168
- package/runtime/python/okstra_vendor/networkx/linalg/laplacianmatrix.py +0 -512
- package/runtime/python/okstra_vendor/networkx/linalg/modularitymatrix.py +0 -166
- package/runtime/python/okstra_vendor/networkx/linalg/spectrum.py +0 -186
- package/runtime/python/okstra_vendor/networkx/readwrite/__init__.py +0 -17
- package/runtime/python/okstra_vendor/networkx/readwrite/adjlist.py +0 -330
- package/runtime/python/okstra_vendor/networkx/readwrite/edgelist.py +0 -489
- package/runtime/python/okstra_vendor/networkx/readwrite/gexf.py +0 -1084
- package/runtime/python/okstra_vendor/networkx/readwrite/gml.py +0 -879
- package/runtime/python/okstra_vendor/networkx/readwrite/graph6.py +0 -427
- package/runtime/python/okstra_vendor/networkx/readwrite/graphml.py +0 -1053
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/__init__.py +0 -19
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/adjacency.py +0 -156
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/cytoscape.py +0 -190
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/node_link.py +0 -261
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tree.py +0 -137
- package/runtime/python/okstra_vendor/networkx/readwrite/leda.py +0 -108
- package/runtime/python/okstra_vendor/networkx/readwrite/multiline_adjlist.py +0 -393
- package/runtime/python/okstra_vendor/networkx/readwrite/p2g.py +0 -113
- package/runtime/python/okstra_vendor/networkx/readwrite/pajek.py +0 -286
- package/runtime/python/okstra_vendor/networkx/readwrite/sparse6.py +0 -379
- package/runtime/python/okstra_vendor/networkx/readwrite/text.py +0 -851
- package/runtime/python/okstra_vendor/networkx/relabel.py +0 -285
- package/runtime/python/okstra_vendor/networkx/utils/__init__.py +0 -8
- package/runtime/python/okstra_vendor/networkx/utils/backends.py +0 -2171
- package/runtime/python/okstra_vendor/networkx/utils/configs.py +0 -396
- package/runtime/python/okstra_vendor/networkx/utils/decorators.py +0 -1233
- package/runtime/python/okstra_vendor/networkx/utils/heaps.py +0 -338
- package/runtime/python/okstra_vendor/networkx/utils/mapped_queue.py +0 -297
- package/runtime/python/okstra_vendor/networkx/utils/misc.py +0 -703
- package/runtime/python/okstra_vendor/networkx/utils/random_sequence.py +0 -198
- package/runtime/python/okstra_vendor/networkx/utils/rcm.py +0 -159
- package/runtime/python/okstra_vendor/networkx/utils/union_find.py +0 -106
- package/runtime/skills/okstra-graphify/SKILL.md +0 -169
- package/src/commands/graphify.mjs +0 -32
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
# assemble node+edge dicts into a NetworkX graph, preserving edge direction
|
|
2
|
-
#
|
|
3
|
-
# Node deduplication — three layers:
|
|
4
|
-
#
|
|
5
|
-
# 1. Within a file (AST): each extractor tracks a `seen_ids` set. A node ID is
|
|
6
|
-
# emitted at most once per file, so duplicate class/function definitions in
|
|
7
|
-
# the same source file are collapsed to the first occurrence.
|
|
8
|
-
#
|
|
9
|
-
# 2. Between files (build): NetworkX G.add_node() is idempotent — calling it
|
|
10
|
-
# twice with the same ID overwrites the attributes with the second call's
|
|
11
|
-
# values. Nodes are added in extraction order (AST first, then semantic),
|
|
12
|
-
# so if the same entity is extracted by both passes the semantic node
|
|
13
|
-
# silently overwrites the AST node. This is intentional: semantic nodes
|
|
14
|
-
# carry richer labels and cross-file context, while AST nodes have precise
|
|
15
|
-
# source_location. If you need to change the priority, reorder extractions
|
|
16
|
-
# passed to build().
|
|
17
|
-
#
|
|
18
|
-
# 3. Semantic merge (skill): before calling build(), the skill merges cached
|
|
19
|
-
# and new semantic results using an explicit `seen` set keyed on node["id"],
|
|
20
|
-
# so duplicates across cache hits and new extractions are resolved there
|
|
21
|
-
# before any graph construction happens.
|
|
22
|
-
#
|
|
23
|
-
from __future__ import annotations
|
|
24
|
-
import re
|
|
25
|
-
import sys
|
|
26
|
-
import networkx as nx
|
|
27
|
-
from .validate import validate_extraction
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _normalize_id(s: str) -> str:
|
|
31
|
-
"""Normalize an ID string the same way extract._make_id does.
|
|
32
|
-
|
|
33
|
-
Used to reconcile edge endpoints when the LLM generates IDs with slightly
|
|
34
|
-
different punctuation or casing than the AST extractor.
|
|
35
|
-
"""
|
|
36
|
-
cleaned = re.sub(r"[^a-zA-Z0-9]+", "_", s)
|
|
37
|
-
return cleaned.strip("_").lower()
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def build_from_json(extraction: dict, *, directed: bool = False) -> nx.Graph:
|
|
41
|
-
"""Build a NetworkX graph from an extraction dict.
|
|
42
|
-
|
|
43
|
-
directed=True produces a DiGraph that preserves edge direction (source→target).
|
|
44
|
-
directed=False (default) produces an undirected Graph for backward compatibility.
|
|
45
|
-
"""
|
|
46
|
-
# NetworkX <= 3.1 serialised edges as "links"; remap to "edges" for compatibility.
|
|
47
|
-
if "edges" not in extraction and "links" in extraction:
|
|
48
|
-
extraction = dict(extraction, edges=extraction["links"])
|
|
49
|
-
errors = validate_extraction(extraction)
|
|
50
|
-
# Dangling edges (stdlib/external imports) are expected - only warn about real schema errors.
|
|
51
|
-
real_errors = [e for e in errors if "does not match any node id" not in e]
|
|
52
|
-
if real_errors:
|
|
53
|
-
print(f"[graphify] Extraction warning ({len(real_errors)} issues): {real_errors[0]}", file=sys.stderr)
|
|
54
|
-
G: nx.Graph = nx.DiGraph() if directed else nx.Graph()
|
|
55
|
-
for node in extraction.get("nodes", []):
|
|
56
|
-
G.add_node(node["id"], **{k: v for k, v in node.items() if k != "id"})
|
|
57
|
-
node_set = set(G.nodes())
|
|
58
|
-
# Normalized ID map: lets edges survive when the LLM generates IDs with
|
|
59
|
-
# slightly different casing or punctuation than the AST extractor.
|
|
60
|
-
# e.g. "Session_ValidateToken" maps to "session_validatetoken".
|
|
61
|
-
norm_to_id: dict[str, str] = {_normalize_id(nid): nid for nid in node_set}
|
|
62
|
-
for edge in extraction.get("edges", []):
|
|
63
|
-
if "source" not in edge and "from" in edge:
|
|
64
|
-
edge["source"] = edge["from"]
|
|
65
|
-
if "target" not in edge and "to" in edge:
|
|
66
|
-
edge["target"] = edge["to"]
|
|
67
|
-
if "source" not in edge or "target" not in edge:
|
|
68
|
-
continue
|
|
69
|
-
src, tgt = edge["source"], edge["target"]
|
|
70
|
-
# Remap mismatched IDs via normalization before dropping the edge.
|
|
71
|
-
if src not in node_set:
|
|
72
|
-
src = norm_to_id.get(_normalize_id(src), src)
|
|
73
|
-
if tgt not in node_set:
|
|
74
|
-
tgt = norm_to_id.get(_normalize_id(tgt), tgt)
|
|
75
|
-
if src not in node_set or tgt not in node_set:
|
|
76
|
-
continue # skip edges to external/stdlib nodes - expected, not an error
|
|
77
|
-
attrs = {k: v for k, v in edge.items() if k not in ("source", "target")}
|
|
78
|
-
# Preserve original edge direction - undirected graphs lose it otherwise,
|
|
79
|
-
# causing display functions to show edges backwards.
|
|
80
|
-
attrs["_src"] = src
|
|
81
|
-
attrs["_tgt"] = tgt
|
|
82
|
-
G.add_edge(src, tgt, **attrs)
|
|
83
|
-
hyperedges = extraction.get("hyperedges", [])
|
|
84
|
-
if hyperedges:
|
|
85
|
-
G.graph["hyperedges"] = hyperedges
|
|
86
|
-
return G
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def build(extractions: list[dict], *, directed: bool = False) -> nx.Graph:
|
|
90
|
-
"""Merge multiple extraction results into one graph.
|
|
91
|
-
|
|
92
|
-
directed=True produces a DiGraph that preserves edge direction (source→target).
|
|
93
|
-
directed=False (default) produces an undirected Graph for backward compatibility.
|
|
94
|
-
|
|
95
|
-
Extractions are merged in order. For nodes with the same ID, the last
|
|
96
|
-
extraction's attributes win (NetworkX add_node overwrites). Pass AST
|
|
97
|
-
results before semantic results so semantic labels take precedence, or
|
|
98
|
-
reverse the order if you prefer AST source_location precision to win.
|
|
99
|
-
"""
|
|
100
|
-
combined: dict = {"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 0, "output_tokens": 0}
|
|
101
|
-
for ext in extractions:
|
|
102
|
-
combined["nodes"].extend(ext.get("nodes", []))
|
|
103
|
-
combined["edges"].extend(ext.get("edges", []))
|
|
104
|
-
combined["hyperedges"].extend(ext.get("hyperedges", []))
|
|
105
|
-
combined["input_tokens"] += ext.get("input_tokens", 0)
|
|
106
|
-
combined["output_tokens"] += ext.get("output_tokens", 0)
|
|
107
|
-
return build_from_json(combined, directed=directed)
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
# per-file extraction cache - skip unchanged files on re-run
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import hashlib
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _body_content(content: bytes) -> bytes:
|
|
11
|
-
"""Strip YAML frontmatter from Markdown content, returning only the body."""
|
|
12
|
-
text = content.decode(errors="replace")
|
|
13
|
-
if text.startswith("---"):
|
|
14
|
-
end = text.find("\n---", 3)
|
|
15
|
-
if end != -1:
|
|
16
|
-
return text[end + 4:].encode()
|
|
17
|
-
return content
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def file_hash(path: Path, root: Path = Path(".")) -> str:
|
|
21
|
-
"""SHA256 of file contents + path relative to root.
|
|
22
|
-
|
|
23
|
-
Using a relative path (not absolute) makes cache entries portable across
|
|
24
|
-
machines and checkout directories, so shared caches and CI work correctly.
|
|
25
|
-
Falls back to the resolved absolute path if the file is outside root.
|
|
26
|
-
|
|
27
|
-
For Markdown files (.md), only the body below the YAML frontmatter is hashed,
|
|
28
|
-
so metadata-only changes (e.g. reviewed, status, tags) do not invalidate the cache.
|
|
29
|
-
"""
|
|
30
|
-
p = Path(path)
|
|
31
|
-
raw = p.read_bytes()
|
|
32
|
-
content = _body_content(raw) if p.suffix.lower() == ".md" else raw
|
|
33
|
-
h = hashlib.sha256()
|
|
34
|
-
h.update(content)
|
|
35
|
-
h.update(b"\x00")
|
|
36
|
-
try:
|
|
37
|
-
rel = p.resolve().relative_to(Path(root).resolve())
|
|
38
|
-
h.update(str(rel).encode())
|
|
39
|
-
except ValueError:
|
|
40
|
-
h.update(str(p.resolve()).encode())
|
|
41
|
-
return h.hexdigest()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def cache_dir(root: Path = Path(".")) -> Path:
|
|
45
|
-
"""Returns graphify-out/cache/ - creates it if needed."""
|
|
46
|
-
d = Path(root).resolve() / "graphify-out" / "cache"
|
|
47
|
-
d.mkdir(parents=True, exist_ok=True)
|
|
48
|
-
return d
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def load_cached(path: Path, root: Path = Path(".")) -> dict | None:
|
|
52
|
-
"""Return cached extraction for this file if hash matches, else None.
|
|
53
|
-
|
|
54
|
-
Cache key: SHA256 of file contents.
|
|
55
|
-
Cache value: stored as graphify-out/cache/{hash}.json
|
|
56
|
-
Returns None if no cache entry or file has changed.
|
|
57
|
-
"""
|
|
58
|
-
try:
|
|
59
|
-
h = file_hash(path, root)
|
|
60
|
-
except OSError:
|
|
61
|
-
return None
|
|
62
|
-
entry = cache_dir(root) / f"{h}.json"
|
|
63
|
-
if not entry.exists():
|
|
64
|
-
return None
|
|
65
|
-
try:
|
|
66
|
-
return json.loads(entry.read_text(encoding="utf-8"))
|
|
67
|
-
except (json.JSONDecodeError, OSError):
|
|
68
|
-
return None
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def save_cached(path: Path, result: dict, root: Path = Path(".")) -> None:
|
|
72
|
-
"""Save extraction result for this file.
|
|
73
|
-
|
|
74
|
-
Stores as graphify-out/cache/{hash}.json where hash = SHA256 of current file contents.
|
|
75
|
-
result should be a dict with 'nodes' and 'edges' lists.
|
|
76
|
-
"""
|
|
77
|
-
h = file_hash(path, root)
|
|
78
|
-
entry = cache_dir(root) / f"{h}.json"
|
|
79
|
-
tmp = entry.with_suffix(".tmp")
|
|
80
|
-
try:
|
|
81
|
-
tmp.write_text(json.dumps(result), encoding="utf-8")
|
|
82
|
-
try:
|
|
83
|
-
os.replace(tmp, entry)
|
|
84
|
-
except PermissionError:
|
|
85
|
-
# Windows: os.replace can fail with WinError 5 if the target is
|
|
86
|
-
# briefly locked. Fall back to copy-then-delete.
|
|
87
|
-
import shutil
|
|
88
|
-
shutil.copy2(tmp, entry)
|
|
89
|
-
tmp.unlink(missing_ok=True)
|
|
90
|
-
except Exception:
|
|
91
|
-
tmp.unlink(missing_ok=True)
|
|
92
|
-
raise
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def cached_files(root: Path = Path(".")) -> set[str]:
|
|
96
|
-
"""Return set of file paths that have a valid cache entry (hash still matches)."""
|
|
97
|
-
d = cache_dir(root)
|
|
98
|
-
return {p.stem for p in d.glob("*.json")}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def clear_cache(root: Path = Path(".")) -> None:
|
|
102
|
-
"""Delete all graphify-out/cache/*.json files."""
|
|
103
|
-
d = cache_dir(root)
|
|
104
|
-
for f in d.glob("*.json"):
|
|
105
|
-
f.unlink()
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def check_semantic_cache(
|
|
109
|
-
files: list[str],
|
|
110
|
-
root: Path = Path("."),
|
|
111
|
-
) -> tuple[list[dict], list[dict], list[dict], list[str]]:
|
|
112
|
-
"""Check semantic extraction cache for a list of absolute file paths.
|
|
113
|
-
|
|
114
|
-
Returns (cached_nodes, cached_edges, cached_hyperedges, uncached_files).
|
|
115
|
-
Uncached files need Claude extraction; cached files are merged directly.
|
|
116
|
-
"""
|
|
117
|
-
cached_nodes: list[dict] = []
|
|
118
|
-
cached_edges: list[dict] = []
|
|
119
|
-
cached_hyperedges: list[dict] = []
|
|
120
|
-
uncached: list[str] = []
|
|
121
|
-
|
|
122
|
-
for fpath in files:
|
|
123
|
-
result = load_cached(Path(fpath), root)
|
|
124
|
-
if result is not None:
|
|
125
|
-
cached_nodes.extend(result.get("nodes", []))
|
|
126
|
-
cached_edges.extend(result.get("edges", []))
|
|
127
|
-
cached_hyperedges.extend(result.get("hyperedges", []))
|
|
128
|
-
else:
|
|
129
|
-
uncached.append(fpath)
|
|
130
|
-
|
|
131
|
-
return cached_nodes, cached_edges, cached_hyperedges, uncached
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def save_semantic_cache(
|
|
135
|
-
nodes: list[dict],
|
|
136
|
-
edges: list[dict],
|
|
137
|
-
hyperedges: list[dict] | None = None,
|
|
138
|
-
root: Path = Path("."),
|
|
139
|
-
) -> int:
|
|
140
|
-
"""Save semantic extraction results to cache, keyed by source_file.
|
|
141
|
-
|
|
142
|
-
Groups nodes and edges by source_file, then saves one cache entry per file.
|
|
143
|
-
Returns the number of files cached.
|
|
144
|
-
"""
|
|
145
|
-
from collections import defaultdict
|
|
146
|
-
|
|
147
|
-
by_file: dict[str, dict] = defaultdict(lambda: {"nodes": [], "edges": [], "hyperedges": []})
|
|
148
|
-
for n in nodes:
|
|
149
|
-
src = n.get("source_file", "")
|
|
150
|
-
if src:
|
|
151
|
-
by_file[src]["nodes"].append(n)
|
|
152
|
-
for e in edges:
|
|
153
|
-
src = e.get("source_file", "")
|
|
154
|
-
if src:
|
|
155
|
-
by_file[src]["edges"].append(e)
|
|
156
|
-
for h in (hyperedges or []):
|
|
157
|
-
src = h.get("source_file", "")
|
|
158
|
-
if src:
|
|
159
|
-
by_file[src]["hyperedges"].append(h)
|
|
160
|
-
|
|
161
|
-
saved = 0
|
|
162
|
-
for fpath, result in by_file.items():
|
|
163
|
-
p = Path(fpath)
|
|
164
|
-
if not p.is_absolute():
|
|
165
|
-
p = Path(root) / p
|
|
166
|
-
if p.exists():
|
|
167
|
-
save_cached(p, result, root)
|
|
168
|
-
saved += 1
|
|
169
|
-
return saved
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
"""Community detection on NetworkX graphs. Uses Leiden (graspologic) if available, falls back to Louvain (networkx). Splits oversized communities. Returns cohesion scores."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
import contextlib
|
|
4
|
-
import inspect
|
|
5
|
-
import io
|
|
6
|
-
import sys
|
|
7
|
-
import networkx as nx
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _suppress_output():
|
|
11
|
-
"""Context manager to suppress stdout/stderr during library calls.
|
|
12
|
-
|
|
13
|
-
graspologic's leiden() emits ANSI escape sequences (progress bars,
|
|
14
|
-
colored warnings) that corrupt PowerShell 5.1's scroll buffer on
|
|
15
|
-
Windows (see issue #19). Redirecting stdout/stderr to devnull during
|
|
16
|
-
the call prevents this without losing any graphify output.
|
|
17
|
-
"""
|
|
18
|
-
return contextlib.redirect_stdout(io.StringIO())
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def _partition(G: nx.Graph) -> dict[str, int]:
|
|
22
|
-
"""Run community detection. Returns {node_id: community_id}.
|
|
23
|
-
|
|
24
|
-
Tries Leiden (graspologic) first — best quality.
|
|
25
|
-
Falls back to Louvain (built into networkx) if graspologic is not installed.
|
|
26
|
-
|
|
27
|
-
Output from graspologic is suppressed to prevent ANSI escape codes
|
|
28
|
-
from corrupting terminal scroll buffers on Windows PowerShell 5.1.
|
|
29
|
-
"""
|
|
30
|
-
try:
|
|
31
|
-
from graspologic.partition import leiden
|
|
32
|
-
# Suppress graspologic output to prevent ANSI escape codes from
|
|
33
|
-
# corrupting PowerShell 5.1 scroll buffer (issue #19)
|
|
34
|
-
old_stderr = sys.stderr
|
|
35
|
-
try:
|
|
36
|
-
sys.stderr = io.StringIO()
|
|
37
|
-
with _suppress_output():
|
|
38
|
-
result = leiden(G)
|
|
39
|
-
finally:
|
|
40
|
-
sys.stderr = old_stderr
|
|
41
|
-
return result
|
|
42
|
-
except ImportError:
|
|
43
|
-
pass
|
|
44
|
-
|
|
45
|
-
# Fallback: networkx louvain (available since networkx 2.7).
|
|
46
|
-
# Inspect kwargs to stay compatible across NetworkX versions — max_level
|
|
47
|
-
# was added in a later release and prevents hangs on large sparse graphs.
|
|
48
|
-
kwargs: dict = {"seed": 42, "threshold": 1e-4}
|
|
49
|
-
if "max_level" in inspect.signature(nx.community.louvain_communities).parameters:
|
|
50
|
-
kwargs["max_level"] = 10
|
|
51
|
-
communities = nx.community.louvain_communities(G, **kwargs)
|
|
52
|
-
return {node: cid for cid, nodes in enumerate(communities) for node in nodes}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
_MAX_COMMUNITY_FRACTION = 0.25 # communities larger than 25% of graph get split
|
|
56
|
-
_MIN_SPLIT_SIZE = 10 # only split if community has at least this many nodes
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def cluster(G: nx.Graph) -> dict[int, list[str]]:
|
|
60
|
-
"""Run Leiden community detection. Returns {community_id: [node_ids]}.
|
|
61
|
-
|
|
62
|
-
Community IDs are stable across runs: 0 = largest community after splitting.
|
|
63
|
-
Oversized communities (> 25% of graph nodes, min 10) are split by running
|
|
64
|
-
a second Leiden pass on the subgraph.
|
|
65
|
-
|
|
66
|
-
Accepts directed or undirected graphs. DiGraphs are converted to undirected
|
|
67
|
-
internally since Louvain/Leiden require undirected input.
|
|
68
|
-
"""
|
|
69
|
-
if G.number_of_nodes() == 0:
|
|
70
|
-
return {}
|
|
71
|
-
if G.is_directed():
|
|
72
|
-
G = G.to_undirected()
|
|
73
|
-
if G.number_of_edges() == 0:
|
|
74
|
-
return {i: [n] for i, n in enumerate(sorted(G.nodes))}
|
|
75
|
-
|
|
76
|
-
# Leiden warns and drops isolates - handle them separately
|
|
77
|
-
isolates = [n for n in G.nodes() if G.degree(n) == 0]
|
|
78
|
-
connected_nodes = [n for n in G.nodes() if G.degree(n) > 0]
|
|
79
|
-
connected = G.subgraph(connected_nodes)
|
|
80
|
-
|
|
81
|
-
raw: dict[int, list[str]] = {}
|
|
82
|
-
if connected.number_of_nodes() > 0:
|
|
83
|
-
partition = _partition(connected)
|
|
84
|
-
for node, cid in partition.items():
|
|
85
|
-
raw.setdefault(cid, []).append(node)
|
|
86
|
-
|
|
87
|
-
# Each isolate becomes its own single-node community
|
|
88
|
-
next_cid = max(raw.keys(), default=-1) + 1
|
|
89
|
-
for node in isolates:
|
|
90
|
-
raw[next_cid] = [node]
|
|
91
|
-
next_cid += 1
|
|
92
|
-
|
|
93
|
-
# Split oversized communities
|
|
94
|
-
max_size = max(_MIN_SPLIT_SIZE, int(G.number_of_nodes() * _MAX_COMMUNITY_FRACTION))
|
|
95
|
-
final_communities: list[list[str]] = []
|
|
96
|
-
for nodes in raw.values():
|
|
97
|
-
if len(nodes) > max_size:
|
|
98
|
-
final_communities.extend(_split_community(G, nodes))
|
|
99
|
-
else:
|
|
100
|
-
final_communities.append(nodes)
|
|
101
|
-
|
|
102
|
-
# Re-index by size descending for deterministic ordering
|
|
103
|
-
final_communities.sort(key=len, reverse=True)
|
|
104
|
-
return {i: sorted(nodes) for i, nodes in enumerate(final_communities)}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def _split_community(G: nx.Graph, nodes: list[str]) -> list[list[str]]:
|
|
108
|
-
"""Run a second Leiden pass on a community subgraph to split it further."""
|
|
109
|
-
subgraph = G.subgraph(nodes)
|
|
110
|
-
if subgraph.number_of_edges() == 0:
|
|
111
|
-
# No edges - split into individual nodes
|
|
112
|
-
return [[n] for n in sorted(nodes)]
|
|
113
|
-
try:
|
|
114
|
-
sub_partition = _partition(subgraph)
|
|
115
|
-
sub_communities: dict[int, list[str]] = {}
|
|
116
|
-
for node, cid in sub_partition.items():
|
|
117
|
-
sub_communities.setdefault(cid, []).append(node)
|
|
118
|
-
if len(sub_communities) <= 1:
|
|
119
|
-
return [sorted(nodes)]
|
|
120
|
-
return [sorted(v) for v in sub_communities.values()]
|
|
121
|
-
except Exception:
|
|
122
|
-
return [sorted(nodes)]
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def cohesion_score(G: nx.Graph, community_nodes: list[str]) -> float:
|
|
126
|
-
"""Ratio of actual intra-community edges to maximum possible."""
|
|
127
|
-
n = len(community_nodes)
|
|
128
|
-
if n <= 1:
|
|
129
|
-
return 1.0
|
|
130
|
-
subgraph = G.subgraph(community_nodes)
|
|
131
|
-
actual = subgraph.number_of_edges()
|
|
132
|
-
possible = n * (n - 1) / 2
|
|
133
|
-
return round(actual / possible, 2) if possible > 0 else 0.0
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def score_all(G: nx.Graph, communities: dict[int, list[str]]) -> dict[int, float]:
|
|
137
|
-
return {cid: cohesion_score(G, nodes) for cid, nodes in communities.items()}
|