@canonical/pragma-cli 0.35.0 → 0.37.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 +2 -2
- package/bundled-skills/add-standard/SKILL.md +338 -0
- package/bundled-skills/adoption-a1-styles/SKILL.md +269 -0
- package/bundled-skills/adoption-a2-components/SKILL.md +291 -0
- package/bundled-skills/adoption-a3-forms/SKILL.md +260 -0
- package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +406 -0
- package/bundled-skills/anatomy-author/SKILL.md +1226 -0
- package/bundled-skills/design-auditor/SKILL.md +455 -0
- package/bundled-skills/implement-component/SKILL.md +333 -0
- package/bundled-skills/specify-component/SKILL.md +331 -0
- package/bundled-skills/specify-pattern/SKILL.md +292 -0
- package/dist/package.json +24 -20
- package/dist/pragma.conf.js +266 -34
- package/dist/pragma.conf.js.map +1 -1
- package/dist/src/bin.js +1 -1
- package/dist/src/bin.js.map +1 -1
- package/dist/src/capabilities/capabilities/hints.js +1 -1
- package/dist/src/capabilities/capabilities/hints.js.map +1 -1
- package/dist/src/capabilities/colophon/colophon.render.js +1 -1
- package/dist/src/capabilities/colophon/colophon.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/create/mount.js +4 -3
- package/dist/src/capabilities/create/mount.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/checkPackageRefs.js +60 -4
- package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkShellCompletions.js +8 -8
- package/dist/src/capabilities/doctor/checks/checkShellCompletions.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js +195 -0
- package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -0
- package/dist/src/capabilities/doctor/checks/index.js +6 -6
- 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 +194 -50
- package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
- package/dist/src/capabilities/doctor/doctor.render.js +43 -64
- package/dist/src/capabilities/doctor/doctor.render.js.map +1 -1
- package/dist/src/capabilities/doctor/doctor.verb.js +6 -2
- package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -1
- package/dist/src/capabilities/doctor/runChecks.js +14 -11
- package/dist/src/capabilities/doctor/runChecks.js.map +1 -1
- package/dist/src/capabilities/graph/query.render.js +2 -2
- package/dist/src/capabilities/graph/query.render.js.map +1 -1
- package/dist/src/capabilities/info/collectInfo.js +8 -2
- package/dist/src/capabilities/info/collectInfo.js.map +1 -1
- package/dist/src/capabilities/info/info.render.js +6 -2
- package/dist/src/capabilities/info/info.render.js.map +1 -1
- package/dist/src/capabilities/prompt/prompt.render.js +2 -2
- package/dist/src/capabilities/prompt/prompt.render.js.map +1 -1
- package/dist/src/capabilities/setup/buildPlan.js +43 -39
- package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
- package/dist/src/capabilities/setup/mode.js +2 -2
- package/dist/src/capabilities/setup/operations/index.js +4 -4
- 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 +156 -58
- package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupLsp.js +136 -38
- package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupMcp.js +101 -52
- package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupSkills.js +329 -49
- package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
- package/dist/src/capabilities/setup/plan.js.map +1 -1
- package/dist/src/capabilities/setup/plan.render.js +144 -66
- package/dist/src/capabilities/setup/plan.render.js.map +1 -1
- package/dist/src/capabilities/setup/setup.render.js +1 -1
- package/dist/src/capabilities/setup/setup.verb.js +165 -63
- package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
- package/dist/src/capabilities/setup/targets.js +122 -51
- package/dist/src/capabilities/setup/targets.js.map +1 -1
- package/dist/src/capabilities/shared/index.js +7 -6
- package/dist/src/capabilities/shared/index.js.map +1 -1
- package/dist/src/capabilities/shared/packageManager.js +300 -54
- package/dist/src/capabilities/shared/packageManager.js.map +1 -1
- package/dist/src/capabilities/skill/discover.js +97 -11
- package/dist/src/capabilities/skill/discover.js.map +1 -1
- package/dist/src/capabilities/skill/render.js +2 -2
- package/dist/src/capabilities/skill/render.js.map +1 -1
- package/dist/src/capabilities/sources/installSkills.js +145 -3
- package/dist/src/capabilities/sources/installSkills.js.map +1 -1
- package/dist/src/capabilities/sources/runUpdate.js +68 -2
- package/dist/src/capabilities/sources/runUpdate.js.map +1 -1
- package/dist/src/capabilities/sources/update.verb.js +3 -3
- package/dist/src/capabilities/sources/update.verb.js.map +1 -1
- package/dist/src/capabilities/upgrade/runUpgrade.js +43 -7
- package/dist/src/capabilities/upgrade/runUpgrade.js.map +1 -1
- package/dist/src/capabilities/upgrade/upgrade.render.js +10 -2
- package/dist/src/capabilities/upgrade/upgrade.render.js.map +1 -1
- package/dist/src/constants.js +13 -2
- package/dist/src/constants.js.map +1 -1
- package/dist/src/kernel/index.js +11 -2
- package/dist/src/kernel/index.js.map +1 -1
- package/dist/src/kernel/interactivity.js +68 -0
- package/dist/src/kernel/interactivity.js.map +1 -0
- package/dist/src/kernel/packs/renderPack.js +3 -3
- package/dist/src/kernel/packs/renderPack.js.map +1 -1
- package/dist/src/kernel/packs/resolveEntity.js +63 -12
- package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
- package/dist/src/kernel/packs/runBodies.js +57 -4
- package/dist/src/kernel/packs/runBodies.js.map +1 -1
- package/dist/src/kernel/packs/schema.js +54 -2
- package/dist/src/kernel/packs/schema.js.map +1 -1
- package/dist/src/kernel/packs/sparql/applyFilters.js +104 -5
- package/dist/src/kernel/packs/sparql/applyFilters.js.map +1 -1
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js +310 -43
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
- package/dist/src/kernel/packs/types.js.map +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
- package/dist/src/kernel/project/cli/dispatch.js +46 -24
- package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
- package/dist/src/kernel/project/cli/globalFlags.js +4 -1
- package/dist/src/kernel/project/cli/globalFlags.js.map +1 -1
- package/dist/src/kernel/project/cli/helpFormat.js +1 -2
- package/dist/src/kernel/project/cli/helpFormat.js.map +1 -1
- package/dist/src/kernel/project/cli/rootHelp.js +42 -9
- package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
- package/dist/src/kernel/project/mcp/registerVerb.js +16 -1
- package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
- package/dist/src/kernel/render/renderers.js +1 -1
- package/dist/src/kernel/render/style.js +29 -9
- package/dist/src/kernel/render/style.js.map +1 -1
- package/dist/src/kernel/render/vocabulary.js +174 -0
- package/dist/src/kernel/render/vocabulary.js.map +1 -0
- 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 +14 -9
- 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/refs/parseRef.js +17 -1
- package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
- package/dist/src/kernel/runtime/refs/resolve.js +9 -3
- 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 +24 -20
- package/pragma.conf.ts +272 -34
- package/src/bin.ts +8 -1
- package/src/capabilities/block.parity.test.ts +25 -11
- package/src/capabilities/block.shipped.exec.test.ts +282 -0
- package/src/capabilities/block.tierRank.exec.test.ts +300 -0
- package/src/capabilities/capabilities/hints.ts +1 -1
- package/src/capabilities/colophon/colophon.render.ts +1 -2
- package/src/capabilities/colophon/colophon.test.ts +2 -2
- package/src/capabilities/config/collectConfigShow.test.ts +1 -0
- 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 +5 -2
- package/src/capabilities/create/createSurface.generated.ts +23 -1
- package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
- package/src/capabilities/create/mount.ts +3 -3
- package/src/capabilities/distribution.parity.test.ts +4 -0
- package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
- package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
- package/src/capabilities/doctor/checks/checkPackageRefs.ts +69 -4
- package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +6 -4
- package/src/capabilities/doctor/checks/checkShellCompletions.ts +8 -8
- package/src/capabilities/doctor/checks/harnessInventory.test.ts +219 -0
- package/src/capabilities/doctor/checks/harnessInventory.ts +298 -0
- package/src/capabilities/doctor/checks/index.ts +6 -6
- 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 +229 -59
- package/src/capabilities/doctor/doctor.render.test.ts +108 -32
- package/src/capabilities/doctor/doctor.render.ts +56 -80
- package/src/capabilities/doctor/doctor.test.ts +247 -35
- package/src/capabilities/doctor/doctor.verb.ts +7 -2
- package/src/capabilities/doctor/runChecks.ts +14 -11
- package/src/capabilities/doctor/types.ts +15 -24
- package/src/capabilities/emptyRecovery.test.ts +10 -2
- package/src/capabilities/graph/query.render.ts +2 -2
- package/src/capabilities/info/collectInfo.ts +8 -1
- package/src/capabilities/info/info.render.ts +6 -2
- package/src/capabilities/info/info.test.ts +37 -1
- package/src/capabilities/info/types.ts +10 -2
- package/src/capabilities/lazy.test.ts +26 -2
- package/src/capabilities/listLookup.shipped.exec.test.ts +340 -0
- package/src/capabilities/prompt/prompt.render.ts +2 -2
- package/src/capabilities/renderVocabularySync.test.ts +82 -0
- package/src/capabilities/resources/resources.test.ts +38 -2
- package/src/capabilities/scopeTypeSync.test.ts +29 -15
- package/src/capabilities/setup/buildPlan.ts +45 -41
- package/src/capabilities/setup/index.ts +1 -1
- package/src/capabilities/setup/mode.ts +2 -2
- package/src/capabilities/setup/operations/index.ts +6 -1
- 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 +227 -84
- package/src/capabilities/setup/operations/setupLsp.ts +203 -38
- package/src/capabilities/setup/operations/setupMcp.ts +139 -62
- package/src/capabilities/setup/operations/setupSkills.ts +399 -60
- package/src/capabilities/setup/plan.render.ts +171 -76
- package/src/capabilities/setup/plan.ts +17 -12
- package/src/capabilities/setup/setup.mcp.correctness.test.ts +9 -9
- package/src/capabilities/setup/setup.render.test.ts +50 -36
- package/src/capabilities/setup/setup.render.ts +1 -1
- package/src/capabilities/setup/setup.test.ts +1126 -50
- package/src/capabilities/setup/setup.verb.ts +174 -68
- package/src/capabilities/setup/targets.ts +138 -60
- package/src/capabilities/setup/types.ts +10 -18
- package/src/capabilities/shared/index.ts +18 -6
- package/src/capabilities/shared/packageManager.test.ts +421 -6
- package/src/capabilities/shared/packageManager.ts +394 -59
- package/src/capabilities/skill/bundledSkills.test.ts +347 -0
- package/src/capabilities/skill/discover.ts +100 -11
- package/src/capabilities/skill/render.ts +2 -2
- package/src/capabilities/skill/skill.test.ts +17 -4
- package/src/capabilities/sources/installSkills.ts +181 -3
- package/src/capabilities/sources/runUpdate.ts +78 -2
- package/src/capabilities/sources/sources.test.ts +228 -0
- package/src/capabilities/sources/update.verb.ts +3 -3
- package/src/capabilities/surface.test.ts +14 -6
- package/src/capabilities/upgrade/runUpgrade.ts +45 -6
- package/src/capabilities/upgrade/types.ts +10 -2
- package/src/capabilities/upgrade/upgrade.render.ts +9 -2
- package/src/capabilities/upgrade/upgrade.test.ts +72 -0
- package/src/constants.ts +14 -1
- package/src/identity.test.ts +18 -5
- package/src/kernel/completion/entitySource.test.ts +15 -2
- package/src/kernel/completion/safety.test.ts +16 -9
- package/src/kernel/config/defaults.test.ts +19 -3
- package/src/kernel/config/readConfig.test.ts +1 -1
- package/src/kernel/index.ts +11 -2
- package/src/kernel/interactivity.test.ts +76 -0
- package/src/kernel/interactivity.ts +69 -0
- package/src/kernel/packs/compile.test.ts +92 -0
- package/src/kernel/packs/renderPack.ts +3 -3
- package/src/kernel/packs/resolveEntity.test.ts +75 -14
- package/src/kernel/packs/resolveEntity.ts +88 -24
- package/src/kernel/packs/runBodies.ts +84 -19
- package/src/kernel/packs/schema.ts +60 -4
- package/src/kernel/packs/sparql/applyFilters.ts +125 -4
- package/src/kernel/packs/sparql/buildLookupQuery.test.ts +290 -0
- package/src/kernel/packs/sparql/buildLookupQuery.ts +327 -45
- package/src/kernel/packs/toolDescription.test.ts +35 -1
- package/src/kernel/packs/types.ts +125 -2
- package/src/kernel/project/cli/buildProgram.test.ts +73 -2
- package/src/kernel/project/cli/buildProgram.ts +8 -1
- package/src/kernel/project/cli/dispatch.test.ts +5 -4
- package/src/kernel/project/cli/dispatch.ts +48 -25
- package/src/kernel/project/cli/globalFlags.ts +4 -1
- package/src/kernel/project/cli/helpFormat.ts +1 -2
- package/src/kernel/project/cli/rootHelp.ts +44 -8
- package/src/kernel/project/mcp/registerVerb.ts +20 -1
- package/src/kernel/render/prefixes.test.ts +2 -0
- package/src/kernel/render/renderers.ts +1 -1
- package/src/kernel/render/style.ts +32 -9
- package/src/kernel/render/vocabulary.ts +276 -0
- 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 +14 -12
- package/src/kernel/runtime/loadSession.ts +3 -1
- package/src/kernel/runtime/probe.ts +1 -1
- 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 +9 -3
- package/src/kernel/runtime/types.ts +17 -0
- package/src/kernel/spec/emitReference.ts +1 -1
- package/src/kernel/spec/types.ts +35 -6
- package/src/testing/PARITY_GAPS.ts +9 -3
- package/src/testing/behavioral/docExamples.test.ts +4 -2
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +110 -7
- package/src/testing/behavioral/journeys.firstInstall.test.ts +7 -2
- package/src/testing/behavioral/journeys.packageStories.test.ts +6 -3
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +26 -1
- package/src/testing/eval/cases/readNouns.ts +245 -9
- package/src/testing/fixtures/blockGraph.ts +28 -13
- package/src/testing/fixtures/graph/canonical.ts +95 -12
- package/src/testing/helpers/runCli.test.ts +44 -0
- package/src/testing/helpers/runCli.ts +27 -3
- package/dist/src/capabilities/shared/bands.js +0 -16
- package/dist/src/capabilities/shared/bands.js.map +0 -1
- package/src/capabilities/shared/bands.ts +0 -16
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `block` story's declared vocabulary, EXECUTED against the shipped pack
|
|
3
|
+
* (PROTECTED).
|
|
4
|
+
*
|
|
5
|
+
* `block.parity.test.ts` asserts the story's semantics over a FIXTURE graph the
|
|
6
|
+
* repository writes. That is a real check, but it can only ever prove the story
|
|
7
|
+
* agrees with the fixture — and when the two drifted apart, that is exactly
|
|
8
|
+
* what happened: the story read `ds:whenToUse`/`ds:whenNotToUse`, the fixture
|
|
9
|
+
* obligingly declared them, the suite stayed green, and every real install
|
|
10
|
+
* rendered `block lookup` with no usage narrative at all, because the ontology
|
|
11
|
+
* had long since retired both terms in favour of a single `ds:usage`. Nothing
|
|
12
|
+
* in the pipeline treats that as an error: the GraphQL projection omits a name
|
|
13
|
+
* it cannot express the way OPTIONAL omits an unbound variable, and the
|
|
14
|
+
* renderer skips a section with no value. Silence all the way down.
|
|
15
|
+
*
|
|
16
|
+
* So this file boots the SHIPPED graph and asserts what a fixture structurally
|
|
17
|
+
* cannot. It distinguishes two failures that look identical in the output but
|
|
18
|
+
* are not the same defect:
|
|
19
|
+
*
|
|
20
|
+
* - a declared property the ontology does not define is a STORY bug — the CLI
|
|
21
|
+
* is asking for a name the schema can never answer, and no upstream content
|
|
22
|
+
* edit can ever make it render. That is this defect, and it is forbidden here.
|
|
23
|
+
* - a declared property the ontology defines but no instance asserts yet is a
|
|
24
|
+
* CONTENT gap upstream (`ds:figmaLink` is one today). The story is correct and
|
|
25
|
+
* degrades gracefully; that is not this suite's business.
|
|
26
|
+
*
|
|
27
|
+
* The `graph/examples.exec.test.ts` precedent: an example is a promise to a
|
|
28
|
+
* stranger, and only running it against what actually ships reads it as one. A
|
|
29
|
+
* declared property is the same kind of promise.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
33
|
+
import {
|
|
34
|
+
isNestedExpand,
|
|
35
|
+
type PackExpandSelect,
|
|
36
|
+
} from "../kernel/packs/types.js";
|
|
37
|
+
import { verbKey } from "../kernel/packs/uniqueness.js";
|
|
38
|
+
import { bootRuntime } from "../kernel/runtime/boot.js";
|
|
39
|
+
import type { PragmaRuntime } from "../kernel/runtime/types.js";
|
|
40
|
+
import type { VerbSpec } from "../kernel/spec/types.js";
|
|
41
|
+
import { TEST_FLAGS } from "../testing/helpers/projectCli.js";
|
|
42
|
+
import { declaredStories, storyModules } from "./distribution.js";
|
|
43
|
+
|
|
44
|
+
const story = declaredStories.get("block");
|
|
45
|
+
if (!story?.lookup) {
|
|
46
|
+
throw new Error('pragma.conf.ts declares no "block" lookup');
|
|
47
|
+
}
|
|
48
|
+
const blockLookup = story.lookup;
|
|
49
|
+
|
|
50
|
+
const blockModule = storyModules.get("block");
|
|
51
|
+
if (!blockModule) {
|
|
52
|
+
throw new Error('pragma.conf.ts declares no story for "block"');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const lookupVerb = blockModule.verbs.find(
|
|
56
|
+
(v) => verbKey(v.path) === "block lookup",
|
|
57
|
+
) as VerbSpec;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The block classes the lookup addresses, read from the story rather than
|
|
61
|
+
* retyped — a class added there widens these checks with it.
|
|
62
|
+
*/
|
|
63
|
+
const TYPE_VALUES = (blockLookup.types ?? []).join(" ");
|
|
64
|
+
|
|
65
|
+
/** One graph term the story names, paired with the site that declares it. */
|
|
66
|
+
interface DeclaredTerm {
|
|
67
|
+
readonly what: string;
|
|
68
|
+
readonly property: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Every term an `expand.select` names, to whatever depth it nests.
|
|
73
|
+
*
|
|
74
|
+
* WALKED, not listed. A hand-maintained enumeration would carry this file's own
|
|
75
|
+
* defect one level down: `ds:hasModifier`, `ds:propertyType` and `ds:optional`
|
|
76
|
+
* could be retired exactly the way `ds:whenToUse` was, and `block lookup` would
|
|
77
|
+
* quietly lose its modifier values and its property table while a list nobody
|
|
78
|
+
* thought to update stayed green. Recursing over the declaration instead means
|
|
79
|
+
* a term added to any `select` is covered the moment it is declared, by the
|
|
80
|
+
* author who declares it and without their help.
|
|
81
|
+
*/
|
|
82
|
+
function selectedTerms(
|
|
83
|
+
site: string,
|
|
84
|
+
select: readonly PackExpandSelect[],
|
|
85
|
+
): readonly DeclaredTerm[] {
|
|
86
|
+
return select.flatMap((entry) =>
|
|
87
|
+
isNestedExpand(entry)
|
|
88
|
+
? [
|
|
89
|
+
{
|
|
90
|
+
what: `${site} → nested expand "${entry.name}"`,
|
|
91
|
+
property: entry.relation,
|
|
92
|
+
},
|
|
93
|
+
...selectedTerms(`${site} → "${entry.name}"`, entry.select),
|
|
94
|
+
]
|
|
95
|
+
: [
|
|
96
|
+
{
|
|
97
|
+
what: `${site} → field "${entry.name}"`,
|
|
98
|
+
property: entry.property,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Every property the story names in the graph, read from the declaration.
|
|
106
|
+
*
|
|
107
|
+
* The identity property, fields, sections, expand relations and everything
|
|
108
|
+
* selected beneath them all name graph vocabulary and all fail the same silent
|
|
109
|
+
* way, so all of them are held to the same standard. `by` is included because a
|
|
110
|
+
* lookup whose identity property the ontology has retired resolves NOTHING —
|
|
111
|
+
* the loudest form of the same defect, and the one no rendering test would
|
|
112
|
+
* reach, since there would be no entity to render.
|
|
113
|
+
*
|
|
114
|
+
* One case per declaration SITE rather than per distinct term: `ds:name` is
|
|
115
|
+
* named at six of them, and a failure that names the site it was declared at
|
|
116
|
+
* is the one a reader can act on.
|
|
117
|
+
*/
|
|
118
|
+
const DECLARED_PROPERTIES: readonly DeclaredTerm[] = [
|
|
119
|
+
{ what: "lookup `by`", property: blockLookup.by },
|
|
120
|
+
...(blockLookup.fields ?? []).map((f) => ({
|
|
121
|
+
what: `field "${f.name}"`,
|
|
122
|
+
property: f.property,
|
|
123
|
+
})),
|
|
124
|
+
...(blockLookup.sections ?? []).map((s) => ({
|
|
125
|
+
what: `section "${s.name}"`,
|
|
126
|
+
property: s.property,
|
|
127
|
+
})),
|
|
128
|
+
...(blockLookup.expand ?? []).flatMap((e) => [
|
|
129
|
+
{ what: `expand "${e.name}"`, property: e.relation },
|
|
130
|
+
...selectedTerms(`expand "${e.name}"`, e.select),
|
|
131
|
+
]),
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
/** The IRI of the block whose usage narrative is asserted on below. */
|
|
135
|
+
const BUTTON = "https://ds.canonical.com/global.component.button";
|
|
136
|
+
|
|
137
|
+
// ONE store for the file — booting the shipped pack is the expensive part, and
|
|
138
|
+
// this package already contends under parallel runs.
|
|
139
|
+
let rt: PragmaRuntime;
|
|
140
|
+
beforeAll(async () => {
|
|
141
|
+
rt = bootRuntime(TEST_FLAGS);
|
|
142
|
+
// Load the shipped store HERE, once. `it.each` splits the vocabulary check
|
|
143
|
+
// into one small case per declared property, and whichever ran first would
|
|
144
|
+
// otherwise pay the whole lazy-load cost and time out under contention.
|
|
145
|
+
await rt.store.get();
|
|
146
|
+
}, 60_000);
|
|
147
|
+
afterAll(async () => {
|
|
148
|
+
(await rt.store.get()).store.dispose();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
/** One scalar from the shipped graph, queried directly. */
|
|
152
|
+
async function scalar(query: string): Promise<string | undefined> {
|
|
153
|
+
const result = await rt.query.sparql(query);
|
|
154
|
+
if (result.type !== "select") return undefined;
|
|
155
|
+
const row = result.bindings[0];
|
|
156
|
+
return row === undefined ? undefined : Object.values(row)[0];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The `llm` rendering of `block lookup <value>`. */
|
|
160
|
+
async function renderLookup(value: string): Promise<string> {
|
|
161
|
+
const out = (await lookupVerb.run({ name: [value] }, rt)) as never;
|
|
162
|
+
return lookupVerb.output.formatters.llm(out);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
describe("the `block` story declares vocabulary the shipped ontology defines (PROTECTED)", () => {
|
|
166
|
+
it("declares at least one property to check", () => {
|
|
167
|
+
expect(DECLARED_PROPERTIES.length).toBeGreaterThan(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it.each(
|
|
171
|
+
DECLARED_PROPERTIES,
|
|
172
|
+
)("$what reads $property, which the shipped ontology defines", async ({
|
|
173
|
+
what,
|
|
174
|
+
property,
|
|
175
|
+
}) => {
|
|
176
|
+
const result = await rt.query.sparql(
|
|
177
|
+
[
|
|
178
|
+
"ASK {",
|
|
179
|
+
` ${property} a ?kind .`,
|
|
180
|
+
" VALUES ?kind { owl:DatatypeProperty owl:ObjectProperty }",
|
|
181
|
+
"}",
|
|
182
|
+
].join("\n"),
|
|
183
|
+
);
|
|
184
|
+
expect(result.type).toBe("ask");
|
|
185
|
+
expect(
|
|
186
|
+
result.type === "ask" ? result.result : false,
|
|
187
|
+
`${what} reads ${property}, which the shipped ontology does not define — it can never render, for any block, on any install`,
|
|
188
|
+
).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("the shipped graph carries the usage narrative on every block (PROTECTED)", () => {
|
|
193
|
+
it("asserts ds:usage on every block, and neither retired term on any", async () => {
|
|
194
|
+
const blocks = await scalar(
|
|
195
|
+
[
|
|
196
|
+
"SELECT (COUNT(DISTINCT ?b) AS ?n) WHERE {",
|
|
197
|
+
` VALUES ?class { ${TYPE_VALUES} }`,
|
|
198
|
+
" ?b a ?class .",
|
|
199
|
+
"}",
|
|
200
|
+
].join("\n"),
|
|
201
|
+
);
|
|
202
|
+
const withUsage = await scalar(
|
|
203
|
+
[
|
|
204
|
+
"SELECT (COUNT(DISTINCT ?b) AS ?n) WHERE {",
|
|
205
|
+
` VALUES ?class { ${TYPE_VALUES} }`,
|
|
206
|
+
" ?b a ?class ; ds:usage ?u .",
|
|
207
|
+
"}",
|
|
208
|
+
].join("\n"),
|
|
209
|
+
);
|
|
210
|
+
expect(Number(blocks)).toBeGreaterThan(0);
|
|
211
|
+
expect(withUsage).toBe(blocks);
|
|
212
|
+
|
|
213
|
+
// The retirement itself, pinned: reading these again would reintroduce the
|
|
214
|
+
// defect, silently, exactly as before.
|
|
215
|
+
for (const retired of ["ds:whenToUse", "ds:whenNotToUse"]) {
|
|
216
|
+
const result = await rt.query.sparql(`ASK { ?s ${retired} ?o }`);
|
|
217
|
+
expect(
|
|
218
|
+
result.type === "ask" ? result.result : true,
|
|
219
|
+
`${retired} is asserted somewhere in the shipped graph`,
|
|
220
|
+
).toBe(false);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("block lookup renders the usage narrative from the shipped graph (PROTECTED)", () => {
|
|
226
|
+
it("renders a Usage section carrying the graph's own literal", async () => {
|
|
227
|
+
const usage = await scalar(`SELECT ?u WHERE { <${BUTTON}> ds:usage ?u }`);
|
|
228
|
+
expect(usage?.trim().length ?? 0).toBeGreaterThan(0);
|
|
229
|
+
|
|
230
|
+
const llm = await renderLookup(BUTTON);
|
|
231
|
+
expect(llm).toContain("### Usage");
|
|
232
|
+
// The narrative itself, not just its heading: the first prose line of the
|
|
233
|
+
// literal is read FROM the graph rather than retyped here, so editing the
|
|
234
|
+
// content upstream cannot leave this asserting yesterday's copy.
|
|
235
|
+
const firstProseLine = (usage ?? "")
|
|
236
|
+
.split("\n")
|
|
237
|
+
.map((line) => line.trim())
|
|
238
|
+
.find((line) => line.length > 0 && !line.startsWith("#")) as string;
|
|
239
|
+
expect(llm).toContain(firstProseLine);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("nests the literal's own headings UNDER the Usage heading", async () => {
|
|
243
|
+
// The live literals carry their own `### When to use` / `### When not to
|
|
244
|
+
// use` sub-sections. Left at `###` they would be siblings of the section
|
|
245
|
+
// heading, which reads as an empty Usage section; the renderer demotes them.
|
|
246
|
+
const heading = await scalar(
|
|
247
|
+
[
|
|
248
|
+
"SELECT ?h WHERE {",
|
|
249
|
+
` <${BUTTON}> ds:usage ?u .`,
|
|
250
|
+
' BIND(REPLACE(STR(?u), "(?s)^.*?\\\\n### ([^\\\\n]+)\\\\n.*$", "$1") AS ?h)',
|
|
251
|
+
"}",
|
|
252
|
+
].join("\n"),
|
|
253
|
+
);
|
|
254
|
+
expect(
|
|
255
|
+
heading,
|
|
256
|
+
"the Button usage literal carries no `### ` heading",
|
|
257
|
+
).toBeDefined();
|
|
258
|
+
|
|
259
|
+
const llm = await renderLookup(BUTTON);
|
|
260
|
+
expect(llm).toContain(`#### ${heading}`);
|
|
261
|
+
expect(llm).not.toContain(`\n### ${heading}\n`);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("prints no Usage heading for a block whose usage literal is empty", async () => {
|
|
265
|
+
// Roughly half the shipped blocks assert `ds:usage ""`. An empty literal
|
|
266
|
+
// must render as no section at all, never as a heading with nothing below.
|
|
267
|
+
const uri = await scalar(
|
|
268
|
+
[
|
|
269
|
+
"SELECT ?b WHERE {",
|
|
270
|
+
` VALUES ?class { ${TYPE_VALUES} }`,
|
|
271
|
+
" ?b a ?class ; ds:usage ?u .",
|
|
272
|
+
' FILTER(STR(?u) = "")',
|
|
273
|
+
"}",
|
|
274
|
+
"ORDER BY ?b",
|
|
275
|
+
"LIMIT 1",
|
|
276
|
+
].join("\n"),
|
|
277
|
+
);
|
|
278
|
+
expect(uri, "no shipped block carries an empty ds:usage").toBeDefined();
|
|
279
|
+
|
|
280
|
+
expect(await renderLookup(uri as string)).not.toContain("### Usage");
|
|
281
|
+
});
|
|
282
|
+
});
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which block a SHARED name means, EXECUTED against the shipped pack
|
|
3
|
+
* (PROTECTED).
|
|
4
|
+
*
|
|
5
|
+
* 25 of the design system's block names are carried by two or three blocks
|
|
6
|
+
* apiece, across tiers. `block lookup` used to answer each of them with whichever
|
|
7
|
+
* IRI sorted first and say nothing about the rest, so `button` returned
|
|
8
|
+
* Launchpad's Button — `apps_launchpad…` sorts before `global…` — and an agent
|
|
9
|
+
* had no way to learn that the block everybody means also existed.
|
|
10
|
+
*
|
|
11
|
+
* Two properties, both asserted here: EVERY block the name reaches comes back,
|
|
12
|
+
* and they come back RANKED. Either alone leaves a caller stuck — a better
|
|
13
|
+
* arbitrary pick is still arbitrary, and an unordered list of two makes the
|
|
14
|
+
* reader do the ranking the graph already knows how to do.
|
|
15
|
+
*
|
|
16
|
+
* This suite is deliberately NOT fixture-backed, and that is the whole point of
|
|
17
|
+
* it. The two nearest specimens in this package show why: `journeys.livePack`
|
|
18
|
+
* is NAMED for ordering determinism and its fixture contains one Button, and
|
|
19
|
+
* the ambiguity fixture in `blockGraph.ts` pits two tiers that do not outrank
|
|
20
|
+
* each other, so neither graph can be wrong about the ranking in the way the
|
|
21
|
+
* live graph was. A fixture written alongside a ranking rule agrees with the
|
|
22
|
+
* rule by construction. The shipped pack does not.
|
|
23
|
+
*
|
|
24
|
+
* So every case here READS the collisions out of the store it is judging. There
|
|
25
|
+
* is no list of block names in this file to fall out of date — add a tier
|
|
26
|
+
* upstream, rename one, retire a duplicate, and these assertions follow.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
30
|
+
import { verbKey } from "../kernel/packs/uniqueness.js";
|
|
31
|
+
import { bootRuntime } from "../kernel/runtime/boot.js";
|
|
32
|
+
import type { PragmaRuntime } from "../kernel/runtime/types.js";
|
|
33
|
+
import type { VerbSpec } from "../kernel/spec/types.js";
|
|
34
|
+
import { TEST_FLAGS } from "../testing/helpers/projectCli.js";
|
|
35
|
+
import { declaredStories, storyModules } from "./distribution.js";
|
|
36
|
+
|
|
37
|
+
const story = declaredStories.get("block");
|
|
38
|
+
if (!story?.lookup)
|
|
39
|
+
throw new Error('pragma.conf.ts declares no "block" lookup');
|
|
40
|
+
const blockLookup = story.lookup;
|
|
41
|
+
|
|
42
|
+
const blockModule = storyModules.get("block");
|
|
43
|
+
if (!blockModule) throw new Error('pragma.conf.ts declares no "block" story');
|
|
44
|
+
|
|
45
|
+
const verbFor = (verb: string): VerbSpec =>
|
|
46
|
+
blockModule.verbs.find(
|
|
47
|
+
(v) => verbKey(v.path) === `block ${verb}`,
|
|
48
|
+
) as VerbSpec;
|
|
49
|
+
const lookupVerb = verbFor("lookup");
|
|
50
|
+
const listVerb = verbFor("list");
|
|
51
|
+
|
|
52
|
+
/** The block classes the story addresses, read from the story rather than retyped. */
|
|
53
|
+
const TYPE_VALUES = (blockLookup.types ?? []).join(" ");
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The scope declaration under test, read from the config it lives in.
|
|
57
|
+
*
|
|
58
|
+
* Reading `falloff` here rather than writing `0.2` is what keeps this suite a
|
|
59
|
+
* check on the RULE instead of on one tuning of it: change the number in
|
|
60
|
+
* `pragma.conf.ts` and these cases still assert that a shallower tier outranks
|
|
61
|
+
* a deeper one, which is the judgement that was signed off.
|
|
62
|
+
*/
|
|
63
|
+
const scope = blockLookup.scopeWeight;
|
|
64
|
+
if (!scope) throw new Error('the "block" lookup declares no scopeWeight');
|
|
65
|
+
|
|
66
|
+
let rt: PragmaRuntime;
|
|
67
|
+
beforeAll(async () => {
|
|
68
|
+
rt = bootRuntime(TEST_FLAGS);
|
|
69
|
+
await rt.store.get();
|
|
70
|
+
}, 60_000);
|
|
71
|
+
afterAll(async () => {
|
|
72
|
+
(await rt.store.get()).store.dispose();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/** Rows from the shipped store, as plain records. */
|
|
76
|
+
async function rows(query: string): Promise<Record<string, string>[]> {
|
|
77
|
+
const result = await rt.query.sparql(query);
|
|
78
|
+
return result.type === "select"
|
|
79
|
+
? (result.bindings as Record<string, string>[])
|
|
80
|
+
: [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One `block lookup <name>`: the IRI it answered with, and the ones it did not. */
|
|
84
|
+
async function lookup(
|
|
85
|
+
name: string,
|
|
86
|
+
): Promise<{ chosen: string; others: string[] }> {
|
|
87
|
+
const out = (await lookupVerb.run({ name: [name] }, rt)) as {
|
|
88
|
+
results: { uri?: string }[];
|
|
89
|
+
};
|
|
90
|
+
// Every entity the name reached, best first. `chosen` is simply the head —
|
|
91
|
+
// the ranking orders the answer now, it does not select it.
|
|
92
|
+
const [chosen, ...others] = out.results.map((entity) => String(entity.uri));
|
|
93
|
+
return { chosen: String(chosen), others };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Every block name the shipped graph gives to more than one block, with each
|
|
98
|
+
* block's IRI, its tier's declared name and whether it is a subcomponent.
|
|
99
|
+
*
|
|
100
|
+
* Read with the SAME `LCASE` the resolve filters by, so a pair that collides
|
|
101
|
+
* only for a lookup (`Button` vs `button`) is in this set too.
|
|
102
|
+
*/
|
|
103
|
+
async function collisions(): Promise<
|
|
104
|
+
Map<string, { uri: string; tier: string; part: boolean }[]>
|
|
105
|
+
> {
|
|
106
|
+
const found = await rows(
|
|
107
|
+
[
|
|
108
|
+
"SELECT ?uri ?key ?tierName ?part WHERE {",
|
|
109
|
+
` VALUES ?class { ${TYPE_VALUES} }`,
|
|
110
|
+
" ?uri a ?class .",
|
|
111
|
+
` ?uri ${blockLookup.by} ?name .`,
|
|
112
|
+
` OPTIONAL { ?uri ${scope.via} ?tier . ?tier ${scope.by} ?tn . }`,
|
|
113
|
+
" BIND(LCASE(STR(?name)) AS ?key)",
|
|
114
|
+
' BIND(COALESCE(?tn, "") AS ?tierName)',
|
|
115
|
+
" BIND(EXISTS { ?uri a ds:Subcomponent } AS ?part)",
|
|
116
|
+
"}",
|
|
117
|
+
].join("\n"),
|
|
118
|
+
);
|
|
119
|
+
const byName = new Map<
|
|
120
|
+
string,
|
|
121
|
+
{ uri: string; tier: string; part: boolean }[]
|
|
122
|
+
>();
|
|
123
|
+
for (const row of found) {
|
|
124
|
+
const key = String(row.key);
|
|
125
|
+
const entry = {
|
|
126
|
+
uri: String(row.uri),
|
|
127
|
+
tier: String(row.tierName ?? ""),
|
|
128
|
+
part: String(row.part) === "true",
|
|
129
|
+
};
|
|
130
|
+
const seen = byName.get(key);
|
|
131
|
+
if (seen) {
|
|
132
|
+
if (!seen.some((e) => e.uri === entry.uri)) seen.push(entry);
|
|
133
|
+
} else byName.set(key, [entry]);
|
|
134
|
+
}
|
|
135
|
+
return new Map([...byName].filter(([, entries]) => entries.length > 1));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** A tier's depth: the separator count in its DECLARED name, +1. */
|
|
139
|
+
const depthOf = (tierName: string): number =>
|
|
140
|
+
tierName === "" ? 1 : tierName.split("/").length;
|
|
141
|
+
|
|
142
|
+
describe("a shared block name reaches every block that carries it (PROTECTED)", () => {
|
|
143
|
+
it("finds shared names in the shipped graph at all", async () => {
|
|
144
|
+
// The guard on every other case in this file: if the graph ever stops
|
|
145
|
+
// sharing names, these assertions become vacuous and must be told to go.
|
|
146
|
+
expect((await collisions()).size).toBeGreaterThan(1);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("answers `button` with the GLOBAL Button, and names the other", async () => {
|
|
150
|
+
// The reported defect, exactly as reported. Two components of equal kind,
|
|
151
|
+
// separated by their tiers alone: Global is depth 1, Apps/Launchpad depth 2.
|
|
152
|
+
const out = await lookup("button");
|
|
153
|
+
expect(out.chosen).toBe("https://ds.canonical.com/global.component.button");
|
|
154
|
+
expect(out.others).toEqual([
|
|
155
|
+
"https://ds.canonical.com/apps_launchpad.component.button",
|
|
156
|
+
]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("answers `TextInput` with the LAUNCHPAD one — a whole block beats a part", async () => {
|
|
160
|
+
// The case that stops the plausible wrong fix. A rule of "global wins", or
|
|
161
|
+
// a tier tiebreak ABOVE the type weight, would answer with the global
|
|
162
|
+
// SUBcomponent here. As a product it does not: 0.6 × 1 < 1 × 0.8. The
|
|
163
|
+
// editorial rule that a subcomponent is a PART of a block, never the block
|
|
164
|
+
// someone means, survives the tier ranking being added on top of it.
|
|
165
|
+
const out = await lookup("TextInput");
|
|
166
|
+
expect(out.chosen).toBe(
|
|
167
|
+
"https://ds.canonical.com/apps_launchpad.component.text_input",
|
|
168
|
+
);
|
|
169
|
+
expect(out.others).toEqual([
|
|
170
|
+
"https://ds.canonical.com/global.subcomponent.text_input",
|
|
171
|
+
]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("answers a name shared by two EQUALLY deep tiers deterministically", async () => {
|
|
175
|
+
// `ThemeSwitcher` is Apps/Anbox against Apps/WorkplaceEngineering — same
|
|
176
|
+
// depth, so the ranking has nothing to say and the total order's final key
|
|
177
|
+
// decides. This is the residual the ranking does NOT resolve, asserted as
|
|
178
|
+
// such: the claim is not that the right one wins, it is that the same one
|
|
179
|
+
// wins every time and the other is never left unmentioned.
|
|
180
|
+
const runs = await Promise.all([
|
|
181
|
+
lookup("ThemeSwitcher"),
|
|
182
|
+
lookup("themeswitcher"),
|
|
183
|
+
lookup("ThemeSwitcher"),
|
|
184
|
+
]);
|
|
185
|
+
for (const out of runs) {
|
|
186
|
+
expect(out.chosen).toBe(runs[0]?.chosen);
|
|
187
|
+
expect(out.others).toEqual(runs[0]?.others);
|
|
188
|
+
expect(out.others).toHaveLength(1);
|
|
189
|
+
// Equal rank, so the order's final key decides — the same key, the same
|
|
190
|
+
// way, as everywhere else in it.
|
|
191
|
+
expect([out.chosen, ...out.others].sort()).toEqual([
|
|
192
|
+
out.chosen,
|
|
193
|
+
...out.others,
|
|
194
|
+
]);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("never leaves a block a shared name reaches unmentioned", async () => {
|
|
199
|
+
// Swept over the whole collision set, not a chosen few: the silent discard
|
|
200
|
+
// is the defect, and it was invisible precisely because it looked like a
|
|
201
|
+
// normal answer. Every block the name reaches is either the answer or named
|
|
202
|
+
// beside it — and named as an IRI, the one address that reaches it.
|
|
203
|
+
for (const [name, entries] of await collisions()) {
|
|
204
|
+
const out = await lookup(name);
|
|
205
|
+
expect(
|
|
206
|
+
[out.chosen, ...out.others].sort(),
|
|
207
|
+
`block lookup ${name}`,
|
|
208
|
+
).toEqual(entries.map((entry) => entry.uri).sort());
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("ranks the winner's tier no worse than every loser's", async () => {
|
|
213
|
+
// The invariant, COMPUTED from the pack rather than listed. It holds for
|
|
214
|
+
// every shared name at once and cannot be satisfied by hard-coding an
|
|
215
|
+
// answer, so it survives a new tier, a renamed one, and a new collision.
|
|
216
|
+
//
|
|
217
|
+
// Stated over WHOLE BLOCKS only. A subcomponent is deliberately allowed to
|
|
218
|
+
// lose to a shallower-ranked component AND to a deeper-ranked one — that is
|
|
219
|
+
// the product, and `TextInput` above is the case that pins it.
|
|
220
|
+
for (const [name, entries] of await collisions()) {
|
|
221
|
+
const blocks = entries.filter((entry) => !entry.part);
|
|
222
|
+
if (blocks.length < 2) continue;
|
|
223
|
+
const { chosen } = await lookup(name);
|
|
224
|
+
const winner = blocks.find((entry) => entry.uri === chosen);
|
|
225
|
+
if (!winner) continue;
|
|
226
|
+
for (const loser of blocks) {
|
|
227
|
+
expect(
|
|
228
|
+
depthOf(winner.tier),
|
|
229
|
+
`block lookup ${name} led with ${winner.uri} (${winner.tier}) over ${loser.uri} (${loser.tier})`,
|
|
230
|
+
).toBeLessThanOrEqual(depthOf(loser.tier));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("`block list` orders its ties totally (PROTECTED)", () => {
|
|
237
|
+
/** The entity IRIs of one `block list` run, in the order it printed them. */
|
|
238
|
+
async function listOrder(): Promise<string[]> {
|
|
239
|
+
const data = (await listVerb.run({}, rt)) as { uri?: string }[];
|
|
240
|
+
return data.map((row) => String(row.uri));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
it("repeats the same order across runs", async () => {
|
|
244
|
+
// The half that looks like the whole defect and is not: across runs the
|
|
245
|
+
// store's scan order can change (a repack, another machine).
|
|
246
|
+
const [first, second] = await Promise.all([listOrder(), listOrder()]);
|
|
247
|
+
expect(first).toEqual(second);
|
|
248
|
+
expect(first.length).toBeGreaterThan(1);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("orders EVERY tie the same way, within one run", async () => {
|
|
252
|
+
// The half that was actually broken. In ONE output `Button` came
|
|
253
|
+
// launchpad-first while `CheckboxInput` came global-first — not flakiness
|
|
254
|
+
// between runs, an inconsistency inside a single answer, because SPARQL
|
|
255
|
+
// says nothing about tied rows and the query's ORDER BY ended at `?name`.
|
|
256
|
+
//
|
|
257
|
+
// Asserted as a RULE over every shared name rather than on the two that
|
|
258
|
+
// happened to disagree: shallower tier leads, and equal depths fall back to
|
|
259
|
+
// a key that cannot tie.
|
|
260
|
+
const order = await listOrder();
|
|
261
|
+
const position = new Map(order.map((uri, index) => [uri, index]));
|
|
262
|
+
const shared = await collisions();
|
|
263
|
+
expect(shared.size).toBeGreaterThan(1);
|
|
264
|
+
for (const [name, entries] of shared) {
|
|
265
|
+
const group = [...entries].sort(
|
|
266
|
+
(a, b) => (position.get(a.uri) ?? -1) - (position.get(b.uri) ?? -1),
|
|
267
|
+
);
|
|
268
|
+
expect(
|
|
269
|
+
group.map((entry) => position.get(entry.uri)),
|
|
270
|
+
`block list rows for ${name}`,
|
|
271
|
+
).not.toContain(undefined);
|
|
272
|
+
for (const [index, entry] of group.entries()) {
|
|
273
|
+
const next = group[index + 1];
|
|
274
|
+
if (!next) continue;
|
|
275
|
+
const keys = [
|
|
276
|
+
[depthOf(entry.tier), entry.uri],
|
|
277
|
+
[depthOf(next.tier), next.uri],
|
|
278
|
+
] as const;
|
|
279
|
+
expect(
|
|
280
|
+
keys[0][0] < keys[1][0] ||
|
|
281
|
+
(keys[0][0] === keys[1][0] && keys[0][1] < keys[1][1]),
|
|
282
|
+
`block list printed ${entry.uri} (${entry.tier}) before ${next.uri} (${next.tier}) for "${name}"`,
|
|
283
|
+
).toBe(true);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("keeps every shared name's rows adjacent, one per block", async () => {
|
|
289
|
+
// The ordering above would also be satisfied by a list that lost a row, so
|
|
290
|
+
// the population is checked with it: the tie-break decides the order of the
|
|
291
|
+
// rows, never how many there are.
|
|
292
|
+
const order = await listOrder();
|
|
293
|
+
for (const [name, entries] of await collisions()) {
|
|
294
|
+
const present = entries.filter((entry) => order.includes(entry.uri));
|
|
295
|
+
expect(present, `block list rows for ${name}`).toHaveLength(
|
|
296
|
+
entries.length,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -213,7 +213,7 @@ export const TOOL_HINTS: Record<string, ToolHint> = {
|
|
|
213
213
|
create_application: {
|
|
214
214
|
category: "write",
|
|
215
215
|
use_when:
|
|
216
|
-
"Scaffolding a new React application with
|
|
216
|
+
"Scaffolding a new React application with routing, optional Relay, and either server-side rendering or a client-only SPA",
|
|
217
217
|
},
|
|
218
218
|
setup: {
|
|
219
219
|
category: "write",
|
|
@@ -66,6 +66,5 @@ export const colophonFormatters: Formatters<ColophonData> = {
|
|
|
66
66
|
|
|
67
67
|
// Zero sections: the dispatcher routes this to stderr (exit 0) so plain
|
|
68
68
|
// stdout stays empty; llm/json keep their own empty shapes.
|
|
69
|
-
|
|
70
|
-
data.sections.length === 0 ? EMPTY_NOTICE : undefined,
|
|
69
|
+
notice: (data) => (data.sections.length === 0 ? EMPTY_NOTICE : undefined),
|
|
71
70
|
};
|
|
@@ -146,14 +146,14 @@ describe("colophon — the configured domain's story, and only that", () => {
|
|
|
146
146
|
// the empty state is pinned here — bare stdout, guidance on stderr.
|
|
147
147
|
const empty: ColophonData = { sections: [] };
|
|
148
148
|
expect(colophonFormatters.plain(empty)).toBe("");
|
|
149
|
-
const notice = colophonFormatters.
|
|
149
|
+
const notice = colophonFormatters.notice?.(empty);
|
|
150
150
|
expect(notice).toContain("No colophon declared");
|
|
151
151
|
expect(notice).toContain("sources update");
|
|
152
152
|
// llm returns before the dispatcher's stderr routing, so it says the same
|
|
153
153
|
// thing in its own body rather than emitting nothing.
|
|
154
154
|
expect(colophonFormatters.llm(empty)).toContain("No colophon declared");
|
|
155
155
|
// A populated payload has nothing to be calm about.
|
|
156
|
-
expect(colophonFormatters.
|
|
156
|
+
expect(colophonFormatters.notice?.(FIXTURE)).toBeUndefined();
|
|
157
157
|
});
|
|
158
158
|
});
|
|
159
159
|
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* `create` verbs take. No new package edge: pragma already depends on
|
|
16
16
|
* summon-core, and never on the summon bin.
|
|
17
17
|
*
|
|
18
|
-
* Five generators are covered: component × {react, svelte, lit},
|
|
18
|
+
* Five generators are covered across seven fixtures: component × {react, svelte, lit},
|
|
19
|
+
* package, and
|
|
19
20
|
* application. A cross-binary subprocess guard lives in byteEquality.subprocess.test.ts.
|
|
20
21
|
*/
|
|
21
22
|
|
|
@@ -175,6 +176,32 @@ const cases: Case[] = [
|
|
|
175
176
|
runInstall: false,
|
|
176
177
|
},
|
|
177
178
|
},
|
|
179
|
+
{
|
|
180
|
+
fixture: "application-spa",
|
|
181
|
+
kind: "application",
|
|
182
|
+
framework: "",
|
|
183
|
+
params: {
|
|
184
|
+
appPath: "my-app",
|
|
185
|
+
forms: true,
|
|
186
|
+
intl: false,
|
|
187
|
+
relay: false,
|
|
188
|
+
rendering: "spa",
|
|
189
|
+
runInstall: false,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
fixture: "application-spa-all",
|
|
194
|
+
kind: "application",
|
|
195
|
+
framework: "",
|
|
196
|
+
params: {
|
|
197
|
+
appPath: "my-app",
|
|
198
|
+
forms: true,
|
|
199
|
+
intl: true,
|
|
200
|
+
relay: true,
|
|
201
|
+
rendering: "spa",
|
|
202
|
+
runInstall: false,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
178
205
|
];
|
|
179
206
|
|
|
180
207
|
describe("byte-equality goldens — pragma create \u2261 the conformance reference (PROTECTED)", () => {
|