sciogen 0.1.2__tar.gz → 0.1.3__tar.gz

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.
Files changed (57) hide show
  1. {sciogen-0.1.2 → sciogen-0.1.3}/PKG-INFO +8 -2
  2. {sciogen-0.1.2 → sciogen-0.1.3}/README.md +7 -1
  3. {sciogen-0.1.2 → sciogen-0.1.3}/pyproject.toml +1 -1
  4. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/__init__.py +1 -1
  5. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/cli/explore/template.html +132 -42
  6. {sciogen-0.1.2 → sciogen-0.1.3}/site/docs.html +36 -1
  7. {sciogen-0.1.2 → sciogen-0.1.3}/.gitignore +0 -0
  8. {sciogen-0.1.2 → sciogen-0.1.3}/LICENSE +0 -0
  9. {sciogen-0.1.2 → sciogen-0.1.3}/docs/architecture.md +0 -0
  10. {sciogen-0.1.2 → sciogen-0.1.3}/docs/cli.md +0 -0
  11. {sciogen-0.1.2 → sciogen-0.1.3}/docs/mcp.md +0 -0
  12. {sciogen-0.1.2 → sciogen-0.1.3}/docs/quickstart.md +0 -0
  13. {sciogen-0.1.2 → sciogen-0.1.3}/docs/schema.md +0 -0
  14. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/__main__.py +0 -0
  15. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/api.py +0 -0
  16. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/cli/__init__.py +0 -0
  17. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/cli/banner.py +0 -0
  18. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/cli/console.py +0 -0
  19. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/cli/explore/__init__.py +0 -0
  20. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/cli/main.py +0 -0
  21. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/config.py +0 -0
  22. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/discovery.py +0 -0
  23. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/embed/__init__.py +0 -0
  24. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/embed/hashing.py +0 -0
  25. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/embed/nomic.py +0 -0
  26. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/index/__init__.py +0 -0
  27. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/index/pipeline.py +0 -0
  28. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/ir.py +0 -0
  29. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/mcp/__init__.py +0 -0
  30. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/mcp/server.py +0 -0
  31. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/normalize/__init__.py +0 -0
  32. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/normalize/base.py +0 -0
  33. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/normalize/generic.py +0 -0
  34. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/normalize/javascript.py +0 -0
  35. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/normalize/python.py +0 -0
  36. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/parsing/__init__.py +0 -0
  37. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/parsing/languages.py +0 -0
  38. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/parsing/parser.py +0 -0
  39. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/query/__init__.py +0 -0
  40. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/query/engine.py +0 -0
  41. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/query/results.py +0 -0
  42. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/resolve/__init__.py +0 -0
  43. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/resolve/resolver.py +0 -0
  44. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/stores/__init__.py +0 -0
  45. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/stores/graph.py +0 -0
  46. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/stores/meta.py +0 -0
  47. {sciogen-0.1.2 → sciogen-0.1.3}/sciogen/stores/vectors.py +0 -0
  48. {sciogen-0.1.2 → sciogen-0.1.3}/site/index.html +0 -0
  49. {sciogen-0.1.2 → sciogen-0.1.3}/site/public/icon.png +0 -0
  50. {sciogen-0.1.2 → sciogen-0.1.3}/site/public/logo.png +0 -0
  51. {sciogen-0.1.2 → sciogen-0.1.3}/tests/conftest.py +0 -0
  52. {sciogen-0.1.2 → sciogen-0.1.3}/tests/test_cli.py +0 -0
  53. {sciogen-0.1.2 → sciogen-0.1.3}/tests/test_discovery.py +0 -0
  54. {sciogen-0.1.2 → sciogen-0.1.3}/tests/test_graph_store.py +0 -0
  55. {sciogen-0.1.2 → sciogen-0.1.3}/tests/test_normalize_python.py +0 -0
  56. {sciogen-0.1.2 → sciogen-0.1.3}/tests/test_pipeline_and_query.py +0 -0
  57. {sciogen-0.1.2 → sciogen-0.1.3}/tests/test_resolver.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sciogen
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Codebase intelligence layer — a queryable knowledge graph over your code.
5
5
  Project-URL: Homepage, https://ayanbag.github.io/sciogen/
