@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,784 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token-noun semantic parity (PROTECTED) — the `token` story `pragma.conf.ts`
|
|
3
|
+
* declares, over a four-symbol fixture graph (the tier/standard-noun pattern).
|
|
4
|
+
*
|
|
5
|
+
* The fixture is authored to make the story's two hard rules FALSIFIABLE, which
|
|
6
|
+
* the shipped corpus cannot do on its own:
|
|
7
|
+
*
|
|
8
|
+
* - the symbol's OWN DEFINITION. Type and description are definition-level
|
|
9
|
+
* facts and a symbol can have many definitions, so the story reads the one
|
|
10
|
+
* the symbol's BASE resolved value was authored in — the head of that value's
|
|
11
|
+
* resolution chain. `color.text` here has three definitions with three
|
|
12
|
+
* different descriptions, one of them the base head and one of them the head
|
|
13
|
+
* of the value at `mode.dark`, which is the only shape that tells the rule
|
|
14
|
+
* apart from both of its neighbours: "sample one definition" would return any
|
|
15
|
+
* of the three, and a walk that forgot to restrict itself to the BASE value
|
|
16
|
+
* would publish two rows for the symbol.
|
|
17
|
+
* - the two S3 surfaces select the chain AND the derivation. The shape admits
|
|
18
|
+
* exactly one of the two, and every value in the token corpus that carries a
|
|
19
|
+
* derivation is a channel routing — so the fixture carries one of each, and
|
|
20
|
+
* asserts that a derived value's own value cell is EMPTY by construction
|
|
21
|
+
* rather than merely absent from the fixture.
|
|
22
|
+
*
|
|
23
|
+
* Counts are pinned here because this file AUTHORS the graph. Nothing in it
|
|
24
|
+
* pins a number the shipped corpus decides; that belongs in
|
|
25
|
+
* `tokenGraph.shipped.exec.test.ts`, which asserts invariants instead.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
29
|
+
import { compilePack } from "../kernel/packs/compile.js";
|
|
30
|
+
import type { LookupOutput } from "../kernel/packs/resolveEntity.js";
|
|
31
|
+
import { distributionSource, type PackPage } from "../kernel/packs/types.js";
|
|
32
|
+
import { verbKey } from "../kernel/packs/uniqueness.js";
|
|
33
|
+
import { DEFAULT_PREFIX_MAP } from "../kernel/render/prefixes.js";
|
|
34
|
+
import type { PragmaRuntime } from "../kernel/runtime/types.js";
|
|
35
|
+
import type { VerbSpec } from "../kernel/spec/types.js";
|
|
36
|
+
import { buildFixtureRuntime } from "../testing/helpers/packRuntime.js";
|
|
37
|
+
import { declaredStories } from "./distribution.js";
|
|
38
|
+
|
|
39
|
+
const tokenPack = declaredStories.get("token");
|
|
40
|
+
if (!tokenPack) {
|
|
41
|
+
throw new Error('pragma.conf.ts declares no story for "token"');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DT = "https://dt.canonical.com/";
|
|
45
|
+
const W3C = "https://dt.canonical.com/w3c-tokens/";
|
|
46
|
+
const DS = "https://ds.canonical.com/";
|
|
47
|
+
const ANATOMY = "https://anatomy.canonical.com/";
|
|
48
|
+
const PREFIXES = {
|
|
49
|
+
dt: DT,
|
|
50
|
+
"w3c-tokens": W3C,
|
|
51
|
+
ds: DS,
|
|
52
|
+
anatomy: ANATOMY,
|
|
53
|
+
rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
54
|
+
rdfs: "http://www.w3.org/2000/01/rdf-schema#",
|
|
55
|
+
owl: "http://www.w3.org/2002/07/owl#",
|
|
56
|
+
sh: "http://www.w3.org/ns/shacl#",
|
|
57
|
+
xsd: "http://www.w3.org/2001/XMLSchema#",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Four symbols, chosen so every branch of the story has a witness:
|
|
62
|
+
*
|
|
63
|
+
* - `color.text` — three definitions agreeing on type and each carrying its OWN
|
|
64
|
+
* description; two resolved values, one at the default position and one at a
|
|
65
|
+
* coordinate, the second carrying a two-link chain whose head is a DIFFERENT
|
|
66
|
+
* definition from the base value's. So the published description is the base
|
|
67
|
+
* head's, and a walk over every value would publish the symbol twice;
|
|
68
|
+
* - `color.border` — one definition and one base value, so both fields publish;
|
|
69
|
+
* it is also the symbol the channel derives from, so it proves the derivation
|
|
70
|
+
* renders a NAME rather than an IRI;
|
|
71
|
+
* - `modifier.color.text` — a CHANNEL: no definitions at all, and one resolved
|
|
72
|
+
* value carrying a derivation, a coordinate and NO chain — so it has no base
|
|
73
|
+
* head and both fields are left unbound;
|
|
74
|
+
* - `number.hue` — a second type, so `--type` is a real narrowing rather than
|
|
75
|
+
* a filter that happens to match everything.
|
|
76
|
+
*/
|
|
77
|
+
const TTL = `
|
|
78
|
+
@prefix dt: <${DT}> .
|
|
79
|
+
@prefix w3c-tokens: <${W3C}> .
|
|
80
|
+
@prefix ds: <${DS}> .
|
|
81
|
+
@prefix anatomy: <${ANATOMY}> .
|
|
82
|
+
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
|
83
|
+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
|
84
|
+
@prefix owl: <http://www.w3.org/2002/07/owl#> .
|
|
85
|
+
@prefix sh: <http://www.w3.org/ns/shacl#> .
|
|
86
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
87
|
+
|
|
88
|
+
dt:TokenSymbol a owl:Class .
|
|
89
|
+
dt:ResolvedValue a owl:Class .
|
|
90
|
+
dt:Coordinate a owl:Class .
|
|
91
|
+
w3c-tokens:Token a owl:Class .
|
|
92
|
+
w3c-tokens:TokenType a owl:Class .
|
|
93
|
+
ds:ModifierFamily a owl:Class .
|
|
94
|
+
anatomy:StyleKey a owl:Class .
|
|
95
|
+
|
|
96
|
+
dt:channelOf a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
|
|
97
|
+
dt:symbol a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
|
|
98
|
+
dt:tokenType a owl:ObjectProperty ; rdfs:range w3c-tokens:TokenType .
|
|
99
|
+
dt:covers a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
|
|
100
|
+
dt:forSymbol a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
|
|
101
|
+
dt:coordinate a owl:ObjectProperty ; rdfs:range dt:Coordinate .
|
|
102
|
+
dt:derivedFrom a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
|
|
103
|
+
dt:resolutionChain a owl:ObjectProperty ; rdfs:range rdf:List .
|
|
104
|
+
dt:resolvesTo a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
105
|
+
w3c-tokens:description a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
106
|
+
w3c-tokens:inFile a owl:ObjectProperty .
|
|
107
|
+
w3c-tokens:path a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
108
|
+
ds:name a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
109
|
+
anatomy:styleKey a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
110
|
+
anatomy:styleState a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
111
|
+
ds:hasTokenBinding a owl:ObjectProperty .
|
|
112
|
+
ds:consumesSymbol a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
|
|
113
|
+
ds:rank a owl:DatatypeProperty ; rdfs:range xsd:integer .
|
|
114
|
+
ds:node a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
115
|
+
ds:viaBlock a owl:ObjectProperty .
|
|
116
|
+
dt:ofSymbol a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
|
|
117
|
+
dt:Variable a owl:Class .
|
|
118
|
+
ds:Component a owl:Class .
|
|
119
|
+
|
|
120
|
+
# The two token types, each carrying the label the story displays and filters on.
|
|
121
|
+
w3c-tokens:color a w3c-tokens:TokenType ; rdfs:label "color" .
|
|
122
|
+
w3c-tokens:number a w3c-tokens:TokenType ; rdfs:label "number" .
|
|
123
|
+
|
|
124
|
+
# The closed interaction-state vocabulary, shaped the way the anatomy pack
|
|
125
|
+
# closes it — this is what 'token consumers --state' reads its roster from.
|
|
126
|
+
dt:stateShape sh:path anatomy:styleState ;
|
|
127
|
+
sh:in ( "hover" "focus" "active" "selected" "disabled" ) .
|
|
128
|
+
|
|
129
|
+
# A style key, so '--key''s roster is non-empty here too.
|
|
130
|
+
anatomy:key.appearance.background a anatomy:StyleKey .
|
|
131
|
+
|
|
132
|
+
# — The symbols ————————————————————————————————————————————————————————————
|
|
133
|
+
dt:color.text a dt:TokenSymbol ; rdfs:label "color.text" .
|
|
134
|
+
dt:color.border a dt:TokenSymbol ; rdfs:label "color.border" .
|
|
135
|
+
dt:number.hue a dt:TokenSymbol ; rdfs:label "number.hue" .
|
|
136
|
+
dt:modifier.color.text a dt:TokenSymbol ;
|
|
137
|
+
rdfs:label "modifier.color.text" ;
|
|
138
|
+
dt:channelOf dt:color.text .
|
|
139
|
+
|
|
140
|
+
# — The definitions ————————————————————————————————————————————————————————
|
|
141
|
+
# Three for color.text: SAME type, THREE different descriptions. The light one
|
|
142
|
+
# is the head of the BASE value's chain and so the one the story publishes; the
|
|
143
|
+
# dark one heads the chain at mode.dark and must NOT reach the row.
|
|
144
|
+
<${DT}file/light.json#color.text> a w3c-tokens:Token ;
|
|
145
|
+
dt:symbol dt:color.text ;
|
|
146
|
+
dt:tokenType w3c-tokens:color ;
|
|
147
|
+
w3c-tokens:inFile <${DT}file/light.json> ;
|
|
148
|
+
w3c-tokens:description "Default text colour." .
|
|
149
|
+
<${DT}file/dark.json#color.text> a w3c-tokens:Token ;
|
|
150
|
+
dt:symbol dt:color.text ;
|
|
151
|
+
dt:tokenType w3c-tokens:color ;
|
|
152
|
+
w3c-tokens:inFile <${DT}file/dark.json> ;
|
|
153
|
+
w3c-tokens:description "Text colour in dark mode." .
|
|
154
|
+
<${DT}file/success.json#color.text> a w3c-tokens:Token ;
|
|
155
|
+
dt:symbol dt:color.text ;
|
|
156
|
+
dt:tokenType w3c-tokens:color ;
|
|
157
|
+
w3c-tokens:inFile <${DT}file/success.json> ;
|
|
158
|
+
w3c-tokens:description "Text colour for a successful outcome." .
|
|
159
|
+
|
|
160
|
+
<${DT}file/light.json#color.border> a w3c-tokens:Token ;
|
|
161
|
+
dt:symbol dt:color.border ;
|
|
162
|
+
dt:tokenType w3c-tokens:color ;
|
|
163
|
+
w3c-tokens:inFile <${DT}file/light.json> ;
|
|
164
|
+
w3c-tokens:description "Border colour." .
|
|
165
|
+
|
|
166
|
+
<${DT}file/light.json#number.hue> a w3c-tokens:Token ;
|
|
167
|
+
dt:symbol dt:number.hue ;
|
|
168
|
+
dt:tokenType w3c-tokens:number ;
|
|
169
|
+
w3c-tokens:inFile <${DT}file/light.json> ;
|
|
170
|
+
w3c-tokens:description "A hue angle." .
|
|
171
|
+
|
|
172
|
+
<${DT}file/light.json> w3c-tokens:path "light.json" .
|
|
173
|
+
<${DT}file/dark.json> w3c-tokens:path "dark.json" .
|
|
174
|
+
<${DT}file/success.json> w3c-tokens:path "success.json" .
|
|
175
|
+
|
|
176
|
+
# — Coverage: it hangs on the FAMILY, so the story reads the inverse ————————
|
|
177
|
+
ds:global.modifier_family.criticality a ds:ModifierFamily ;
|
|
178
|
+
ds:name "Criticality" ;
|
|
179
|
+
dt:covers dt:color.text .
|
|
180
|
+
|
|
181
|
+
# — The bindings, and the CSS names that spell the symbol they consume ——————
|
|
182
|
+
# TWO spellings of color.text, so '--variable' has to collapse them: both
|
|
183
|
+
# resolve through dt:ofSymbol to one symbol, so both must answer the same set.
|
|
184
|
+
<${DT}s4/web/--color-text> a dt:Variable ;
|
|
185
|
+
rdfs:label "color-text" ;
|
|
186
|
+
dt:ofSymbol dt:color.text .
|
|
187
|
+
<${DT}s4/web/--color-text-legacy> a dt:Variable ;
|
|
188
|
+
rdfs:label "color-text-legacy" ;
|
|
189
|
+
dt:ofSymbol dt:color.text .
|
|
190
|
+
# And one standing for NO symbol, which therefore has no answer down the
|
|
191
|
+
# consumer path at all — an empty answer for a reason, not evidence that
|
|
192
|
+
# nothing consumes it.
|
|
193
|
+
<${DT}s4/web/--button-color-background> a dt:Variable ;
|
|
194
|
+
rdfs:label "button-color-background" .
|
|
195
|
+
|
|
196
|
+
# Every record carries ds:viaBlock, and on a block's OWN tree it names the
|
|
197
|
+
# block itself — the shipped shape, which the term's own definition states
|
|
198
|
+
# ("Equal to the subject in the own-tree case"). Button's two records are
|
|
199
|
+
# own-tree, so their via must print BLANK; Modal's is the SAME binding reached
|
|
200
|
+
# through the Button its anatomy embeds, so its via must print "Button" and its
|
|
201
|
+
# block must print "Modal". Those two rows are what tell a consuming-block
|
|
202
|
+
# column from a via-block one: COALESCE'ing the via over the block printed both
|
|
203
|
+
# as "Button", byte-identical and indistinguishable.
|
|
204
|
+
ds:button a ds:Component ; ds:name "Button" ;
|
|
205
|
+
ds:hasTokenBinding <${DT}binding/button-bg> , <${DT}binding/button-bg-hover> .
|
|
206
|
+
<${DT}binding/button-bg>
|
|
207
|
+
ds:consumesSymbol dt:color.text ;
|
|
208
|
+
anatomy:styleKey "appearance.background" ;
|
|
209
|
+
ds:rank "1" ;
|
|
210
|
+
ds:viaBlock ds:button ;
|
|
211
|
+
ds:node ".root" .
|
|
212
|
+
<${DT}binding/button-bg-hover>
|
|
213
|
+
ds:consumesSymbol dt:color.text ;
|
|
214
|
+
anatomy:styleKey "appearance.background" ;
|
|
215
|
+
anatomy:styleState "hover" ;
|
|
216
|
+
ds:rank "2" ;
|
|
217
|
+
ds:viaBlock ds:button ;
|
|
218
|
+
ds:node ".root" .
|
|
219
|
+
|
|
220
|
+
ds:modal a ds:Component ; ds:name "Modal" ;
|
|
221
|
+
ds:hasTokenBinding <${DT}binding/modal-button-bg> .
|
|
222
|
+
<${DT}binding/modal-button-bg>
|
|
223
|
+
ds:consumesSymbol dt:color.text ;
|
|
224
|
+
anatomy:styleKey "appearance.background" ;
|
|
225
|
+
ds:rank "1" ;
|
|
226
|
+
ds:viaBlock ds:button ;
|
|
227
|
+
ds:node ".root" .
|
|
228
|
+
|
|
229
|
+
# — The coordinates ————————————————————————————————————————————————————————
|
|
230
|
+
dt:coordinate.mode.dark a dt:Coordinate .
|
|
231
|
+
dt:coordinate.criticality.success a dt:Coordinate .
|
|
232
|
+
|
|
233
|
+
# — The resolved values ————————————————————————————————————————————————————
|
|
234
|
+
# Default position: no coordinate at all, one-link chain. This value's chain
|
|
235
|
+
# HEAD is what the list and the lookup publish as the symbol's own type and
|
|
236
|
+
# description, so every symbol with a definition carries one — which is the
|
|
237
|
+
# shipped graph's shape too.
|
|
238
|
+
[] a dt:ResolvedValue ;
|
|
239
|
+
dt:forSymbol dt:color.text ;
|
|
240
|
+
dt:resolvesTo "black" ;
|
|
241
|
+
dt:resolutionChain ( <${DT}file/light.json#color.text> ) .
|
|
242
|
+
[] a dt:ResolvedValue ;
|
|
243
|
+
dt:forSymbol dt:color.border ;
|
|
244
|
+
dt:resolvesTo "grey" ;
|
|
245
|
+
dt:resolutionChain ( <${DT}file/light.json#color.border> ) .
|
|
246
|
+
[] a dt:ResolvedValue ;
|
|
247
|
+
dt:forSymbol dt:number.hue ;
|
|
248
|
+
dt:resolvesTo "240" ;
|
|
249
|
+
dt:resolutionChain ( <${DT}file/light.json#number.hue> ) .
|
|
250
|
+
# A coordinate, and a TWO-link chain: the authored definition first, what it
|
|
251
|
+
# aliased through second. The lookup's expand shows the head; the verb shows both.
|
|
252
|
+
[] a dt:ResolvedValue ;
|
|
253
|
+
dt:forSymbol dt:color.text ;
|
|
254
|
+
dt:coordinate dt:coordinate.mode.dark ;
|
|
255
|
+
dt:resolvesTo "white" ;
|
|
256
|
+
dt:resolutionChain (
|
|
257
|
+
<${DT}file/dark.json#color.text>
|
|
258
|
+
<${DT}file/light.json#color.border>
|
|
259
|
+
) .
|
|
260
|
+
# A channel routing: a derivation, a coordinate, and NO value and NO chain.
|
|
261
|
+
[] a dt:ResolvedValue ;
|
|
262
|
+
dt:forSymbol dt:modifier.color.text ;
|
|
263
|
+
dt:coordinate dt:coordinate.criticality.success ;
|
|
264
|
+
dt:derivedFrom dt:color.border .
|
|
265
|
+
`;
|
|
266
|
+
|
|
267
|
+
const verbs = () =>
|
|
268
|
+
compilePack(
|
|
269
|
+
tokenPack,
|
|
270
|
+
distributionSource("pragma.conf.ts"),
|
|
271
|
+
DEFAULT_PREFIX_MAP,
|
|
272
|
+
);
|
|
273
|
+
const verb = (label: string) =>
|
|
274
|
+
verbs().find((v) => verbKey(v.path) === `token ${label}`) as VerbSpec;
|
|
275
|
+
|
|
276
|
+
let rt: PragmaRuntime;
|
|
277
|
+
beforeAll(async () => {
|
|
278
|
+
({ rt } = await buildFixtureRuntime({ ttl: TTL, prefixes: PREFIXES }));
|
|
279
|
+
});
|
|
280
|
+
afterAll(async () => {
|
|
281
|
+
(await rt.store.get()).store.dispose();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
/** One page of a list-shaped verb. */
|
|
285
|
+
const page = (label: string, params: Record<string, unknown> = {}) =>
|
|
286
|
+
verb(label).run(params, rt) as Promise<PackPage>;
|
|
287
|
+
|
|
288
|
+
/** The rows of a list-shaped verb, as records. */
|
|
289
|
+
async function rows(
|
|
290
|
+
label: string,
|
|
291
|
+
params: Record<string, unknown> = {},
|
|
292
|
+
): Promise<Record<string, string>[]> {
|
|
293
|
+
return [...(await page(label, params)).rows];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function lookup(name: string): Promise<Record<string, unknown>> {
|
|
297
|
+
const out = (await verb("lookup").run({ name: [name] }, rt)) as LookupOutput;
|
|
298
|
+
expect(out.errors).toEqual([]);
|
|
299
|
+
return out.results.at(0) as Record<string, unknown>;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** An expand's child records, which the sub-SELECT returns unordered. */
|
|
303
|
+
function children(
|
|
304
|
+
entity: Record<string, unknown>,
|
|
305
|
+
name: string,
|
|
306
|
+
): Record<string, string>[] {
|
|
307
|
+
return (entity[name] ?? []) as Record<string, string>[];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
describe("token list — the symbols, and their own definitions (PROTECTED)", () => {
|
|
311
|
+
it("publishes one row per symbol, ordered by the dotted name", async () => {
|
|
312
|
+
expect((await rows("list")).map((row) => row.name)).toEqual([
|
|
313
|
+
"color.border",
|
|
314
|
+
"color.text",
|
|
315
|
+
"modifier.color.text",
|
|
316
|
+
"number.hue",
|
|
317
|
+
]);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("publishes the type and description of the symbol's OWN definition", async () => {
|
|
321
|
+
const byName = new Map((await rows("list")).map((r) => [r.name, r]));
|
|
322
|
+
// One definition, which is therefore the base value's chain head.
|
|
323
|
+
expect(byName.get("color.border")).toMatchObject({
|
|
324
|
+
type: "color",
|
|
325
|
+
description: "Border colour.",
|
|
326
|
+
});
|
|
327
|
+
// Three definitions with three different descriptions. The published one
|
|
328
|
+
// is the BASE value's head — `light.json` — and not `dark.json`'s, which
|
|
329
|
+
// heads the chain at `mode.dark`, nor `success.json`'s, which heads no
|
|
330
|
+
// value at all.
|
|
331
|
+
expect(byName.get("color.text")).toMatchObject({
|
|
332
|
+
type: "color",
|
|
333
|
+
description: "Default text colour.",
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("publishes ONE row per symbol even where several values carry chains", async () => {
|
|
338
|
+
// `color.text` has a base value and a value at `mode.dark`, each with its
|
|
339
|
+
// own chain head and its own description. A walk over every value would
|
|
340
|
+
// publish the symbol twice — once per head — which is what restricting to
|
|
341
|
+
// the value with no coordinate prevents.
|
|
342
|
+
const named = (await rows("list")).filter((r) => r.name === "color.text");
|
|
343
|
+
expect(named).toHaveLength(1);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("leaves BOTH fields unbound for a symbol with no base chain head", async () => {
|
|
347
|
+
// A minted channel has no definition anywhere and its only value is a
|
|
348
|
+
// derivation, so there is no head to read. The cells are absent rather
|
|
349
|
+
// than guessed.
|
|
350
|
+
const channel = (await rows("list")).find(
|
|
351
|
+
(row) => row.name === "modifier.color.text",
|
|
352
|
+
);
|
|
353
|
+
expect(channel).toBeDefined();
|
|
354
|
+
expect(channel?.type).toBeUndefined();
|
|
355
|
+
expect(channel?.description).toBeUndefined();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("binds the channel relation to the base symbol's NAME, not its IRI", async () => {
|
|
359
|
+
// A filterable column is the displayed column, so an unbound or IRI-valued
|
|
360
|
+
// cell would both render an IRI and refuse the name a caller would type.
|
|
361
|
+
const byName = new Map((await rows("list")).map((r) => [r.name, r]));
|
|
362
|
+
expect(byName.get("modifier.color.text")?.channelOf).toBe("color.text");
|
|
363
|
+
expect(byName.get("color.text")?.channelOf).toBeUndefined();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("--type narrows to the definition's type, and an unbound type matches nothing", async () => {
|
|
367
|
+
expect((await rows("list", { type: "color" })).map((r) => r.name)).toEqual([
|
|
368
|
+
"color.border",
|
|
369
|
+
"color.text",
|
|
370
|
+
]);
|
|
371
|
+
expect((await rows("list", { type: "number" })).map((r) => r.name)).toEqual(
|
|
372
|
+
["number.hue"],
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("--channel-of answers the channels that provision one symbol", async () => {
|
|
377
|
+
expect(
|
|
378
|
+
(await rows("list", { channelOf: "color.text" })).map((r) => r.name),
|
|
379
|
+
).toEqual(["modifier.color.text"]);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("--channel-of admits a symbol nothing provisions, and answers empty", async () => {
|
|
383
|
+
// The roster is every symbol, so this is the documented calm empty list
|
|
384
|
+
// rather than INVALID_INPUT — the distinction the graph-read vocabulary
|
|
385
|
+
// exists to preserve.
|
|
386
|
+
const answered = await page("list", { channelOf: "number.hue" });
|
|
387
|
+
expect(answered.rows).toEqual([]);
|
|
388
|
+
expect(answered.nextAfter).toBeUndefined();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("refuses a type the graph does not admit, naming the ones it does", async () => {
|
|
392
|
+
await expect(page("list", { type: "shadow" })).rejects.toMatchObject({
|
|
393
|
+
code: "INVALID_INPUT",
|
|
394
|
+
validOptions: ["color", "number"],
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("--search reads the name and the published description", async () => {
|
|
399
|
+
expect(
|
|
400
|
+
(await rows("list", { search: "border" })).map((r) => r.name),
|
|
401
|
+
).toEqual(["color.border"]);
|
|
402
|
+
// Only the published description is searchable. A word that appears solely
|
|
403
|
+
// in a definition this symbol's base value was NOT authored in reaches no
|
|
404
|
+
// row — `successful` is `success.json`'s wording, and `dark mode` is the
|
|
405
|
+
// head at `mode.dark`. Both are readable through the `definitions` expand
|
|
406
|
+
// and neither is a symbol-level fact.
|
|
407
|
+
expect(
|
|
408
|
+
(await rows("list", { search: "successful" })).map((r) => r.name),
|
|
409
|
+
).toEqual([]);
|
|
410
|
+
expect(
|
|
411
|
+
(await rows("list", { search: "dark mode" })).map((r) => r.name),
|
|
412
|
+
).toEqual([]);
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe("token values — chain AND derivation on the same surface (PROTECTED)", () => {
|
|
417
|
+
it("publishes one row per (symbol, position) the graph materialises", async () => {
|
|
418
|
+
expect(
|
|
419
|
+
(await rows("values")).map((row) => [row.symbol, row.position ?? ""]),
|
|
420
|
+
).toEqual([
|
|
421
|
+
["color.border", ""],
|
|
422
|
+
["color.text", ""],
|
|
423
|
+
["color.text", "mode.dark"],
|
|
424
|
+
["modifier.color.text", "criticality.success"],
|
|
425
|
+
["number.hue", ""],
|
|
426
|
+
]);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("names the position by its dotted coordinate, and leaves the default empty", async () => {
|
|
430
|
+
const named = await rows("values");
|
|
431
|
+
const darkRow = named.find(
|
|
432
|
+
(row) => row.symbol === "color.text" && row.position,
|
|
433
|
+
);
|
|
434
|
+
const defaultRow = named.find(
|
|
435
|
+
(row) => row.symbol === "color.text" && !row.position,
|
|
436
|
+
);
|
|
437
|
+
// Not `dt:coordinate.mode.dark`: the class prefix its IRI carries is
|
|
438
|
+
// dropped, so a position reads and filters the way it is written elsewhere.
|
|
439
|
+
expect(darkRow?.position).toBe("mode.dark");
|
|
440
|
+
expect(defaultRow?.position).toBeUndefined();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("concatenates the WHOLE resolution chain, as a set", async () => {
|
|
444
|
+
// Every link, which is the point — the lookup's expand shows the head
|
|
445
|
+
// alone, and this verb is where the whole walk is readable.
|
|
446
|
+
//
|
|
447
|
+
// As a SET, not a sequence, and asserted that way after pinning one
|
|
448
|
+
// spelling of it flaked: `GROUP_CONCAT` has no defined order in SPARQL and
|
|
449
|
+
// takes no `ORDER BY`, so a two-link chain may serialise either way round.
|
|
450
|
+
// Pinning the order would be pinning the engine's evaluation, and the
|
|
451
|
+
// story's own note now says the cell answers WHICH definitions a value
|
|
452
|
+
// passed through rather than in what order.
|
|
453
|
+
const darkRow = (await rows("values")).find(
|
|
454
|
+
(row) => row.symbol === "color.text" && row.position === "mode.dark",
|
|
455
|
+
);
|
|
456
|
+
expect(darkRow?.chain?.split(" ").sort()).toEqual([
|
|
457
|
+
"dark.json#color.text",
|
|
458
|
+
"light.json#color.border",
|
|
459
|
+
]);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it("a derived value carries its derivation by NAME and no value cell", async () => {
|
|
463
|
+
const derived = (await rows("values")).find(
|
|
464
|
+
(row) => row.symbol === "modifier.color.text",
|
|
465
|
+
);
|
|
466
|
+
expect(derived).toMatchObject({
|
|
467
|
+
symbol: "modifier.color.text",
|
|
468
|
+
position: "criticality.success",
|
|
469
|
+
derivedFrom: "color.border",
|
|
470
|
+
});
|
|
471
|
+
// Empty BY CONSTRUCTION — the shape admits a chain or a derivation, never
|
|
472
|
+
// both, and a routing carries no value of its own. A surface selecting only
|
|
473
|
+
// the chain would render this row blank. Asserted as "no chain, however the
|
|
474
|
+
// row spells it": the projection omits an empty cell rather than carrying
|
|
475
|
+
// the empty string a `GROUP_CONCAT` over nothing produces, and which of the
|
|
476
|
+
// two a caller sees is the formatter's business, not this rule's.
|
|
477
|
+
expect(derived?.value).toBeUndefined();
|
|
478
|
+
expect(derived?.chain ?? "").toBe("");
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it("--position set-matches one coordinate", async () => {
|
|
482
|
+
expect(
|
|
483
|
+
(await rows("values", { position: "mode.dark" })).map((r) => r.symbol),
|
|
484
|
+
).toEqual(["color.text"]);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("--symbol narrows to one symbol's positions", async () => {
|
|
488
|
+
expect(await rows("values", { symbol: "color.text" })).toHaveLength(2);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
describe("token consumers — the binding tuple, either spelling (PROTECTED)", () => {
|
|
493
|
+
it("publishes one row per binding, with every identity column", async () => {
|
|
494
|
+
// The fixture records two bindings on Button that differ ONLY in state and
|
|
495
|
+
// rank, and both are published: they are two different facts, so a
|
|
496
|
+
// narrower row would publish rows a caller cannot tell apart and a
|
|
497
|
+
// deduplicating consumer would silently lose one.
|
|
498
|
+
const answered = await rows("consumers");
|
|
499
|
+
expect(answered).toHaveLength(3);
|
|
500
|
+
expect(
|
|
501
|
+
answered.map((row) => [
|
|
502
|
+
row.block,
|
|
503
|
+
row.via ?? "",
|
|
504
|
+
row.key,
|
|
505
|
+
row.state ?? "",
|
|
506
|
+
row.rank,
|
|
507
|
+
]),
|
|
508
|
+
).toEqual([
|
|
509
|
+
["Button", "", "appearance.background", "", "1"],
|
|
510
|
+
["Button", "", "appearance.background", "hover", "2"],
|
|
511
|
+
["Modal", "Button", "appearance.background", "", "1"],
|
|
512
|
+
]);
|
|
513
|
+
for (const row of answered) {
|
|
514
|
+
expect(row.symbol).toBe("color.text");
|
|
515
|
+
expect(row.node).toBe(".root");
|
|
516
|
+
// Still SELECTed — the row's subject, and what keeps two bindings
|
|
517
|
+
// differing only in state apart — though no longer a rendered column.
|
|
518
|
+
expect(row.uri).toBeTruthy();
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("names the CONSUMING block, and the via only when it differs", async () => {
|
|
523
|
+
// The fact the verb exists for. Modal's row is Button's binding reached
|
|
524
|
+
// through the Button its anatomy embeds: the block that CHANGES if the
|
|
525
|
+
// symbol does is Modal, and the tree the binding was authored in is
|
|
526
|
+
// Button's. Coalescing the two printed Modal's row as "Button" —
|
|
527
|
+
// byte-identical to Button's own, so `--symbol color.text` answered the
|
|
528
|
+
// same row twice and no reader could tell which component was meant.
|
|
529
|
+
const answered = await rows("consumers", { symbol: "color.text" });
|
|
530
|
+
expect(answered.map((row) => row.block)).toEqual([
|
|
531
|
+
"Button",
|
|
532
|
+
"Button",
|
|
533
|
+
"Modal",
|
|
534
|
+
]);
|
|
535
|
+
|
|
536
|
+
// Blank on a record sitting on the block's OWN tree, where the fixture
|
|
537
|
+
// asserts `ds:viaBlock` equal to the block — the shipped shape. A blank
|
|
538
|
+
// and not the block's own name repeated, which is never news.
|
|
539
|
+
const own = answered.filter((row) => row.block === "Button");
|
|
540
|
+
expect(own).toHaveLength(2);
|
|
541
|
+
for (const row of own) expect(row.via ?? "").toBe("");
|
|
542
|
+
|
|
543
|
+
const inherited = answered.find((row) => row.block === "Modal");
|
|
544
|
+
expect(inherited?.via).toBe("Button");
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("searches the via as well as the consuming block", async () => {
|
|
548
|
+
// A reader who knows a symbol reaches everything embedding a Button holds
|
|
549
|
+
// the word "Button", not the name of each embedder — so the via is
|
|
550
|
+
// searchable beside the block. Modal's row carries "Button" ONLY in its
|
|
551
|
+
// via, so a search that reached it can only have read that column.
|
|
552
|
+
const found = await rows("consumers", { search: "Modal" });
|
|
553
|
+
expect(found.map((row) => [row.block, row.via])).toEqual([
|
|
554
|
+
["Modal", "Button"],
|
|
555
|
+
]);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("carries EVERY spelling of the consumed symbol in one cell", async () => {
|
|
559
|
+
// A set, not a join that multiplies: two variables stand for this symbol,
|
|
560
|
+
// so binding one row per variable would have published each binding twice.
|
|
561
|
+
const answered = await rows("consumers");
|
|
562
|
+
for (const row of answered) {
|
|
563
|
+
expect(row.variable?.split(" ").sort()).toEqual([
|
|
564
|
+
"color-text",
|
|
565
|
+
"color-text-legacy",
|
|
566
|
+
]);
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("--variable and --symbol answer the SAME set, by either name", async () => {
|
|
571
|
+
// The whole point of the filter: a web implementer holds a CSS name, not a
|
|
572
|
+
// dotted symbol, and should not have to translate it first.
|
|
573
|
+
const bySymbol = await rows("consumers", { symbol: "color.text" });
|
|
574
|
+
const byVariable = await rows("consumers", { variable: "color-text" });
|
|
575
|
+
expect(byVariable).toEqual(bySymbol);
|
|
576
|
+
expect(byVariable).toHaveLength(3);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it("both spellings of one symbol collapse to the same answer", async () => {
|
|
580
|
+
// Both resolve through the same edge to one symbol, so neither partitions
|
|
581
|
+
// the set. If they ever diverged, half the consumers of a symbol would be
|
|
582
|
+
// invisible from one of its two names.
|
|
583
|
+
expect(await rows("consumers", { variable: "color-text-legacy" })).toEqual(
|
|
584
|
+
await rows("consumers", { variable: "color-text" }),
|
|
585
|
+
);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it("--symbol and --variable together INTERSECT, like any two filters", async () => {
|
|
589
|
+
// They are two spellings of one constraint, and the kernel's meaning for
|
|
590
|
+
// two filters is conjunction — so naming the same thing twice narrows to
|
|
591
|
+
// itself, and naming two different things narrows to nothing. Documented
|
|
592
|
+
// rather than refused: a per-story mutual exclusion is not something the
|
|
593
|
+
// grammar can express, and inventing one here would be a rule no other
|
|
594
|
+
// story follows.
|
|
595
|
+
expect(
|
|
596
|
+
await rows("consumers", { symbol: "color.text", variable: "color-text" }),
|
|
597
|
+
).toHaveLength(3);
|
|
598
|
+
expect(
|
|
599
|
+
await rows("consumers", {
|
|
600
|
+
symbol: "color.border",
|
|
601
|
+
variable: "color-text",
|
|
602
|
+
}),
|
|
603
|
+
).toEqual([]);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it("a variable standing for NO symbol answers empty, naming the filter", async () => {
|
|
607
|
+
// Admitted, because it is a real variable — 236 of them stand for no
|
|
608
|
+
// symbol. What it is not is evidence that nothing consumes it, so the
|
|
609
|
+
// answer names the filter that came back empty.
|
|
610
|
+
//
|
|
611
|
+
// The story's `emptyRecovery` follows that sentence rather than replacing
|
|
612
|
+
// it. Zero rows cannot say which of the two emptinesses this is — nothing
|
|
613
|
+
// consumes this variable, or nothing consumes anything yet — so the answer
|
|
614
|
+
// says both: the filter that came back empty, then the story's account of
|
|
615
|
+
// where consumers are recorded. What the recovery may NOT do is assert the
|
|
616
|
+
// population is empty, which is the defect users hit as "No token symbols
|
|
617
|
+
// in the store" over 745 present symbols; `token consumers` opens on the
|
|
618
|
+
// two readings instead, so it stays true here.
|
|
619
|
+
const answered = await page("consumers", {
|
|
620
|
+
variable: "button-color-background",
|
|
621
|
+
});
|
|
622
|
+
expect(answered.rows).toEqual([]);
|
|
623
|
+
const notice = verb("consumers").output.formatters.notice?.(
|
|
624
|
+
answered as never,
|
|
625
|
+
);
|
|
626
|
+
expect(notice).toContain("matches `--variable button-color-background`.");
|
|
627
|
+
expect(notice).toContain("Either no component uses this token");
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it("--key and --state narrow to one tuple each", async () => {
|
|
631
|
+
expect(
|
|
632
|
+
await rows("consumers", { key: "appearance.background" }),
|
|
633
|
+
).toHaveLength(3);
|
|
634
|
+
const hover = await rows("consumers", { state: "hover" });
|
|
635
|
+
expect(hover).toHaveLength(1);
|
|
636
|
+
expect(hover[0]?.rank).toBe("2");
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it("refuses a state the closing shape does not admit", async () => {
|
|
640
|
+
await expect(page("consumers", { state: "pressed" })).rejects.toMatchObject(
|
|
641
|
+
{
|
|
642
|
+
code: "INVALID_INPUT",
|
|
643
|
+
validOptions: ["active", "disabled", "focus", "hover", "selected"],
|
|
644
|
+
},
|
|
645
|
+
);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it("refuses a variable name the graph does not carry", async () => {
|
|
649
|
+
await expect(
|
|
650
|
+
page("consumers", { variable: "not-a-variable" }),
|
|
651
|
+
).rejects.toMatchObject({ code: "INVALID_INPUT" });
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
describe("token lookup — one symbol, end to end (PROTECTED)", () => {
|
|
656
|
+
it("resolves the dotted name the list publishes, verbatim", async () => {
|
|
657
|
+
expect(await lookup("color.text")).toMatchObject({ name: "color.text" });
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it("resolves the same entity by prefixed IRI", async () => {
|
|
661
|
+
expect(await lookup("dt:color.text")).toMatchObject({ name: "color.text" });
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it("carries the SAME type and description the list row publishes", async () => {
|
|
665
|
+
// Two spellings of one rule: the list reads the base value's chain head in
|
|
666
|
+
// its own SPARQL, the lookup reads it as a property path. A divergence here
|
|
667
|
+
// would mean a caller who looked a symbol up saw a different description
|
|
668
|
+
// from the one that brought them to it.
|
|
669
|
+
//
|
|
670
|
+
// Asserted on `color.border`, which has ONE chain head. `color.text` is
|
|
671
|
+
// deliberately the other case: this fixture gives its two values two
|
|
672
|
+
// DIFFERENT heads, and a property path cannot spell the base restriction
|
|
673
|
+
// that tells them apart, so the lookup's cell is whichever row the store
|
|
674
|
+
// returns first. That is not a defect this fixture can fix — it is a
|
|
675
|
+
// CONDITION on the corpus, and `tokenGraph.shipped.exec.test.ts` pins it:
|
|
676
|
+
// over the shipped graph no symbol's heads name two types or two
|
|
677
|
+
// descriptions, which is what makes the path's answer the list's answer.
|
|
678
|
+
const row = (await rows("list")).find((r) => r.name === "color.border");
|
|
679
|
+
expect(row?.description).toBe("Border colour.");
|
|
680
|
+
expect(await lookup("color.border")).toMatchObject({
|
|
681
|
+
type: row?.type,
|
|
682
|
+
description: row?.description,
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it("leaves the pair unbound for a symbol with no base chain head", async () => {
|
|
687
|
+
const channel = await lookup("modifier.color.text");
|
|
688
|
+
expect(channel.type).toBeUndefined();
|
|
689
|
+
expect(channel.description).toBeUndefined();
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it("shows every definition APART, with its own type and description", async () => {
|
|
693
|
+
// The only surface that can: the symbol-level pair names ONE definition —
|
|
694
|
+
// the base value's head — and this is where the other two behind
|
|
695
|
+
// `color.text` stay readable rather than being outvoted or discarded.
|
|
696
|
+
const definitions = children(await lookup("color.text"), "definitions");
|
|
697
|
+
expect(definitions).toHaveLength(3);
|
|
698
|
+
expect(definitions.map((d) => d.file).sort()).toEqual([
|
|
699
|
+
"dark.json",
|
|
700
|
+
"light.json",
|
|
701
|
+
"success.json",
|
|
702
|
+
]);
|
|
703
|
+
expect(new Set(definitions.map((d) => d.description)).size).toBe(3);
|
|
704
|
+
expect(new Set(definitions.map((d) => d.type))).toEqual(new Set(["color"]));
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it("reads coverage from the family end of the relation", async () => {
|
|
708
|
+
expect(children(await lookup("color.text"), "coverage")).toEqual([
|
|
709
|
+
{ family: "Criticality" },
|
|
710
|
+
]);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it("shows one row per resolved value, and the chain's HEAD on each", async () => {
|
|
714
|
+
const values = children(await lookup("color.text"), "values");
|
|
715
|
+
// TWO rows for two positions. Selecting the whole chain here would have
|
|
716
|
+
// made the two-link value render twice, and a caller counting positions
|
|
717
|
+
// would have read three.
|
|
718
|
+
expect(values).toHaveLength(2);
|
|
719
|
+
const dark = values.find((v) => v.position?.includes("mode.dark"));
|
|
720
|
+
expect(dark?.value).toBe("white");
|
|
721
|
+
expect(dark?.chain).toContain("dark.json#color.text");
|
|
722
|
+
expect(dark?.chain).not.toContain("color.border");
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it("a channel names the symbol it provisions, and its value is a derivation", async () => {
|
|
726
|
+
const channel = await lookup("modifier.color.text");
|
|
727
|
+
expect(channel).toMatchObject({ channelOf: "color.text" });
|
|
728
|
+
const values = children(channel, "values");
|
|
729
|
+
expect(values).toHaveLength(1);
|
|
730
|
+
expect(values[0]).toMatchObject({ derivedFrom: "color.border" });
|
|
731
|
+
expect(values[0]?.value).toBeUndefined();
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
it("a miss carries did-you-mean suggestions", async () => {
|
|
735
|
+
let caught: unknown;
|
|
736
|
+
try {
|
|
737
|
+
await verb("lookup").run({ name: ["color.txt"] }, rt);
|
|
738
|
+
} catch (error) {
|
|
739
|
+
caught = error;
|
|
740
|
+
}
|
|
741
|
+
expect(caught).toMatchObject({ code: "ENTITY_NOT_FOUND" });
|
|
742
|
+
expect((caught as { suggestions: string[] }).suggestions).toContain(
|
|
743
|
+
"color.text",
|
|
744
|
+
);
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
describe("token — the name literal is the whole grammar (PROTECTED)", () => {
|
|
749
|
+
it("every published name resolves, and every symbol publishes one", async () => {
|
|
750
|
+
// The two-step promise, over the fixture: a row's `name` goes VERBATIM to
|
|
751
|
+
// lookup. Both halves key on `rdfs:label` and NEITHER declares an
|
|
752
|
+
// IRI-derived fallback, so they are empty together or populated together —
|
|
753
|
+
// never one publishing names the other cannot answer.
|
|
754
|
+
const names = (await rows("list")).map((row) => row.name);
|
|
755
|
+
const out = (await verb("lookup").run({ name: names }, rt)) as LookupOutput;
|
|
756
|
+
expect(out.errors).toEqual([]);
|
|
757
|
+
expect(out.results.map((r) => (r as { name: string }).name).sort()).toEqual(
|
|
758
|
+
[...names].sort(),
|
|
759
|
+
);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it("a symbol carrying no label is published by neither half", async () => {
|
|
763
|
+
// The consequence of requiring the literal, stated as a test rather than
|
|
764
|
+
// as a comment: an unlabelled symbol is not addressable AND not listed, so
|
|
765
|
+
// the two halves cannot disagree about it.
|
|
766
|
+
const unlabelled = await rt.query.sparql(
|
|
767
|
+
`SELECT (COUNT(?s) AS ?n) WHERE {
|
|
768
|
+
?s a dt:TokenSymbol .
|
|
769
|
+
FILTER NOT EXISTS { ?s rdfs:label ?l }
|
|
770
|
+
}`,
|
|
771
|
+
);
|
|
772
|
+
expect(unlabelled.type).toBe("select");
|
|
773
|
+
// The fixture labels all four, so this is the invariant rather than a
|
|
774
|
+
// count: whatever the graph holds, the list publishes exactly the labelled
|
|
775
|
+
// ones.
|
|
776
|
+
const listed = (await rows("list")).length;
|
|
777
|
+
const labelled = await rt.query.sparql(
|
|
778
|
+
"SELECT (COUNT(DISTINCT ?s) AS ?n) WHERE { ?s a dt:TokenSymbol ; rdfs:label ?l }",
|
|
779
|
+
);
|
|
780
|
+
expect(
|
|
781
|
+
labelled.type === "select" ? Number(labelled.bindings[0]?.n) : -1,
|
|
782
|
+
).toBe(listed);
|
|
783
|
+
});
|
|
784
|
+
});
|