@suss/resolution 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _suss_datalog from '@suss/datalog';
2
- import { Database, Atom, Tuple, Proof } from '@suss/datalog';
2
+ import { Database, Atom, Tuple, Proof, Rule } from '@suss/datalog';
3
3
 
4
4
  /** The tuples of one relation, as strings, which is all a case compares. */
5
5
  type FactsOf = (relation: string) => string[][];
@@ -148,6 +148,82 @@ declare function mountPathsOf(edges: MountEdges, childId: string): readonly stri
148
148
  */
149
149
  declare function agreedMountPrefix(edges: MountEdges, childId: string): string | null;
150
150
 
151
+ /**
152
+ * The result shape a why session hands back, shared by every language
153
+ * adapter so the CLI reads one type regardless of which adapter
154
+ * answered. Each adapter follows its own source to a witness proof
155
+ * over `RESOLUTION_RULES` and renders it through `explainResolutionProof`;
156
+ * this is what that render comes back as.
157
+ *
158
+ * `explainResolvedKey` is the proof pass itself, for an adapter that
159
+ * keeps its facts in a `Database` keyed by source location: it reruns
160
+ * the rules with witnesses over a copy of those facts, reads the one
161
+ * target the asked-about key resolves to, and renders the proof. An
162
+ * adapter supplies how a key is said in a sentence and nothing else.
163
+ */
164
+
165
+ /** A value or function said the way an answer prints it. */
166
+ interface ValueLocation {
167
+ name: string;
168
+ /** Relative to the session root when under it, absolute otherwise. */
169
+ file: string;
170
+ line: number;
171
+ }
172
+ /** What one witness re-evaluation cost, said rather than hidden. */
173
+ interface ExplainStats {
174
+ /** Facts the run had extracted, which the proof pass reran. */
175
+ baseFacts: number;
176
+ /** Facts the exhaustive pass derived on top of those. */
177
+ derivedFacts: number;
178
+ evaluateMs: number;
179
+ }
180
+ interface WhyExplained {
181
+ explanation: ResolutionExplanation;
182
+ /** The chain's atoms, each said in source terms. */
183
+ chain: string[];
184
+ /** The chain and its reasons as printable lines. */
185
+ lines: string[];
186
+ target: ValueLocation;
187
+ stats: ExplainStats;
188
+ }
189
+ interface ExplainResolvedKeyOptions {
190
+ /** The facts the session extracted. Read, never written to. */
191
+ db: Database;
192
+ /** The resolution rules plus whatever the language adds to them. */
193
+ rules: Rule[];
194
+ /** The fact key of the value asked about. */
195
+ key: string;
196
+ /** Where a fact key was written, or null for one the session never indexed. */
197
+ locate: (key: string) => ValueLocation | null;
198
+ /** How to say a key that has no location, which is a file path. */
199
+ displayPath: (key: string) => string;
200
+ maxDepth?: number;
201
+ phrases?: Record<string, StepPhrase>;
202
+ }
203
+ /**
204
+ * Why `key` resolves to what it does: the witness proof, flattened to
205
+ * the chain and rendered. Null when the key resolves to nothing or to
206
+ * more than one function, which the caller says in its own words.
207
+ */
208
+ declare function explainResolvedKey(options: ExplainResolvedKeyOptions): WhyExplained | null;
209
+
210
+ /**
211
+ * The single-answer policy shared by every question shaped `[key,
212
+ * answer]`, such as `wantedIsWrittenAs` or `wantedSubjectWritten`. A
213
+ * call is written as itself, so a call asked about directly always
214
+ * matches its own key; that row is dropped before the count so one
215
+ * other answer settles it instead of counting as two.
216
+ *
217
+ * A placeholder write, `client = None` before a guard fills it in, is
218
+ * set aside the same way when the key has any other answer. A key
219
+ * written only as a placeholder keeps it.
220
+ */
221
+
222
+ /** The one answer each key settles on, once a key's match against itself is dropped. */
223
+ declare function singleAnswers(rows: Iterable<Tuple>, placeholders?: ReadonlySet<string>): Map<string, string>;
224
+ /** The keys an adapter marked `placeholderValue`, for passing to `singleAnswers`. */
225
+ declare function placeholderValues(db: Database): ReadonlySet<string>;
226
+
151
227
  /** A step to the value x is written as. */
152
228
  declare const VALUE_STEP: _suss_datalog.Term;
