@ttsc/graph 0.19.2 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/lib/TtscGraphApplication.js +5 -1
  2. package/lib/TtscGraphApplication.js.map +1 -1
  3. package/lib/bin.js +15 -3
  4. package/lib/bin.js.map +1 -1
  5. package/lib/index.d.ts +13 -2
  6. package/lib/index.js +25 -30
  7. package/lib/index.js.map +1 -1
  8. package/lib/launcherArgs.d.ts +24 -0
  9. package/lib/launcherArgs.js +136 -0
  10. package/lib/launcherArgs.js.map +1 -0
  11. package/lib/model/TtscGraphMemory.d.ts +9 -5
  12. package/lib/model/TtscGraphMemory.js +35 -58
  13. package/lib/model/TtscGraphMemory.js.map +1 -1
  14. package/lib/model/TtscGraphNodeId.d.ts +17 -0
  15. package/lib/model/TtscGraphNodeId.js +69 -0
  16. package/lib/model/TtscGraphNodeId.js.map +1 -0
  17. package/lib/model/TtscGraphSession.d.ts +23 -7
  18. package/lib/model/TtscGraphSession.js +423 -171
  19. package/lib/model/TtscGraphSession.js.map +1 -1
  20. package/lib/model/TtscGraphSourceReader.d.ts +27 -0
  21. package/lib/model/TtscGraphSourceReader.js +97 -0
  22. package/lib/model/TtscGraphSourceReader.js.map +1 -0
  23. package/lib/model/loadGraph.d.ts +14 -0
  24. package/lib/model/loadGraph.js +256 -143
  25. package/lib/model/loadGraph.js.map +1 -1
  26. package/lib/reduce.d.ts +4 -1
  27. package/lib/reduce.js +112 -29
  28. package/lib/reduce.js.map +1 -1
  29. package/lib/resolveGraphBinary.d.ts +9 -3
  30. package/lib/resolveGraphBinary.js +9 -3
  31. package/lib/resolveGraphBinary.js.map +1 -1
  32. package/lib/server/createServer.js +22 -11
  33. package/lib/server/createServer.js.map +1 -1
  34. package/lib/server/pathPolicy.d.ts +6 -0
  35. package/lib/server/pathPolicy.js +10 -1
  36. package/lib/server/pathPolicy.js.map +1 -1
  37. package/lib/server/resolveHandle.js +22 -18
  38. package/lib/server/resolveHandle.js.map +1 -1
  39. package/lib/server/resultAudit.d.ts +56 -14
  40. package/lib/server/resultAudit.js +90 -24
  41. package/lib/server/resultAudit.js.map +1 -1
  42. package/lib/server/resultNext.d.ts +5 -0
  43. package/lib/server/resultNext.js.map +1 -1
  44. package/lib/server/runDetails.d.ts +9 -2
  45. package/lib/server/runDetails.js +132 -157
  46. package/lib/server/runDetails.js.map +1 -1
  47. package/lib/server/runEntrypoints.js +1 -1
  48. package/lib/server/runEntrypoints.js.map +1 -1
  49. package/lib/server/runLookup.js +1 -1
  50. package/lib/server/runLookup.js.map +1 -1
  51. package/lib/server/runTour.js +113 -30
  52. package/lib/server/runTour.js.map +1 -1
  53. package/lib/server/runTrace.d.ts +22 -0
  54. package/lib/server/runTrace.js +293 -63
  55. package/lib/server/runTrace.js.map +1 -1
  56. package/lib/structures/ITtscGraphApplication.d.ts +8 -7
  57. package/lib/structures/ITtscGraphDetails.d.ts +18 -8
  58. package/lib/structures/ITtscGraphDump.d.ts +13 -13
  59. package/lib/structures/ITtscGraphNode.d.ts +71 -0
  60. package/lib/structures/ITtscGraphTour.d.ts +1 -1
  61. package/lib/structures/ITtscGraphTrace.d.ts +1 -1
  62. package/lib/view.js +18 -28
  63. package/lib/view.js.map +1 -1
  64. package/package.json +7 -4
  65. package/src/TtscGraphApplication.ts +8 -1
  66. package/src/bin.ts +13 -3
  67. package/src/index.ts +35 -27
  68. package/src/launcherArgs.ts +168 -0
  69. package/src/model/TtscGraphMemory.ts +34 -60
  70. package/src/model/TtscGraphNodeId.ts +77 -0
  71. package/src/model/TtscGraphSession.ts +253 -53
  72. package/src/model/TtscGraphSourceReader.ts +117 -0
  73. package/src/model/loadGraph.ts +6 -1
  74. package/src/reduce.ts +136 -31
  75. package/src/resolveGraphBinary.ts +9 -3
  76. package/src/server/createServer.ts +12 -2
  77. package/src/server/pathPolicy.ts +10 -1
  78. package/src/server/resolveHandle.ts +21 -22
  79. package/src/server/resultAudit.ts +93 -23
  80. package/src/server/resultNext.ts +6 -0
  81. package/src/server/runDetails.ts +134 -165
  82. package/src/server/runEntrypoints.ts +1 -1
  83. package/src/server/runLookup.ts +1 -1
  84. package/src/server/runTour.ts +133 -39
  85. package/src/server/runTrace.ts +401 -65
  86. package/src/structures/ITtscGraphApplication.ts +8 -7
  87. package/src/structures/ITtscGraphDetails.ts +18 -8
  88. package/src/structures/ITtscGraphDump.ts +13 -13
  89. package/src/structures/ITtscGraphNode.ts +80 -0
  90. package/src/structures/ITtscGraphTour.ts +1 -1
  91. package/src/structures/ITtscGraphTrace.ts +1 -1
  92. package/src/view.ts +25 -23
