@canonical/pragma-cli 0.36.0 → 0.38.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/README.md +3 -2
- package/bundled-skills/adoption-a1-styles/SKILL.md +24 -0
- package/bundled-skills/adoption-a2-components/SKILL.md +22 -6
- package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +1397 -327
- package/bundled-skills/anatomy-author/SKILL.md +840 -137
- package/bundled-skills/design-auditor/SKILL.md +133 -9
- package/bundled-skills/implement-component/SKILL.md +192 -23
- package/bundled-skills/specify-component/SKILL.md +124 -12
- package/bundled-skills/specify-pattern/SKILL.md +47 -8
- package/dist/package.json +23 -20
- package/dist/pragma.conf.js +963 -40
- package/dist/pragma.conf.js.map +1 -1
- package/dist/src/capabilities/capabilities/catalog.js +14 -5
- package/dist/src/capabilities/capabilities/catalog.js.map +1 -1
- package/dist/src/capabilities/capabilities/hints.js +33 -5
- package/dist/src/capabilities/capabilities/hints.js.map +1 -1
- package/dist/src/capabilities/config/show.render.js +30 -2
- package/dist/src/capabilities/config/show.render.js.map +1 -1
- package/dist/src/capabilities/create/create.verb.js +2 -1
- package/dist/src/capabilities/create/create.verb.js.map +1 -1
- package/dist/src/capabilities/create/createSurface.generated.js +23 -1
- package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
- package/dist/src/capabilities/doctor/checks/targetHealth.js +104 -14
- package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
- package/dist/src/capabilities/graph/connect.render.js +101 -0
- package/dist/src/capabilities/graph/connect.render.js.map +1 -0
- package/dist/src/capabilities/graph/connect.types.js +21 -0
- package/dist/src/capabilities/graph/connect.types.js.map +1 -0
- package/dist/src/capabilities/graph/connect.verb.js +147 -0
- package/dist/src/capabilities/graph/connect.verb.js.map +1 -0
- package/dist/src/capabilities/graph/index.js +5 -3
- package/dist/src/capabilities/graph/index.js.map +1 -1
- package/dist/src/capabilities/graph/queryText.js +206 -0
- package/dist/src/capabilities/graph/queryText.js.map +1 -0
- package/dist/src/capabilities/graph/relationIndex.js +302 -0
- package/dist/src/capabilities/graph/relationIndex.js.map +1 -0
- package/dist/src/capabilities/graph/runConnect.js +456 -0
- package/dist/src/capabilities/graph/runConnect.js.map +1 -0
- package/dist/src/capabilities/graph/runQuery.js +21 -4
- package/dist/src/capabilities/graph/runQuery.js.map +1 -1
- package/dist/src/capabilities/setup/buildPlan.js +6 -2
- package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
- package/dist/src/capabilities/setup/operations/index.js +7 -2
- package/dist/src/capabilities/setup/operations/index.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
- package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
- package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupGenerator.js +72 -14
- package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupLsp.js +372 -62
- package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupMcp.js +150 -5
- package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupSkills.js +4 -2
- package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
- package/dist/src/capabilities/setup/operations/writability.js +114 -0
- package/dist/src/capabilities/setup/operations/writability.js.map +1 -0
- package/dist/src/capabilities/setup/plan.js.map +1 -1
- package/dist/src/capabilities/setup/setup.verb.js +80 -31
- package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
- package/dist/src/capabilities/setup/targets.js +124 -42
- package/dist/src/capabilities/setup/targets.js.map +1 -1
- package/dist/src/capabilities/shared/index.js +1 -1
- package/dist/src/capabilities/shared/index.js.map +1 -1
- package/dist/src/constants.js +7 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/kernel/error/renderError.js +3 -2
- package/dist/src/kernel/error/renderError.js.map +1 -1
- package/dist/src/kernel/error/validOptions.js +41 -0
- package/dist/src/kernel/error/validOptions.js.map +1 -0
- package/dist/src/kernel/packs/compile.js +90 -6
- package/dist/src/kernel/packs/compile.js.map +1 -1
- package/dist/src/kernel/packs/cursor.js +135 -0
- package/dist/src/kernel/packs/cursor.js.map +1 -0
- package/dist/src/kernel/packs/graphql/buildLookupDocument.js +9 -2
- package/dist/src/kernel/packs/graphql/buildLookupDocument.js.map +1 -1
- package/dist/src/kernel/packs/paging.js +108 -0
- package/dist/src/kernel/packs/paging.js.map +1 -0
- package/dist/src/kernel/packs/renderPack.js +154 -5
- package/dist/src/kernel/packs/renderPack.js.map +1 -1
- package/dist/src/kernel/packs/resolveEntity.js +118 -19
- package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
- package/dist/src/kernel/packs/runBodies.js +156 -18
- package/dist/src/kernel/packs/runBodies.js.map +1 -1
- package/dist/src/kernel/packs/schema.js +160 -12
- package/dist/src/kernel/packs/schema.js.map +1 -1
- package/dist/src/kernel/packs/sparql/authorQuery.js +227 -0
- package/dist/src/kernel/packs/sparql/authorQuery.js.map +1 -0
- package/dist/src/kernel/packs/sparql/buildListQuery.js +251 -0
- package/dist/src/kernel/packs/sparql/buildListQuery.js.map +1 -0
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js +125 -22
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
- package/dist/src/kernel/packs/sparql/filterValues.js +145 -0
- package/dist/src/kernel/packs/sparql/filterValues.js.map +1 -0
- package/dist/src/kernel/packs/sparql/index.js +24 -14
- package/dist/src/kernel/packs/sparql/index.js.map +1 -1
- package/dist/src/kernel/packs/sparql/runSelect.js +10 -4
- package/dist/src/kernel/packs/sparql/runSelect.js.map +1 -1
- package/dist/src/kernel/packs/sparql/searchTerm.js +25 -0
- package/dist/src/kernel/packs/sparql/searchTerm.js.map +1 -0
- package/dist/src/kernel/packs/storyRules.js +151 -0
- package/dist/src/kernel/packs/storyRules.js.map +1 -0
- package/dist/src/kernel/packs/tierScope.js +310 -0
- package/dist/src/kernel/packs/tierScope.js.map +1 -0
- package/dist/src/kernel/packs/types.js +80 -0
- package/dist/src/kernel/packs/types.js.map +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js +10 -1
- package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
- package/dist/src/kernel/project/cli/dispatch.js +104 -3
- package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
- package/dist/src/kernel/project/cli/rootHelp.js +8 -5
- package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
- package/dist/src/kernel/project/cli/suggestNames.js +31 -7
- package/dist/src/kernel/project/cli/suggestNames.js.map +1 -1
- package/dist/src/kernel/project/mcp/registerVerb.js +5 -1
- package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
- package/dist/src/kernel/render/renderers.js +34 -2
- package/dist/src/kernel/render/renderers.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
- package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
- package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/read.js +17 -10
- package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
- package/dist/src/kernel/runtime/loadSession.js +3 -1
- package/dist/src/kernel/runtime/loadSession.js.map +1 -1
- package/dist/src/kernel/runtime/probe.js +1 -1
- package/dist/src/kernel/runtime/probe.js.map +1 -1
- package/dist/src/kernel/runtime/readEntity.js +19 -4
- package/dist/src/kernel/runtime/readEntity.js.map +1 -1
- package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
- package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
- package/dist/src/kernel/runtime/refs/resolve.js +38 -5
- package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
- package/dist/src/kernel/spec/emitReference.js +1 -1
- package/dist/src/kernel/spec/emitReference.js.map +1 -1
- package/package.json +23 -20
- package/pragma.conf.ts +983 -40
- package/src/capabilities/block.parity.test.ts +207 -27
- package/src/capabilities/block.shipped.exec.test.ts +156 -14
- package/src/capabilities/block.tierRank.exec.test.ts +46 -9
- package/src/capabilities/capabilities/catalog.ts +14 -5
- package/src/capabilities/capabilities/hints.ts +40 -5
- package/src/capabilities/config/collectConfigShow.test.ts +1 -0
- package/src/capabilities/config/set.test.ts +24 -25
- package/src/capabilities/config/show.render.test.ts +9 -4
- package/src/capabilities/config/show.render.ts +30 -2
- package/src/capabilities/create/byteEquality.test.ts +28 -1
- package/src/capabilities/create/create.test.ts +1 -0
- package/src/capabilities/create/create.verb.ts +2 -1
- package/src/capabilities/create/createGrammar.test.ts +4 -1
- package/src/capabilities/create/createSurface.generated.ts +23 -1
- package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
- package/src/capabilities/distribution.parity.test.ts +12 -11
- package/src/capabilities/distribution.test.ts +77 -7
- package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
- package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
- package/src/capabilities/doctor/checks/harnessInventory.test.ts +13 -6
- package/src/capabilities/doctor/checks/harnessInventory.ts +8 -4
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
- package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
- package/src/capabilities/doctor/checks/targetHealth.ts +114 -13
- package/src/capabilities/doctor/doctor.test.ts +327 -36
- package/src/capabilities/emptyRecovery.test.ts +84 -28
- package/src/capabilities/graph/connect.render.ts +141 -0
- package/src/capabilities/graph/connect.shipped.exec.test.ts +370 -0
- package/src/capabilities/graph/connect.test.ts +361 -0
- package/src/capabilities/graph/connect.types.ts +136 -0
- package/src/capabilities/graph/connect.verb.ts +165 -0
- package/src/capabilities/graph/index.ts +5 -3
- package/src/capabilities/graph/query.shipped.exec.test.ts +66 -0
- package/src/capabilities/graph/query.test.ts +17 -0
- package/src/capabilities/graph/queryText.test.ts +121 -0
- package/src/capabilities/graph/queryText.ts +240 -0
- package/src/capabilities/graph/relationIndex.ts +372 -0
- package/src/capabilities/graph/runConnect.ts +577 -0
- package/src/capabilities/graph/runQuery.ts +21 -6
- package/src/capabilities/lazy.test.ts +32 -2
- package/src/capabilities/listBudget.shipped.exec.test.ts +239 -0
- package/src/capabilities/listLookup.shipped.exec.test.ts +387 -0
- package/src/capabilities/listQuery.shipped.exec.test.ts +400 -0
- package/src/capabilities/resources/resources.test.ts +38 -2
- package/src/capabilities/setup/buildPlan.ts +7 -1
- package/src/capabilities/setup/operations/index.ts +14 -0
- package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
- package/src/capabilities/setup/operations/setupConfig.ts +7 -2
- package/src/capabilities/setup/operations/setupGenerator.ts +93 -15
- package/src/capabilities/setup/operations/setupLsp.ts +544 -85
- package/src/capabilities/setup/operations/setupMcp.ts +212 -4
- package/src/capabilities/setup/operations/setupSkills.ts +7 -2
- package/src/capabilities/setup/operations/writability.test.ts +151 -0
- package/src/capabilities/setup/operations/writability.ts +135 -0
- package/src/capabilities/setup/plan.ts +11 -0
- package/src/capabilities/setup/setup.test.ts +992 -16
- package/src/capabilities/setup/setup.verb.ts +84 -36
- package/src/capabilities/setup/targets.ts +152 -48
- package/src/capabilities/setup/types.ts +41 -5
- package/src/capabilities/shared/index.ts +1 -0
- package/src/capabilities/sources/sources.test.ts +55 -0
- package/src/capabilities/surface.test.ts +59 -11
- package/src/capabilities/tier.parity.test.ts +2 -2
- package/src/capabilities/token.parity.test.ts +784 -0
- package/src/capabilities/tokenGraph.cli.test.ts +279 -0
- package/src/capabilities/tokenGraph.shipped.exec.test.ts +653 -0
- package/src/capabilities/variable.parity.test.ts +448 -0
- package/src/constants.ts +8 -0
- package/src/kernel/completion/shellDrive.test.ts +32 -30
- package/src/kernel/config/defaults.test.ts +21 -2
- package/src/kernel/config/readConfig.test.ts +1 -1
- package/src/kernel/error/renderError.ts +3 -2
- package/src/kernel/error/validOptions.test.ts +57 -0
- package/src/kernel/error/validOptions.ts +45 -0
- package/src/kernel/packs/collect.test.ts +2 -0
- package/src/kernel/packs/compile.test.ts +188 -15
- package/src/kernel/packs/compile.ts +109 -14
- package/src/kernel/packs/cursor.ts +135 -0
- package/src/kernel/packs/filteredEmpty.test.ts +213 -0
- package/src/kernel/packs/graphql/buildLookupDocument.ts +16 -9
- package/src/kernel/packs/graphqlSource.test.ts +152 -0
- package/src/kernel/packs/listNotice.test.ts +156 -0
- package/src/kernel/packs/paging.ts +110 -0
- package/src/kernel/packs/renderPack.ts +186 -13
- package/src/kernel/packs/resolveEntity.test.ts +62 -3
- package/src/kernel/packs/resolveEntity.ts +186 -26
- package/src/kernel/packs/runBodies.ts +196 -26
- package/src/kernel/packs/schema.ts +180 -13
- package/src/kernel/packs/sparql/authorQuery.test.ts +179 -0
- package/src/kernel/packs/sparql/authorQuery.ts +257 -0
- package/src/kernel/packs/sparql/buildListQuery.test.ts +344 -0
- package/src/kernel/packs/sparql/buildListQuery.ts +344 -0
- package/src/kernel/packs/sparql/buildLookupQuery.test.ts +138 -0
- package/src/kernel/packs/sparql/buildLookupQuery.ts +140 -20
- package/src/kernel/packs/sparql/filterValues.test.ts +86 -0
- package/src/kernel/packs/sparql/filterValues.ts +182 -0
- package/src/kernel/packs/sparql/index.ts +30 -14
- package/src/kernel/packs/sparql/runSelect.test.ts +63 -0
- package/src/kernel/packs/sparql/runSelect.ts +42 -2
- package/src/kernel/packs/sparql/searchTerm.ts +29 -0
- package/src/kernel/packs/storyRules.ts +180 -0
- package/src/kernel/packs/tierScope.test.ts +487 -0
- package/src/kernel/packs/tierScope.ts +409 -0
- package/src/kernel/packs/toolDescription.test.ts +13 -10
- package/src/kernel/packs/types.ts +311 -9
- package/src/kernel/project/cli/buildProgram.ts +17 -1
- package/src/kernel/project/cli/dispatch.test.ts +87 -0
- package/src/kernel/project/cli/dispatch.ts +114 -4
- package/src/kernel/project/cli/rootHelp.ts +8 -5
- package/src/kernel/project/cli/suggest.test.ts +35 -0
- package/src/kernel/project/cli/suggestNames.ts +34 -7
- package/src/kernel/project/mcp/registerVerb.ts +5 -1
- package/src/kernel/render/contracts.ts +2 -0
- package/src/kernel/render/prefixes.test.ts +2 -0
- package/src/kernel/render/renderers.test.ts +43 -0
- package/src/kernel/render/renderers.ts +34 -1
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
- package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
- package/src/kernel/runtime/graphpack/embedded.ts +14 -6
- package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
- package/src/kernel/runtime/graphpack/read.ts +17 -13
- package/src/kernel/runtime/loadSession.ts +3 -1
- package/src/kernel/runtime/probe.ts +1 -1
- package/src/kernel/runtime/readEntity.ts +19 -4
- package/src/kernel/runtime/refs/parseRef.test.ts +49 -1
- package/src/kernel/runtime/refs/parseRef.ts +25 -1
- package/src/kernel/runtime/refs/resolve.ts +39 -5
- package/src/kernel/runtime/types.ts +30 -0
- package/src/kernel/spec/emitReference.ts +1 -1
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/kernel/spec/types.ts +17 -0
- package/src/testing/PARITY_GAPS.ts +4 -4
- package/src/testing/behavioral/agentSession.mcp.test.ts +43 -39
- package/src/testing/behavioral/completion.test.ts +7 -6
- package/src/testing/behavioral/disclosure.test.ts +39 -37
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +82 -64
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +11 -10
- package/src/testing/behavioral/journeys.cli.test.ts +71 -32
- package/src/testing/behavioral/journeys.defaultPack.test.ts +6 -4
- package/src/testing/behavioral/journeys.firstInstall.test.ts +70 -5
- package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +4 -3
- package/src/testing/behavioral/journeys.packageStories.test.ts +2 -1
- package/src/testing/behavioral/mcpSurface.test.ts +28 -25
- package/src/testing/behavioral/parity.test.ts +25 -23
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +20 -0
- package/src/testing/eval/cases/effectful.ts +7 -1
- package/src/testing/eval/cases/readNouns.ts +100 -11
- package/src/testing/fixtures/blockGraph.ts +89 -5
- package/src/testing/fixtures/connectGraph.ts +159 -0
- package/src/testing/fixtures/graph/canonical.ts +128 -32
- package/src/testing/helpers/fsProbe.ts +88 -0
- package/src/testing/helpers/runCli.test.ts +44 -0
- package/src/testing/helpers/runCli.ts +27 -3
- package/src/testing/perf/budgets.test.ts +3 -1
- package/src/testing/perf/budgets.ts +138 -54
- package/dist/src/kernel/packs/sparql/applyFilters.js +0 -167
- package/dist/src/kernel/packs/sparql/applyFilters.js.map +0 -1
- package/dist/src/kernel/packs/sparql/applySearch.js +0 -25
- package/dist/src/kernel/packs/sparql/applySearch.js.map +0 -1
- package/src/kernel/packs/sparql/applyFilters.ts +0 -203
- package/src/kernel/packs/sparql/applySearch.ts +0 -33
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The relation graph: which edges `graph connect` may walk, and the rosters it
|
|
3
|
+
* may not.
|
|
4
|
+
*
|
|
5
|
+
* ## What a relation is
|
|
6
|
+
*
|
|
7
|
+
* The kernel's entity reader already draws this distinction, and defends it in
|
|
8
|
+
* its own source (`readEntity.ts:37-50`): a RELATION "fans in narrowly and
|
|
9
|
+
* every subject is part of the answer", a ROSTER "fans in without bound because
|
|
10
|
+
* it grows with the data rather than the model" and "is already answered by
|
|
11
|
+
* `pragma/instanceCount` and by the noun's list verb". Then the sentence that
|
|
12
|
+
* makes it reusable here: "The two are told apart by FAN-IN, not by predicate
|
|
13
|
+
* name, so the kernel names no vocabulary."
|
|
14
|
+
*
|
|
15
|
+
* This module applies that same rule to TRAVERSAL. An edge `(s, p, o)` is
|
|
16
|
+
* walkable only if neither of its groups is a roster:
|
|
17
|
+
*
|
|
18
|
+
* - at most {@link ROSTER_THRESHOLD} subjects share its `(p, o)` pair, and
|
|
19
|
+
* - at most that many objects share its `(s, p)` pair.
|
|
20
|
+
*
|
|
21
|
+
* The threshold is IMPORTED, never restated — one constant with two consumers,
|
|
22
|
+
* so the traversal cannot drift from the doctrine it claims to obey.
|
|
23
|
+
*
|
|
24
|
+
* ## Why not a list of predicates
|
|
25
|
+
*
|
|
26
|
+
* Because the rule is per edge, per node and per direction, and a list cannot
|
|
27
|
+
* be. On the pack this CLI ships, twelve predicates carry both kinds of edge:
|
|
28
|
+
* `ds:tier` fans in 218-deep for the global tier and 1-to-a-handful for a small
|
|
29
|
+
* one, so the same predicate is a roster at one node and a real relation at
|
|
30
|
+
* another. A list would also be an editorial judgement about five vocabularies
|
|
31
|
+
* frozen into this file, stale the day a pack ships a predicate nobody listed.
|
|
32
|
+
* Nothing in this module names a predicate, and nothing may start to: the rule
|
|
33
|
+
* has to hold for a graph whose rosters hang off terms this code never saw.
|
|
34
|
+
*
|
|
35
|
+
* Nor can the distinction be read off the ontology. It is not asserted there:
|
|
36
|
+
* 72 predicates are declared `owl:ObjectProperty` and every one of the eleven
|
|
37
|
+
* classifiers is among them. The fan-in rule is a measurement standing in for a
|
|
38
|
+
* statement the graph does not make — which is exactly why it is computed here,
|
|
39
|
+
* fresh, from whatever is loaded, rather than written down anywhere.
|
|
40
|
+
*
|
|
41
|
+
* ## What the scan covers, and what it costs
|
|
42
|
+
*
|
|
43
|
+
* ONE scan of every triple with a non-literal object. Both group tables are
|
|
44
|
+
* counted over that same edge set, because a literal object is not an edge and
|
|
45
|
+
* cannot be walked — counting attributes into a fan-in would refuse edges on
|
|
46
|
+
* the strength of triples no traversal could ever use. The counts must be taken
|
|
47
|
+
* over the whole edge set before any edge is admitted, so the two tests cannot
|
|
48
|
+
* be applied in sequence: dropping a hub edge first would shrink the group its
|
|
49
|
+
* sibling is measured against and quietly admit an edge the rule refuses.
|
|
50
|
+
*
|
|
51
|
+
* The index is built ONCE per store session and memoised on it, and nothing
|
|
52
|
+
* here is written into the pack (`C.05` forbids a pre-joined view artifact, and
|
|
53
|
+
* prescribes exactly this: a thin loader that joins at runtime, because "the
|
|
54
|
+
* join is milliseconds at this scale — code, not data"). It is reached only
|
|
55
|
+
* through the connect verb's dynamically-imported run body, behind
|
|
56
|
+
* `needsStore`, so a process that never asks a connect question never loads
|
|
57
|
+
* this module, let alone runs the scan.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
import { compactUri } from "../../kernel/render/index.js";
|
|
61
|
+
import {
|
|
62
|
+
isInformativeName,
|
|
63
|
+
nameIndex,
|
|
64
|
+
ROSTER_THRESHOLD,
|
|
65
|
+
} from "../../kernel/runtime/readEntity.js";
|
|
66
|
+
import type { QueryFacade, StoreSession } from "../../kernel/runtime/types.js";
|
|
67
|
+
import type { ConnectTerm, SharedRoster } from "./connect.types.js";
|
|
68
|
+
|
|
69
|
+
/** Every triple whose object is a node rather than an attribute — one scan. */
|
|
70
|
+
const EDGE_SCAN = "SELECT ?s ?p ?o WHERE { ?s ?p ?o . FILTER(!isLiteral(?o)) }";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Field separator for composite map keys.
|
|
74
|
+
*
|
|
75
|
+
* A NUL, written as an ESCAPE so this file stays text: a literal NUL byte in
|
|
76
|
+
* the source makes git classify the module as binary, and a reviewer then gets
|
|
77
|
+
* no diff at all. It is the one character that cannot appear in an IRI or in a
|
|
78
|
+
* blank-node label, so a composite key is unambiguous.
|
|
79
|
+
*/
|
|
80
|
+
const SEP = "\u0000";
|
|
81
|
+
|
|
82
|
+
/** One walkable edge, as seen from one of its two ends. */
|
|
83
|
+
export interface RelationEdge {
|
|
84
|
+
/** The node at the other end. */
|
|
85
|
+
readonly to: string;
|
|
86
|
+
readonly predicate: string;
|
|
87
|
+
/** True when the asserted triple is `(this node, predicate, to)`. */
|
|
88
|
+
readonly forward: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A roster group a node belongs to, decoded from its key. */
|
|
92
|
+
interface RosterGroup {
|
|
93
|
+
readonly side: "subjects" | "objects";
|
|
94
|
+
readonly predicate: string;
|
|
95
|
+
readonly node: string;
|
|
96
|
+
readonly members: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The relation graph of one store session. */
|
|
100
|
+
export interface RelationIndex {
|
|
101
|
+
/** Walkable edges by node, both directions. */
|
|
102
|
+
readonly adjacency: ReadonlyMap<string, readonly RelationEdge[]>;
|
|
103
|
+
/** Roster-group keys by node — the memberships an edge was refused for. */
|
|
104
|
+
readonly rosters: ReadonlyMap<string, readonly string[]>;
|
|
105
|
+
/** Every roster group, by key. */
|
|
106
|
+
readonly groups: ReadonlyMap<string, RosterGroup>;
|
|
107
|
+
/** Distinct triples the scan returned (edges, before the rule). */
|
|
108
|
+
readonly scannedEdges: number;
|
|
109
|
+
/** Edges the rule admitted. */
|
|
110
|
+
readonly relationEdges: number;
|
|
111
|
+
/** Nodes carrying at least one admitted edge. */
|
|
112
|
+
readonly relationNodes: number;
|
|
113
|
+
/** Milliseconds the scan took, and milliseconds the build took. */
|
|
114
|
+
readonly scanMs: number;
|
|
115
|
+
readonly buildMs: number;
|
|
116
|
+
/** Short form + human name for any node the index has seen. */
|
|
117
|
+
term(value: string, blank?: boolean): ConnectTerm;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The memo. Keyed on the session OBJECT, so it lives exactly as long as the
|
|
122
|
+
* session does: the lazy store hands out one immutable session per boot and
|
|
123
|
+
* `invalidate()` replaces it, which means a rebuilt store gets a rebuilt index
|
|
124
|
+
* with no invalidation hook of its own to forget to call. A WeakMap rather than
|
|
125
|
+
* a field on the session, because the index is this capability's concern and
|
|
126
|
+
* the kernel should not carry a structure only one verb reads.
|
|
127
|
+
*/
|
|
128
|
+
const memo = new WeakMap<StoreSession, Promise<RelationIndex>>();
|
|
129
|
+
|
|
130
|
+
/** What the index build needs: the session, and a way to query it. */
|
|
131
|
+
export interface IndexContext {
|
|
132
|
+
readonly session: StoreSession;
|
|
133
|
+
readonly sparql: QueryFacade["sparql"];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The relation index for a session, built on first use and reused after.
|
|
138
|
+
*
|
|
139
|
+
* @param ctx - The booted session and its query facade.
|
|
140
|
+
* @returns The session's relation index.
|
|
141
|
+
* @note Impure — scans the store on the first call for a given session.
|
|
142
|
+
*/
|
|
143
|
+
export function relationIndex(ctx: IndexContext): Promise<RelationIndex> {
|
|
144
|
+
const cached = memo.get(ctx.session);
|
|
145
|
+
if (cached) return cached;
|
|
146
|
+
// Memoise the PROMISE, not the result, so two concurrent questions on one
|
|
147
|
+
// session share a single scan instead of racing two.
|
|
148
|
+
const building = buildRelationIndex(ctx).catch((error: unknown) => {
|
|
149
|
+
memo.delete(ctx.session);
|
|
150
|
+
throw error;
|
|
151
|
+
});
|
|
152
|
+
memo.set(ctx.session, building);
|
|
153
|
+
return building;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** One scanned triple, with its two ends flagged. */
|
|
157
|
+
interface ScannedEdge {
|
|
158
|
+
readonly s: string;
|
|
159
|
+
readonly p: string;
|
|
160
|
+
readonly o: string;
|
|
161
|
+
readonly sBlank: boolean;
|
|
162
|
+
readonly oBlank: boolean;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Read every edge out of the store, deduplicated. */
|
|
166
|
+
async function scanEdges(ctx: IndexContext): Promise<ScannedEdge[]> {
|
|
167
|
+
const result = await ctx.sparql(EDGE_SCAN);
|
|
168
|
+
if (result.type !== "select") return [];
|
|
169
|
+
const edges: ScannedEdge[] = [];
|
|
170
|
+
// Deduplicate by triple: ke queries with `use_default_graph_as_union` and
|
|
171
|
+
// oxigraph does not deduplicate across that union, so one triple asserted in
|
|
172
|
+
// two named graphs arrives twice. Left in, it would inflate every fan-in
|
|
173
|
+
// count it touches AND return the same path twice.
|
|
174
|
+
const seen = new Set<string>();
|
|
175
|
+
for (const binding of result.termBindings) {
|
|
176
|
+
const s = binding.s;
|
|
177
|
+
const p = binding.p;
|
|
178
|
+
const o = binding.o;
|
|
179
|
+
if (!s || !p || !o) continue;
|
|
180
|
+
const key = `${s.value}${SEP}${p.value}${SEP}${o.value}`;
|
|
181
|
+
if (seen.has(key)) continue;
|
|
182
|
+
seen.add(key);
|
|
183
|
+
edges.push({
|
|
184
|
+
s: s.value,
|
|
185
|
+
p: p.value,
|
|
186
|
+
o: o.value,
|
|
187
|
+
sBlank: s.termType === "BlankNode",
|
|
188
|
+
oBlank: o.termType === "BlankNode",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return edges;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Build the relation index: one scan, two group tables, then the rule.
|
|
196
|
+
*
|
|
197
|
+
* @param ctx - The booted session and its query facade.
|
|
198
|
+
* @returns The relation index, with the cost of producing it recorded.
|
|
199
|
+
* @note Impure — queries the store.
|
|
200
|
+
*/
|
|
201
|
+
async function buildRelationIndex(ctx: IndexContext): Promise<RelationIndex> {
|
|
202
|
+
const scanStart = performance.now();
|
|
203
|
+
const edges = await scanEdges(ctx);
|
|
204
|
+
const scanMs = performance.now() - scanStart;
|
|
205
|
+
|
|
206
|
+
const buildStart = performance.now();
|
|
207
|
+
// Both tables over the SAME edge set, before anything is admitted.
|
|
208
|
+
const bySubjectSide = new Map<string, number>();
|
|
209
|
+
const byObjectSide = new Map<string, number>();
|
|
210
|
+
for (const edge of edges) {
|
|
211
|
+
const inbound = `${edge.p}${SEP}${edge.o}`;
|
|
212
|
+
const outbound = `${edge.s}${SEP}${edge.p}`;
|
|
213
|
+
bySubjectSide.set(inbound, (bySubjectSide.get(inbound) ?? 0) + 1);
|
|
214
|
+
byObjectSide.set(outbound, (byObjectSide.get(outbound) ?? 0) + 1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const adjacency = new Map<string, RelationEdge[]>();
|
|
218
|
+
const rosters = new Map<string, string[]>();
|
|
219
|
+
const groups = new Map<string, RosterGroup>();
|
|
220
|
+
const blanks = new Set<string>();
|
|
221
|
+
let relationEdges = 0;
|
|
222
|
+
|
|
223
|
+
const link = (from: string, edge: RelationEdge): void => {
|
|
224
|
+
const list = adjacency.get(from);
|
|
225
|
+
if (list) list.push(edge);
|
|
226
|
+
else adjacency.set(from, [edge]);
|
|
227
|
+
};
|
|
228
|
+
const enrol = (node: string, key: string, group: RosterGroup): void => {
|
|
229
|
+
if (!groups.has(key)) groups.set(key, group);
|
|
230
|
+
const list = rosters.get(node);
|
|
231
|
+
if (list) {
|
|
232
|
+
if (!list.includes(key)) list.push(key);
|
|
233
|
+
} else rosters.set(node, [key]);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
for (const edge of edges) {
|
|
237
|
+
if (edge.sBlank) blanks.add(edge.s);
|
|
238
|
+
if (edge.oBlank) blanks.add(edge.o);
|
|
239
|
+
const inboundKey = `${edge.p}${SEP}${edge.o}`;
|
|
240
|
+
const outboundKey = `${edge.s}${SEP}${edge.p}`;
|
|
241
|
+
const subjectsSharing = bySubjectSide.get(inboundKey) ?? 0;
|
|
242
|
+
const objectsSharing = byObjectSide.get(outboundKey) ?? 0;
|
|
243
|
+
const inboundRoster = subjectsSharing > ROSTER_THRESHOLD;
|
|
244
|
+
const outboundRoster = objectsSharing > ROSTER_THRESHOLD;
|
|
245
|
+
|
|
246
|
+
if (!inboundRoster && !outboundRoster) {
|
|
247
|
+
relationEdges++;
|
|
248
|
+
link(edge.s, { to: edge.o, predicate: edge.p, forward: true });
|
|
249
|
+
link(edge.o, { to: edge.s, predicate: edge.p, forward: false });
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
// A refused edge still says something true: it records the node's
|
|
253
|
+
// MEMBERSHIP of the group that refused it, which is the whole content of a
|
|
254
|
+
// negative answer about two things that share only a classification.
|
|
255
|
+
if (inboundRoster) {
|
|
256
|
+
enrol(edge.s, `subjects${SEP}${inboundKey}`, {
|
|
257
|
+
side: "subjects",
|
|
258
|
+
predicate: edge.p,
|
|
259
|
+
node: edge.o,
|
|
260
|
+
members: subjectsSharing,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
if (outboundRoster) {
|
|
264
|
+
enrol(edge.o, `objects${SEP}${outboundKey}`, {
|
|
265
|
+
side: "objects",
|
|
266
|
+
predicate: edge.p,
|
|
267
|
+
node: edge.s,
|
|
268
|
+
members: objectsSharing,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const names = nameIndex(ctx.session.index);
|
|
274
|
+
const prefixes = ctx.session.prefixes;
|
|
275
|
+
const term = (value: string, blank?: boolean): ConnectTerm => {
|
|
276
|
+
if (blank ?? blanks.has(value)) {
|
|
277
|
+
return { value, addressable: false };
|
|
278
|
+
}
|
|
279
|
+
const prefixed = compactUri(value, prefixes);
|
|
280
|
+
const name = names.get(value);
|
|
281
|
+
return {
|
|
282
|
+
value,
|
|
283
|
+
// `compactUri` returns its input when no namespace matches, so an
|
|
284
|
+
// unmatched IRI carries no `prefixed` echoing `value`.
|
|
285
|
+
...(prefixed === value ? {} : { prefixed }),
|
|
286
|
+
...(name && isInformativeName(name, prefixed) ? { title: name } : {}),
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
adjacency,
|
|
292
|
+
rosters,
|
|
293
|
+
groups,
|
|
294
|
+
scannedEdges: edges.length,
|
|
295
|
+
relationEdges,
|
|
296
|
+
relationNodes: adjacency.size,
|
|
297
|
+
scanMs,
|
|
298
|
+
buildMs: performance.now() - buildStart,
|
|
299
|
+
term,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* The roster groups two nodes both belong to, ordered deterministically.
|
|
305
|
+
*
|
|
306
|
+
* Scoped to one hop in each direction, which is what makes it cost nothing —
|
|
307
|
+
* and what it cannot see: two blocks whose shared library is attributed to
|
|
308
|
+
* their implementation objects rather than to themselves share that roster one
|
|
309
|
+
* step removed, and this will not report it.
|
|
310
|
+
*
|
|
311
|
+
* @param index - The session's relation index.
|
|
312
|
+
* @param a - One node's IRI or blank label.
|
|
313
|
+
* @param b - The other node's IRI or blank label.
|
|
314
|
+
* @returns The shared roster groups, largest first then by predicate.
|
|
315
|
+
*/
|
|
316
|
+
export function sharedRosters(
|
|
317
|
+
index: RelationIndex,
|
|
318
|
+
a: string,
|
|
319
|
+
b: string,
|
|
320
|
+
): SharedRoster[] {
|
|
321
|
+
const mine = index.rosters.get(a) ?? [];
|
|
322
|
+
const theirs = new Set(index.rosters.get(b) ?? []);
|
|
323
|
+
const shared: SharedRoster[] = [];
|
|
324
|
+
for (const key of mine) {
|
|
325
|
+
if (!theirs.has(key)) continue;
|
|
326
|
+
const group = index.groups.get(key);
|
|
327
|
+
if (!group) continue;
|
|
328
|
+
shared.push({
|
|
329
|
+
side: group.side,
|
|
330
|
+
predicate: index.term(group.predicate),
|
|
331
|
+
node: index.term(group.node),
|
|
332
|
+
members: group.members,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return sortRosters(shared);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The roster groups one node belongs to — what classifies an entity that
|
|
340
|
+
* carries no relation at all.
|
|
341
|
+
*
|
|
342
|
+
* @param index - The session's relation index.
|
|
343
|
+
* @param node - The node's IRI or blank label.
|
|
344
|
+
* @returns The node's roster groups, largest first then by predicate.
|
|
345
|
+
*/
|
|
346
|
+
export function rosterMemberships(
|
|
347
|
+
index: RelationIndex,
|
|
348
|
+
node: string,
|
|
349
|
+
): SharedRoster[] {
|
|
350
|
+
const groups: SharedRoster[] = [];
|
|
351
|
+
for (const key of index.rosters.get(node) ?? []) {
|
|
352
|
+
const group = index.groups.get(key);
|
|
353
|
+
if (!group) continue;
|
|
354
|
+
groups.push({
|
|
355
|
+
side: group.side,
|
|
356
|
+
predicate: index.term(group.predicate),
|
|
357
|
+
node: index.term(group.node),
|
|
358
|
+
members: group.members,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return sortRosters(groups);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Order rosters by size then by name, so two runs agree. */
|
|
365
|
+
function sortRosters(groups: SharedRoster[]): SharedRoster[] {
|
|
366
|
+
return groups.sort(
|
|
367
|
+
(x, y) =>
|
|
368
|
+
y.members - x.members ||
|
|
369
|
+
x.predicate.value.localeCompare(y.predicate.value) ||
|
|
370
|
+
x.node.value.localeCompare(y.node.value),
|
|
371
|
+
);
|
|
372
|
+
}
|