153
229
  /**
@@ -196,4 +272,4 @@ declare const RESOLUTION_QUESTIONS: _suss_datalog.Rule[];
196
272
  /** The relations `RESOLUTION_QUESTIONS` answers into. */
197
273
  declare const ANSWER_RELATIONS: string[];
198
274
 
199
- export { ANSWER_RELATIONS, type CaseFiles, type ContractCase, type ContractOptions, type DescribeAtom, type ExplainOptions, FACT_CONTRACT_CASES, type FactsOf, type MountEdge, type MountEdges, NAMESPACE_IMPORT, NAMESPACE_IMPORT_NAME, RESOLUTION_QUESTIONS, RESOLUTION_RULES, RESULT_STEP, type ResolutionExplanation, type ResolutionStep, type StepContext, type StepPhrase, VALUE_STEP, agreedMountPrefix, checkFactContract, explainResolutionProof, joinMountedPath, mountPathsOf, renderExplanation };
275
+ export { ANSWER_RELATIONS, type CaseFiles, type ContractCase, type ContractOptions, type DescribeAtom, type ExplainOptions, type ExplainResolvedKeyOptions, type ExplainStats, FACT_CONTRACT_CASES, type FactsOf, type MountEdge, type MountEdges, NAMESPACE_IMPORT, NAMESPACE_IMPORT_NAME, RESOLUTION_QUESTIONS, RESOLUTION_RULES, RESULT_STEP, type ResolutionExplanation, type ResolutionStep, type StepContext, type StepPhrase, VALUE_STEP, type ValueLocation, type WhyExplained, agreedMountPrefix, checkFactContract, explainResolutionProof, explainResolvedKey, joinMountedPath, mountPathsOf, placeholderValues, renderExplanation, singleAnswers };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _suss_datalog from '@suss/datalog';
2
- import { Database, Atom, Tuple, Proof } from '@suss/datalog';
2
+ import { Database, Atom, Tuple, Proof, Rule } from '@suss/datalog';
3
3
 
4
4
  /** The tuples of one relation, as strings, which is all a case compares. */
5
5
  type FactsOf = (relation: string) => string[][];
@@ -148,6 +148,82 @@ declare function mountPathsOf(edges: MountEdges, childId: string): readonly stri
148
148
  */
149
149
  declare function agreedMountPrefix(edges: MountEdges, childId: string): string | null;
150
150
 
151
+ /**
152
+ * The result shape a why session hands back, shared by every language
153
+ * adapter so the CLI reads one type regardless of which adapter
154
+ * answered. Each adapter follows its own source to a witness proof
155
+ * over `RESOLUTION_RULES` and renders it through `explainResolutionProof`;
156
+ * this is what that render comes back as.
157
+ *
158
+ * `explainResolvedKey` is the proof pass itself, for an adapter that
159
+ * keeps its facts in a `Database` keyed by source location: it reruns
160
+ * the rules with witnesses over a copy of those facts, reads the one
161
+ * target the asked-about key resolves to, and renders the proof. An
162
+ * adapter supplies how a key is said in a sentence and nothing else.
163
+ */
164
+
165
+ /** A value or function said the way an answer prints it. */
166
+ interface ValueLocation {
167
+ name: string;
168
+ /** Relative to the session root when under it, absolute otherwise. */
169
+ file: string;
170
+ line: number;
171
+ }
172
+ /** What one witness re-evaluation cost, said rather than hidden. */
173
+ interface ExplainStats {
174
+ /** Facts the run had extracted, which the proof pass reran. */
175
+ baseFacts: number;
176
+ /** Facts the exhaustive pass derived on top of those. */
177
+ derivedFacts: number;
178
+ evaluateMs: number;
179
+ }
180
+ interface WhyExplained {
181
+ explanation: ResolutionExplanation;
182
+ /** The chain's atoms, each said in source terms. */
183
+ chain: string[];
184
+ /** The chain and its reasons as printable lines. */
185
+ lines: string[];
186
+ target: ValueLocation;
187
+ stats: ExplainStats;
188
+ }
189
+ interface ExplainResolvedKeyOptions {
190
+ /** The facts the session extracted. Read, never written to. */
191
+ db: Database;
192
+ /** The resolution rules plus whatever the language adds to them. */
193
+ rules: Rule[];
194
+ /** The fact key of the value asked about. */
195
+ key: string;
196
+ /** Where a fact key was written, or null for one the session never indexed. */
197
+ locate: (key: string) => ValueLocation | null;
198
+ /** How to say a key that has no location, which is a file path. */
199
+ displayPath: (key: string) => string;
200
+ maxDepth?: number;
201
+ phrases?: Record<string, StepPhrase>;
202
+ }
203
+ /**
204
+ * Why `key` resolves to what it does: the witness proof, flattened to
205
+ * the chain and rendered. Null when the key resolves to nothing or to
206
+ * more than one function, which the caller says in its own words.
207
+ */
208
+ declare function explainResolvedKey(options: ExplainResolvedKeyOptions): WhyExplained | null;
209
+
210
+ /**
211
+ * The single-answer policy shared by every question shaped `[key,
212
+ * answer]`, such as `wantedIsWrittenAs` or `wantedSubjectWritten`. A
213
+ * call is written as itself, so a call asked about directly always
214
+ * matches its own key; that row is dropped before the count so one
215
+ * other answer settles it instead of counting as two.
216
+ *
217
+ * A placeholder write, `client = None` before a guard fills it in, is
218
+ * set aside the same way when the key has any other answer. A key
219
+ * written only as a placeholder keeps it.
220
+ */
221
+
222
+ /** The one answer each key settles on, once a key's match against itself is dropped. */
223
+ declare function singleAnswers(rows: Iterable<Tuple>, placeholders?: ReadonlySet<string>): Map<string, string>;
224
+ /** The keys an adapter marked `placeholderValue`, for passing to `singleAnswers`. */
225
+ declare function placeholderValues(db: Database): ReadonlySet<string>;
226
+
151
227
  /** A step to the value x is written as. */
