@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,3 @@
|
|
|
1
|
+
// AUTO-GENERATED by scripts/bundle.ts — do not edit by hand.
|
|
2
|
+
// Regenerate from the packs pragma.conf.ts declares: `bun run bundle`.
|
|
3
|
+
export const manifestJson = "{\"name\":\"pragma\",\"version\":\"0.36.0\",\"sourceRef\":\"@canonical/design-system@git:ce00818852628857da9c4a67bf8556c7cfda287d, @canonical/anatomy-dsl@npm:0.3.0, @canonical/token-ontology@npm:0.8.1, @canonical/code-standards@git:fcd3ac2dea458775e5b79344b3237a3fcb37add9, @canonical/ds-implementations@self:v0.36.0\",\"contentHash\":\"7ff2bbd5fd059584ad3285c7b1c7d59c734efef8006fb0bba726db03254392e8\",\"prefixes\":{\"rdf\":\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\",\"rdfs\":\"http://www.w3.org/2000/01/rdf-schema#\",\"owl\":\"http://www.w3.org/2002/07/owl#\",\"xsd\":\"http://www.w3.org/2001/XMLSchema#\",\"skos\":\"http://www.w3.org/2004/02/skos/core#\",\"sh\":\"http://www.w3.org/ns/shacl#\",\"dcterms\":\"http://purl.org/dc/terms/\",\"ds\":\"https://ds.canonical.com/\",\"anatomy\":\"http://anatomy-dsl.example.org/ontology#\",\"graphql\":\"https://pragma.canonical.com/graphql#\",\"w3c-tokens\":\"https://dt.canonical.com/w3c-tokens/\",\"dt\":\"https://dt.canonical.com/\",\"cs\":\"http://pragma.canonical.com/codestandards#\",\"dso\":\"https://ds.canonical.com/ontology#\"},\"createdAt\":\"2026-09-01T20:29:05.995Z\",\"tripleCount\":30346,\"entityCount\":2906}";
|
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
*
|
|
19
19
|
* The inlined strings are materialized into the ordinary content-addressed pack
|
|
20
20
|
* cache on first use and then read back through {@link readPack}, so the
|
|
21
|
-
* embedded pack and a built pack share one boot path. The story records
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* embedded pack and a built pack share one boot path. The story records, the
|
|
22
|
+
* index and the MANIFEST each live in their own generated module: every one of
|
|
23
|
+
* them is read on a path that does not need the n-quads, and leaving any of
|
|
24
|
+
* them beside `pack.generated.ts` would load its ~2.1 MB with them — a measured
|
|
25
|
+
* +28 ms on every invocation for the stories, and ~370 ms per CLI invocation
|
|
26
|
+
* for the manifest at a 30,340-triple pack. Inlining as JS strings
|
|
25
27
|
* (rather than file assets) means the pack travels with the emitted modules —
|
|
26
28
|
* it needs no asset step in any build, and no file lookup at run time.
|
|
27
29
|
*/
|
|
@@ -36,8 +38,8 @@ import {
|
|
|
36
38
|
} from "node:fs";
|
|
37
39
|
import { join } from "node:path";
|
|
38
40
|
import { packDir, packsCacheDir } from "../paths.js";
|
|
39
|
-
import { dataNq, manifestJson, schemaJson } from "./embedded/pack.generated.js";
|
|
40
41
|
import { indexJson } from "./embedded/pack.index.generated.js";
|
|
42
|
+
import { manifestJson } from "./embedded/pack.manifest.generated.js";
|
|
41
43
|
import { storiesJson } from "./embedded/pack.stories.generated.js";
|
|
42
44
|
import { packIsComplete } from "./manifest.js";
|
|
43
45
|
import { manifestSchema } from "./schemas.js";
|
|
@@ -72,10 +74,16 @@ export function embeddedManifest(): Manifest {
|
|
|
72
74
|
* @returns The pack directory, ready for {@link readPack}.
|
|
73
75
|
* @note Impure — writes the inlined pack files into the cache.
|
|
74
76
|
*/
|
|
75
|
-
export function materializeEmbeddedPack(): string {
|
|
77
|
+
export async function materializeEmbeddedPack(): Promise<string> {
|
|
76
78
|
const dir = packDir(embeddedManifest().contentHash);
|
|
77
79
|
if (packIsComplete(dir)) return dir;
|
|
78
80
|
|
|
81
|
+
// Loaded HERE, not at module scope: this is the only branch that needs the
|
|
82
|
+
// payload, and it is the branch that does NOT run once the pack is cached.
|
|
83
|
+
// A static import would parse ~2.1 MB of inlined n-quads on every boot to
|
|
84
|
+
// reach a return statement six lines above.
|
|
85
|
+
const { dataNq, schemaJson } = await import("./embedded/pack.generated.js");
|
|
86
|
+
|
|
79
87
|
mkdirSync(packsCacheDir(), { recursive: true });
|
|
80
88
|
const temp = mkdtempSync(join(packsCacheDir(), ".embed-"));
|
|
81
89
|
try {
|
|
@@ -144,15 +144,21 @@ describe("the committed embedded pack (PROTECTED)", () => {
|
|
|
144
144
|
// directory holds — which the next build then reuses, silently dropping the
|
|
145
145
|
// difference. Comparing the two directories catches that on the day it lands.
|
|
146
146
|
const built = await build([{ path: "a.ttl", content: TTL }]);
|
|
147
|
-
expect(readdirSync(materializeEmbeddedPack()).sort()).toEqual(
|
|
147
|
+
expect(readdirSync(await materializeEmbeddedPack()).sort()).toEqual(
|
|
148
148
|
readdirSync(built.dir).sort(),
|
|
149
149
|
);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
// 60s, not the 5s default. This test does real work twice: it BUILDS a
|
|
151
|
+
// pack from source, and it materialises the embedded one — which on a cold
|
|
152
|
+
// cache takes the miss branch and imports the multi-megabyte payload. Both
|
|
153
|
+
// costs grow with the pack, and the pack grows whenever a source is added.
|
|
154
|
+
// A default that a pack size can outgrow turns a real assertion into an
|
|
155
|
+
// intermittent one.
|
|
156
|
+
}, 60_000);
|
|
157
|
+
|
|
158
|
+
it("is self-consistent: complete, content-addressed, and non-empty", async () => {
|
|
153
159
|
// No network, so CI runs it: the committed strings really do materialize a
|
|
154
160
|
// bootable pack whose parts agree with each other.
|
|
155
|
-
const dir = materializeEmbeddedPack();
|
|
161
|
+
const dir = await materializeEmbeddedPack();
|
|
156
162
|
const manifest = embeddedManifest();
|
|
157
163
|
expect(packIsComplete(dir)).toBe(true);
|
|
158
164
|
expect(basename(dir)).toBe(manifest.contentHash);
|
|
@@ -162,6 +168,26 @@ describe("the committed embedded pack (PROTECTED)", () => {
|
|
|
162
168
|
expect(index.contentHash).toBe(manifest.contentHash);
|
|
163
169
|
expect(manifest.tripleCount ?? 0).toBeGreaterThan(0);
|
|
164
170
|
expect(manifest.entityCount ?? 0).toBeGreaterThan(0);
|
|
171
|
+
|
|
172
|
+
// EXACT, not "> 0", and it earns its keep twice.
|
|
173
|
+
//
|
|
174
|
+
// The manifest and the n-quads now live in SEPARATE generated modules, so
|
|
175
|
+
// a partial regeneration or a one-file `git add` can leave them describing
|
|
176
|
+
// different packs — a skew that could not exist while they shared a
|
|
177
|
+
// module. `read.ts` tolerates a superset deliberately, so it would serve
|
|
178
|
+
// the skew rather than refuse it.
|
|
179
|
+
//
|
|
180
|
+
// It also pins the equivalence `read.ts` depends on but cannot state:
|
|
181
|
+
// `store.size` (quads) against a `tripleCount` that `build.ts` computes
|
|
182
|
+
// with `SELECT (COUNT(*))` over the union graph. They agree on oxigraph
|
|
183
|
+
// 0.5.6; oxigraph is pinned by caret, so this is where a future version
|
|
184
|
+
// that deduplicates the union would be caught.
|
|
185
|
+
const session = await readPack(dir);
|
|
186
|
+
try {
|
|
187
|
+
expect(session.store.size).toBe(manifest.tripleCount);
|
|
188
|
+
} finally {
|
|
189
|
+
session.store.dispose();
|
|
190
|
+
}
|
|
165
191
|
});
|
|
166
192
|
});
|
|
167
193
|
|
|
@@ -80,15 +80,25 @@ export async function readPack(dir: string): Promise<StoreSession> {
|
|
|
80
80
|
// "populated index but empty store" check (via the now-empty dump,
|
|
81
81
|
// `packIsComplete` turns false so recovery rebuilds) rather than serving an
|
|
82
82
|
// empty graph as the pack.
|
|
83
|
-
|
|
83
|
+
// `store.size` is a field oxigraph maintains. The equivalent query —
|
|
84
|
+
// `SELECT (COUNT(*)) WHERE { ?s ?p ?o }`, which is what `build.ts` still runs
|
|
85
|
+
// to WRITE `tripleCount` — is a scan, and it ran on every boot to read a
|
|
86
|
+
// number the store already knew.
|
|
87
|
+
//
|
|
88
|
+
// The two agree: ke queries with `use_default_graph_as_union`, and oxigraph
|
|
89
|
+
// does not deduplicate across that union, so a quad count and the union's
|
|
90
|
+
// solution count are the same number even for a triple repeated across
|
|
91
|
+
// graphs. Verified against 0.5.6 rather than assumed — and the guard is `<`,
|
|
92
|
+
// which tolerates the direction a set-semantic union would drift.
|
|
93
|
+
const actualQuads = store.size;
|
|
84
94
|
if (manifest.tripleCount !== undefined) {
|
|
85
|
-
if (
|
|
95
|
+
if (actualQuads < manifest.tripleCount) {
|
|
86
96
|
store.dispose();
|
|
87
97
|
throw packUnavailable(
|
|
88
|
-
`The pack at ${dir} has a corrupt data cache (expected at least ${manifest.tripleCount} triples, loaded ${
|
|
98
|
+
`The pack at ${dir} has a corrupt data cache (expected at least ${manifest.tripleCount} triples, loaded ${actualQuads}).`,
|
|
89
99
|
);
|
|
90
100
|
}
|
|
91
|
-
} else if (index.entities.length > 0 &&
|
|
101
|
+
} else if (index.entities.length > 0 && actualQuads === 0) {
|
|
92
102
|
store.dispose();
|
|
93
103
|
throw packUnavailable(`The pack at ${dir} has a corrupt data cache.`);
|
|
94
104
|
}
|
|
@@ -101,11 +111,3 @@ export async function readPack(dir: string): Promise<StoreSession> {
|
|
|
101
111
|
index,
|
|
102
112
|
};
|
|
103
113
|
}
|
|
104
|
-
|
|
105
|
-
/** Count the booted store's triples (a cheap aggregate over the union graph). */
|
|
106
|
-
async function countTriples(store: StoreSession["store"]): Promise<number> {
|
|
107
|
-
const result = (await store.query(
|
|
108
|
-
"SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }" as never,
|
|
109
|
-
)) as import("@canonical/ke").SelectResult;
|
|
110
|
-
return Number(result.bindings.at(0)?.n ?? 0);
|
|
111
|
-
}
|
|
@@ -21,7 +21,7 @@ export async function runStoreProbe(): Promise<string> {
|
|
|
21
21
|
import("./graphpack/embedded.js"),
|
|
22
22
|
import("./graphpack/read.js"),
|
|
23
23
|
]);
|
|
24
|
-
const session = await readPack(materializeEmbeddedPack());
|
|
24
|
+
const session = await readPack(await materializeEmbeddedPack());
|
|
25
25
|
try {
|
|
26
26
|
const result = await session.store.query(
|
|
27
27
|
"SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }" as never,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { redactUrl } from "./parseRef.js";
|
|
2
|
+
import { parsePackDeclaration, redactUrl } from "./parseRef.js";
|
|
3
3
|
|
|
4
4
|
describe("redactUrl", () => {
|
|
5
5
|
it("strips user:password userinfo so an inlined token never reaches output", () => {
|
|
@@ -32,3 +32,51 @@ describe("redactUrl", () => {
|
|
|
32
32
|
);
|
|
33
33
|
});
|
|
34
34
|
});
|
|
35
|
+
|
|
36
|
+
describe("parsePackDeclaration — a git source may name a subdirectory", () => {
|
|
37
|
+
const url = "git+https://github.com/org/repo.git";
|
|
38
|
+
|
|
39
|
+
it("reaches a package nested in a monorepo", () => {
|
|
40
|
+
expect(
|
|
41
|
+
parsePackDeclaration({
|
|
42
|
+
name: "@org/thing",
|
|
43
|
+
source: `${url}#main:pkgs/thing`,
|
|
44
|
+
}),
|
|
45
|
+
).toEqual({
|
|
46
|
+
kind: "git",
|
|
47
|
+
pkg: "@org/thing",
|
|
48
|
+
url: "https://github.com/org/repo.git",
|
|
49
|
+
ref: "main",
|
|
50
|
+
subdir: "pkgs/thing",
|
|
51
|
+
source: `${url}#main:pkgs/thing`,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("leaves a plain ref untouched, with no subdir key", () => {
|
|
56
|
+
const ref = parsePackDeclaration({
|
|
57
|
+
name: "@org/thing",
|
|
58
|
+
source: `${url}#main`,
|
|
59
|
+
});
|
|
60
|
+
expect(ref).toEqual({
|
|
61
|
+
kind: "git",
|
|
62
|
+
pkg: "@org/thing",
|
|
63
|
+
url: "https://github.com/org/repo.git",
|
|
64
|
+
ref: "main",
|
|
65
|
+
source: `${url}#main`,
|
|
66
|
+
});
|
|
67
|
+
expect("subdir" in ref).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("splits on the FIRST colon, so a nested path survives", () => {
|
|
71
|
+
// The ref cannot contain a colon; the subdirectory can contain slashes.
|
|
72
|
+
expect(
|
|
73
|
+
parsePackDeclaration({ name: "@org/t", source: `${url}#v1.2:a/b/c` }),
|
|
74
|
+
).toMatchObject({ ref: "v1.2", subdir: "a/b/c" });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("rejects an empty subdirectory rather than silently ignoring it", () => {
|
|
78
|
+
expect(() =>
|
|
79
|
+
parsePackDeclaration({ name: "@org/thing", source: `${url}#main:` }),
|
|
80
|
+
).toThrow(/subdirectory after : is empty/);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -25,6 +25,8 @@ export type PackageRef =
|
|
|
25
25
|
readonly pkg: string;
|
|
26
26
|
readonly url: string;
|
|
27
27
|
readonly ref: string;
|
|
28
|
+
/** Package root WITHIN the clone, for a package nested in a monorepo. */
|
|
29
|
+
readonly subdir?: string;
|
|
28
30
|
readonly source: string;
|
|
29
31
|
};
|
|
30
32
|
|
|
@@ -72,11 +74,33 @@ export function parsePackDeclaration(entry: PackDeclaration): PackageRef {
|
|
|
72
74
|
},
|
|
73
75
|
);
|
|
74
76
|
}
|
|
77
|
+
// `#<ref>` or `#<ref>:<subdir>`. A git ref cannot contain a colon
|
|
78
|
+
// (git check-ref-format), so the split is unambiguous.
|
|
79
|
+
const fragment = source.slice(hashIdx + 1);
|
|
80
|
+
const colonIdx = fragment.indexOf(":");
|
|
81
|
+
if (colonIdx !== -1 && colonIdx === fragment.length - 1) {
|
|
82
|
+
throw PragmaError.configError(
|
|
83
|
+
`Invalid source for "${name}": the subdirectory after : is empty.`,
|
|
84
|
+
{
|
|
85
|
+
recovery: {
|
|
86
|
+
message:
|
|
87
|
+
"Example: git+https://github.com/org/repo.git#main:packages/thing",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const ref = colonIdx === -1 ? fragment : fragment.slice(0, colonIdx);
|
|
93
|
+
if (ref.length === 0) {
|
|
94
|
+
throw PragmaError.configError(
|
|
95
|
+
`Invalid source for "${name}": git URL must include a ref after #.`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
75
98
|
return {
|
|
76
99
|
kind: "git",
|
|
77
100
|
pkg: name,
|
|
78
101
|
url: source.slice("git+".length, hashIdx),
|
|
79
|
-
ref
|
|
102
|
+
ref,
|
|
103
|
+
...(colonIdx === -1 ? {} : { subdir: fragment.slice(colonIdx + 1) }),
|
|
80
104
|
source,
|
|
81
105
|
};
|
|
82
106
|
}
|
|
@@ -383,13 +383,19 @@ export async function resolvePackage(
|
|
|
383
383
|
},
|
|
384
384
|
);
|
|
385
385
|
}
|
|
386
|
+
// The clone lives at `dir`; the PACKAGE may be nested within it. A git
|
|
387
|
+
// source names a repository, so a package inside a monorepo is reached
|
|
388
|
+
// by its subdirectory — without which `readTtlSources` would scan a
|
|
389
|
+
// repository root that has no `definitions/` or `data/` and contribute
|
|
390
|
+
// nothing at all.
|
|
391
|
+
const packageRoot = ref.subdir ? join(dir, ref.subdir) : dir;
|
|
386
392
|
return {
|
|
387
393
|
name: ref.pkg,
|
|
388
394
|
source: ref.source,
|
|
389
395
|
resolved,
|
|
390
|
-
root:
|
|
391
|
-
sources: readTtlSources(
|
|
392
|
-
stories: readStorySources(
|
|
396
|
+
root: packageRoot,
|
|
397
|
+
sources: readTtlSources(packageRoot, ref.pkg),
|
|
398
|
+
stories: readStorySources(packageRoot, ref.pkg),
|
|
393
399
|
};
|
|
394
400
|
}
|
|
395
401
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* that are actually populated by the time it runs.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import type { UndoOutcome } from "@canonical/task/node";
|
|
11
12
|
import type { DetailLevel, OutputFormat } from "../../constants.js";
|
|
12
13
|
import type { ConfigLayers } from "../config/types.js";
|
|
13
14
|
|
|
@@ -221,4 +222,20 @@ export interface PragmaRuntime {
|
|
|
221
222
|
* plan of its own, which is what keeps the raw-effect render the default.
|
|
222
223
|
*/
|
|
223
224
|
planData?: unknown;
|
|
225
|
+
/**
|
|
226
|
+
* The post-undo projection seam. `--undo` is two-phase: the undo
|
|
227
|
+
* interpreter WALKS the verb's task with every effect mocked to collect the
|
|
228
|
+
* reversals, and only then executes what it collected — so nothing the task
|
|
229
|
+
* body says while it is interpreted describes what the reversals did (the
|
|
230
|
+
* body only ever runs under the mocked walk). What DOES describe them is
|
|
231
|
+
* the interpreter's own per-undo outcomes, correlated by the `undoKey` each
|
|
232
|
+
* reversal was declared with. A verb that reports per-target results sets
|
|
233
|
+
* this (the same way it sets `exec`/`planData`, before returning its Task);
|
|
234
|
+
* the projector's undo branch awaits it with those outcomes AFTER the
|
|
235
|
+
* collected undos executed and before the `Undid N step(s).` line, so the
|
|
236
|
+
* verb projects them onto its own rows through the same path its forward
|
|
237
|
+
* run uses. A throw here is rendered as the run's error (nonzero exit) —
|
|
238
|
+
* the honest outcome when a reversal did not take.
|
|
239
|
+
*/
|
|
240
|
+
undoReport?: (outcomes: readonly UndoOutcome[]) => void | Promise<void>;
|
|
224
241
|
}
|
|
@@ -541,7 +541,7 @@ const CONFIG_FIELD_DOCS: Record<keyof RawConfig, ConfigFieldDoc> = {
|
|
|
541
541
|
logo: {
|
|
542
542
|
type: "string[] (optional)",
|
|
543
543
|
notes:
|
|
544
|
-
"Distribution-only — see below. ASCII-art wordmark lines shown above the header on root `--help`, tinted with the terminal palette's orange slot. Declared content, not code: a wordmark spells the distribution's name, so a fork ships its own art or omits the field and gets no wordmark. Omit or leave empty for a bare header.",
|
|
544
|
+
"Distribution-only — see below. ASCII-art wordmark lines shown above the header on root `--help`, tinted with the terminal palette's orange slot. Shown to a HUMAN reader only: the front door omits the art for MACHINE-ORIENTED output — `--format llm` or `--format json`, plus the `llm` a non-interactive stdout auto-infers — so a piped `--help` spends none of an agent's budget on a picture of the name it just typed. The gate is the output SHAPE, not whether stdout is a terminal, and explicit beats inferred in both directions: `--format plain` keeps the art down a pipe (as does `PRAGMA_NO_AUTO_LLM`, which turns the inference off), and `--format llm` drops it on a TTY. Declared content, not code: a wordmark spells the distribution's name, so a fork ships its own art or omits the field and gets no wordmark. Omit or leave empty for a bare header.",
|
|
545
545
|
},
|
|
546
546
|
colophon: {
|
|
547
547
|
type: "object (optional)",
|
package/src/kernel/spec/types.ts
CHANGED
|
@@ -149,11 +149,40 @@ export type ParamSpec =
|
|
|
149
149
|
* list-shaped formatters read it; `llm` (the byte-frozen agent contract) and
|
|
150
150
|
* `json` (the envelope) never see it.
|
|
151
151
|
*
|
|
152
|
-
* `
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
152
|
+
* `notice` is the seam for what the DATA cannot say about itself. Return the
|
|
153
|
+
* calm sentence when the payload would otherwise misrepresent the read; return
|
|
154
|
+
* `undefined` (or omit the member) when it speaks for itself.
|
|
155
|
+
*
|
|
156
|
+
* Two shapes qualify, and they are the same failure. A zero-record result
|
|
157
|
+
* ("empty ≠ silence"): an unbuilt store, a mistyped filter and a genuinely empty
|
|
158
|
+
* result were all `{"ok":true,"data":[],"meta":{}}`. And a result that is one of
|
|
159
|
+
* several it could have been — a `lookup` whose name reached more entities than
|
|
160
|
+
* the one it answers with. A caller shown Launchpad's `Button` has no way to
|
|
161
|
+
* discover the global one from a payload that looks exactly like an unambiguous
|
|
162
|
+
* hit; the notice names the IRIs the answer did not take, which is the only
|
|
163
|
+
* address that reaches them.
|
|
164
|
+
*
|
|
165
|
+
* It was called `emptyNotice` while emptiness was the only case. The channel is
|
|
166
|
+
* unchanged — same key, same three surfaces — so this is one seam covering both,
|
|
167
|
+
* not a second one grown beside it.
|
|
168
|
+
*
|
|
169
|
+
* Three surfaces route it, each in its own register — it is one seam, not a
|
|
170
|
+
* plain-mode courtesy:
|
|
171
|
+
* - `plain`: the dispatcher writes it to STDERR with exit 0, keeping the stdout
|
|
172
|
+
* data stream free of human sentences a pipe would read as records. `--quiet`
|
|
173
|
+
* mutes it (success-path guidance).
|
|
174
|
+
* - `--format json` AND the MCP tool result: it rides the envelope as
|
|
175
|
+
* `meta.notice` (`project/cli/dispatch.ts#renderData`,
|
|
176
|
+
* `project/mcp/registerVerb.ts#emptyMeta` — the same key from the same seam,
|
|
177
|
+
* so the two MACHINE surfaces stay byte-equal). `data` keeps its uniform empty
|
|
178
|
+
* shape; `[]` stays `[]`. Without it an agent could not tell an unbuilt store,
|
|
179
|
+
* a mistyped filter and a genuinely empty result apart — all three were
|
|
180
|
+
* `{"ok":true,"data":[],"meta":{}}`.
|
|
181
|
+
* - `llm` (the byte-frozen agent contract) is the ONE mode that still ignores
|
|
182
|
+
* it: it renders one stream and its bytes are frozen.
|
|
183
|
+
*
|
|
184
|
+
* The formatters themselves never see the notice: `json(d)` renders the data
|
|
185
|
+
* alone, and the ENVELOPE layer carries the notice beside it.
|
|
157
186
|
*/
|
|
158
187
|
export interface Formatters<T> {
|
|
159
188
|
readonly plain: (
|
|
@@ -162,7 +191,7 @@ export interface Formatters<T> {
|
|
|
162
191
|
) => string;
|
|
163
192
|
readonly llm: (d: T) => string;
|
|
164
193
|
readonly json: (d: T) => string;
|
|
165
|
-
readonly
|
|
194
|
+
readonly notice?: (d: T) => string | undefined;
|
|
166
195
|
}
|
|
167
196
|
|
|
168
197
|
/** A usage example shown in verb help. */
|
|
@@ -42,13 +42,13 @@ export const PARITY_GAPS: readonly ParityGapEntry[] = [
|
|
|
42
42
|
id: "read-meta-always-empty",
|
|
43
43
|
area: "envelope",
|
|
44
44
|
description:
|
|
45
|
-
"
|
|
45
|
+
"NARROWED: a read envelope still carries no `meta.count`/`meta.total` — the only way to know a list is empty is `data.length === 0`, not a count field. But `meta` is no longer ALWAYS `{}` on a read. A zero-record read now carries the verb's own `notice` as `meta.notice` on BOTH machine surfaces (`project/cli/dispatch.ts#renderData` for `--format json`, `project/mcp/registerVerb.ts#emptyMeta` for MCP — one seam, the same key, so the two stay byte-equal). `data` keeps its uniform empty shape; `[]` is still `[]`. The divergence this entry records is the ABSENCE of counts, not the emptiness of `meta`: without the notice an agent could not tell an unbuilt store, a mistyped filter and a genuinely empty result apart, all three being `{ok:true,data:[],meta:{}}`.",
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
id: "no-empty-hook-on-free-filter",
|
|
49
49
|
area: "pack list / empty-state",
|
|
50
50
|
description:
|
|
51
|
-
"A zero-row pack list —
|
|
51
|
+
"STILL TRUE OF EMPTINESS, NARROWED ON BAD ARGUMENTS. A zero-row pack list — narrowed by a declared filter, by an unmatched `--search`, or unconditionally empty — is a SUCCESS: `ok:true, data:[]` at exit 0 with a calm notice (stderr in plain mode, `meta.notice` on the machine surfaces). pragma v2 still never raises EMPTY_RESULTS for an empty list, and U5's removal of the enum-filter-only `buildListEmptyError` hook stands. What is no longer true is that an UNMATCHED free-filter value stays successful: a value-free filter now rejects a value its vocabulary does not admit with INVALID_INPUT and the admissible values as `validOptions`, the same courtesy a declared-`values` filter always extended (`packs/sparql/applyFilters.ts`). The vocabulary is the graph's, not the returned rows' — a filter declares where it lives (`PackFilter.vocabulary`), so a real value that happens to match no row (a category with zero standards, a `ds:ConceptType` no concept uses) is a calm empty list, and only a value the graph does not know is an error.",
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
54
|
id: "raw-iri-in-data",
|
|
@@ -84,7 +84,7 @@ export const PARITY_GAPS: readonly ParityGapEntry[] = [
|
|
|
84
84
|
id: "block-lookup-not-tier-scoped",
|
|
85
85
|
area: "block",
|
|
86
86
|
description:
|
|
87
|
-
"`block lookup` resolves by `ds:name`
|
|
87
|
+
"CLOSED, by REVERSAL of the entry it used to be. This gap read: `block lookup` resolves by `ds:name` globally, a same-named block at two tiers cannot be disambiguated by plain name, address it by prefixed IRI instead. That was accepted while the answer was merely arbitrary; it stopped being acceptable once measured — 25 live block names are carried by 2–3 blocks, and EVERY one of them was answered with whichever IRI sorted first and no sign the others existed (`block lookup button` returned Launchpad's Button, because `apps_launchpad…` precedes `global…`). 'Address it by IRI' is not a recovery an agent can reach when nothing tells it there is a second block to address. Two things replace it, and neither is a change of arity — one name still answers with one entity, and a glob is still the declared multi-entity form. (1) The discard is no longer SILENT: the resolve is unlimited, the winner is answered with, and the IRIs it outranked ride the verb's `notice` — the same seam a zero-record read uses (stderr in plain mode, `meta.notice` on `--format json` and MCP), because an ambiguous hit is the same failure as an empty one, a result that misrepresents itself. `LookupOutput.ambiguous` carries them in `data` for the machine surfaces, and is ABSENT (not `[]`) when nothing was ambiguous, so every unambiguous read keeps the payload it had. Note the recovery is not reachable any other way: a glob expands over DISTINCT names, so `block lookup 'Butt*'` returned Launchpad's Button and ButtonLink and the global Button appeared nowhere. (2) The order is a declared, derived RANKING (`PackLookup.scopeWeight` in `pragma.conf.ts`, generated by `packs/sparql/buildLookupQuery.ts#rankingClause`): a tier's depth comes from the `/` count in its OWN `ds:name` (`\"Apps/Launchpad\"` → 2, never from the `_` in `ds:apps_launchpad`), and it MULTIPLIES the existing type weight rather than tiebreaking above it, so a whole component in a nested tier still outranks a global SUBcomponent (`TextInput`). Nothing is enumerated, so a tier added upstream is placed without an edit here; an asserted `ds:tierRank` would take precedence over the derived depth and retire the config declaration with no code change. WHAT REMAINS a gap: `lookup` is still not tier-SCOPED — there is no way to ask for one tier's answer only, the ranking merely decides which comes first — and since L-OPEN-9 nothing is tier/channel-FILTERED (the hand-written `block list` went with its `tierChain.ts`; see `block-list-unfiltered`). Two top-level tiers still cannot be ordered against each other by depth alone, which today leaves `Navigation` (`global` vs `apps`) and `InfoTable` (`global` vs `sites`) decided by the IRI fallback; naming a precedence among the depth-1 tiers is an ontology question, deliberately NOT invented here.",
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
id: "block-list-unfiltered",
|
|
@@ -182,4 +182,10 @@ export const PARITY_GAPS: readonly ParityGapEntry[] = [
|
|
|
182
182
|
description:
|
|
183
183
|
"The native MCP `prompts/list` (and the `prompt_list`/`prompt_lookup` content tools) project `ds:Prompt` KG entities read from the ACTIVE pack index (`kernel/project/mcp/prompts/source.ts#listPromptSummaries`) — project-supplied data, NOT shipped in the embedded/bundled pack. A bare install (no project-authored `ds:Prompt`) advertises the prompts capability but lists ZERO prompts, never booting the store on the list path. Non-empty listings are exercised only against the seeded fixture graph (`testing/fixtures/graph/canonical.ts`); the storeless empty-by-default case is pinned by `prompts.test.ts` ('lists zero prompts … when no prompt entities exist').",
|
|
184
184
|
},
|
|
185
|
+
{
|
|
186
|
+
id: "scope-not-band-in-machine-output",
|
|
187
|
+
area: "setup / doctor `--format json`",
|
|
188
|
+
description:
|
|
189
|
+
"A setup plan row and a doctor check carry their config scope under the key `scope` (values `global`/`project`), not `band`. The old shell's type layer said `band` (`ScopeBand`, `PlanRow.band`, `CheckResult.band`) while every user-facing string had already moved to the scope vocabulary the `--global`/`--local` flags use; the type-layer rename (kernel/render/vocabulary.ts) finished the move, and `--format json`/MCP emit the model verbatim, so the machine key moved with it. Any consumer reading `.band` off `setup --format json` or `doctor --format json` reads `.scope` now — `setup.render.test.ts` pins that neither spelling of `band` can return through any render, JSON included.",
|
|
190
|
+
},
|
|
185
191
|
] as const;
|
|
@@ -164,9 +164,11 @@ const READ_CASES: readonly ReadCase[] = [
|
|
|
164
164
|
},
|
|
165
165
|
{ command: "pragma standard list", key: "standard list", params: {} },
|
|
166
166
|
{
|
|
167
|
-
|
|
167
|
+
// The doc's headline example, and the one the roll-up is about: a parent
|
|
168
|
+
// category answering for its whole branch.
|
|
169
|
+
command: "pragma standard list --category testing",
|
|
168
170
|
key: "standard list",
|
|
169
|
-
params: { category: ["
|
|
171
|
+
params: { category: ["testing"] },
|
|
170
172
|
},
|
|
171
173
|
{
|
|
172
174
|
command: "pragma standard categories",
|
|
@@ -11,10 +11,16 @@
|
|
|
11
11
|
* a total miss; only a PARTIAL batch reports the miss while staying
|
|
12
12
|
* `ok:true` (that shape is B1's job, `agentSession.mcp.test.ts`). See
|
|
13
13
|
* PARITY_GAPS `single-lookup-miss-fails-batch-partial-reports`.
|
|
14
|
-
* 2. A filtered list narrowed to zero rows is `{ok:true, data:[]
|
|
15
|
-
*
|
|
16
|
-
* always
|
|
17
|
-
* `read
|
|
14
|
+
* 2. A filtered list narrowed to zero rows is `{ok:true, data:[]}` — there is
|
|
15
|
+
* still no `meta.count` field on any read envelope, which is the divergence
|
|
16
|
+
* `read-meta-always-empty` now records (it no longer claims `meta` is always
|
|
17
|
+
* `{}`: a zero-record read carries the list's own `notice` as
|
|
18
|
+
* `meta.notice` on both machine surfaces, asserted below).
|
|
19
|
+
* 3. A value-free filter given a value its vocabulary does not admit is
|
|
20
|
+
* INVALID_INPUT, not a successful empty list — the narrowing recorded in
|
|
21
|
+
* PARITY_GAPS `no-empty-hook-on-free-filter`. Emptiness itself is still a
|
|
22
|
+
* success: a value the GRAPH admits but no row carries (a category with zero
|
|
23
|
+
* standards) is a calm empty list, asserted below.
|
|
18
24
|
*/
|
|
19
25
|
|
|
20
26
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
@@ -88,7 +94,22 @@ describe("lookup miss — total miss fails the call (B3, adapted)", () => {
|
|
|
88
94
|
});
|
|
89
95
|
});
|
|
90
96
|
|
|
91
|
-
|
|
97
|
+
/**
|
|
98
|
+
* CHANGED DELIBERATELY. This used to assert that ANY filter value narrowed to a
|
|
99
|
+
* plain `{"ok":true,"data":[],"meta":{}}`, which was written when no value-free
|
|
100
|
+
* filter could tell a typo from an empty answer. It could: the rows it is
|
|
101
|
+
* filtering carry the vocabulary. Over MCP the old shape made a mistyped
|
|
102
|
+
* `--category`, an unbuilt store and a genuinely empty category byte-identical —
|
|
103
|
+
* the silence a model has no way to recover from.
|
|
104
|
+
*
|
|
105
|
+
* A value the graph does not know is now INVALID_INPUT with the admissible
|
|
106
|
+
* values as `validOptions`, exactly as a filter declaring `values` has always
|
|
107
|
+
* behaved; the difference is that the vocabulary is READ FROM THE GRAPH rather
|
|
108
|
+
* than copied into the story. Zero rows for a value that DOES exist stays a calm
|
|
109
|
+
* empty list — that is a real answer, not a bad argument — and "exists" means
|
|
110
|
+
* the graph declares it, not that some returned row happened to carry it.
|
|
111
|
+
*/
|
|
112
|
+
describe("list — a filter value the graph does not carry is INVALID_INPUT (B3, adapted)", () => {
|
|
92
113
|
it("has at least one filtered list verb to sweep", () => {
|
|
93
114
|
// A guard, not a no-op: if this ever goes empty the sweep below silently
|
|
94
115
|
// covers nothing, which would be a silent coverage loss worth noticing.
|
|
@@ -97,16 +118,98 @@ describe("list — a narrowing filter to zero rows is a plain empty list (B3, ad
|
|
|
97
118
|
|
|
98
119
|
it.each(
|
|
99
120
|
filteredListVerbs,
|
|
100
|
-
)("$tool: an
|
|
121
|
+
)("$tool: an unobserved filter value is rejected, naming the observed ones", async ({
|
|
101
122
|
tool,
|
|
102
123
|
param,
|
|
103
124
|
}) => {
|
|
104
125
|
const result = await mcp.callTool(tool, {
|
|
105
126
|
[param]: "zzz-definitely-not-a-real-value",
|
|
106
127
|
});
|
|
128
|
+
expect(result.ok).toBe(false);
|
|
129
|
+
const error = result.error as { code: string; validOptions?: string[] };
|
|
130
|
+
expect(error.code).toBe("INVALID_INPUT");
|
|
131
|
+
expect(error.validOptions?.length).toBeGreaterThan(0);
|
|
132
|
+
expect(error.validOptions).not.toContain("zzz-definitely-not-a-real-value");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it.each(
|
|
136
|
+
filteredListVerbs,
|
|
137
|
+
)("$tool: every value the graph DOES carry narrows to a calm list", async ({
|
|
138
|
+
tool,
|
|
139
|
+
param,
|
|
140
|
+
}) => {
|
|
141
|
+
const rejected = await mcp.callTool(tool, { [param]: "zzz-nope" });
|
|
142
|
+
const admissible = (rejected.error as { validOptions: string[] })
|
|
143
|
+
.validOptions;
|
|
144
|
+
expect(admissible.length).toBeGreaterThan(0);
|
|
145
|
+
// EVERY admissible value is accepted, and at least one is populated. Not
|
|
146
|
+
// "the first one returns rows": the vocabulary is the graph's, so it can
|
|
147
|
+
// legitimately contain a value no row carries (a category with zero
|
|
148
|
+
// standards) — that value is a calm empty list, not a rejection, and the
|
|
149
|
+
// sweep would otherwise read a real answer as a failure.
|
|
150
|
+
let populated = 0;
|
|
151
|
+
for (const value of admissible) {
|
|
152
|
+
const result = await mcp.callTool(tool, { [param]: value });
|
|
153
|
+
expect(result.ok).toBe(true);
|
|
154
|
+
if ((result.data as unknown[]).length > 0) populated += 1;
|
|
155
|
+
}
|
|
156
|
+
expect(populated).toBeGreaterThan(0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("standard_list: a declared category with NO standards is a calm empty list", async () => {
|
|
160
|
+
// The case that separates "the graph is the vocabulary" from "the returned
|
|
161
|
+
// rows are the vocabulary". `standard_categories` publishes every declared
|
|
162
|
+
// slug with its count, zeros included, and tells the agent to pick one —
|
|
163
|
+
// so rejecting a zero-count slug as INVALID_INPUT tells it the slug the
|
|
164
|
+
// tool just handed it was a typo. Validity read off the rows did exactly
|
|
165
|
+
// that, because a category with no standards appears in no row.
|
|
166
|
+
const categories = await mcp.callTool("standard_categories");
|
|
167
|
+
const zero = (categories.data as { name: string; count: string }[]).filter(
|
|
168
|
+
(row) => Number(row.count) === 0,
|
|
169
|
+
);
|
|
170
|
+
expect(zero.length).toBeGreaterThan(0);
|
|
171
|
+
for (const row of zero) {
|
|
172
|
+
const result = await mcp.callTool("standard_list", {
|
|
173
|
+
category: row.name,
|
|
174
|
+
});
|
|
175
|
+
expect(result.ok).toBe(true);
|
|
176
|
+
expect(result.data).toEqual([]);
|
|
177
|
+
// And it is a real empty answer, so it carries the calm notice rather
|
|
178
|
+
// than passing in silence.
|
|
179
|
+
expect((result.meta as { notice?: string }).notice ?? "").not.toBe("");
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("standard_list: the rejection names the declared slugs, not just the used ones", async () => {
|
|
184
|
+
const result = await mcp.callTool("standard_list", {
|
|
185
|
+
category: "zzz-definitely-not-a-slug",
|
|
186
|
+
});
|
|
187
|
+
expect(result.ok).toBe(false);
|
|
188
|
+
const error = result.error as { code: string; validOptions: string[] };
|
|
189
|
+
expect(error.code).toBe("INVALID_INPUT");
|
|
190
|
+
const categories = await mcp.callTool("standard_categories");
|
|
191
|
+
const declared = (categories.data as { name: string }[]).map((r) => r.name);
|
|
192
|
+
// Same vocabulary, both surfaces: whatever `standard_categories` lists is
|
|
193
|
+
// what `--category` accepts. Read from `cs:Category`/`cs:slug` on both
|
|
194
|
+
// sides, so the two cannot drift.
|
|
195
|
+
expect([...error.validOptions].sort()).toEqual([...declared].sort());
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("empty ≠ silence on the machine surfaces", () => {
|
|
200
|
+
it("carries the list's empty-state guidance in the envelope meta", async () => {
|
|
201
|
+
// `notice` had exactly one consumer — the CLI dispatcher's stderr — so
|
|
202
|
+
// an agent received `{"ok":true,"data":[],"meta":{}}` and nothing else. The
|
|
203
|
+
// search path is the honest way to reach an empty list without a bad
|
|
204
|
+
// argument.
|
|
205
|
+
const result = await mcp.callTool("standard_list", {
|
|
206
|
+
search: "zzz-nothing-matches-this",
|
|
207
|
+
});
|
|
107
208
|
expect(result.ok).toBe(true);
|
|
108
209
|
expect(result.data).toEqual([]);
|
|
109
|
-
|
|
210
|
+
const notice = (result.meta as { notice?: string }).notice ?? "";
|
|
211
|
+
expect(notice).toContain("No standard entries found.");
|
|
212
|
+
expect(notice).toContain("pragma standard categories");
|
|
110
213
|
});
|
|
111
214
|
});
|
|
112
215
|
|
|
@@ -158,10 +158,15 @@ describe("first install — the surfaces say where the answers come from", () =>
|
|
|
158
158
|
expect(status.entityCount).toBeGreaterThan(0);
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
-
it("doctor passes,
|
|
161
|
+
it("doctor passes, saying the packs came with the CLI rather than from the config", async () => {
|
|
162
162
|
const result = await checkPackageRefs(bootRuntime(JSON_FLAGS, emptyCwd()));
|
|
163
163
|
expect(result.status).toBe("pass");
|
|
164
|
-
|
|
164
|
+
// What is pinned is the DISTINCTION — a fresh install reads packs it
|
|
165
|
+
// shipped with, not packs this project configured — and that the row says
|
|
166
|
+
// how to change that. The wording is the reader's ("shipped with the CLI"),
|
|
167
|
+
// not the build's ("embedded snapshot"); the per-pack revisions moved to
|
|
168
|
+
// the row's sub-items when each pack got its own line.
|
|
169
|
+
expect(result.detail).toContain("shipped with the CLI");
|
|
165
170
|
expect(result.detail).toContain("pragma sources update");
|
|
166
171
|
});
|
|
167
172
|
});
|