6
6
  Project-URL: Repository, https://github.com/ayanbag/sciogen
@@ -34,7 +34,13 @@ Requires-Dist: einops; extra == 'embeddings'
34
34
  Requires-Dist: sentence-transformers>=3.0; extra == 'embeddings'
35
35
  Description-Content-Type: text/markdown
36
36
 
37
- <img src="site/public/logo.png" alt="Image" width="100%" />
37
+ <img src="https://github.com/user-attachments/assets/1f736f23-c647-4838-8901-5db0edb91616" alt="Image" width="100%" />
38
+
39
+
40
+
41
+
42
+
43
+
38
44
 
39
45
  <div align="center">
40
46
  <p>
@@ -1,4 +1,10 @@
1
- <img src="site/public/logo.png" alt="Image" width="100%" />
1
+ <img src="https://github.com/user-attachments/assets/1f736f23-c647-4838-8901-5db0edb91616" alt="Image" width="100%" />
2
+
3
+
4
+
5
+
6
+
7
+
2
8
 
3
9
  <div align="center">
4
10
  <p>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sciogen"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "Codebase intelligence layer — a queryable knowledge graph over your code."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -19,6 +19,6 @@ Public entry point::
19
19
 
20
20
  from sciogen.api import SciogenGraph, open # noqa: A004 - deliberate, mirrors sqlite3.connect ergonomics
21
21
 
22
- __version__ = "0.1.1"
22
+ __version__ = "0.1.3"
23
23
 
24
24
  __all__ = ["SciogenGraph", "open", "__version__"]
@@ -284,7 +284,8 @@ GRAPH.edges.forEach(e => {
284
284
  if (adjacency.has(e.dst)) adjacency.get(e.dst).push(e);
285
285
  });
286
286
  const degree = id => GRAPH.in_degrees[id] || 0;
287
- const maxDegree = Math.max(1, ...Object.values(GRAPH.in_degrees));
287
+ let maxDegree = 1;
288
+ for (const v of Object.values(GRAPH.in_degrees)) if (v > maxDegree) maxDegree = v;
288
289
  const nodeSize = id => 22 + 36 * Math.sqrt(degree(id) / maxDegree);
289
290
  const shortLabel = n => n.kind === "FileNode" ? n.name.split("/").pop() : (n.name.split(".").pop() || n.name);
290
291
 