152
228
  declare const VALUE_STEP: _suss_datalog.Term;
153
229
  /**
@@ -196,4 +272,4 @@ declare const RESOLUTION_QUESTIONS: _suss_datalog.Rule[];
196
272
  /** The relations `RESOLUTION_QUESTIONS` answers into. */
197
273
  declare const ANSWER_RELATIONS: string[];
198
274
 
199
- export { ANSWER_RELATIONS, type CaseFiles, type ContractCase, type ContractOptions, type DescribeAtom, type ExplainOptions, FACT_CONTRACT_CASES, type FactsOf, type MountEdge, type MountEdges, NAMESPACE_IMPORT, NAMESPACE_IMPORT_NAME, RESOLUTION_QUESTIONS, RESOLUTION_RULES, RESULT_STEP, type ResolutionExplanation, type ResolutionStep, type StepContext, type StepPhrase, VALUE_STEP, agreedMountPrefix, checkFactContract, explainResolutionProof, joinMountedPath, mountPathsOf, renderExplanation };
275
+ export { ANSWER_RELATIONS, type CaseFiles, type ContractCase, type ContractOptions, type DescribeAtom, type ExplainOptions, type ExplainResolvedKeyOptions, type ExplainStats, FACT_CONTRACT_CASES, type FactsOf, type MountEdge, type MountEdges, NAMESPACE_IMPORT, NAMESPACE_IMPORT_NAME, RESOLUTION_QUESTIONS, RESOLUTION_RULES, RESULT_STEP, type ResolutionExplanation, type ResolutionStep, type StepContext, type StepPhrase, VALUE_STEP, type ValueLocation, type WhyExplained, agreedMountPrefix, checkFactContract, explainResolutionProof, explainResolvedKey, joinMountedPath, mountPathsOf, placeholderValues, renderExplanation, singleAnswers };
package/dist/index.js CHANGED
@@ -477,6 +477,93 @@ function pathsOf(edges, memo, childId, visiting) {
477
477
  return result;
478
478
  }
479
479
 
480
+ // src/session.ts
481
+ import { Database, evaluate, proofOf, witnesses } from "@suss/datalog";
482
+ function explainResolvedKey(options) {
483
+ const { db, rules, key, locate, displayPath } = options;
484
+ const proofDb = new Database();
485
+ let baseFacts = 0;
486
+ for (const relation of db.relationNames()) {
487
+ for (const tuple of db.facts(relation)) {
488
+ proofDb.add(relation, tuple);
489
+ baseFacts++;
490
+ }
491
+ }
492
+ const started = performance.now();
493
+ evaluate(proofDb, rules, witnesses);
494
+ const evaluateMs = performance.now() - started;
495
+ const derivedFacts = proofDb.relationNames().reduce((count, relation) => count + proofDb.size(relation), 0) - baseFacts;
496
+ const targets = new Set(
497
+ proofDb.lookup("resolves", 0, key).map((tuple) => String(tuple[1]))
498
+ );
499
+ if (targets.size !== 1) {
500
+ return null;
501
+ }
502
+ const target = [...targets][0];
503
+ const proof = proofOf(
504
+ proofDb,
505
+ "resolves",
506
+ [key, target],
507
+ options.maxDepth === void 0 ? {} : { maxDepth: options.maxDepth }
508
+ );
509
+ const describe = (atom) => {
510
+ const location = locate(String(atom));
511
+ return location === null ? displayPath(String(atom)) : `${location.name} (${location.file}:${location.line})`;
512
+ };
513
+ const explanation = explainResolutionProof(proof, {
514
+ describe,
515
+ ...options.phrases === void 0 ? {} : { phrases: options.phrases }
516
+ });
517
+ if (explanation === null) {
518
+ return null;
519
+ }
520
+ const targetLocation = locate(target);
521
+ if (targetLocation === null) {
522
+ return null;
523
+ }
524
+ return {
525
+ explanation,
526
+ chain: explanation.atoms.map(describe),
527
+ lines: renderExplanation(explanation, describe),
528
+ target: targetLocation,
529
+ stats: { baseFacts, derivedFacts, evaluateMs }
530
+ };
531
+ }
532
+
533
+ // src/singleAnswer.ts
534
+ var NO_PLACEHOLDERS = /* @__PURE__ */ new Set();
535
+ function singleAnswers(rows, placeholders = NO_PLACEHOLDERS) {
536
+ const candidates = /* @__PURE__ */ new Map();
537
+ for (const row of rows) {
538
+ const key = String(row[0]);
539
+ const answer = String(row[1]);
540
+ if (answer === key) {
541
+ continue;
542
+ }
543
+ const set = candidates.get(key) ?? /* @__PURE__ */ new Set();
544
+ set.add(answer);
545
+ candidates.set(key, set);
546
+ }
547
+ const settled = /* @__PURE__ */ new Map();
548
+ for (const [key, set] of candidates) {
549
+ const answers = withoutPlaceholders(set, placeholders);
550
+ if (answers.length === 1) {
551
+ settled.set(key, answers[0]);
552
+ }
553
+ }
554
+ return settled;
555
+ }
556
+ function placeholderValues(db) {
557
+ return new Set(db.facts("placeholderValue").map((row) => String(row[0])));
558
+ }
559
+ function withoutPlaceholders(answers, placeholders) {
560
+ const kept = [...answers].filter((answer) => !placeholders.has(answer));
561
+ if (kept.length === 0) {
562
+ return [...answers];
563
+ }
564
+ return kept;
565
+ }
566
+
480
567
  // src/index.ts
481
568
  import { constant, lit, rule, variable as v } from "@suss/datalog";
482
569
  var VALUE_STEP = constant("value");
@@ -592,9 +679,9 @@ var RESOLUTION_RULES = [
592
679
  [lit("invokes", v("r"), v("f")), lit("returnsValue", v("f"), v("ret"))],
593
680
  "call result"
594
681
  ),
