@descryy/core 0.6.3 → 0.7.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/query/index.d.ts +3 -1
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +2 -1
- package/dist/query/index.js.map +1 -1
- package/dist/query/model-table.d.ts +7 -0
- package/dist/query/model-table.d.ts.map +1 -1
- package/dist/query/model-table.js +7 -4
- package/dist/query/model-table.js.map +1 -1
- package/dist/query/persistence-reach.d.ts +150 -0
- package/dist/query/persistence-reach.d.ts.map +1 -0
- package/dist/query/persistence-reach.js +393 -0
- package/dist/query/persistence-reach.js.map +1 -0
- package/dist/store/reader.d.ts.map +1 -1
- package/dist/store/reader.js +6 -1
- package/dist/store/reader.js.map +1 -1
- package/dist/store/schema.d.ts +2 -2
- package/dist/store/schema.d.ts.map +1 -1
- package/dist/store/schema.js +2 -1
- package/dist/store/schema.js.map +1 -1
- package/dist/store/writer.d.ts.map +1 -1
- package/dist/store/writer.js +4 -3
- package/dist/store/writer.js.map +1 -1
- package/dist/tiers/certify.d.ts +174 -0
- package/dist/tiers/certify.d.ts.map +1 -1
- package/dist/tiers/certify.js +309 -5
- package/dist/tiers/certify.js.map +1 -1
- package/dist/tiers/index.d.ts +1 -1
- package/dist/tiers/index.d.ts.map +1 -1
- package/dist/tiers/index.js.map +1 -1
- package/package.json +2 -2
package/dist/query/index.d.ts
CHANGED
|
@@ -31,5 +31,7 @@ export { rankRootCause, DEFAULT_ROOT_CAUSE_WEIGHTS, ROOT_CAUSE_TRACE_EDGE_TYPES,
|
|
|
31
31
|
export type { RootCauseWeights, RootCauseOptions, RankedRootCauseCandidate, RootCauseRanking, ChangeCouplingEvidence, } from "./root-cause-score.ts";
|
|
32
32
|
export { createResolutionFloor } from "./resolution-floor.ts";
|
|
33
33
|
export type { ResolutionFloor } from "./resolution-floor.ts";
|
|
34
|
-
export { tableForModel, modelsForTable } from "./model-table.ts";
|
|
34
|
+
export { tableForModel, modelsForTable, tableIdForModel } from "./model-table.ts";
|
|
35
|
+
export { endpointsReachingTable, MODEL_ACCESS_EDGE_TYPES, WRITE_MEANING_NOTE } from "./persistence-reach.ts";
|
|
36
|
+
export type { AccessDirection, EndpointTableAccess, PersistenceHopGap, PersistencePath, PersistenceReach, PersistenceReachOptions, } from "./persistence-reach.ts";
|
|
35
37
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/query/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/query/index.ts"],"names":[],"mappings":"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"}
|
package/dist/query/index.js
CHANGED
|
@@ -15,5 +15,6 @@ export { applyDeclaredValues, DECLARED_VALUE_RELIABILITY } from "./declared-valu
|
|
|
15
15
|
export { describeRowClosures } from "./row-closure-picture.js";
|
|
16
16
|
export { rankRootCause, DEFAULT_ROOT_CAUSE_WEIGHTS, ROOT_CAUSE_TRACE_EDGE_TYPES, INCIDENT_SATURATION } from "./root-cause-score.js";
|
|
17
17
|
export { createResolutionFloor } from "./resolution-floor.js";
|
|
18
|
-
export { tableForModel, modelsForTable } from "./model-table.js";
|
|
18
|
+
export { tableForModel, modelsForTable, tableIdForModel } from "./model-table.js";
|
|
19
|
+
export { endpointsReachingTable, MODEL_ACCESS_EDGE_TYPES, WRITE_MEANING_NOTE } from "./persistence-reach.js";
|
|
19
20
|
//# sourceMappingURL=index.js.map
|
package/dist/query/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/query/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAQjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGnD,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAGxG,OAAO,EACL,QAAQ,EACR,eAAe,EACf,aAAa,EACb,YAAY,EACZ,oBAAoB,EACpB,QAAQ,EACR,UAAU,EACV,WAAW,GACZ,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AASrG,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAG9E,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAG3D,OAAO,EAAE,0BAA0B,EAAE,wBAAwB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAQ7G,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGzE,OAAO,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAC5F,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1D,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAIrG,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AAO9F,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAQ/D,OAAO,EAAE,aAAa,EAAE,0BAA0B,EAAE,2BAA2B,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AASpI,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAG9D,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/query/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAQjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGnD,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAGxG,OAAO,EACL,QAAQ,EACR,eAAe,EACf,aAAa,EACb,YAAY,EACZ,oBAAoB,EACpB,QAAQ,EACR,UAAU,EACV,WAAW,GACZ,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AASrG,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAG9E,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAG3D,OAAO,EAAE,0BAA0B,EAAE,wBAAwB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAQ7G,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGzE,OAAO,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAC5F,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1D,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAIrG,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AAO9F,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAQ/D,OAAO,EAAE,aAAa,EAAE,0BAA0B,EAAE,2BAA2B,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AASpI,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAG9D,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAElF,OAAO,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC"}
|
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
* time, not stored" precedent as `contracts/orphans.ts`'s `keyOf()`. */
|
|
17
17
|
import { type IdentityScope, type IRNode } from "@descryy/ir";
|
|
18
18
|
import type { AdjacencyProvider } from "./provider.ts";
|
|
19
|
+
/** The table id a `MODEL` node's own declared shape computes to, or
|
|
20
|
+
* `undefined` when it names no table — refused rather than guessed at mint
|
|
21
|
+
* time (`model-tables.ts`), so this is a real absence, not a lookup miss.
|
|
22
|
+
* Exported so a composed query can enumerate `MODEL` once and both filter and
|
|
23
|
+
* probe from that one enumeration; a second `nodesOfType` call to recover the
|
|
24
|
+
* same set is a scan the store does not need to repeat. */
|
|
25
|
+
export declare function tableIdForModel(model: IRNode, scope: IdentityScope): string | undefined;
|
|
19
26
|
/** A `MODEL` node's `DATABASE_TABLE` counterpart, or `undefined` when the
|
|
20
27
|
* model names no table, or when the table it names was never observed in
|
|
21
28
|
* this graph (no adapter emitted it, or it belongs to a different scope).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-table.d.ts","sourceRoot":"","sources":["../../src/query/model-table.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;yEAeyE;AAEzE,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,MAAM,EACZ,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,iBAAiB,EAAW,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"model-table.d.ts","sourceRoot":"","sources":["../../src/query/model-table.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;yEAeyE;AAEzE,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,MAAM,EACZ,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,iBAAiB,EAAW,MAAM,eAAe,CAAC;AAEhE;;;;;4DAK4D;AAC5D,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CAKvF;AAED;;;;;8EAK8E;AAC9E,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,iBAAiB,EAC3B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,aAAa,GACnB,MAAM,GAAG,SAAS,CAGpB;AAED;;;;mCAImC;AACnC,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,iBAAiB,EAC3B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,aAAa,EACpB,KAAK,CAAC,EAAE,MAAM,GACb;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAIrE"}
|
|
@@ -17,8 +17,11 @@
|
|
|
17
17
|
import { DEFAULT_MODEL_SCHEMA, nodeId, tableQsp, } from "@descryy/ir";
|
|
18
18
|
/** The table id a `MODEL` node's own declared shape computes to, or
|
|
19
19
|
* `undefined` when it names no table — refused rather than guessed at mint
|
|
20
|
-
* time (`model-tables.ts`), so this is a real absence, not a lookup miss.
|
|
21
|
-
|
|
20
|
+
* time (`model-tables.ts`), so this is a real absence, not a lookup miss.
|
|
21
|
+
* Exported so a composed query can enumerate `MODEL` once and both filter and
|
|
22
|
+
* probe from that one enumeration; a second `nodesOfType` call to recover the
|
|
23
|
+
* same set is a scan the store does not need to repeat. */
|
|
24
|
+
export function tableIdForModel(model, scope) {
|
|
22
25
|
const table = model.attrs["table"];
|
|
23
26
|
if (typeof table !== "string" || table === "")
|
|
24
27
|
return undefined;
|
|
@@ -32,7 +35,7 @@ function declaredTableId(model, scope) {
|
|
|
32
35
|
* same `IdentityScope` the graph was built with; `DATABASE_TABLE` is
|
|
33
36
|
* workspace-scoped (`NODE_SCOPE`), so a mismatched scope silently misses. */
|
|
34
37
|
export function tableForModel(provider, model, scope) {
|
|
35
|
-
const tableId =
|
|
38
|
+
const tableId = tableIdForModel(model, scope);
|
|
36
39
|
return tableId === undefined ? undefined : provider.node(tableId);
|
|
37
40
|
}
|
|
38
41
|
/** The reverse direction: every `MODEL` node in this graph whose declared
|
|
@@ -42,7 +45,7 @@ export function tableForModel(provider, model, scope) {
|
|
|
42
45
|
* walk in the other direction. */
|
|
43
46
|
export function modelsForTable(provider, table, scope, limit) {
|
|
44
47
|
const set = limit === undefined ? provider.nodesOfType("MODEL") : provider.nodesOfType("MODEL", limit);
|
|
45
|
-
const models = set.nodes.filter((model) =>
|
|
48
|
+
const models = set.nodes.filter((model) => tableIdForModel(model, scope) === table.id);
|
|
46
49
|
return { models, truncated: set.truncated };
|
|
47
50
|
}
|
|
48
51
|
//# sourceMappingURL=model-table.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-table.js","sourceRoot":"","sources":["../../src/query/model-table.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;yEAeyE;AAEzE,OAAO,EACL,oBAAoB,EACpB,MAAM,EACN,QAAQ,GAGT,MAAM,aAAa,CAAC;AAIrB
|
|
1
|
+
{"version":3,"file":"model-table.js","sourceRoot":"","sources":["../../src/query/model-table.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;yEAeyE;AAEzE,OAAO,EACL,oBAAoB,EACpB,MAAM,EACN,QAAQ,GAGT,MAAM,aAAa,CAAC;AAIrB;;;;;4DAK4D;AAC5D,MAAM,UAAU,eAAe,CAAC,KAAa,EAAE,KAAoB;IACjE,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IAChE,MAAM,MAAM,GAAG,OAAO,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAC;IACxG,OAAO,MAAM,CAAC,KAAK,EAAE,gBAAgB,EAAE,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC,CAAC;AACxE,CAAC;AAED;;;;;8EAK8E;AAC9E,MAAM,UAAU,aAAa,CAC3B,QAA2B,EAC3B,KAAa,EACb,KAAoB;IAEpB,MAAM,OAAO,GAAG,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IAC9C,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACpE,CAAC;AAED;;;;mCAImC;AACnC,MAAM,UAAU,cAAc,CAC5B,QAA2B,EAC3B,KAAa,EACb,KAAoB,EACpB,KAAc;IAEd,MAAM,GAAG,GAAY,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAChH,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,KAAK,CAAC,EAAE,CAAC,CAAC;IACvF,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/** "Which endpoints reach this table, and which of them write to it" — the
|
|
2
|
+
* persistence half of the bug-relevant subgraph, made askable.
|
|
3
|
+
*
|
|
4
|
+
* Until this query existed the question could not be asked in any language:
|
|
5
|
+
* `DATABASE_TABLE`/`DATABASE_COLUMN` are in no edge, of any type, in either
|
|
6
|
+
* direction. That is not a defect — it is the consequence of a ruling. The
|
|
7
|
+
* model-to-table join was deliberately built **by shared node identity rather
|
|
8
|
+
* than by an edge** (`databaseTableNodesFromModel`, `@descryy/ir`, DEC-240),
|
|
9
|
+
* so an ORM-derived producer and a raw-DDL-derived producer land on the same
|
|
10
|
+
* id for the same real table with no coordination. Identity merging and
|
|
11
|
+
* reachability are different problems, and solving the first left the second
|
|
12
|
+
* untouched. This module solves the second **without touching the first**:
|
|
13
|
+
* no new edge type, no new node type, no adapter change
|
|
14
|
+
* (`DEC-NEXT-database-table-column-traversability-vs-queryability.md`,
|
|
15
|
+
* `DEC-NEXT-persistence-half-of-the-bug-relevant-subgraph-is-unreachable.md`
|
|
16
|
+
* option 1).
|
|
17
|
+
*
|
|
18
|
+
* ## The three hops, walked backwards from the table
|
|
19
|
+
*
|
|
20
|
+
* 1. **table -> model** — `modelsForTable` (`model-table.ts`), an O(1) exact
|
|
21
|
+
* id match per model, recomputed from the model's own declared table name.
|
|
22
|
+
* **This leg has no edge under it.** Its evidence is two declarations: a
|
|
23
|
+
* string in the model and the table node's identity. That is why the whole
|
|
24
|
+
* composed claim is name-level and can never reach reliability class A,
|
|
25
|
+
* however well-resolved the other two hops are.
|
|
26
|
+
* 2. **model -> accessor** — `READS`/`ASSIGNS` edges pointing at the model.
|
|
27
|
+
* 3. **accessor -> route -> endpoint** — one budgeted inbound `CALLS` walk
|
|
28
|
+
* seeded on every accessor at once (never one walk per accessor), carrying
|
|
29
|
+
* the set of accessors each node reaches, then
|
|
30
|
+
* `API_ROUTE --SERVES_API--> API_ENDPOINT`.
|
|
31
|
+
*
|
|
32
|
+
* ## The write gate, and why it names no language
|
|
33
|
+
*
|
|
34
|
+
* A developer acting on "this endpoint writes to your users table" changes
|
|
35
|
+
* code. Only some producers distinguish an assignment from a read; the rest
|
|
36
|
+
* emit every field access as `READS`, so answering the write question from a
|
|
37
|
+
* `READS` edge would state the opposite of what the code does for those
|
|
38
|
+
* producers. It must therefore be gated — but gating on a language name above
|
|
39
|
+
* the IR is a boundary breach (rule 1), and the mechanical boundary lint is
|
|
40
|
+
* right to forbid it.
|
|
41
|
+
*
|
|
42
|
+
* So the gate is **evidence, not configuration**: a producer is
|
|
43
|
+
* direction-aware iff it is observed emitting at least one `ASSIGNS` edge
|
|
44
|
+
* into a `MODEL` in this graph. An `ASSIGNS` edge is self-evidencing and
|
|
45
|
+
* needs no gate at all. A `READS` edge from a direction-aware producer is a
|
|
46
|
+
* genuine read. A `READS` edge from a producer never seen emitting `ASSIGNS`
|
|
47
|
+
* is `unknown-direction` — reported as not analysable, folded into neither
|
|
48
|
+
* the read set nor the write set. The failure mode is one-directional by
|
|
49
|
+
* construction: the worst this rule can do is decline to answer, and it can
|
|
50
|
+
* never manufacture a write. */
|
|
51
|
+
import { type EdgeType, type IdentityScope, type IREdge, type IRNode, type ReliabilityClass } from "@descryy/ir";
|
|
52
|
+
import type { AdjacencyProvider, QueryResult } from "./provider.ts";
|
|
53
|
+
/** The two edge types that carry a function's access to a model's fields.
|
|
54
|
+
* Named once so the accessor read and the direction probe cannot drift. */
|
|
55
|
+
export declare const MODEL_ACCESS_EDGE_TYPES: readonly EdgeType[];
|
|
56
|
+
/** What this query is willing to say about an endpoint's access to a table.
|
|
57
|
+
*
|
|
58
|
+
* `writes` means **an assignment to a mapped column of the model backing this
|
|
59
|
+
* table**, which is what the underlying edge actually records. It is not a
|
|
60
|
+
* witnessed database write: whether the assignment reaches the database
|
|
61
|
+
* depends on the object being session-attached and the session being flushed,
|
|
62
|
+
* and nothing in the graph establishes either. Rule 4 governs the difference —
|
|
63
|
+
* this is a behavioural prediction until a runtime run confirms it, and the
|
|
64
|
+
* reliability cap and `WRITE_MEANING_NOTE` both say so.
|
|
65
|
+
*
|
|
66
|
+
* `unknown-direction` is a real answer, not a missing one: the endpoint
|
|
67
|
+
* demonstrably reaches the table, and the direction is not analysable from
|
|
68
|
+
* the evidence the producer emitted. */
|
|
69
|
+
export type AccessDirection = "writes" | "reads" | "unknown-direction";
|
|
70
|
+
/** Which hop stopped, and where. A missing hop is disclosed rather than
|
|
71
|
+
* silently yielding a shorter answer — an endpoint absent from the result
|
|
72
|
+
* because hop 2 had nothing to walk is not an endpoint that does not touch
|
|
73
|
+
* the table. */
|
|
74
|
+
export interface PersistenceHopGap {
|
|
75
|
+
readonly hop: "model-for-table" | "accessor-for-model" | "route-for-accessor" | "endpoint-for-route";
|
|
76
|
+
/** The node the hop was walked from. */
|
|
77
|
+
readonly node: IRNode;
|
|
78
|
+
readonly reason: string;
|
|
79
|
+
}
|
|
80
|
+
/** One traced route from an endpoint to the table, with every edge under it. */
|
|
81
|
+
export interface PersistencePath {
|
|
82
|
+
readonly endpoint: IRNode;
|
|
83
|
+
readonly route: IRNode;
|
|
84
|
+
/** The function carrying the model-access edge — the end of the call chain. */
|
|
85
|
+
readonly accessor: IRNode;
|
|
86
|
+
readonly model: IRNode;
|
|
87
|
+
readonly direction: AccessDirection;
|
|
88
|
+
/** Columns the access edge names, where it names any. */
|
|
89
|
+
readonly columns: readonly string[];
|
|
90
|
+
/** `CALLS` hops from the route to the accessor. 1 when the route calls it directly. */
|
|
91
|
+
readonly callDepth: number;
|
|
92
|
+
/** Every edge this claim rests on, in path order: route->endpoint,
|
|
93
|
+
* route->..->accessor, accessor->model. The model->table leg is absent by
|
|
94
|
+
* construction — it is an identity match, not an edge. */
|
|
95
|
+
readonly edges: readonly IREdge[];
|
|
96
|
+
}
|
|
97
|
+
export interface EndpointTableAccess {
|
|
98
|
+
readonly endpoint: IRNode;
|
|
99
|
+
/** Precedence is `writes` > `unknown-direction` > `reads`: one undetermined
|
|
100
|
+
* path is enough to forbid the claim that this endpoint only reads. */
|
|
101
|
+
readonly direction: AccessDirection;
|
|
102
|
+
readonly columns: readonly string[];
|
|
103
|
+
readonly paths: readonly PersistencePath[];
|
|
104
|
+
}
|
|
105
|
+
export interface PersistenceReach {
|
|
106
|
+
readonly table: IRNode;
|
|
107
|
+
/** The `MODEL` nodes whose declared table is this one. More than one is
|
|
108
|
+
* legitimate — two entities intentionally sharing a table. */
|
|
109
|
+
readonly models: readonly IRNode[];
|
|
110
|
+
readonly endpoints: readonly EndpointTableAccess[];
|
|
111
|
+
readonly hopGaps: readonly PersistenceHopGap[];
|
|
112
|
+
/** Producers observed emitting `ASSIGNS` into a `MODEL` in this graph:
|
|
113
|
+
* their `READS` edges are genuine reads. */
|
|
114
|
+
readonly directionAwareProducers: readonly string[];
|
|
115
|
+
/** Producers on a path here that were never observed emitting `ASSIGNS`:
|
|
116
|
+
* their `READS` edges carry no direction, and are reported as such. */
|
|
117
|
+
readonly directionBlindProducers: readonly string[];
|
|
118
|
+
/** Capped at B unconditionally — see the module doc's hop 1. */
|
|
119
|
+
readonly reliability: ReliabilityClass;
|
|
120
|
+
}
|
|
121
|
+
export interface PersistenceReachOptions {
|
|
122
|
+
/** `CALLS` hops from a route to an accessor. Default 4. */
|
|
123
|
+
readonly maxCallDepth?: number;
|
|
124
|
+
/** Budget for the inbound `CALLS` walk, counted in (node, accessor) pairs
|
|
125
|
+
* rather than nodes — the walk re-enters a node for a new accessor, so
|
|
126
|
+
* pairs are what actually bound it. DEC-007's 200/1,000/5,000. Default 1,000. */
|
|
127
|
+
readonly budget?: number;
|
|
128
|
+
/** Fan-out cap per hop, and the cap on accessors read per model. Default 200. */
|
|
129
|
+
readonly perNodeLimit?: number;
|
|
130
|
+
/** Cap on `MODEL` nodes enumerated, for both the table join and the
|
|
131
|
+
* direction probe. Default 5,000 — the provider's own default. */
|
|
132
|
+
readonly modelLimit?: number;
|
|
133
|
+
}
|
|
134
|
+
/** Emitted whenever a write is reported. The distinction it draws is the one a
|
|
135
|
+
* reader will otherwise collapse, and collapsing it turns a provable
|
|
136
|
+
* statement about source into an unproven one about a running database. */
|
|
137
|
+
export declare const WRITE_MEANING_NOTE: string;
|
|
138
|
+
/**
|
|
139
|
+
* Every `API_ENDPOINT` that reaches `table`, and which of them write to it.
|
|
140
|
+
*
|
|
141
|
+
* `scope` must be the `IdentityScope` the graph was built with —
|
|
142
|
+
* `DATABASE_TABLE` is workspace-scoped, so a mismatched scope misses silently
|
|
143
|
+
* rather than erroring, and the miss surfaces as a `model-for-table` gap.
|
|
144
|
+
*
|
|
145
|
+
* Never throws on graph content. A node of the wrong type, a model whose
|
|
146
|
+
* declared table is not a string, a dangling edge — each degrades to a named
|
|
147
|
+
* reason in `hopGaps` or `notes`.
|
|
148
|
+
*/
|
|
149
|
+
export declare function endpointsReachingTable(provider: AdjacencyProvider, table: IRNode, scope: IdentityScope, options?: PersistenceReachOptions): QueryResult<PersistenceReach>;
|
|
150
|
+
//# sourceMappingURL=persistence-reach.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistence-reach.d.ts","sourceRoot":"","sources":["../../src/query/persistence-reach.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/** "Which endpoints reach this table, and which of them write to it" — the
|
|
2
|
+
* persistence half of the bug-relevant subgraph, made askable.
|
|
3
|
+
*
|
|
4
|
+
* Until this query existed the question could not be asked in any language:
|
|
5
|
+
* `DATABASE_TABLE`/`DATABASE_COLUMN` are in no edge, of any type, in either
|
|
6
|
+
* direction. That is not a defect — it is the consequence of a ruling. The
|
|
7
|
+
* model-to-table join was deliberately built **by shared node identity rather
|
|
8
|
+
* than by an edge** (`databaseTableNodesFromModel`, `@descryy/ir`, DEC-240),
|
|
9
|
+
* so an ORM-derived producer and a raw-DDL-derived producer land on the same
|
|
10
|
+
* id for the same real table with no coordination. Identity merging and
|
|
11
|
+
* reachability are different problems, and solving the first left the second
|
|
12
|
+
* untouched. This module solves the second **without touching the first**:
|
|
13
|
+
* no new edge type, no new node type, no adapter change
|
|
14
|
+
* (`DEC-NEXT-database-table-column-traversability-vs-queryability.md`,
|
|
15
|
+
* `DEC-NEXT-persistence-half-of-the-bug-relevant-subgraph-is-unreachable.md`
|
|
16
|
+
* option 1).
|
|
17
|
+
*
|
|
18
|
+
* ## The three hops, walked backwards from the table
|
|
19
|
+
*
|
|
20
|
+
* 1. **table -> model** — `modelsForTable` (`model-table.ts`), an O(1) exact
|
|
21
|
+
* id match per model, recomputed from the model's own declared table name.
|
|
22
|
+
* **This leg has no edge under it.** Its evidence is two declarations: a
|
|
23
|
+
* string in the model and the table node's identity. That is why the whole
|
|
24
|
+
* composed claim is name-level and can never reach reliability class A,
|
|
25
|
+
* however well-resolved the other two hops are.
|
|
26
|
+
* 2. **model -> accessor** — `READS`/`ASSIGNS` edges pointing at the model.
|
|
27
|
+
* 3. **accessor -> route -> endpoint** — one budgeted inbound `CALLS` walk
|
|
28
|
+
* seeded on every accessor at once (never one walk per accessor), carrying
|
|
29
|
+
* the set of accessors each node reaches, then
|
|
30
|
+
* `API_ROUTE --SERVES_API--> API_ENDPOINT`.
|
|
31
|
+
*
|
|
32
|
+
* ## The write gate, and why it names no language
|
|
33
|
+
*
|
|
34
|
+
* A developer acting on "this endpoint writes to your users table" changes
|
|
35
|
+
* code. Only some producers distinguish an assignment from a read; the rest
|
|
36
|
+
* emit every field access as `READS`, so answering the write question from a
|
|
37
|
+
* `READS` edge would state the opposite of what the code does for those
|
|
38
|
+
* producers. It must therefore be gated — but gating on a language name above
|
|
39
|
+
* the IR is a boundary breach (rule 1), and the mechanical boundary lint is
|
|
40
|
+
* right to forbid it.
|
|
41
|
+
*
|
|
42
|
+
* So the gate is **evidence, not configuration**: a producer is
|
|
43
|
+
* direction-aware iff it is observed emitting at least one `ASSIGNS` edge
|
|
44
|
+
* into a `MODEL` in this graph. An `ASSIGNS` edge is self-evidencing and
|
|
45
|
+
* needs no gate at all. A `READS` edge from a direction-aware producer is a
|
|
46
|
+
* genuine read. A `READS` edge from a producer never seen emitting `ASSIGNS`
|
|
47
|
+
* is `unknown-direction` — reported as not analysable, folded into neither
|
|
48
|
+
* the read set nor the write set. The failure mode is one-directional by
|
|
49
|
+
* construction: the worst this rule can do is decline to answer, and it can
|
|
50
|
+
* never manufacture a write. */
|
|
51
|
+
import { reliabilityCap, } from "@descryy/ir";
|
|
52
|
+
import { createResolutionFloor } from "./resolution-floor.js";
|
|
53
|
+
import { tableIdForModel } from "./model-table.js";
|
|
54
|
+
/** The two edge types that carry a function's access to a model's fields.
|
|
55
|
+
* Named once so the accessor read and the direction probe cannot drift. */
|
|
56
|
+
export const MODEL_ACCESS_EDGE_TYPES = ["READS", "ASSIGNS"];
|
|
57
|
+
const DEFAULT_MAX_CALL_DEPTH = 4;
|
|
58
|
+
const DEFAULT_BUDGET = 1_000;
|
|
59
|
+
const DEFAULT_PER_NODE_LIMIT = 200;
|
|
60
|
+
const DEFAULT_MODEL_LIMIT = 5_000;
|
|
61
|
+
const NAME_MATCH_NOTE = "The step from a model to its table is a name match on a declared string, not a graph edge — " +
|
|
62
|
+
"two producers agree on a table because they hash the same name, and nothing verifies that the " +
|
|
63
|
+
"name is the table the code reaches at run time. This claim is therefore name-level and is " +
|
|
64
|
+
"capped at reliability class B however well the call path under it resolves.";
|
|
65
|
+
/** Emitted whenever a write is reported. The distinction it draws is the one a
|
|
66
|
+
* reader will otherwise collapse, and collapsing it turns a provable
|
|
67
|
+
* statement about source into an unproven one about a running database. */
|
|
68
|
+
export const WRITE_MEANING_NOTE = "A write here means the handler assigns a mapped column of the model that backs this table. " +
|
|
69
|
+
"That is read from the source, and it is not the same claim as a database write being issued: " +
|
|
70
|
+
"whether the assignment reaches the table depends on the object being attached to a live " +
|
|
71
|
+
"session and that session being flushed, and neither is established here. Treat it as a " +
|
|
72
|
+
"prediction to confirm at run time, not as an observed update.";
|
|
73
|
+
function empty(provider, table, floor, notes, overrides = {}) {
|
|
74
|
+
const structural = reliabilityCap(floor, true);
|
|
75
|
+
return {
|
|
76
|
+
data: {
|
|
77
|
+
table,
|
|
78
|
+
models: [],
|
|
79
|
+
endpoints: [],
|
|
80
|
+
hopGaps: [],
|
|
81
|
+
directionAwareProducers: [],
|
|
82
|
+
directionBlindProducers: [],
|
|
83
|
+
reliability: structural === "A" ? "B" : structural,
|
|
84
|
+
...overrides,
|
|
85
|
+
},
|
|
86
|
+
stamp: stampFor(provider, floor),
|
|
87
|
+
truncated: false,
|
|
88
|
+
notes,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function stampFor(provider, resolutionFloor) {
|
|
92
|
+
return { ...provider.baseStamp(), resolutionFloor };
|
|
93
|
+
}
|
|
94
|
+
/** `attrs.fields` on an access edge, where the producer names the columns. */
|
|
95
|
+
function columnsOf(edge) {
|
|
96
|
+
const fields = edge.attrs?.["fields"];
|
|
97
|
+
if (!Array.isArray(fields))
|
|
98
|
+
return [];
|
|
99
|
+
return fields.filter((f) => typeof f === "string");
|
|
100
|
+
}
|
|
101
|
+
/** Producers observed emitting at least one `ASSIGNS` into a `MODEL`. The
|
|
102
|
+
* probe is bounded and it reports its own incompleteness: a producer missed
|
|
103
|
+
* because the probe stopped early is reported as direction-blind, which
|
|
104
|
+
* under-claims rather than over-claims. */
|
|
105
|
+
function probeDirectionAwareProducers(provider, models, limit) {
|
|
106
|
+
const producers = new Set();
|
|
107
|
+
const examined = Math.min(models.length, limit);
|
|
108
|
+
for (let i = 0; i < examined; i += 1) {
|
|
109
|
+
for (const edge of provider.incoming(models[i].id, { types: ["ASSIGNS"], limit: 1 })) {
|
|
110
|
+
producers.add(edge.producedBy);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { producers, complete: examined === models.length };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Every `API_ENDPOINT` that reaches `table`, and which of them write to it.
|
|
117
|
+
*
|
|
118
|
+
* `scope` must be the `IdentityScope` the graph was built with —
|
|
119
|
+
* `DATABASE_TABLE` is workspace-scoped, so a mismatched scope misses silently
|
|
120
|
+
* rather than erroring, and the miss surfaces as a `model-for-table` gap.
|
|
121
|
+
*
|
|
122
|
+
* Never throws on graph content. A node of the wrong type, a model whose
|
|
123
|
+
* declared table is not a string, a dangling edge — each degrades to a named
|
|
124
|
+
* reason in `hopGaps` or `notes`.
|
|
125
|
+
*/
|
|
126
|
+
export function endpointsReachingTable(provider, table, scope, options = {}) {
|
|
127
|
+
const maxCallDepth = options.maxCallDepth ?? DEFAULT_MAX_CALL_DEPTH;
|
|
128
|
+
const budget = options.budget ?? DEFAULT_BUDGET;
|
|
129
|
+
const perNodeLimit = options.perNodeLimit ?? DEFAULT_PER_NODE_LIMIT;
|
|
130
|
+
const modelLimit = options.modelLimit ?? DEFAULT_MODEL_LIMIT;
|
|
131
|
+
const floor = createResolutionFloor();
|
|
132
|
+
const notes = [];
|
|
133
|
+
if (table === null || typeof table !== "object" || table.type !== "DATABASE_TABLE") {
|
|
134
|
+
return empty(provider, table, 0, [
|
|
135
|
+
`This query answers about a DATABASE_TABLE node; it was given a ${typeof table === "object" && table !== null ? String(table.type) : "non-node value"}. Nothing was examined, and nothing here says the table is unreached.`,
|
|
136
|
+
]);
|
|
137
|
+
}
|
|
138
|
+
// The table's own resolution is evidence under every claim below: the join
|
|
139
|
+
// rests on this node's identity, and no edge carries it.
|
|
140
|
+
floor.observe(table.resolution);
|
|
141
|
+
// --- hop 1: table -> models, plus the direction probe over the same set ---
|
|
142
|
+
const modelSet = provider.nodesOfType("MODEL", modelLimit);
|
|
143
|
+
let truncated = modelSet.truncated;
|
|
144
|
+
if (modelSet.truncated) {
|
|
145
|
+
notes.push("More models exist than were enumerated, so a model mapping to this table may have been " +
|
|
146
|
+
"missed. Endpoints absent from this answer are not reported as absent from the code.");
|
|
147
|
+
}
|
|
148
|
+
const models = modelSet.nodes.filter((model) => tableIdForModel(model, scope) === table.id);
|
|
149
|
+
for (const model of models)
|
|
150
|
+
floor.observe(model.resolution);
|
|
151
|
+
const probe = probeDirectionAwareProducers(provider, modelSet.nodes, modelLimit);
|
|
152
|
+
if (!probe.complete) {
|
|
153
|
+
truncated = true;
|
|
154
|
+
notes.push("The probe for which producers distinguish an assignment from a read did not cover every " +
|
|
155
|
+
"model. A producer it did not reach is reported as unable to answer the write question, " +
|
|
156
|
+
"never as having answered it.");
|
|
157
|
+
}
|
|
158
|
+
if (models.length === 0) {
|
|
159
|
+
notes.push(NAME_MATCH_NOTE);
|
|
160
|
+
return empty(provider, table, floor.value(), notes, {
|
|
161
|
+
hopGaps: [
|
|
162
|
+
{
|
|
163
|
+
hop: "model-for-table",
|
|
164
|
+
node: table,
|
|
165
|
+
reason: "No model in this graph declares this table. Either nothing read the mapping for its " +
|
|
166
|
+
"language, or the code names the table only where a mapping was refused rather than guessed.",
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// --- hop 2: models -> accessors (the functions holding READS/ASSIGNS) -----
|
|
172
|
+
const hopGaps = [];
|
|
173
|
+
/** accessor function id -> the access edges on it, and the model each reaches. */
|
|
174
|
+
const accessByFunction = new Map();
|
|
175
|
+
for (const model of models) {
|
|
176
|
+
const access = provider.incoming(model.id, {
|
|
177
|
+
types: MODEL_ACCESS_EDGE_TYPES,
|
|
178
|
+
limit: perNodeLimit,
|
|
179
|
+
});
|
|
180
|
+
if (access.length === 0) {
|
|
181
|
+
hopGaps.push({
|
|
182
|
+
hop: "accessor-for-model",
|
|
183
|
+
node: model,
|
|
184
|
+
reason: "No function in this graph reads or assigns a field of this model. Code reaching the " +
|
|
185
|
+
"table by raw query text, or through a field access nothing resolved, is invisible here.",
|
|
186
|
+
});
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (access.length === perNodeLimit) {
|
|
190
|
+
truncated = true;
|
|
191
|
+
notes.push(`More than ${perNodeLimit} functions access one of this table's models; the rest were not examined.`);
|
|
192
|
+
}
|
|
193
|
+
for (const edge of access) {
|
|
194
|
+
const bucket = accessByFunction.get(edge.from);
|
|
195
|
+
if (bucket === undefined)
|
|
196
|
+
accessByFunction.set(edge.from, [{ edge, model }]);
|
|
197
|
+
else
|
|
198
|
+
bucket.push({ edge, model });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (accessByFunction.size === 0) {
|
|
202
|
+
notes.push(NAME_MATCH_NOTE);
|
|
203
|
+
return empty(provider, table, floor.value(), notes, { models, hopGaps });
|
|
204
|
+
}
|
|
205
|
+
// --- hop 3: accessors -> routes -> endpoints ------------------------------
|
|
206
|
+
//
|
|
207
|
+
// One inbound `CALLS` walk carrying, for each node it reaches, the **set of
|
|
208
|
+
// accessors** that node can reach. A plain multi-source BFS cannot be used
|
|
209
|
+
// here and the difference is not cosmetic: it marks each node seen once, so
|
|
210
|
+
// a route calling both a reading helper and a writing one is attributed to
|
|
211
|
+
// whichever the walk happened to discover first, and the write is silently
|
|
212
|
+
// lost. "Which endpoint writes to this table" must not depend on traversal
|
|
213
|
+
// order, so a node is re-entered when a *new* accessor reaches it, and the
|
|
214
|
+
// budget counts (node, accessor) pairs rather than nodes.
|
|
215
|
+
const seeds = [...accessByFunction.keys()];
|
|
216
|
+
/** `${node}\u0000${accessor}` -> the edge that reached it, null at a seed. */
|
|
217
|
+
const via = new Map();
|
|
218
|
+
const pairKey = (node, seed) => `${node}\u0000${seed}`;
|
|
219
|
+
const queue = [];
|
|
220
|
+
for (const seed of seeds) {
|
|
221
|
+
via.set(pairKey(seed, seed), null);
|
|
222
|
+
queue.push({ id: seed, seed, depth: 0 });
|
|
223
|
+
}
|
|
224
|
+
for (let head = 0; head < queue.length; head += 1) {
|
|
225
|
+
const current = queue[head];
|
|
226
|
+
if (current.depth >= maxCallDepth)
|
|
227
|
+
continue;
|
|
228
|
+
for (const edge of provider.incoming(current.id, { types: ["CALLS"], limit: perNodeLimit })) {
|
|
229
|
+
const key = pairKey(edge.from, current.seed);
|
|
230
|
+
if (via.has(key))
|
|
231
|
+
continue;
|
|
232
|
+
if (via.size >= budget) {
|
|
233
|
+
truncated = true;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
via.set(key, edge);
|
|
237
|
+
queue.push({ id: edge.from, seed: current.seed, depth: current.depth + 1 });
|
|
238
|
+
}
|
|
239
|
+
if (truncated)
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
/** The `CALLS` chain from `node` back to `seed`, route-first. `undefined`
|
|
243
|
+
* when that accessor does not reach this node. */
|
|
244
|
+
function chainTo(node, seed) {
|
|
245
|
+
const edges = [];
|
|
246
|
+
let cursor = node;
|
|
247
|
+
// Bounded by the walk's own depth; the guard is against a malformed chain,
|
|
248
|
+
// not an expected case.
|
|
249
|
+
for (let step = 0; step <= maxCallDepth + 1; step += 1) {
|
|
250
|
+
const key = pairKey(cursor, seed);
|
|
251
|
+
if (!via.has(key))
|
|
252
|
+
return undefined;
|
|
253
|
+
const edge = via.get(key);
|
|
254
|
+
if (edge === null || edge === undefined)
|
|
255
|
+
return edges;
|
|
256
|
+
edges.push(edge);
|
|
257
|
+
cursor = edge.from === cursor ? edge.to : edge.from;
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
/** Every node the walk touched, once, regardless of how many accessors reached it. */
|
|
262
|
+
const touched = new Set();
|
|
263
|
+
for (const key of via.keys())
|
|
264
|
+
touched.add(key.slice(0, key.indexOf("\u0000")));
|
|
265
|
+
const accessorsWithARoute = new Set();
|
|
266
|
+
const byEndpoint = new Map();
|
|
267
|
+
for (const nodeId of touched) {
|
|
268
|
+
const route = provider.node(nodeId);
|
|
269
|
+
if (route === undefined || route.type !== "API_ROUTE")
|
|
270
|
+
continue;
|
|
271
|
+
// Every accessor this route reaches, not only the first one found.
|
|
272
|
+
const chains = [];
|
|
273
|
+
for (const seed of seeds) {
|
|
274
|
+
const chain = chainTo(route.id, seed);
|
|
275
|
+
if (chain === undefined)
|
|
276
|
+
continue;
|
|
277
|
+
const accessor = provider.node(seed);
|
|
278
|
+
if (accessor === undefined)
|
|
279
|
+
continue;
|
|
280
|
+
accessorsWithARoute.add(seed);
|
|
281
|
+
// `chainTo` is accessor-first; path order runs route -> .. -> accessor.
|
|
282
|
+
chains.push({ accessor, callEdges: [...chain].reverse() });
|
|
283
|
+
}
|
|
284
|
+
if (chains.length === 0)
|
|
285
|
+
continue;
|
|
286
|
+
const serves = provider.outgoing(route.id, { types: ["SERVES_API"], limit: perNodeLimit });
|
|
287
|
+
if (serves.length === 0) {
|
|
288
|
+
hopGaps.push({
|
|
289
|
+
hop: "endpoint-for-route",
|
|
290
|
+
node: route,
|
|
291
|
+
reason: "This route reaches the table but is joined to no endpoint, so it cannot be named by " +
|
|
292
|
+
"the method and path a caller would use.",
|
|
293
|
+
});
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
for (const servesEdge of serves) {
|
|
297
|
+
const endpoint = provider.node(servesEdge.to);
|
|
298
|
+
if (endpoint === undefined)
|
|
299
|
+
continue;
|
|
300
|
+
for (const { accessor, callEdges } of chains) {
|
|
301
|
+
for (const { edge: accessEdge, model } of accessByFunction.get(accessor.id) ?? []) {
|
|
302
|
+
const direction = accessEdge.type === "ASSIGNS"
|
|
303
|
+
? "writes"
|
|
304
|
+
: probe.producers.has(accessEdge.producedBy)
|
|
305
|
+
? "reads"
|
|
306
|
+
: "unknown-direction";
|
|
307
|
+
// Path order: route -> endpoint, then route -> .. -> accessor, then accessor -> model.
|
|
308
|
+
const edges = [servesEdge, ...callEdges, accessEdge];
|
|
309
|
+
for (const e of edges)
|
|
310
|
+
floor.observe(e.resolution);
|
|
311
|
+
const path = {
|
|
312
|
+
endpoint,
|
|
313
|
+
route,
|
|
314
|
+
accessor,
|
|
315
|
+
model,
|
|
316
|
+
direction,
|
|
317
|
+
columns: columnsOf(accessEdge),
|
|
318
|
+
callDepth: callEdges.length,
|
|
319
|
+
edges,
|
|
320
|
+
};
|
|
321
|
+
const bucket = byEndpoint.get(endpoint.id);
|
|
322
|
+
if (bucket === undefined)
|
|
323
|
+
byEndpoint.set(endpoint.id, { endpoint, paths: [path] });
|
|
324
|
+
else
|
|
325
|
+
bucket.paths.push(path);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
for (const accessorId of seeds) {
|
|
331
|
+
if (accessorsWithARoute.has(accessorId))
|
|
332
|
+
continue;
|
|
333
|
+
const accessor = provider.node(accessorId);
|
|
334
|
+
if (accessor === undefined)
|
|
335
|
+
continue;
|
|
336
|
+
hopGaps.push({
|
|
337
|
+
hop: "route-for-accessor",
|
|
338
|
+
node: accessor,
|
|
339
|
+
reason: `No route reaches this function within ${maxCallDepth} call hops. It may be reached only ` +
|
|
340
|
+
"by a longer chain, by a caller nothing resolved, or by no request path at all.",
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
// --- assemble -------------------------------------------------------------
|
|
344
|
+
const endpoints = [];
|
|
345
|
+
const directionBlind = new Set();
|
|
346
|
+
for (const { endpoint, paths } of byEndpoint.values()) {
|
|
347
|
+
const direction = paths.some((p) => p.direction === "writes")
|
|
348
|
+
? "writes"
|
|
349
|
+
: paths.some((p) => p.direction === "unknown-direction")
|
|
350
|
+
? "unknown-direction"
|
|
351
|
+
: "reads";
|
|
352
|
+
for (const p of paths) {
|
|
353
|
+
if (p.direction === "unknown-direction") {
|
|
354
|
+
const producer = p.edges[p.edges.length - 1]?.producedBy;
|
|
355
|
+
if (producer !== undefined)
|
|
356
|
+
directionBlind.add(producer);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const columns = [...new Set(paths.flatMap((p) => p.columns))].sort();
|
|
360
|
+
endpoints.push({ endpoint, direction, columns, paths });
|
|
361
|
+
}
|
|
362
|
+
endpoints.sort((a, b) => a.endpoint.id.localeCompare(b.endpoint.id));
|
|
363
|
+
notes.push(NAME_MATCH_NOTE);
|
|
364
|
+
if (endpoints.some((e) => e.direction === "writes"))
|
|
365
|
+
notes.push(WRITE_MEANING_NOTE);
|
|
366
|
+
if (directionBlind.size > 0) {
|
|
367
|
+
notes.push(`${directionBlind.size} producer(s) under this answer emit every field access as a read, so ` +
|
|
368
|
+
"this answer does not distinguish a write from a read on the paths resting on them. Those " +
|
|
369
|
+
"endpoints are reported as reaching the table with the direction not analysable: not as " +
|
|
370
|
+
"readers, and never as writers — a write is never inferred from a read.");
|
|
371
|
+
}
|
|
372
|
+
if (truncated) {
|
|
373
|
+
notes.push("The node budget was reached, so this answer is partial. Nodes beyond the budget were not " +
|
|
374
|
+
"examined and are not reported as clean.");
|
|
375
|
+
}
|
|
376
|
+
const structural = reliabilityCap(floor.value(), true);
|
|
377
|
+
return {
|
|
378
|
+
data: {
|
|
379
|
+
table,
|
|
380
|
+
models,
|
|
381
|
+
endpoints,
|
|
382
|
+
hopGaps,
|
|
383
|
+
directionAwareProducers: [...probe.producers].sort(),
|
|
384
|
+
directionBlindProducers: [...directionBlind].sort(),
|
|
385
|
+
// Cap, never a promotion: the name-match leg forbids class A outright.
|
|
386
|
+
reliability: structural === "A" ? "B" : structural,
|
|
387
|
+
},
|
|
388
|
+
stamp: stampFor(provider, floor.value()),
|
|
389
|
+
truncated,
|
|
390
|
+
notes,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
//# sourceMappingURL=persistence-reach.js.map
|