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,175 +0,0 @@
|
|
|
1
|
-
# generate GRAPH_REPORT.md - the human-readable audit trail
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
import re
|
|
4
|
-
from datetime import date
|
|
5
|
-
import networkx as nx
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def _safe_community_name(label: str) -> str:
|
|
9
|
-
"""Mirrors export.safe_name so community hub filenames and report wikilinks always agree."""
|
|
10
|
-
cleaned = re.sub(r'[\\/*?:"<>|#^[\]]', "", label.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")).strip()
|
|
11
|
-
cleaned = re.sub(r"\.(md|mdx|markdown)$", "", cleaned, flags=re.IGNORECASE)
|
|
12
|
-
return cleaned or "unnamed"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def generate(
|
|
16
|
-
G: nx.Graph,
|
|
17
|
-
communities: dict[int, list[str]],
|
|
18
|
-
cohesion_scores: dict[int, float],
|
|
19
|
-
community_labels: dict[int, str],
|
|
20
|
-
god_node_list: list[dict],
|
|
21
|
-
surprise_list: list[dict],
|
|
22
|
-
detection_result: dict,
|
|
23
|
-
token_cost: dict,
|
|
24
|
-
root: str,
|
|
25
|
-
suggested_questions: list[dict] | None = None,
|
|
26
|
-
) -> str:
|
|
27
|
-
today = date.today().isoformat()
|
|
28
|
-
|
|
29
|
-
confidences = [d.get("confidence", "EXTRACTED") for _, _, d in G.edges(data=True)]
|
|
30
|
-
total = len(confidences) or 1
|
|
31
|
-
ext_pct = round(confidences.count("EXTRACTED") / total * 100)
|
|
32
|
-
inf_pct = round(confidences.count("INFERRED") / total * 100)
|
|
33
|
-
amb_pct = round(confidences.count("AMBIGUOUS") / total * 100)
|
|
34
|
-
|
|
35
|
-
inf_edges = [(u, v, d) for u, v, d in G.edges(data=True) if d.get("confidence") == "INFERRED"]
|
|
36
|
-
inf_scores = [d.get("confidence_score", 0.5) for _, _, d in inf_edges]
|
|
37
|
-
inf_avg = round(sum(inf_scores) / len(inf_scores), 2) if inf_scores else None
|
|
38
|
-
|
|
39
|
-
lines = [
|
|
40
|
-
f"# Graph Report - {root} ({today})",
|
|
41
|
-
"",
|
|
42
|
-
"## Corpus Check",
|
|
43
|
-
]
|
|
44
|
-
if detection_result.get("warning"):
|
|
45
|
-
lines.append(f"- {detection_result['warning']}")
|
|
46
|
-
else:
|
|
47
|
-
lines += [
|
|
48
|
-
f"- {detection_result['total_files']} files · ~{detection_result['total_words']:,} words",
|
|
49
|
-
"- Verdict: corpus is large enough that graph structure adds value.",
|
|
50
|
-
]
|
|
51
|
-
|
|
52
|
-
lines += [
|
|
53
|
-
"",
|
|
54
|
-
"## Summary",
|
|
55
|
-
f"- {G.number_of_nodes()} nodes · {G.number_of_edges()} edges · {len(communities)} communities detected",
|
|
56
|
-
f"- Extraction: {ext_pct}% EXTRACTED · {inf_pct}% INFERRED · {amb_pct}% AMBIGUOUS"
|
|
57
|
-
+ (f" · INFERRED: {len(inf_edges)} edges (avg confidence: {inf_avg})" if inf_avg is not None else ""),
|
|
58
|
-
f"- Token cost: {token_cost.get('input', 0):,} input · {token_cost.get('output', 0):,} output",
|
|
59
|
-
]
|
|
60
|
-
|
|
61
|
-
# Community hub navigation - links to _COMMUNITY_*.md files in the Obsidian vault.
|
|
62
|
-
# Without these, GRAPH_REPORT.md is a dead-end and the vault splits into disconnected components.
|
|
63
|
-
if communities:
|
|
64
|
-
lines += ["", "## Community Hubs (Navigation)"]
|
|
65
|
-
for cid in communities:
|
|
66
|
-
label = community_labels.get(cid, f"Community {cid}")
|
|
67
|
-
safe = _safe_community_name(label)
|
|
68
|
-
lines.append(f"- [[_COMMUNITY_{safe}|{label}]]")
|
|
69
|
-
|
|
70
|
-
lines += [
|
|
71
|
-
"",
|
|
72
|
-
"## God Nodes (most connected - your core abstractions)",
|
|
73
|
-
]
|
|
74
|
-
for i, node in enumerate(god_node_list, 1):
|
|
75
|
-
lines.append(f"{i}. `{node['label']}` - {node['degree']} edges")
|
|
76
|
-
|
|
77
|
-
lines += ["", "## Surprising Connections (you probably didn't know these)"]
|
|
78
|
-
if surprise_list:
|
|
79
|
-
for s in surprise_list:
|
|
80
|
-
relation = s.get("relation", "related_to")
|
|
81
|
-
note = s.get("note", "")
|
|
82
|
-
files = s.get("source_files", ["", ""])
|
|
83
|
-
conf = s.get("confidence", "EXTRACTED")
|
|
84
|
-
cscore = s.get("confidence_score")
|
|
85
|
-
if conf == "INFERRED" and cscore is not None:
|
|
86
|
-
conf_tag = f"INFERRED {cscore:.2f}"
|
|
87
|
-
else:
|
|
88
|
-
conf_tag = conf
|
|
89
|
-
sem_tag = " [semantically similar]" if relation == "semantically_similar_to" else ""
|
|
90
|
-
lines += [
|
|
91
|
-
f"- `{s['source']}` --{relation}--> `{s['target']}` [{conf_tag}]{sem_tag}",
|
|
92
|
-
f" {files[0]} → {files[1]}" + (f" _{note}_" if note else ""),
|
|
93
|
-
]
|
|
94
|
-
else:
|
|
95
|
-
lines.append("- None detected - all connections are within the same source files.")
|
|
96
|
-
|
|
97
|
-
hyperedges = G.graph.get("hyperedges", [])
|
|
98
|
-
if hyperedges:
|
|
99
|
-
lines += ["", "## Hyperedges (group relationships)"]
|
|
100
|
-
for h in hyperedges:
|
|
101
|
-
node_labels = ", ".join(h.get("nodes", []))
|
|
102
|
-
conf = h.get("confidence", "INFERRED")
|
|
103
|
-
cscore = h.get("confidence_score")
|
|
104
|
-
conf_tag = f"{conf} {cscore:.2f}" if cscore is not None else conf
|
|
105
|
-
lines.append(f"- **{h.get('label', h.get('id', ''))}** — {node_labels} [{conf_tag}]")
|
|
106
|
-
|
|
107
|
-
lines += ["", "## Communities"]
|
|
108
|
-
from .analyze import _is_file_node as _ifn
|
|
109
|
-
for cid, nodes in communities.items():
|
|
110
|
-
label = community_labels.get(cid, f"Community {cid}")
|
|
111
|
-
score = cohesion_scores.get(cid, 0.0)
|
|
112
|
-
# Filter method/function stubs from display - they're structural noise
|
|
113
|
-
real_nodes = [n for n in nodes if not _ifn(G, n)]
|
|
114
|
-
display = [G.nodes[n].get("label", n) for n in real_nodes[:8]]
|
|
115
|
-
suffix = f" (+{len(real_nodes)-8} more)" if len(real_nodes) > 8 else ""
|
|
116
|
-
lines += [
|
|
117
|
-
"",
|
|
118
|
-
f"### Community {cid} - \"{label}\"",
|
|
119
|
-
f"Cohesion: {score}",
|
|
120
|
-
f"Nodes ({len(real_nodes)}): {', '.join(display)}{suffix}",
|
|
121
|
-
]
|
|
122
|
-
|
|
123
|
-
ambiguous = [(u, v, d) for u, v, d in G.edges(data=True) if d.get("confidence") == "AMBIGUOUS"]
|
|
124
|
-
if ambiguous:
|
|
125
|
-
lines += ["", "## Ambiguous Edges - Review These"]
|
|
126
|
-
for u, v, d in ambiguous:
|
|
127
|
-
ul = G.nodes[u].get("label", u)
|
|
128
|
-
vl = G.nodes[v].get("label", v)
|
|
129
|
-
lines += [
|
|
130
|
-
f"- `{ul}` → `{vl}` [AMBIGUOUS]",
|
|
131
|
-
f" {d.get('source_file', '')} · relation: {d.get('relation', 'unknown')}",
|
|
132
|
-
]
|
|
133
|
-
|
|
134
|
-
# --- Gaps section ---
|
|
135
|
-
from .analyze import _is_file_node, _is_concept_node
|
|
136
|
-
|
|
137
|
-
isolated = [
|
|
138
|
-
n for n in G.nodes()
|
|
139
|
-
if G.degree(n) <= 1 and not _is_file_node(G, n) and not _is_concept_node(G, n)
|
|
140
|
-
]
|
|
141
|
-
thin_communities = {
|
|
142
|
-
cid: nodes for cid, nodes in communities.items() if len(nodes) < 3
|
|
143
|
-
}
|
|
144
|
-
gap_count = len(isolated) + len(thin_communities)
|
|
145
|
-
|
|
146
|
-
if gap_count > 0 or amb_pct > 20:
|
|
147
|
-
lines += ["", "## Knowledge Gaps"]
|
|
148
|
-
if isolated:
|
|
149
|
-
isolated_labels = [G.nodes[n].get("label", n) for n in isolated[:5]]
|
|
150
|
-
suffix = f" (+{len(isolated)-5} more)" if len(isolated) > 5 else ""
|
|
151
|
-
lines.append(f"- **{len(isolated)} isolated node(s):** {', '.join(f'`{l}`' for l in isolated_labels)}{suffix}")
|
|
152
|
-
lines.append(" These have ≤1 connection - possible missing edges or undocumented components.")
|
|
153
|
-
if thin_communities:
|
|
154
|
-
for cid, nodes in thin_communities.items():
|
|
155
|
-
label = community_labels.get(cid, f"Community {cid}")
|
|
156
|
-
node_labels = [G.nodes[n].get("label", n) for n in nodes]
|
|
157
|
-
lines.append(f"- **Thin community `{label}`** ({len(nodes)} nodes): {', '.join(f'`{l}`' for l in node_labels)}")
|
|
158
|
-
lines.append(" Too small to be a meaningful cluster - may be noise or needs more connections extracted.")
|
|
159
|
-
if amb_pct > 20:
|
|
160
|
-
lines.append(f"- **High ambiguity: {amb_pct}% of edges are AMBIGUOUS.** Review the Ambiguous Edges section above.")
|
|
161
|
-
|
|
162
|
-
if suggested_questions:
|
|
163
|
-
lines += ["", "## Suggested Questions"]
|
|
164
|
-
no_signal = len(suggested_questions) == 1 and suggested_questions[0].get("type") == "no_signal"
|
|
165
|
-
if no_signal:
|
|
166
|
-
lines.append(f"_{suggested_questions[0]['why']}_")
|
|
167
|
-
else:
|
|
168
|
-
lines.append("_Questions this graph is uniquely positioned to answer:_")
|
|
169
|
-
lines.append("")
|
|
170
|
-
for q in suggested_questions:
|
|
171
|
-
if q.get("question"):
|
|
172
|
-
lines.append(f"- **{q['question']}**")
|
|
173
|
-
lines.append(f" _{q['why']}_")
|
|
174
|
-
|
|
175
|
-
return "\n".join(lines)
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
# Security helpers - URL validation, safe fetch, path guards, label sanitisation
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import html
|
|
5
|
-
import re
|
|
6
|
-
import urllib.error
|
|
7
|
-
import urllib.parse
|
|
8
|
-
import urllib.request
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
import ipaddress
|
|
12
|
-
import socket
|
|
13
|
-
|
|
14
|
-
_ALLOWED_SCHEMES = {"http", "https"}
|
|
15
|
-
_MAX_FETCH_BYTES = 52_428_800 # 50 MB hard cap for binary downloads
|
|
16
|
-
_MAX_TEXT_BYTES = 10_485_760 # 10 MB hard cap for HTML / text
|
|
17
|
-
|
|
18
|
-
# AWS metadata, link-local, and common cloud metadata endpoints
|
|
19
|
-
_BLOCKED_HOSTS = {"metadata.google.internal", "metadata.google.com"}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# ---------------------------------------------------------------------------
|
|
23
|
-
# URL validation
|
|
24
|
-
# ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
def validate_url(url: str) -> str:
|
|
27
|
-
"""Raise ValueError if *url* is not http or https, or targets a private/internal IP.
|
|
28
|
-
|
|
29
|
-
Blocks file://, ftp://, data:, and any other scheme that could be used
|
|
30
|
-
for SSRF or local file access. Also blocks requests to private/reserved
|
|
31
|
-
IP ranges (127.x, 10.x, 169.254.x, etc.) and cloud metadata endpoints
|
|
32
|
-
to prevent SSRF in cloud environments.
|
|
33
|
-
"""
|
|
34
|
-
parsed = urllib.parse.urlparse(url)
|
|
35
|
-
if parsed.scheme.lower() not in _ALLOWED_SCHEMES:
|
|
36
|
-
raise ValueError(
|
|
37
|
-
f"Blocked URL scheme '{parsed.scheme}' - only http and https are allowed. "
|
|
38
|
-
f"Got: {url!r}"
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
hostname = parsed.hostname
|
|
42
|
-
if hostname:
|
|
43
|
-
# Block known cloud metadata hostnames
|
|
44
|
-
if hostname.lower() in _BLOCKED_HOSTS:
|
|
45
|
-
raise ValueError(
|
|
46
|
-
f"Blocked cloud metadata endpoint '{hostname}'. "
|
|
47
|
-
f"Got: {url!r}"
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
# Resolve hostname and block private/reserved IP ranges
|
|
51
|
-
try:
|
|
52
|
-
infos = socket.getaddrinfo(hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM)
|
|
53
|
-
for info in infos:
|
|
54
|
-
addr = info[4][0]
|
|
55
|
-
ip = ipaddress.ip_address(addr)
|
|
56
|
-
if ip.is_private or ip.is_reserved or ip.is_loopback or ip.is_link_local:
|
|
57
|
-
raise ValueError(
|
|
58
|
-
f"Blocked private/internal IP {addr} (resolved from '{hostname}'). "
|
|
59
|
-
f"Got: {url!r}"
|
|
60
|
-
)
|
|
61
|
-
except socket.gaierror:
|
|
62
|
-
pass # DNS failure will surface later during fetch
|
|
63
|
-
|
|
64
|
-
return url
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class _NoFileRedirectHandler(urllib.request.HTTPRedirectHandler):
|
|
68
|
-
"""Redirect handler that re-validates every redirect target.
|
|
69
|
-
|
|
70
|
-
Prevents open-redirect SSRF attacks where an http:// URL redirects
|
|
71
|
-
to file:// or an internal address.
|
|
72
|
-
"""
|
|
73
|
-
|
|
74
|
-
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
75
|
-
validate_url(newurl) # raises ValueError if scheme is wrong
|
|
76
|
-
return super().redirect_request(req, fp, code, msg, headers, newurl)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def _build_opener() -> urllib.request.OpenerDirector:
|
|
80
|
-
return urllib.request.build_opener(_NoFileRedirectHandler)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
# ---------------------------------------------------------------------------
|
|
84
|
-
# Safe fetch
|
|
85
|
-
# ---------------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
def safe_fetch(url: str, max_bytes: int = _MAX_FETCH_BYTES, timeout: int = 30) -> bytes:
|
|
88
|
-
"""Fetch *url* and return raw bytes.
|
|
89
|
-
|
|
90
|
-
Protections applied:
|
|
91
|
-
- URL scheme validated (http / https only)
|
|
92
|
-
- Redirects re-validated via _NoFileRedirectHandler
|
|
93
|
-
- Response body capped at *max_bytes* (streaming read)
|
|
94
|
-
- Non-2xx status raises urllib.error.HTTPError
|
|
95
|
-
- Network errors propagate as urllib.error.URLError / OSError
|
|
96
|
-
|
|
97
|
-
Raises:
|
|
98
|
-
ValueError - disallowed scheme or redirect target
|
|
99
|
-
urllib.error.HTTPError - non-2xx HTTP status
|
|
100
|
-
urllib.error.URLError - DNS / connection failure
|
|
101
|
-
OSError - size cap exceeded
|
|
102
|
-
"""
|
|
103
|
-
validate_url(url)
|
|
104
|
-
opener = _build_opener()
|
|
105
|
-
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 graphify/1.0"})
|
|
106
|
-
|
|
107
|
-
with opener.open(req, timeout=timeout) as resp:
|
|
108
|
-
# urllib raises HTTPError for non-2xx when using urlopen directly;
|
|
109
|
-
# with a custom opener we check manually to be safe.
|
|
110
|
-
status = getattr(resp, "status", None) or getattr(resp, "code", None)
|
|
111
|
-
if status is not None and not (200 <= status < 300):
|
|
112
|
-
raise urllib.error.HTTPError(url, status, f"HTTP {status}", {}, None)
|
|
113
|
-
|
|
114
|
-
chunks: list[bytes] = []
|
|
115
|
-
total = 0
|
|
116
|
-
while True:
|
|
117
|
-
chunk = resp.read(65_536)
|
|
118
|
-
if not chunk:
|
|
119
|
-
break
|
|
120
|
-
total += len(chunk)
|
|
121
|
-
if total > max_bytes:
|
|
122
|
-
raise OSError(
|
|
123
|
-
f"Response from {url!r} exceeds size limit "
|
|
124
|
-
f"({max_bytes // 1_048_576} MB). Aborting download."
|
|
125
|
-
)
|
|
126
|
-
chunks.append(chunk)
|
|
127
|
-
|
|
128
|
-
return b"".join(chunks)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def safe_fetch_text(url: str, max_bytes: int = _MAX_TEXT_BYTES, timeout: int = 15) -> str:
|
|
132
|
-
"""Fetch *url* and return decoded text (UTF-8, replacing bad bytes).
|
|
133
|
-
|
|
134
|
-
Wraps safe_fetch with tighter defaults for HTML / text content.
|
|
135
|
-
"""
|
|
136
|
-
raw = safe_fetch(url, max_bytes=max_bytes, timeout=timeout)
|
|
137
|
-
return raw.decode("utf-8", errors="replace")
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
# ---------------------------------------------------------------------------
|
|
141
|
-
# Path validation
|
|
142
|
-
# ---------------------------------------------------------------------------
|
|
143
|
-
|
|
144
|
-
def validate_graph_path(path: str | Path, base: Path | None = None) -> Path:
|
|
145
|
-
"""Resolve *path* and verify it stays inside *base*.
|
|
146
|
-
|
|
147
|
-
*base* defaults to the `graphify-out` directory relative to CWD.
|
|
148
|
-
Also requires the base directory to exist, so a caller cannot
|
|
149
|
-
trick graphify into reading files before any graph has been built.
|
|
150
|
-
|
|
151
|
-
Raises:
|
|
152
|
-
ValueError - path escapes base, or base does not exist
|
|
153
|
-
FileNotFoundError - resolved path does not exist
|
|
154
|
-
"""
|
|
155
|
-
if base is None:
|
|
156
|
-
resolved_hint = Path(path).resolve()
|
|
157
|
-
for candidate in [resolved_hint, *resolved_hint.parents]:
|
|
158
|
-
if candidate.name == "graphify-out":
|
|
159
|
-
base = candidate
|
|
160
|
-
break
|
|
161
|
-
if base is None:
|
|
162
|
-
base = Path("graphify-out").resolve()
|
|
163
|
-
|
|
164
|
-
base = base.resolve()
|
|
165
|
-
if not base.exists():
|
|
166
|
-
raise ValueError(
|
|
167
|
-
f"Graph base directory does not exist: {base}. "
|
|
168
|
-
"Run /graphify first to build the graph."
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
resolved = Path(path).resolve()
|
|
172
|
-
try:
|
|
173
|
-
resolved.relative_to(base)
|
|
174
|
-
except ValueError:
|
|
175
|
-
raise ValueError(
|
|
176
|
-
f"Path {path!r} escapes the allowed directory {base}. "
|
|
177
|
-
"Only paths inside graphify-out/ are permitted."
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
if not resolved.exists():
|
|
181
|
-
raise FileNotFoundError(f"Graph file not found: {resolved}")
|
|
182
|
-
|
|
183
|
-
return resolved
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
# ---------------------------------------------------------------------------
|
|
187
|
-
# Label sanitisation (mirrors code-review-graph's _sanitize_name pattern)
|
|
188
|
-
# ---------------------------------------------------------------------------
|
|
189
|
-
|
|
190
|
-
_CONTROL_CHAR_RE = re.compile(r"[\x00-\x1f\x7f]")
|
|
191
|
-
_MAX_LABEL_LEN = 256
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def sanitize_label(text: str) -> str:
|
|
195
|
-
"""Strip control characters and cap length.
|
|
196
|
-
|
|
197
|
-
Safe for embedding in JSON data (inside <script> tags) and plain text.
|
|
198
|
-
For direct HTML injection, wrap the result with html.escape().
|
|
199
|
-
"""
|
|
200
|
-
text = _CONTROL_CHAR_RE.sub("", text)
|
|
201
|
-
if len(text) > _MAX_LABEL_LEN:
|
|
202
|
-
text = text[:_MAX_LABEL_LEN]
|
|
203
|
-
return text
|