595
- // Where a walk gets to, and whether it ran a call on the way. A value
596
- // step keeps whatever the rest of the walk was, and a result step
597
- // makes the whole walk one that ran a call.
682
+ // Where a walk gets to, and whether it ran a call on the way. The walk
683
+ // so far comes first, so demand stays on the value asked about; the
684
+ // README says what the other order cost.
598
685
  rule(
599
686
  "reaches",
600
687
  [v("x"), v("z"), v("kind")],
@@ -604,27 +691,27 @@ var RESOLUTION_RULES = [
604
691
  "reaches",
605
692
  [v("x"), v("z"), v("kind")],
606
693
  [
607
- lit("stepsTo", v("x"), v("y"), VALUE_STEP),
608
- lit("reaches", v("y"), v("z"), v("kind"))
694
+ lit("reaches", v("x"), v("y"), VALUE_STEP),
695
+ lit("stepsTo", v("y"), v("z"), v("kind"))
609
696
  ]
610
697
  ),
611
- // Two rules rather than one leaving the rest of the walk's kind free.
612
- // A free kind is a second question about the same relation, and a
698
+ // Two rules rather than one leaving the next step's kind free. A free
699
+ // kind is a second question about the same relation, and a
613
700
  // demand-driven run then derives both to answer either.
614
701
  rule(
615
702
  "reaches",
616
703
  [v("x"), v("z"), RESULT_STEP],
617
704
  [
618
- lit("stepsTo", v("x"), v("y"), RESULT_STEP),
619
- lit("reaches", v("y"), v("z"), VALUE_STEP)
705
+ lit("reaches", v("x"), v("y"), RESULT_STEP),
706
+ lit("stepsTo", v("y"), v("z"), VALUE_STEP)
620
707
  ]
621
708
  ),
622
709
  rule(
623
710
  "reaches",
624
711
  [v("x"), v("z"), RESULT_STEP],
625
712
  [
626
- lit("stepsTo", v("x"), v("y"), RESULT_STEP),
627
- lit("reaches", v("y"), v("z"), RESULT_STEP)
713
+ lit("reaches", v("x"), v("y"), RESULT_STEP),
714
+ lit("stepsTo", v("y"), v("z"), RESULT_STEP)
628
715
  ]
629
716
  ),
630
717
  // What a value comes down to: the walk that never ran a call, stopped
@@ -670,6 +757,18 @@ var RESOLUTION_RULES = [
670
757
  [v("x"), v("z")],
671
758
  [lit("reaches", v("x"), v("z"), VALUE_STEP), lit("objectValue", v("z"))]
672
759
  ),
760
+ // A call is written as whatever its callee's return value is written
761
+ // as. Unlike `comesTo`, this has nothing to protect: which expression
762
+ // a value was written as does not compete with unwrapping it.
763
+ rule(
764
+ "isWrittenAs",
765
+ [v("x"), v("z")],
766
+ [
767
+ lit("invokes", v("x"), v("f")),
768
+ lit("returnsValue", v("f"), v("ret")),
769
+ lit("isWrittenAs", v("ret"), v("z"))
770
+ ]
771
+ ),
673
772
  // What one call site put in a parameter, told apart from what the
674
773
  // other callers passed.
675
774
  rule(
@@ -970,29 +1069,14 @@ var RESOLUTION_QUESTIONS = [
970
1069
  // here demands reaches or callsInto. The chains are closures with
971
1070
  // no depth bound: an alias run of any length resolves.
972
1071
  rule("callOriginChain", [v("x"), v("x")], [lit("wantedCallOrigin", v("x"))]),
1072
+ // The chain with at least one hop taken, which is what tells a
1073
+ // destructured binding apart from asking about a node directly. The
1074
+ // whole chain is the seed plus those hops.
973
1075
  rule(
974
1076
  "callOriginChain",
975
1077
  [v("x"), v("z")],
976
- [lit("callOriginChain", v("x"), v("y")), lit("binds", v("y"), v("z"))]
977
- ),
978
- rule(
979
- "callOriginChain",
980
- [v("x"), v("z")],
981
- [
982
- lit("callOriginChain", v("x"), v("y")),
983
- lit("endsHolding", v("y"), v("z"))
984
- ]
985
- ),
986
- rule(
987
- "callOriginChain",
988
- [v("x"), v("z")],
989
- [
990
- lit("callOriginChain", v("x"), v("y")),
991
- lit("fallbackBranch", v("y"), v("z"))
992
- ]
1078
+ [lit("callOriginChainStepped", v("x"), v("z"))]
993
1079
  ),
994
- // The chain with at least one hop taken, which is what tells a
995
- // destructured binding apart from asking about a node directly.
996
1080
  rule(
997
1081
  "callOriginChainStepped",
998
1082
  [v("x"), v("z")],
@@ -1231,7 +1315,10 @@ export {
1231
1315
  agreedMountPrefix,
1232
1316
  checkFactContract,
1233
1317
  explainResolutionProof,
1318
+ explainResolvedKey,
1234
1319
  joinMountedPath,
1235
1320
  mountPathsOf,
1236
- renderExplanation
1321
+ placeholderValues,
1322
+ renderExplanation,
1323
+ singleAnswers
1237
1324
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/resolution",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "type": "module",
5
5
  "description": "Language-neutral rules for following a value to the function it comes down to.",
6
6
  "keywords": [
@@ -53,6 +53,6 @@
53
53
  "access": "public"
54
54
  },
55
55
  "dependencies": {
56
- "@suss/datalog": "0.21.0"
56
+ "@suss/datalog": "0.22.0"
57
57
  }
58
58
  }