@@ -293,6 +294,8 @@ const visible = new Set(GRAPH.nodes.filter(n => n.kind === "FileNode").map(n =>
293
294
  const revealedBy = new Map();
294
295
  const expanded = new Set();
295
296
  const hiddenKinds = new Set();
297
+ const savedPos = new Map(); // id → {x,y}; lets nodes reappear where they were
298
+ const LAYOUT_ANIM_MAX = 700; // above this many visible nodes, layout runs without animation
296
299
  let minConf = 0;
297
300
  let currentLayout = "force";
298
301
  let shownEdges = 0;
@@ -374,45 +377,129 @@ const cy = cytoscape({
374
377
  wheelSensitivity: 0.25,
375
378
  });
376
379
 
377
- /* stagger-fade the initial file nodes in */
380
+ /* persist node positions after any layout or manual drag, so incremental
381
+ updates never re-simulate — reappearing nodes land where they were */
382
+ cy.on("layoutstop", () => {
383
+ cy.nodes().forEach(n => savedPos.set(n.id(), { x: n.position("x"), y: n.position("y") }));
384
+ });
385
+ cy.on("dragfree", "node", e =>
386
+ savedPos.set(e.target.id(), { x: e.target.position("x"), y: e.target.position("y") }));
387
+
388
+ /* fade the initial file nodes in as one group (not one timer per node) */
378
389
  if (!reduce) {
379
- cy.nodes().forEach((n, i) => {
380
- n.style("opacity", 0);
381
- setTimeout(() => n.animate({ style: { opacity: 1 } }, { duration: 260, easing: "ease-out" }), 24 * i);
390
+ const first = cy.nodes();
391
+ first.style("opacity", 0);
392
+ first.animate({ style: { opacity: 1 } }, { duration: 300, easing: "ease-out" });
393
+ }
394
+
395
+ /* Desired on-screen sets. Edges are collected by walking only the adjacency
396
+ of visible nodes — never the full GRAPH.edges array — so cost scales with
397
+ what's shown, not with the size of the whole codebase. */
398
+ function computeWanted() {
399
+ const wantNodes = new Set();
400
+ for (const id of visible) {
401
+ const n = nodesById.get(id);
402
+ if (n && !hiddenKinds.has(n.kind)) wantNodes.add(id);
403
+ }
404
+ const wantEdges = new Map();
405
+ for (const id of wantNodes) {
406
+ const adj = adjacency.get(id);
407
+ if (!adj) continue;
408
+ for (const e of adj) {
409
+ if (e.kind === "DEFINES" || e.kind === "PART_OF") continue;
410
+ if (e.confidence < minConf) continue;
411
+ if (!wantNodes.has(e.src) || !wantNodes.has(e.dst)) continue;
412
+ const key = e.src + "→" + e.dst + "→" + e.kind;
413
+ if (!wantEdges.has(key)) wantEdges.set(key, e);
414
+ }
415
+ }
416
+ shownEdges = wantEdges.size;
417
+ return { wantNodes, wantEdges };
418
+ }
419
+
420
+ /* Place freshly-revealed nodes in a ring around whatever expanded them —
421
+ no physics, no global relayout, existing nodes never move. */
422
+ function positionNew(added, curNodes) {
423
+ const groups = new Map();
424
+ added.forEach(n => {
425
+ const id = n.id();
426
+ if (savedPos.has(id)) { n.position(savedPos.get(id)); return; }
427
+ const by = revealedBy.get(id) || "__root";
428
+ (groups.get(by) || groups.set(by, []).get(by)).push(n);
382
429
  });
430
+ // position groups whose anchor already existed first, so anchors added in
431
+ // this same batch are themselves placed before their children reference them
432
+ const order = [...groups.keys()].sort((a, b) =>
433
+ (curNodes.has(a) ? 0 : 1) - (curNodes.has(b) ? 0 : 1));
434
+ for (const by of order) {
435
+ const members = groups.get(by);
436
+ const anchor = by !== "__root" ? cy.getElementById(by) : null;
437
+ const has = anchor && anchor.nonempty();
438
+ const cx = has ? anchor.position("x") : cy.width() / 2;
439
+ const cyy = has ? anchor.position("y") : cy.height() / 2;
440
+ const r = 55 + members.length * 7;
441
+ members.forEach((m, i) => {
442
+ const a = (2 * Math.PI * i) / members.length - Math.PI / 2;
443
+ m.position({ x: cx + r * Math.cos(a), y: cyy + r * Math.sin(a) });
444
+ savedPos.set(m.id(), { x: m.position("x"), y: m.position("y") });
445
+ });
446
+ }
383
447
  }
384
448
 
449
+ /* Incremental reconciliation: diff current vs wanted, add/remove only the
450
+ delta inside one batched frame. No cy.json() teardown, no relayout. */
385
451
  function refresh(opts) {
386
452
  opts = opts || {};
387
- const before = new Set(cy.nodes().map(n => n.id()));
388
- const els = buildElements();
389
- const after = new Set(els.filter(e => !e.data.source).map(e => e.data.id));
390
-
391
- const doUpdate = () => {
392
- cy.json({ elements: els });
393
- cy.style(cyStyle());
394
- if (!reduce) {
395
- cy.nodes().forEach(n => {
396
- if (!before.has(n.id())) {
397
- n.style("opacity", 0);
398
- n.animate({ style: { opacity: 1 } }, { duration: 240, easing: "ease-out" });
399
- }
400
- });
453
+ const { wantNodes, wantEdges } = computeWanted();
454
+ const curNodes = new Set(cy.nodes().map(n => n.id()));
455
+ const curEdges = new Set(cy.edges().map(e => e.id()));
456
+
457
+ let remove = cy.collection();
458
+ cy.nodes().forEach(n => {
459
+ if (!wantNodes.has(n.id())) {
460
+ savedPos.set(n.id(), { x: n.position("x"), y: n.position("y") });
461
+ remove = remove.union(n);
401
462
  }
402
- cy.layout(LAYOUTS[currentLayout]).run();
403
- if (opts.fit) cy.animate({ fit: { padding: 40 }, duration: reduce ? 0 : 350, easing: "ease-out" });
404
- updateStats();
405
- if (opts.then) opts.then();
406
- };
463
+ });
464
+ cy.edges().forEach(e => { if (!wantEdges.has(e.id())) remove = remove.union(e); });
465
+
466
+ const addNodes = [], addEdges = [];
467
+ for (const id of wantNodes) {
468
+ if (curNodes.has(id)) continue;
469
+ const n = nodesById.get(id);
470
+ addNodes.push({ group: "nodes", data: { id, label: shortLabel(n), kind: n.kind, size: nodeSize(id) } });
471
+ }
472
+ for (const [key, e] of wantEdges) {
473
+ if (curEdges.has(key)) continue;
474
+ addEdges.push({ group: "edges", data: { id: key, source: e.src, target: e.dst, kind: e.kind, confidence: e.confidence }, classes: edgeClass(e.confidence) });
475
+ }
476
+
477
+ let added = cy.collection();
478
+ cy.batch(() => {
479
+ if (remove.nonempty()) cy.remove(remove);
480
+ if (addNodes.length) { added = cy.add(addNodes); positionNew(added, curNodes); }
481
+ if (addEdges.length) cy.add(addEdges);
482
+ });
407
483
 
408
- // fade out nodes that are leaving before the graph snaps
409
- const leaving = cy.nodes().filter(n => !after.has(n.id()));
410
- if (!reduce && leaving.length && leaving.length < 80) {
411
- leaving.animate({ style: { opacity: 0 } }, { duration: 150, easing: "ease-in" });
412
- setTimeout(doUpdate, 160);
413
- } else {
414
- doUpdate();
484
+ if (!reduce && added.nonempty()) {
485
+ added.style("opacity", 0);
486
+ added.animate({ style: { opacity: 1 } }, { duration: 240, easing: "ease-out" });
415
487
  }
488
+ if (opts.fit) cy.animate({ fit: { padding: 40 }, duration: reduce ? 0 : 350, easing: "ease-out" });
489
+ updateStats();
490
+ if (opts.then) opts.then();
491
+ }
492
+
493
+ /* Explicit, user-triggered global relayout (layout buttons, expand-all).
494
+ Skips animation past LAYOUT_ANIM_MAX so big graphs snap instead of grind. */
495
+ function runLayout(opts) {
496
+ opts = opts || {};
497
+ const big = cy.nodes().length > LAYOUT_ANIM_MAX;
498
+ const cfg = Object.assign({}, LAYOUTS[currentLayout], {
499
+ animate: (reduce || big) ? false : (currentLayout === "force" ? "end" : true),
500
+ fit: !!opts.fit,
501
+ });
502
+ cy.layout(cfg).run();
416
503
  }
417
504
 
418
505
  function updateStats() {
@@ -497,8 +584,7 @@ function esc(s) { const d = document.createElement("div"); d.textContent = s; re
497
584
  cy.on("mouseover", "node", evt => {
498
585
  const n = evt.target;
499
586
  const hood = n.closedNeighborhood();
500
- cy.elements().not(hood).addClass("dim");
501
- hood.addClass("hot");
587
+ cy.batch(() => { cy.elements().not(hood).addClass("dim"); hood.addClass("hot"); });
502
588
  tip.innerHTML = nodeTip(n);
503
589
  tip.classList.add("show");
504
590
  moveTip(evt);
@@ -506,15 +592,17 @@ cy.on("mouseover", "node", evt => {
506
592
  });
507
593
  cy.on("mouseover", "edge", evt => {
508
594
  const e = evt.target;
509
- cy.elements().not(e.connectedNodes().union(e)).addClass("dim");
510
- e.addClass("hot"); e.connectedNodes().addClass("hot");
595
+ cy.batch(() => {
596
+ cy.elements().not(e.connectedNodes().union(e)).addClass("dim");
597
+ e.addClass("hot"); e.connectedNodes().addClass("hot");
598
+ });
511
599
  tip.innerHTML = edgeTip(e);
512
600
  tip.classList.add("show");
513
601
  moveTip(evt);
514
602
  });
515
603
  cy.on("mousemove", "node, edge", moveTip);
516
604
  cy.on("mouseout", "node, edge", () => {
517
- cy.elements().removeClass("dim hot");
605
+ cy.batch(() => cy.elements().removeClass("dim hot"));
518
606
  tip.classList.remove("show");
519
607
  document.getElementById("cy").style.cursor = "";
520
608
  });
@@ -651,15 +739,15 @@ searchInput.addEventListener("blur", () => setTimeout(closeSug, 150));
651
739
 
652
740
  /* ---------------------------------------------------- toolbar controls */
653
741
  document.getElementById("btn-expand").onclick = () => {
654
- GRAPH.nodes.forEach(n => visible.add(n.id));
655
- GRAPH.nodes.forEach(n => expanded.add(n.id));
656
- refresh({ fit: true });
742
+ GRAPH.nodes.forEach(n => { visible.add(n.id); expanded.add(n.id); });
743
+ refresh(); // add the full delta incrementally (no layout inside)
744
+ runLayout({ fit: true }); // one explicit relayout to arrange the whole graph
657
745
  };
658
746
  document.getElementById("btn-collapse").onclick = () => {
659
747
  visible.clear(); revealedBy.clear(); expanded.clear();
660
748
  GRAPH.nodes.filter(n => n.kind === "FileNode").forEach(n => visible.add(n.id));
661
749
  hidePanel();
662
- refresh({ fit: true });
750
+ refresh({ fit: true }); // file nodes stay put on their saved positions
663
751
  };
664
752
  document.getElementById("btn-fit").onclick = () =>
665
753
  cy.animate({ fit: { padding: 40 }, duration: reduce ? 0 : 350, easing: "ease-out" });
@@ -669,16 +757,18 @@ function setLayout(name) {
669
757
  currentLayout = name;
670
758
  btnForce.classList.toggle("active", name === "force");
671
759
  btnHier.classList.toggle("active", name === "hier");
672
- refresh({ fit: true });
760
+ runLayout({ fit: true }); // relayout existing nodes in place — no rebuild
673
761
  }
674
762
  btnForce.onclick = () => setLayout("force");
675
763
  btnHier.onclick = () => setLayout("hier");
676
764
 
677
765
  const confSlider = document.getElementById("conf"), confVal = document.getElementById("conf-val");
766
+ let confTimer = null;
678
767
  confSlider.addEventListener("input", () => {
679
768
  minConf = parseFloat(confSlider.value);
680
769
  confVal.textContent = minConf.toFixed(2);
681
- refresh();
770
+ clearTimeout(confTimer);
771
+ confTimer = setTimeout(refresh, 90); // debounce; only edges change, nodes never move
682
772
  });
683
773
 
684
774
  /* ------------------------------------------------------ legend filters */
@@ -367,6 +367,10 @@
367
367
  <a href="#cli">CLI reference</a>
368
368
  <a href="#configuration">Configuration</a>
369
369
  </div>
370
+ <div class="group">
371
+ <div class="group-label">Community</div>
372
+ <a href="#contributing">Contributing</a>
373
+ </div>
370
374
  </aside>
371
375
 
372
376
  <main class="doc">
@@ -817,10 +821,41 @@
817
821
  <tr><td><code>SCIOGEN_DATA_DIR</code></td><td>Override the index location (default <code>&lt;project&gt;/.sciogen</code>)</td></tr>
818
822
  </tbody>
819
823
  </table></div>
824
+ </section>
825
+
826
+ <!-- ============================ CONTRIBUTING ============================ -->
827
+ <section class="block" id="contributing">
828
+ <h2 class="reveal">Contributing <a class="hash" href="#contributing">#</a></h2>
829
+ <p class="reveal">sciogen is open source and contributions are welcome — bug reports, language-normalizer improvements, docs, and features all move it forward. The complete guide lives in <a href="https://github.com/ayanbag/sciogen/blob/master/CONTRIBUTING.md" target="_blank" rel="noopener">CONTRIBUTING.md</a>; the essentials are below.</p>
830
+
831
+ <h3 class="reveal">Set up a dev environment</h3>
832
+ <p class="reveal">sciogen targets <b>Python 3.10+</b> and builds with hatchling. Install it <b>editable</b> (<code>-e</code>) so source edits — including the version and CLI banner — take effect immediately, with no reinstall:</p>
833
+ <div class="code reveal"><span class="lang">shell</span><button class="copy" type="button" data-copy='git clone https://github.com/ayanbag/sciogen && cd sciogen && pip install -e ".[dev,embeddings]"' aria-label="Copy"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg></button>
834
+ <pre><span class="g">$</span> git clone https://github.com/ayanbag/sciogen
835
+ <span class="g">$</span> cd sciogen
836
+ <span class="g">$</span> pip install -e ".[dev,embeddings]"</pre></div>
837
+ <p class="reveal">Then confirm your environment is live — both must print the <b>same</b> version; if they disagree you're running a stale install:</p>
838
+ <div class="code reveal"><span class="lang">shell</span><button class="copy" type="button" data-copy='python -c "import sciogen; print(sciogen.__version__)" && python -m sciogen --version' aria-label="Copy"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg></button>
839
+ <pre><span class="g">$</span> python -c "import sciogen; print(sciogen.__version__)"
840
+ <span class="g">$</span> python -m sciogen --version</pre></div>
841
+
842
+ <h3 class="reveal">Run the tests</h3>
843
+ <p class="reveal">Every change should keep the suite green. New behavior needs a test; a bug fix should add a regression test that fails before and passes after.</p>
844
+ <div class="code reveal"><span class="lang">shell</span><button class="copy" type="button" data-copy="pytest" aria-label="Copy"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg></button>
845
+ <pre><span class="g">$</span> pytest <span class="mut"># full suite</span>
846
+ <span class="g">$</span> pytest -p no:randomly <span class="mut"># deterministic order</span></pre></div>
847
+
848
+ <div class="callout reveal">
849
+ <span class="t">What belongs in sciogen</span>
850
+ <p>sciogen is a static index, not a generative tool. A few constraints are non-negotiable, and a PR that breaks one will be asked to change: <b>no LLM calls</b> anywhere; <b>no token counting or context-window logic</b>; query methods return <b>typed nodes, never raw source</b>; the pipeline <b>writes every edge</b> and confidence is filtered at query time, not at write time. Language-specific logic lives in exactly one place — the <b>AST normalizer</b> — so everything downstream stays language-agnostic.</p>
851
+ </div>
852
+
853
+ <h3 class="reveal">Versioning</h3>
854
+ <p class="reveal">The version lives in a <b>single place</b> — <code>__version__</code> in <code>sciogen/__init__.py</code>. <code>pyproject.toml</code> reads it via hatchling and the CLI banner imports the same constant, so bump that one line and everything stays in lockstep. Never add a second version string.</p>
820
855
 
821
856
  <div class="doc-nav reveal">
822
857
  <a href="index.html"><span class="lbl">← Back</span><div class="ttl">sciogen home</div></a>
823
- <a class="next" href="https://github.com/ayanbag/sciogen" target="_blank" rel="noopener"><span class="lbl">Source →</span><div class="ttl">GitHub repository</div></a>
858
+ <a class="next" href="https://github.com/ayanbag/sciogen/blob/master/CONTRIBUTING.md" target="_blank" rel="noopener"><span class="lbl">Full guide →</span><div class="ttl">CONTRIBUTING.md</div></a>
824
859
  </div>
825
860
  </section>
826
861
  </main>
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes