okstra 0.130.4 → 0.131.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +2019 -96
- 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,182 +0,0 @@
|
|
|
1
|
-
# Video transcription using faster-whisper
|
|
2
|
-
# Converts video/audio files to text transcripts for graph extraction
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import os
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
VIDEO_EXTENSIONS = {'.mp4', '.mov', '.webm', '.mkv', '.avi', '.m4v', '.mp3', '.wav', '.m4a', '.ogg'}
|
|
10
|
-
URL_PREFIXES = ('http://', 'https://', 'www.')
|
|
11
|
-
|
|
12
|
-
_DEFAULT_MODEL = "base"
|
|
13
|
-
_TRANSCRIPTS_DIR = "graphify-out/transcripts"
|
|
14
|
-
_FALLBACK_PROMPT = "Use proper punctuation and paragraph breaks."
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _model_name() -> str:
|
|
18
|
-
return os.environ.get("GRAPHIFY_WHISPER_MODEL", _DEFAULT_MODEL)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def _get_whisper():
|
|
22
|
-
try:
|
|
23
|
-
from faster_whisper import WhisperModel
|
|
24
|
-
return WhisperModel
|
|
25
|
-
except ImportError as exc:
|
|
26
|
-
raise ImportError(
|
|
27
|
-
"Video transcription requires faster-whisper. "
|
|
28
|
-
"Run: pip install 'graphifyy[video]'"
|
|
29
|
-
) from exc
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _get_yt_dlp():
|
|
33
|
-
try:
|
|
34
|
-
import yt_dlp
|
|
35
|
-
return yt_dlp
|
|
36
|
-
except ImportError as exc:
|
|
37
|
-
raise ImportError(
|
|
38
|
-
"YouTube/URL download requires yt-dlp. "
|
|
39
|
-
"Run: pip install 'graphifyy[video]'"
|
|
40
|
-
) from exc
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def is_url(path: str) -> bool:
|
|
44
|
-
"""Return True if the string looks like a URL rather than a file path."""
|
|
45
|
-
return any(path.startswith(p) for p in URL_PREFIXES)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def download_audio(url: str, output_dir: Path) -> Path:
|
|
49
|
-
"""Download audio-only stream from a URL using yt-dlp.
|
|
50
|
-
|
|
51
|
-
Returns the path to the downloaded audio file (.m4a or .opus).
|
|
52
|
-
Uses cached file if already downloaded.
|
|
53
|
-
"""
|
|
54
|
-
yt_dlp = _get_yt_dlp()
|
|
55
|
-
output_dir.mkdir(parents=True, exist_ok=True)
|
|
56
|
-
|
|
57
|
-
# yt-dlp uses %(title)s which can be long/weird — use a stable name based on URL hash
|
|
58
|
-
import hashlib
|
|
59
|
-
url_hash = hashlib.sha1(url.encode()).hexdigest()[:12]
|
|
60
|
-
out_template = str(output_dir / f"yt_{url_hash}.%(ext)s")
|
|
61
|
-
|
|
62
|
-
# Check for already-downloaded file
|
|
63
|
-
for ext in ('.m4a', '.opus', '.mp3', '.ogg', '.wav', '.webm'):
|
|
64
|
-
candidate = output_dir / f"yt_{url_hash}{ext}"
|
|
65
|
-
if candidate.exists():
|
|
66
|
-
print(f" cached audio: {candidate.name}")
|
|
67
|
-
return candidate
|
|
68
|
-
|
|
69
|
-
ydl_opts = {
|
|
70
|
-
'format': 'bestaudio[ext=m4a]/bestaudio/best',
|
|
71
|
-
'outtmpl': out_template,
|
|
72
|
-
'quiet': True,
|
|
73
|
-
'no_warnings': True,
|
|
74
|
-
'noplaylist': True,
|
|
75
|
-
'postprocessors': [], # no ffmpeg needed — use native audio
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
print(f" downloading audio: {url[:80]} ...", flush=True)
|
|
79
|
-
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
|
80
|
-
info = ydl.extract_info(url, download=True)
|
|
81
|
-
ext = info.get('ext', 'm4a')
|
|
82
|
-
downloaded = output_dir / f"yt_{url_hash}.{ext}"
|
|
83
|
-
if not downloaded.exists():
|
|
84
|
-
# yt-dlp may have picked a different extension
|
|
85
|
-
for p in output_dir.glob(f"yt_{url_hash}.*"):
|
|
86
|
-
downloaded = p
|
|
87
|
-
break
|
|
88
|
-
return downloaded
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def build_whisper_prompt(god_nodes: list[dict]) -> str:
|
|
92
|
-
"""Build a domain hint for Whisper from god nodes extracted from the corpus.
|
|
93
|
-
|
|
94
|
-
Formats the top god node labels into a topic string for Whisper.
|
|
95
|
-
The coding agent (Claude Code, Codex, etc.) generates the actual one-sentence
|
|
96
|
-
domain hint from these labels and passes it via GRAPHIFY_WHISPER_PROMPT or
|
|
97
|
-
as initial_prompt — no separate API call needed here.
|
|
98
|
-
"""
|
|
99
|
-
if not god_nodes:
|
|
100
|
-
return _FALLBACK_PROMPT
|
|
101
|
-
|
|
102
|
-
override = os.environ.get("GRAPHIFY_WHISPER_PROMPT")
|
|
103
|
-
if override:
|
|
104
|
-
return override
|
|
105
|
-
|
|
106
|
-
labels = [n.get("label", "") for n in god_nodes[:10] if n.get("label")]
|
|
107
|
-
if not labels:
|
|
108
|
-
return _FALLBACK_PROMPT
|
|
109
|
-
|
|
110
|
-
topics = ", ".join(labels[:5])
|
|
111
|
-
return f"Technical discussion about {topics}. Use proper punctuation and paragraph breaks."
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def transcribe(
|
|
115
|
-
video_path: Path | str,
|
|
116
|
-
output_dir: Path | None = None,
|
|
117
|
-
initial_prompt: str | None = None,
|
|
118
|
-
force: bool = False,
|
|
119
|
-
) -> Path:
|
|
120
|
-
"""Transcribe a video/audio file or URL to a .txt transcript.
|
|
121
|
-
|
|
122
|
-
If video_path is a URL, audio is downloaded first via yt-dlp.
|
|
123
|
-
Returns the path to the saved transcript file.
|
|
124
|
-
Uses cached transcript if it exists unless force=True.
|
|
125
|
-
|
|
126
|
-
initial_prompt: domain hint for Whisper (built from corpus god nodes).
|
|
127
|
-
force: re-transcribe even if transcript already exists.
|
|
128
|
-
"""
|
|
129
|
-
out_dir = Path(output_dir) if output_dir else Path(_TRANSCRIPTS_DIR)
|
|
130
|
-
out_dir.mkdir(parents=True, exist_ok=True)
|
|
131
|
-
|
|
132
|
-
if is_url(str(video_path)):
|
|
133
|
-
audio_path = download_audio(str(video_path), out_dir / "downloads")
|
|
134
|
-
else:
|
|
135
|
-
audio_path = Path(video_path)
|
|
136
|
-
|
|
137
|
-
transcript_path = out_dir / (audio_path.stem + ".txt")
|
|
138
|
-
if transcript_path.exists() and not force:
|
|
139
|
-
return transcript_path
|
|
140
|
-
|
|
141
|
-
WhisperModel = _get_whisper()
|
|
142
|
-
model_name = _model_name()
|
|
143
|
-
prompt = initial_prompt or _FALLBACK_PROMPT
|
|
144
|
-
|
|
145
|
-
print(f" transcribing {audio_path.name} (model={model_name}) ...", flush=True)
|
|
146
|
-
model = WhisperModel(model_name, device="cpu", compute_type="int8")
|
|
147
|
-
segments, info = model.transcribe(
|
|
148
|
-
str(audio_path),
|
|
149
|
-
beam_size=5,
|
|
150
|
-
initial_prompt=prompt,
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
lines = [segment.text.strip() for segment in segments if segment.text.strip()]
|
|
154
|
-
transcript = "\n".join(lines)
|
|
155
|
-
|
|
156
|
-
transcript_path.write_text(transcript, encoding="utf-8")
|
|
157
|
-
lang = info.language if hasattr(info, "language") else "unknown"
|
|
158
|
-
print(f" transcript saved -> {transcript_path} (lang={lang}, {len(lines)} segments)")
|
|
159
|
-
return transcript_path
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def transcribe_all(
|
|
163
|
-
video_files: list[str],
|
|
164
|
-
output_dir: Path | None = None,
|
|
165
|
-
initial_prompt: str | None = None,
|
|
166
|
-
) -> list[str]:
|
|
167
|
-
"""Transcribe a list of video/audio files or URLs, return paths to transcript .txt files.
|
|
168
|
-
|
|
169
|
-
Already-transcribed files are returned from cache instantly.
|
|
170
|
-
initial_prompt is shared across all files — built once from corpus god nodes.
|
|
171
|
-
"""
|
|
172
|
-
if not video_files:
|
|
173
|
-
return []
|
|
174
|
-
|
|
175
|
-
transcript_paths = []
|
|
176
|
-
for vf in video_files:
|
|
177
|
-
try:
|
|
178
|
-
t = transcribe(vf, output_dir, initial_prompt=initial_prompt)
|
|
179
|
-
transcript_paths.append(str(t))
|
|
180
|
-
except Exception as exc:
|
|
181
|
-
print(f" warning: could not transcribe {vf}: {exc}")
|
|
182
|
-
return transcript_paths
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
# validate extraction JSON against the graphify schema before graph assembly
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
VALID_FILE_TYPES = {"code", "document", "paper", "image", "rationale"}
|
|
5
|
-
VALID_CONFIDENCES = {"EXTRACTED", "INFERRED", "AMBIGUOUS"}
|
|
6
|
-
REQUIRED_NODE_FIELDS = {"id", "label", "file_type", "source_file"}
|
|
7
|
-
REQUIRED_EDGE_FIELDS = {"source", "target", "relation", "confidence", "source_file"}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def validate_extraction(data: dict) -> list[str]:
|
|
11
|
-
"""
|
|
12
|
-
Validate an extraction JSON dict against the graphify schema.
|
|
13
|
-
Returns a list of error strings - empty list means valid.
|
|
14
|
-
"""
|
|
15
|
-
if not isinstance(data, dict):
|
|
16
|
-
return ["Extraction must be a JSON object"]
|
|
17
|
-
|
|
18
|
-
errors: list[str] = []
|
|
19
|
-
|
|
20
|
-
# Nodes
|
|
21
|
-
if "nodes" not in data:
|
|
22
|
-
errors.append("Missing required key 'nodes'")
|
|
23
|
-
elif not isinstance(data["nodes"], list):
|
|
24
|
-
errors.append("'nodes' must be a list")
|
|
25
|
-
else:
|
|
26
|
-
for i, node in enumerate(data["nodes"]):
|
|
27
|
-
if not isinstance(node, dict):
|
|
28
|
-
errors.append(f"Node {i} must be an object")
|
|
29
|
-
continue
|
|
30
|
-
for field in REQUIRED_NODE_FIELDS:
|
|
31
|
-
if field not in node:
|
|
32
|
-
errors.append(f"Node {i} (id={node.get('id', '?')!r}) missing required field '{field}'")
|
|
33
|
-
if "file_type" in node and node["file_type"] not in VALID_FILE_TYPES:
|
|
34
|
-
errors.append(
|
|
35
|
-
f"Node {i} (id={node.get('id', '?')!r}) has invalid file_type "
|
|
36
|
-
f"'{node['file_type']}' - must be one of {sorted(VALID_FILE_TYPES)}"
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
# Edges - accept "links" (NetworkX <= 3.1) as fallback for "edges"
|
|
40
|
-
edge_list = data.get("edges") if "edges" in data else data.get("links")
|
|
41
|
-
if edge_list is None:
|
|
42
|
-
errors.append("Missing required key 'edges'")
|
|
43
|
-
elif not isinstance(edge_list, list):
|
|
44
|
-
errors.append("'edges' must be a list")
|
|
45
|
-
else:
|
|
46
|
-
node_ids = {n["id"] for n in data.get("nodes", []) if isinstance(n, dict) and "id" in n}
|
|
47
|
-
for i, edge in enumerate(edge_list):
|
|
48
|
-
if not isinstance(edge, dict):
|
|
49
|
-
errors.append(f"Edge {i} must be an object")
|
|
50
|
-
continue
|
|
51
|
-
for field in REQUIRED_EDGE_FIELDS:
|
|
52
|
-
if field not in edge:
|
|
53
|
-
errors.append(f"Edge {i} missing required field '{field}'")
|
|
54
|
-
if "confidence" in edge and edge["confidence"] not in VALID_CONFIDENCES:
|
|
55
|
-
errors.append(
|
|
56
|
-
f"Edge {i} has invalid confidence '{edge['confidence']}' "
|
|
57
|
-
f"- must be one of {sorted(VALID_CONFIDENCES)}"
|
|
58
|
-
)
|
|
59
|
-
if "source" in edge and node_ids and edge["source"] not in node_ids:
|
|
60
|
-
errors.append(f"Edge {i} source '{edge['source']}' does not match any node id")
|
|
61
|
-
if "target" in edge and node_ids and edge["target"] not in node_ids:
|
|
62
|
-
errors.append(f"Edge {i} target '{edge['target']}' does not match any node id")
|
|
63
|
-
|
|
64
|
-
return errors
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def assert_valid(data: dict) -> None:
|
|
68
|
-
"""Raise ValueError with all errors if extraction is invalid."""
|
|
69
|
-
errors = validate_extraction(data)
|
|
70
|
-
if errors:
|
|
71
|
-
msg = f"Extraction JSON has {len(errors)} error(s):\n" + "\n".join(f" • {e}" for e in errors)
|
|
72
|
-
raise ValueError(msg)
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
# monitor a folder and auto-trigger --update when files change
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
import json
|
|
4
|
-
import sys
|
|
5
|
-
import time
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
from graphify.detect import CODE_EXTENSIONS, DOC_EXTENSIONS, PAPER_EXTENSIONS, IMAGE_EXTENSIONS
|
|
10
|
-
|
|
11
|
-
_WATCHED_EXTENSIONS = CODE_EXTENSIONS | DOC_EXTENSIONS | PAPER_EXTENSIONS | IMAGE_EXTENSIONS
|
|
12
|
-
_CODE_EXTENSIONS = CODE_EXTENSIONS
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _rebuild_code(watch_path: Path, *, follow_symlinks: bool = False) -> bool:
|
|
16
|
-
"""Re-run AST extraction + build + cluster + report for code files. No LLM needed.
|
|
17
|
-
|
|
18
|
-
Returns True on success, False on error.
|
|
19
|
-
"""
|
|
20
|
-
watch_path = watch_path.resolve()
|
|
21
|
-
try:
|
|
22
|
-
from graphify.extract import extract
|
|
23
|
-
from graphify.detect import detect
|
|
24
|
-
from graphify.build import build_from_json
|
|
25
|
-
from graphify.cluster import cluster, score_all
|
|
26
|
-
from graphify.analyze import god_nodes, surprising_connections, suggest_questions
|
|
27
|
-
from graphify.report import generate
|
|
28
|
-
from graphify.export import to_json, to_html
|
|
29
|
-
|
|
30
|
-
detected = detect(watch_path, follow_symlinks=follow_symlinks)
|
|
31
|
-
code_files = [Path(f) for f in detected['files']['code']]
|
|
32
|
-
|
|
33
|
-
if not code_files:
|
|
34
|
-
print("[graphify watch] No code files found - nothing to rebuild.")
|
|
35
|
-
return False
|
|
36
|
-
|
|
37
|
-
result = extract(code_files, cache_root=watch_path)
|
|
38
|
-
|
|
39
|
-
# Preserve semantic nodes/edges from a previous full run.
|
|
40
|
-
# AST-only rebuild replaces code nodes; doc/paper/image nodes are kept.
|
|
41
|
-
out = watch_path / "graphify-out"
|
|
42
|
-
existing_graph = out / "graph.json"
|
|
43
|
-
if existing_graph.exists():
|
|
44
|
-
try:
|
|
45
|
-
existing = json.loads(existing_graph.read_text(encoding="utf-8"))
|
|
46
|
-
code_ids = {n["id"] for n in existing.get("nodes", []) if n.get("file_type") == "code"}
|
|
47
|
-
sem_nodes = [n for n in existing.get("nodes", []) if n.get("file_type") != "code"]
|
|
48
|
-
sem_edges = [e for e in existing.get("links", existing.get("edges", []))
|
|
49
|
-
if e.get("confidence") in ("INFERRED", "AMBIGUOUS")
|
|
50
|
-
or (e.get("source") not in code_ids and e.get("target") not in code_ids)]
|
|
51
|
-
result = {
|
|
52
|
-
"nodes": result["nodes"] + sem_nodes,
|
|
53
|
-
"edges": result["edges"] + sem_edges,
|
|
54
|
-
"hyperedges": existing.get("hyperedges", []),
|
|
55
|
-
"input_tokens": 0,
|
|
56
|
-
"output_tokens": 0,
|
|
57
|
-
}
|
|
58
|
-
except Exception:
|
|
59
|
-
pass # corrupt graph.json - proceed with AST-only
|
|
60
|
-
|
|
61
|
-
detection = {
|
|
62
|
-
"files": {"code": [str(f) for f in code_files], "document": [], "paper": [], "image": []},
|
|
63
|
-
"total_files": len(code_files),
|
|
64
|
-
"total_words": detected.get("total_words", 0),
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
G = build_from_json(result)
|
|
68
|
-
communities = cluster(G)
|
|
69
|
-
cohesion = score_all(G, communities)
|
|
70
|
-
gods = god_nodes(G)
|
|
71
|
-
surprises = surprising_connections(G, communities)
|
|
72
|
-
labels = {cid: "Community " + str(cid) for cid in communities}
|
|
73
|
-
questions = suggest_questions(G, communities, labels)
|
|
74
|
-
|
|
75
|
-
out.mkdir(exist_ok=True)
|
|
76
|
-
|
|
77
|
-
report = generate(G, communities, cohesion, labels, gods, surprises, detection,
|
|
78
|
-
{"input": 0, "output": 0}, str(watch_path), suggested_questions=questions)
|
|
79
|
-
(out / "GRAPH_REPORT.md").write_text(report, encoding="utf-8")
|
|
80
|
-
to_json(G, communities, str(out / "graph.json"))
|
|
81
|
-
to_html(G, communities, str(out / "graph.html"), community_labels=labels or None)
|
|
82
|
-
|
|
83
|
-
# clear stale needs_update flag if present
|
|
84
|
-
flag = out / "needs_update"
|
|
85
|
-
if flag.exists():
|
|
86
|
-
flag.unlink()
|
|
87
|
-
|
|
88
|
-
print(f"[graphify watch] Rebuilt: {G.number_of_nodes()} nodes, "
|
|
89
|
-
f"{G.number_of_edges()} edges, {len(communities)} communities")
|
|
90
|
-
print(f"[graphify watch] graph.json, graph.html and GRAPH_REPORT.md updated in {out}")
|
|
91
|
-
return True
|
|
92
|
-
|
|
93
|
-
except Exception as exc:
|
|
94
|
-
print(f"[graphify watch] Rebuild failed: {exc}")
|
|
95
|
-
return False
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def _notify_only(watch_path: Path) -> None:
|
|
99
|
-
"""Write a flag file and print a notification (fallback for non-code-only corpora)."""
|
|
100
|
-
flag = watch_path / "graphify-out" / "needs_update"
|
|
101
|
-
flag.parent.mkdir(parents=True, exist_ok=True)
|
|
102
|
-
flag.write_text("1", encoding="utf-8")
|
|
103
|
-
print(f"\n[graphify watch] New or changed files detected in {watch_path}")
|
|
104
|
-
print("[graphify watch] Non-code files changed - semantic re-extraction requires LLM.")
|
|
105
|
-
print("[graphify watch] Run `/graphify --update` in Claude Code to update the graph.")
|
|
106
|
-
print(f"[graphify watch] Flag written to {flag}")
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _has_non_code(changed_paths: list[Path]) -> bool:
|
|
110
|
-
return any(p.suffix.lower() not in _CODE_EXTENSIONS for p in changed_paths)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def watch(watch_path: Path, debounce: float = 3.0) -> None:
|
|
114
|
-
"""
|
|
115
|
-
Watch watch_path for new or modified files and auto-update the graph.
|
|
116
|
-
|
|
117
|
-
For code-only changes: re-runs AST extraction + rebuild immediately (no LLM).
|
|
118
|
-
For doc/paper/image changes: writes a needs_update flag and notifies the user
|
|
119
|
-
to run /graphify --update (LLM extraction required).
|
|
120
|
-
|
|
121
|
-
debounce: seconds to wait after the last change before triggering (avoids
|
|
122
|
-
running on every keystroke when many files are saved at once).
|
|
123
|
-
"""
|
|
124
|
-
try:
|
|
125
|
-
from watchdog.observers import Observer
|
|
126
|
-
from watchdog.observers.polling import PollingObserver
|
|
127
|
-
from watchdog.events import FileSystemEventHandler
|
|
128
|
-
except ImportError as e:
|
|
129
|
-
raise ImportError("watchdog not installed. Run: pip install watchdog") from e
|
|
130
|
-
|
|
131
|
-
last_trigger: float = 0.0
|
|
132
|
-
pending: bool = False
|
|
133
|
-
changed: set[Path] = set()
|
|
134
|
-
|
|
135
|
-
class Handler(FileSystemEventHandler):
|
|
136
|
-
def on_any_event(self, event):
|
|
137
|
-
nonlocal last_trigger, pending
|
|
138
|
-
if event.is_directory:
|
|
139
|
-
return
|
|
140
|
-
path = Path(event.src_path)
|
|
141
|
-
if path.suffix.lower() not in _WATCHED_EXTENSIONS:
|
|
142
|
-
return
|
|
143
|
-
if any(part.startswith(".") for part in path.parts):
|
|
144
|
-
return
|
|
145
|
-
if "graphify-out" in path.parts:
|
|
146
|
-
return
|
|
147
|
-
last_trigger = time.monotonic()
|
|
148
|
-
pending = True
|
|
149
|
-
changed.add(path)
|
|
150
|
-
|
|
151
|
-
handler = Handler()
|
|
152
|
-
# Use polling observer on macOS — FSEvents can miss rapid saves in some editors
|
|
153
|
-
observer = PollingObserver() if sys.platform == "darwin" else Observer()
|
|
154
|
-
observer.schedule(handler, str(watch_path), recursive=True)
|
|
155
|
-
observer.start()
|
|
156
|
-
|
|
157
|
-
print(f"[graphify watch] Watching {watch_path.resolve()} - press Ctrl+C to stop")
|
|
158
|
-
print(f"[graphify watch] Code changes rebuild graph automatically. "
|
|
159
|
-
f"Doc/image changes require /graphify --update.")
|
|
160
|
-
print(f"[graphify watch] Debounce: {debounce}s")
|
|
161
|
-
|
|
162
|
-
try:
|
|
163
|
-
while True:
|
|
164
|
-
time.sleep(0.5)
|
|
165
|
-
if pending and (time.monotonic() - last_trigger) >= debounce:
|
|
166
|
-
pending = False
|
|
167
|
-
batch = list(changed)
|
|
168
|
-
changed.clear()
|
|
169
|
-
print(f"\n[graphify watch] {len(batch)} file(s) changed")
|
|
170
|
-
if _has_non_code(batch):
|
|
171
|
-
_notify_only(watch_path)
|
|
172
|
-
else:
|
|
173
|
-
_rebuild_code(watch_path)
|
|
174
|
-
except KeyboardInterrupt:
|
|
175
|
-
print("\n[graphify watch] Stopped.")
|
|
176
|
-
finally:
|
|
177
|
-
observer.stop()
|
|
178
|
-
observer.join()
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if __name__ == "__main__":
|
|
182
|
-
import argparse
|
|
183
|
-
parser = argparse.ArgumentParser(description="Watch a folder and auto-update the graphify graph")
|
|
184
|
-
parser.add_argument("path", nargs="?", default=".", help="Folder to watch (default: .)")
|
|
185
|
-
parser.add_argument("--debounce", type=float, default=3.0,
|
|
186
|
-
help="Seconds to wait after last change before updating (default: 3)")
|
|
187
|
-
args = parser.parse_args()
|
|
188
|
-
watch(Path(args.path), debounce=args.debounce)
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
# Wiki export - Wikipedia-style markdown articles from the knowledge graph
|
|
2
|
-
# Generates an agent-crawlable wiki: index.md + one article per community + god node articles
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
from collections import Counter
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
import networkx as nx
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def _safe_filename(name: str) -> str:
|
|
10
|
-
return name.replace("/", "-").replace(" ", "_").replace(":", "-")
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def _cross_community_links(G: nx.Graph, nodes: list[str], own_cid: int, labels: dict[int, str]) -> list[tuple[str, int]]:
|
|
14
|
-
"""Return (community_label, edge_count) pairs for cross-community connections, sorted descending."""
|
|
15
|
-
counts: dict[str, int] = Counter()
|
|
16
|
-
for nid in nodes:
|
|
17
|
-
for neighbor in G.neighbors(nid):
|
|
18
|
-
nd = G.nodes[neighbor]
|
|
19
|
-
ncid = nd.get("community")
|
|
20
|
-
if ncid is not None and ncid != own_cid:
|
|
21
|
-
counts[labels.get(ncid, f"Community {ncid}")] += 1
|
|
22
|
-
return sorted(counts.items(), key=lambda x: -x[1])
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _community_article(
|
|
26
|
-
G: nx.Graph,
|
|
27
|
-
cid: int,
|
|
28
|
-
nodes: list[str],
|
|
29
|
-
label: str,
|
|
30
|
-
labels: dict[int, str],
|
|
31
|
-
cohesion: float | None,
|
|
32
|
-
) -> str:
|
|
33
|
-
top_nodes = sorted(nodes, key=lambda n: G.degree(n), reverse=True)[:25]
|
|
34
|
-
cross = _cross_community_links(G, nodes, cid, labels)
|
|
35
|
-
|
|
36
|
-
# Edge confidence breakdown
|
|
37
|
-
conf_counts: Counter = Counter()
|
|
38
|
-
for nid in nodes:
|
|
39
|
-
for neighbor in G.neighbors(nid):
|
|
40
|
-
ed = G.edges[nid, neighbor]
|
|
41
|
-
conf_counts[ed.get("confidence", "EXTRACTED")] += 1
|
|
42
|
-
total_edges = sum(conf_counts.values()) or 1
|
|
43
|
-
|
|
44
|
-
sources = sorted({G.nodes[n].get("source_file", "") for n in nodes} - {""})
|
|
45
|
-
|
|
46
|
-
lines: list[str] = []
|
|
47
|
-
lines += [f"# {label}", ""]
|
|
48
|
-
|
|
49
|
-
meta_parts = [f"{len(nodes)} nodes"]
|
|
50
|
-
if cohesion is not None:
|
|
51
|
-
meta_parts.append(f"cohesion {cohesion:.2f}")
|
|
52
|
-
lines += [f"> {' · '.join(meta_parts)}", ""]
|
|
53
|
-
|
|
54
|
-
lines += ["## Key Concepts", ""]
|
|
55
|
-
for nid in top_nodes:
|
|
56
|
-
d = G.nodes[nid]
|
|
57
|
-
node_label = d.get("label", nid)
|
|
58
|
-
src = d.get("source_file", "")
|
|
59
|
-
degree = G.degree(nid)
|
|
60
|
-
src_str = f" — `{src}`" if src else ""
|
|
61
|
-
lines.append(f"- **{node_label}** ({degree} connections){src_str}")
|
|
62
|
-
remaining = len(nodes) - len(top_nodes)
|
|
63
|
-
if remaining > 0:
|
|
64
|
-
lines.append(f"- *... and {remaining} more nodes in this community*")
|
|
65
|
-
lines.append("")
|
|
66
|
-
|
|
67
|
-
lines += ["## Relationships", ""]
|
|
68
|
-
if cross:
|
|
69
|
-
for other_label, count in cross[:12]:
|
|
70
|
-
lines.append(f"- [[{other_label}]] ({count} shared connections)")
|
|
71
|
-
else:
|
|
72
|
-
lines.append("- No strong cross-community connections detected")
|
|
73
|
-
lines.append("")
|
|
74
|
-
|
|
75
|
-
if sources:
|
|
76
|
-
lines += ["## Source Files", ""]
|
|
77
|
-
for src in sources[:20]:
|
|
78
|
-
lines.append(f"- `{src}`")
|
|
79
|
-
lines.append("")
|
|
80
|
-
|
|
81
|
-
lines += ["## Audit Trail", ""]
|
|
82
|
-
for conf in ("EXTRACTED", "INFERRED", "AMBIGUOUS"):
|
|
83
|
-
n = conf_counts.get(conf, 0)
|
|
84
|
-
pct = round(n / total_edges * 100)
|
|
85
|
-
lines.append(f"- {conf}: {n} ({pct}%)")
|
|
86
|
-
lines.append("")
|
|
87
|
-
|
|
88
|
-
lines += ["---", "", "*Part of the graphify knowledge wiki. See [[index]] to navigate.*"]
|
|
89
|
-
return "\n".join(lines)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def _god_node_article(G: nx.Graph, nid: str, labels: dict[int, str]) -> str:
|
|
93
|
-
d = G.nodes[nid]
|
|
94
|
-
node_label = d.get("label", nid)
|
|
95
|
-
src = d.get("source_file", "")
|
|
96
|
-
cid = d.get("community")
|
|
97
|
-
community_name = labels.get(cid, f"Community {cid}") if cid is not None else None
|
|
98
|
-
|
|
99
|
-
lines: list[str] = []
|
|
100
|
-
lines += [f"# {node_label}", ""]
|
|
101
|
-
lines += [f"> God node · {G.degree(nid)} connections · `{src}`", ""]
|
|
102
|
-
|
|
103
|
-
if community_name:
|
|
104
|
-
lines += [f"**Community:** [[{community_name}]]", ""]
|
|
105
|
-
|
|
106
|
-
# Group neighbors by relation type
|
|
107
|
-
by_relation: dict[str, list[str]] = {}
|
|
108
|
-
for neighbor in sorted(G.neighbors(nid), key=lambda n: G.degree(n), reverse=True):
|
|
109
|
-
nd = G.nodes[neighbor]
|
|
110
|
-
ed = G.edges[nid, neighbor]
|
|
111
|
-
rel = ed.get("relation", "related")
|
|
112
|
-
neighbor_label = nd.get("label", neighbor)
|
|
113
|
-
conf = ed.get("confidence", "")
|
|
114
|
-
conf_str = f" `{conf}`" if conf else ""
|
|
115
|
-
by_relation.setdefault(rel, []).append(f"[[{neighbor_label}]]{conf_str}")
|
|
116
|
-
|
|
117
|
-
lines += ["## Connections by Relation", ""]
|
|
118
|
-
for rel, targets in sorted(by_relation.items()):
|
|
119
|
-
lines.append(f"### {rel}")
|
|
120
|
-
for t in targets[:20]:
|
|
121
|
-
lines.append(f"- {t}")
|
|
122
|
-
lines.append("")
|
|
123
|
-
|
|
124
|
-
lines += ["---", "", "*Part of the graphify knowledge wiki. See [[index]] to navigate.*"]
|
|
125
|
-
return "\n".join(lines)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def _index_md(
|
|
129
|
-
communities: dict[int, list[str]],
|
|
130
|
-
labels: dict[int, str],
|
|
131
|
-
god_nodes_data: list[dict],
|
|
132
|
-
total_nodes: int,
|
|
133
|
-
total_edges: int,
|
|
134
|
-
) -> str:
|
|
135
|
-
lines: list[str] = [
|
|
136
|
-
"# Knowledge Graph Index",
|
|
137
|
-
"",
|
|
138
|
-
"> Auto-generated by graphify. Start here — read community articles for context, then drill into god nodes for detail.",
|
|
139
|
-
"",
|
|
140
|
-
f"**{total_nodes} nodes · {total_edges} edges · {len(communities)} communities**",
|
|
141
|
-
"",
|
|
142
|
-
"---",
|
|
143
|
-
"",
|
|
144
|
-
"## Communities",
|
|
145
|
-
"(sorted by size, largest first)",
|
|
146
|
-
"",
|
|
147
|
-
]
|
|
148
|
-
|
|
149
|
-
for cid, nodes in sorted(communities.items(), key=lambda x: -len(x[1])):
|
|
150
|
-
label = labels.get(cid, f"Community {cid}")
|
|
151
|
-
lines.append(f"- [[{label}]] — {len(nodes)} nodes")
|
|
152
|
-
lines.append("")
|
|
153
|
-
|
|
154
|
-
if god_nodes_data:
|
|
155
|
-
lines += ["## God Nodes", "(most connected concepts — the load-bearing abstractions)", ""]
|
|
156
|
-
for node in god_nodes_data:
|
|
157
|
-
lines.append(f"- [[{node['label']}]] — {node['degree']} connections")
|
|
158
|
-
lines.append("")
|
|
159
|
-
|
|
160
|
-
lines += [
|
|
161
|
-
"---",
|
|
162
|
-
"",
|
|
163
|
-
"*Generated by [graphify](https://github.com/safishamsi/graphify)*",
|
|
164
|
-
]
|
|
165
|
-
return "\n".join(lines)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def to_wiki(
|
|
169
|
-
G: nx.Graph,
|
|
170
|
-
communities: dict[int, list[str]],
|
|
171
|
-
output_dir: str | Path,
|
|
172
|
-
community_labels: dict[int, str] | None = None,
|
|
173
|
-
cohesion: dict[int, float] | None = None,
|
|
174
|
-
god_nodes_data: list[dict] | None = None,
|
|
175
|
-
) -> int:
|
|
176
|
-
"""Generate a Wikipedia-style wiki from the graph.
|
|
177
|
-
|
|
178
|
-
Writes:
|
|
179
|
-
- index.md — agent entry point, catalog of all articles
|
|
180
|
-
- <CommunityName>.md — one article per community
|
|
181
|
-
- <GodNodeLabel>.md — one article per god node
|
|
182
|
-
|
|
183
|
-
Returns the number of articles written (excluding index.md).
|
|
184
|
-
"""
|
|
185
|
-
out = Path(output_dir)
|
|
186
|
-
out.mkdir(parents=True, exist_ok=True)
|
|
187
|
-
|
|
188
|
-
labels = community_labels or {cid: f"Community {cid}" for cid in communities}
|
|
189
|
-
cohesion = cohesion or {}
|
|
190
|
-
god_nodes_data = god_nodes_data or []
|
|
191
|
-
|
|
192
|
-
count = 0
|
|
193
|
-
|
|
194
|
-
# Community articles
|
|
195
|
-
for cid, nodes in communities.items():
|
|
196
|
-
label = labels.get(cid, f"Community {cid}")
|
|
197
|
-
article = _community_article(G, cid, nodes, label, labels, cohesion.get(cid))
|
|
198
|
-
(out / f"{_safe_filename(label)}.md").write_text(article)
|
|
199
|
-
count += 1
|
|
200
|
-
|
|
201
|
-
# God node articles
|
|
202
|
-
for node_data in god_nodes_data:
|
|
203
|
-
nid = node_data.get("id")
|
|
204
|
-
if nid and nid in G:
|
|
205
|
-
article = _god_node_article(G, nid, labels)
|
|
206
|
-
(out / f"{_safe_filename(node_data['label'])}.md").write_text(article)
|
|
207
|
-
count += 1
|
|
208
|
-
|
|
209
|
-
# Index
|
|
210
|
-
(out / "index.md").write_text(
|
|
211
|
-
_index_md(communities, labels, god_nodes_data, G.number_of_nodes(), G.number_of_edges())
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
return count
|