@@ -13,7 +13,7 @@ import { resolveGraphHandle } from "./resolveHandle";
13
13
  import { IRunnerOutput, resultNext } from "./resultNext";
14
14
  import { decoratorsOf, docOf, runDetails, signatureOf } from "./runDetails";
15
15
  import { runEntrypoints } from "./runEntrypoints";
16
- import { runTrace } from "./runTrace";
16
+ import { hasDeclarationBody, runTrace } from "./runTrace";
17
17
 
18
18
  const DEFAULT_LIMIT = 5;
19
19
  const MAX_LIMIT = 5;
@@ -110,6 +110,24 @@ export function runTour(
110
110
  // so keep the first and walk on to one that tells something else.
111
111
  const primaryFlow: ITtscGraphTour.IFlow[] = [];
112
112
  const told: Set<string>[] = [];
113
+ // A flow whose every hop the hub cut would remove is demoted, not deleted.
114
+ // Deleting it is what #809 reports: eleven callers kept the flow and a
115
+ // twelfth erased it, so the tour answered a question about a terminal action
116
+ // with nothing. Telling it regardless is what the cut was written against: a
117
+ // one-line wrapper around a logger became one of the tour's four flows.
118
+ //
119
+ // Demotion settles both without a rule that can tell a commit from a logger,
120
+ // which the graph cannot: the flow is held back, and told only if the tour
121
+ // finishes with nothing else to say. The first such candidate is kept because
122
+ // the seeds arrive ranked.
123
+ let demoted:
124
+ | {
125
+ start: ITtscGraphTrace.INode;
126
+ hops: ITtscGraphTrace.IHop[];
127
+ reached: ITtscGraphTrace.INode[];
128
+ truncated: boolean;
129
+ }
130
+ | undefined;
113
131
 
114
132
  for (const id of flowSeedIdsOf(
115
133
  tourSeedsOf(
@@ -132,11 +150,28 @@ export function runTour(
132
150
  }).result;
133
151
  const start = trace.start;
134
152
  if (start === undefined) continue;
135
- const hops = trace.hops.filter((hop) => isTourHop(graph, hop));
136
- if (hops.length === 0) continue;
137
- const reached = trace.reached.filter((node) =>
138
- isTourTraceNode(graph, node),
139
- );
153
+ const { kept: hops, demotable } = tourHops(graph, trace.hops);
154
+ if (hops.length === 0) {
155
+ // Held back under the same rule the published path uses: a candidate that
156
+ // reached nothing is not a flow, so it is not worth holding either.
157
+ const demotedReach =
158
+ demotable.length === 0 ? [] : reachedOf(graph, trace, demotable);
159
+ if (demoted === undefined && demotedReach.length > 0)
160
+ demoted = {
161
+ start,
162
+ hops: demotable,
163
+ reached: demotedReach,
164
+ truncated: trace.truncated === true,
165
+ };
166
+ continue;
167
+ }
168
+ const reached = reachedOf(graph, trace, hops);
169
+ // A flow that reached nothing is not a flow. It can still have a hop —
170
+ // `runTrace` records a back-edge to the start without adding a node,
171
+ // because the start travels separately — so `hops.length` does not answer
172
+ // this. What a flow is for is the handles a caller goes on with, and this
173
+ // one has none.
174
+ if (reached.length === 0) continue;
140
175
  const landed = new Set(reached.map((node) => node.id));
141
176
  if (told.some((earlier) => overlaps(landed, earlier))) continue;
142
177
  told.push(landed);
@@ -160,6 +195,16 @@ export function runTour(
160
195
  });
161
196
  }
162
197
 
198
+ if (primaryFlow.length === 0 && demoted !== undefined)
199
+ primaryFlow.push({
200
+ start: flowStartOf(demoted.start),
201
+ steps: demoted.hops
202
+ .slice(0, MAX_FLOW_ANCHORS)
203
+ .map((hop) => flowStepOf(graph, hop)),
204
+ reached: demoted.reached.map(traceNodeOf),
205
+ ...(demoted.truncated ? { truncated: true } : {}),
206
+ });
207
+
163
208
  const details =
164
209
  seedIds.length === 0
165
210
  ? undefined
@@ -354,8 +399,8 @@ function graphNodeOf(
354
399
  node: ITtscGraphNode,
355
400
  ): ITtscGraphTour.INode {
356
401
  const span = node.implementation ?? node.evidence;
357
- const signature = signatureOf(graph.project, node);
358
- const doc = docOf(graph.project, node);
402
+ const signature = signatureOf(graph, node);
403
+ const doc = docOf(graph, node);
359
404
  const decorators = decoratorsOf(node);
360
405
  return {
361
406
  id: node.id,
@@ -561,13 +606,17 @@ function computeCentrality(graph: TtscGraphMemory): Map<string, number> {
561
606
 
562
607
  const invoked = (id: string): string[] => {
563
608
  const out: string[] = [];
564
- let hasBody = false;
565
609
  for (const edge of graph.outgoing(id)) {
566
- if (!INVOKE_KINDS.has(edge.kind)) continue;
567
- hasBody = true;
568
- out.push(edge.to);
610
+ if (INVOKE_KINDS.has(edge.kind)) out.push(edge.to);
569
611
  }
570
- if (!hasBody) {
612
+ // Whether the declaration has a body is a declaration fact, not an edge
613
+ // count. A concrete method that returns a literal, does local arithmetic or
614
+ // throws has a body and no invocation edges at all, and counting edges
615
+ // ranked it as though it were an abstract member — then walked its
616
+ // implementations as if the call had to dispatch. `runTrace` stopped using
617
+ // that proxy in #828; this was the second copy of it.
618
+ const node = graph.node(id);
619
+ if (node !== undefined && !hasDeclarationBody(graph, node)) {
571
620
  for (const edge of graph.incoming(id)) {
572
621
  if (DISPATCH_KINDS.has(edge.kind)) out.push(edge.from);
573
622
  }
@@ -691,11 +740,20 @@ function flowSeedIdsOf(seeds: ITtscGraphNode[]): string[] {
691
740
  * twice. Overlap is measured against the smaller flow, so a short chain fully
692
741
  * contained in a longer one counts as told, which is what a sibling entry
693
742
  * (`parse` beside `safeParse`) actually is.
743
+ *
744
+ * Neither side matches when it is empty, and the two are empty for different
745
+ * reasons. An empty CANDIDATE reached nothing, so it is not a flow at all and
746
+ * the caller settles it before asking here. An empty TOLD would mean the tour
747
+ * had published a flow that reached nothing; it cannot now, and while it could,
748
+ * this function reported every later candidate as a synonym of that emptiness.
749
+ * One directly self-recursive function — a retry loop, a tree walk, a parser's
750
+ * descent — produced exactly that set, and the tour lost every real flow ranked
751
+ * after it.
694
752
  */
695
753
  function overlaps(candidate: Set<string>, told: Set<string>): boolean {
754
+ if (candidate.size === 0 || told.size === 0) return false;
696
755
  const smaller = candidate.size <= told.size ? candidate : told;
697
756
  const larger = smaller === candidate ? told : candidate;
698
- if (smaller.size === 0) return true;
699
757
  let shared = 0;
700
758
  for (const id of smaller) if (larger.has(id)) shared++;
701
759
  return shared / smaller.size >= FLOW_OVERLAP;
@@ -705,13 +763,61 @@ function isTourTraceNode(
705
763
  graph: TtscGraphMemory,
706
764
  node: ITtscGraphTrace.INode,
707
765
  ): boolean {
708
- return (
709
- graph.node(node.id)?.closure !== true &&
710
- !isNoisePath(node.file) &&
711
- !isSharedUtility(graph, node.id)
766
+ return graph.node(node.id)?.closure !== true && !isNoisePath(node.file);
767
+ }
768
+
769
+ /**
770
+ * The hops a flow keeps, and the hops it would keep if the cut were not
771
+ * applied.
772
+ *
773
+ * Degree cannot tell a logger from a database commit. Both are called from many
774
+ * sites and call nothing onward, so no threshold on `isSharedUtility` separates
775
+ * the noise the cut exists to remove from the point where a runtime flow does
776
+ * its work. Rather than guess, this returns both readings and lets the caller
777
+ * demote instead of delete.
778
+ *
779
+ * A hub the flow continues past always keeps its inbound hop. Dropping it left
780
+ * the outbound step narrating a chain from a node the same flow reported it
781
+ * never reached, which is incoherent whatever that node means, so this one is
782
+ * not a judgement call and is applied unconditionally.
783
+ *
784
+ * `demotable` is non-empty exactly when the cut would empty the flow — when
785
+ * every hop it has lands on a hub. That is the shape the caller holds back and
786
+ * tells only if the tour finishes with nothing else, so a wrapper around a
787
+ * logger never displaces a real chain and a lone terminal action is never
788
+ * erased.
789
+ */
790
+ /**
791
+ * The nodes a flow reached, derived from the hops that survived.
792
+ *
793
+ * Deriving it rather than filtering it beside them is what makes a dangling
794
+ * step impossible: a step can only name endpoints of a kept hop, and every such
795
+ * endpoint is here. Filtering the two independently is what let a step narrate
796
+ * a chain from a node the same flow reported it had never reached.
797
+ */
798
+ function reachedOf(
799
+ graph: TtscGraphMemory,
800
+ trace: ITtscGraphTrace,
801
+ hops: readonly ITtscGraphTrace.IHop[],
802
+ ): ITtscGraphTrace.INode[] {
803
+ const touched = new Set(hops.flatMap((hop) => [hop.from, hop.to]));
804
+ return trace.reached.filter(
805
+ (node) => touched.has(node.id) && isTourTraceNode(graph, node),
712
806
  );
713
807
  }
714
808
 
809
+ function tourHops(
810
+ graph: TtscGraphMemory,
811
+ hops: readonly ITtscGraphTrace.IHop[],
812
+ ): { kept: ITtscGraphTrace.IHop[]; demotable: ITtscGraphTrace.IHop[] } {
813
+ const eligible = hops.filter((hop) => isTourHop(graph, hop));
814
+ const departures = new Set(eligible.map((hop) => hop.from));
815
+ const kept = eligible.filter(
816
+ (hop) => !isSharedUtility(graph, hop.to) || departures.has(hop.to),
817
+ );
818
+ return { kept, demotable: kept.length === 0 ? eligible : [] };
819
+ }
820
+
715
821
  function isTourHop(graph: TtscGraphMemory, hop: ITtscGraphTrace.IHop): boolean {
716
822
  const from = graph.node(hop.from);
717
823
  const to = graph.node(hop.to);
@@ -722,8 +828,7 @@ function isTourHop(graph: TtscGraphMemory, hop: ITtscGraphTrace.IHop): boolean {
722
828
  to.closure !== true &&
723
829
  !STRUCTURAL_KINDS.has(hop.kind) &&
724
830
  !isNoisePath(from.file) &&
725
- !isNoisePath(to.file) &&
726
- !isSharedUtility(graph, hop.to)
831
+ !isNoisePath(to.file)
727
832
  );
728
833
  }
729
834
 
@@ -739,8 +844,15 @@ function isTourHop(graph: TtscGraphMemory, hop: ITtscGraphTrace.IHop): boolean {
739
844
  // "top few percent of hubs" band regardless of project size, while the absolute
740
845
  // floor makes it a no-op on small graphs that have no genuine hub. The `out <= 1`
741
846
  // guard keeps thin pass-throughs out but never prunes a real branching step.
847
+ //
848
+ // The fan-in counted is EXECUTION fan-in. `realDegree` also counts `type_ref`,
849
+ // `extends`, `decorates`, and `tests`, and a widely referenced type or a
850
+ // widely decorated symbol is not a call hub: a `Config` class named in twelve
851
+ // parameter positions and constructed once was read as a hub and cut out of the
852
+ // flow that constructs it. Popularity as a name is not popularity as a call.
742
853
  function isSharedUtility(graph: TtscGraphMemory, id: string): boolean {
743
- return realDegree(graph, id).in >= 12 && executionDegree(graph, id).out <= 1;
854
+ const execution = executionDegree(graph, id);
855
+ return execution.in >= 12 && execution.out <= 1;
744
856
  }
745
857
 
746
858
  function flowStepOf(graph: TtscGraphMemory, hop: ITtscGraphTrace.IHop): string {
@@ -767,24 +879,6 @@ function touchesClosure(graph: TtscGraphMemory, id: string): boolean {
767
879
  return graph.node(id)?.closure === true;
768
880
  }
769
881
 
770
- function realDegree(
771
- graph: TtscGraphMemory,
772
- id: string,
773
- ): {
774
- in: number;
775
- out: number;
776
- } {
777
- let incoming = 0;
778
- let outgoing = 0;
779
- for (const edge of graph.outgoing(id))
780
- if (!STRUCTURAL_KINDS.has(edge.kind) && !touchesClosure(graph, edge.to))
781
- outgoing++;
782
- for (const edge of graph.incoming(id))
783
- if (!STRUCTURAL_KINDS.has(edge.kind) && !touchesClosure(graph, edge.from))
784
- incoming++;
785
- return { in: incoming, out: outgoing };
786
- }
787
-
788
882
  function executionDegree(
789
883
  graph: TtscGraphMemory,
790
884
  id: string,