@canonical/pragma-cli 0.35.0 → 0.36.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 +16 -15
- package/dist/pragma.conf.js +236 -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/colophon/colophon.render.js +1 -1
- package/dist/src/capabilities/colophon/colophon.render.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/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/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/setupGenerator.js +92 -45
- package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupLsp.js +88 -36
- package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupMcp.js +97 -51
- package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupSkills.js +325 -47
- 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 +86 -33
- package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
- package/dist/src/capabilities/setup/targets.js +110 -45
- package/dist/src/capabilities/setup/targets.js.map +1 -1
- package/dist/src/capabilities/shared/index.js +6 -5
- 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 +6 -1
- 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 +306 -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 +23 -23
- 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 +34 -4
- 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/spec/emitReference.js +1 -1
- package/dist/src/kernel/spec/emitReference.js.map +1 -1
- package/package.json +16 -15
- package/pragma.conf.ts +241 -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/colophon/colophon.render.ts +1 -2
- package/src/capabilities/colophon/colophon.test.ts +2 -2
- package/src/capabilities/create/createGrammar.test.ts +1 -1
- package/src/capabilities/create/mount.ts +3 -3
- package/src/capabilities/distribution.parity.test.ts +4 -0
- 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/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/prompt/prompt.render.ts +2 -2
- package/src/capabilities/renderVocabularySync.test.ts +82 -0
- 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/setupGenerator.ts +144 -70
- package/src/capabilities/setup/operations/setupLsp.ts +129 -37
- package/src/capabilities/setup/operations/setupMcp.ts +132 -61
- package/src/capabilities/setup/operations/setupSkills.ts +392 -58
- 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 +881 -50
- package/src/capabilities/setup/setup.verb.ts +91 -33
- package/src/capabilities/setup/targets.ts +120 -52
- package/src/capabilities/setup/types.ts +10 -18
- package/src/capabilities/shared/index.ts +17 -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 +2 -2
- 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 +6 -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 +2 -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 +323 -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 +25 -24
- 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 +36 -3
- package/src/kernel/project/mcp/registerVerb.ts +20 -1
- 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/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/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
|
@@ -485,6 +485,98 @@ describe("weights validation (a weight that can never apply is rejected)", () =>
|
|
|
485
485
|
});
|
|
486
486
|
});
|
|
487
487
|
|
|
488
|
+
describe("nameFallback validation (a derived name needs a class to vouch for it)", () => {
|
|
489
|
+
it("accepts the fallback beside a class constraint", () => {
|
|
490
|
+
const definition = {
|
|
491
|
+
noun: "gadget",
|
|
492
|
+
lookup: { by: "ex:name", nameFallback: "iri", type: "ex:Widget" },
|
|
493
|
+
};
|
|
494
|
+
expect(parsePackDefinition(definition, "t")).toEqual(definition);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("REJECTS the fallback with no class constraint", () => {
|
|
498
|
+
// Without one there is no triple bounding `?uri`, so the derived-name
|
|
499
|
+
// population would be scanned over the whole graph — and a derived name is
|
|
500
|
+
// only as trustworthy as the class vouching for the entity it came from.
|
|
501
|
+
expect(() =>
|
|
502
|
+
parsePackDefinition(
|
|
503
|
+
{ noun: "gadget", lookup: { by: "ex:name", nameFallback: "iri" } },
|
|
504
|
+
"t",
|
|
505
|
+
),
|
|
506
|
+
).toThrow(/nameFallback/);
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
describe("filter vocabulary validation (where a value-free filter's values live)", () => {
|
|
511
|
+
const listShape = {
|
|
512
|
+
query: "SELECT ?uri ?kind WHERE { ?uri a ex:Widget ; ex:kind ?kind }",
|
|
513
|
+
columns: [{ field: "uri" }],
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
it("accepts a vocabulary SELECT on a value-free filter", () => {
|
|
517
|
+
const definition = {
|
|
518
|
+
noun: "widget",
|
|
519
|
+
list: {
|
|
520
|
+
...listShape,
|
|
521
|
+
filters: [
|
|
522
|
+
{
|
|
523
|
+
param: "kind",
|
|
524
|
+
variable: "kind",
|
|
525
|
+
vocabulary: { query: "SELECT ?kind WHERE { ?k a ex:Kind }" },
|
|
526
|
+
},
|
|
527
|
+
],
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
expect(parsePackDefinition(definition, "t")).toEqual(definition);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("REJECTS a vocabulary beside a declared `values` set", () => {
|
|
534
|
+
// A declared set IS the vocabulary — the filter projects it as an enum and
|
|
535
|
+
// canonicalizes against it. A second, graph-read one alongside is a silent
|
|
536
|
+
// no-op at best and a disagreement at worst.
|
|
537
|
+
expect(() =>
|
|
538
|
+
parsePackDefinition(
|
|
539
|
+
{
|
|
540
|
+
noun: "widget",
|
|
541
|
+
list: {
|
|
542
|
+
...listShape,
|
|
543
|
+
filters: [
|
|
544
|
+
{
|
|
545
|
+
param: "kind",
|
|
546
|
+
variable: "kind",
|
|
547
|
+
values: ["a", "b"],
|
|
548
|
+
vocabulary: { query: "SELECT ?kind WHERE { ?k a ex:Kind }" },
|
|
549
|
+
},
|
|
550
|
+
],
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
"t",
|
|
554
|
+
),
|
|
555
|
+
).toThrow(/mutually exclusive/);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("REJECTS a vocabulary query that is not a SELECT", () => {
|
|
559
|
+
expect(() =>
|
|
560
|
+
parsePackDefinition(
|
|
561
|
+
{
|
|
562
|
+
noun: "widget",
|
|
563
|
+
list: {
|
|
564
|
+
...listShape,
|
|
565
|
+
filters: [
|
|
566
|
+
{
|
|
567
|
+
param: "kind",
|
|
568
|
+
variable: "kind",
|
|
569
|
+
vocabulary: { query: "ASK { ?k a ex:Kind }" },
|
|
570
|
+
},
|
|
571
|
+
],
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
"t",
|
|
575
|
+
),
|
|
576
|
+
).toThrow(/SELECT/);
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
488
580
|
describe("compileListable — absolute IRIs reach the prefixed contract", () => {
|
|
489
581
|
it("compacts an absolute lookup type, and its weight key with it", () => {
|
|
490
582
|
// `PackLookup.type`/`types` accept a prefixed name OR an absolute IRI, but
|
|
@@ -55,7 +55,7 @@ export interface RenderMeta {
|
|
|
55
55
|
* list on a BUILT store (a cold store would have failed with STORE_UNAVAILABLE
|
|
56
56
|
* first) means "nothing matched", so point at both possible fixes.
|
|
57
57
|
*/
|
|
58
|
-
const DEFAULT_EMPTY_HINT = `
|
|
58
|
+
const DEFAULT_EMPTY_HINT = `Either nothing matched — try a wider filter — or the store has nothing in it yet: build it with \`${BIN_NAME} sources update\`.`;
|
|
59
59
|
|
|
60
60
|
/** Build the list formatters for a list-shaped verb (list or an extra verb). */
|
|
61
61
|
export function listFormatters(
|
|
@@ -89,7 +89,7 @@ export function listFormatters(
|
|
|
89
89
|
json: (rows) => JSON.stringify(rows, null, 2),
|
|
90
90
|
// Zero rows: the dispatcher routes this to stderr (exit 0) so the plain
|
|
91
91
|
// stdout stream stays pure data; llm/json keep their own empty shapes.
|
|
92
|
-
|
|
92
|
+
notice: (rows) =>
|
|
93
93
|
rows.length === 0 ? renderListEmptyNotice(options) : undefined,
|
|
94
94
|
};
|
|
95
95
|
}
|
|
@@ -132,7 +132,7 @@ export function lookupOptions(
|
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
/** Build the lookup formatters (
|
|
135
|
+
/** Build the lookup formatters (every resolved entity, then errors, then the notice). */
|
|
136
136
|
export function lookupFormatters(
|
|
137
137
|
lookup: PackLookup,
|
|
138
138
|
prefixes: Readonly<Record<string, string>>,
|
|
@@ -7,8 +7,18 @@
|
|
|
7
7
|
* and nothing else — as accepting a name, a prefixed name, an absolute IRI, or
|
|
8
8
|
* a glob. These assert that the resolver honours the shape it is handed rather
|
|
9
9
|
* than the source its pack declares, that an entity is addressable by IRI even
|
|
10
|
-
* when it carries no `by` value, and that an ambiguous name
|
|
11
|
-
* same entity on every store and every machine
|
|
10
|
+
* when it carries no `by` value, and that an ambiguous name answers with the
|
|
11
|
+
* same entity on every store and every machine — while NAMING the ones it did
|
|
12
|
+
* not answer with.
|
|
13
|
+
*
|
|
14
|
+
* That last clause is a REVERSAL, signed off by the owner. It used to read "an
|
|
15
|
+
* ambiguous name resolves to the SAME entity every time", and the suite proved
|
|
16
|
+
* it: a total `ORDER BY` under a `LIMIT 1` does make the answer reproducible.
|
|
17
|
+
* It just made it reproducibly SILENT — `block lookup button` answered with
|
|
18
|
+
* Launchpad's Button on every machine, because `apps_launchpad…` sorts before
|
|
19
|
+
* `global…`, and nothing in the payload said the global one existed. The arity
|
|
20
|
+
* is unchanged and deliberately so; what the old assertions took for the whole
|
|
21
|
+
* property, determinism, was only half of it.
|
|
12
22
|
*/
|
|
13
23
|
|
|
14
24
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
@@ -20,7 +30,7 @@ import {
|
|
|
20
30
|
import { buildFixtureRuntime } from "../../testing/helpers/packRuntime.js";
|
|
21
31
|
import type { PragmaRuntime } from "../runtime/types.js";
|
|
22
32
|
import { compilePack } from "./compile.js";
|
|
23
|
-
import { lookupOptions } from "./renderPack.js";
|
|
33
|
+
import { lookupFormatters, lookupOptions } from "./renderPack.js";
|
|
24
34
|
import type { LookupOutput } from "./resolveEntity.js";
|
|
25
35
|
import type { PackDefinition, PackLookup } from "./types.js";
|
|
26
36
|
import { distributionSource } from "./types.js";
|
|
@@ -79,6 +89,14 @@ describe("pack lookup addressing (PROTECTED)", () => {
|
|
|
79
89
|
return verb.run({ name }, rt) as Promise<LookupOutput>;
|
|
80
90
|
};
|
|
81
91
|
|
|
92
|
+
const lookupNoticeVia = (
|
|
93
|
+
definition: PackDefinition,
|
|
94
|
+
out: LookupOutput,
|
|
95
|
+
): string | undefined =>
|
|
96
|
+
lookupFormatters(definition.lookup as PackLookup, BLOCK_PREFIXES).notice?.(
|
|
97
|
+
out,
|
|
98
|
+
);
|
|
99
|
+
|
|
82
100
|
const uris = (out: LookupOutput): string[] =>
|
|
83
101
|
out.results.map((entity) => String(entity.uri));
|
|
84
102
|
|
|
@@ -182,32 +200,75 @@ describe("pack lookup addressing (PROTECTED)", () => {
|
|
|
182
200
|
});
|
|
183
201
|
});
|
|
184
202
|
|
|
185
|
-
describe("an ambiguous name
|
|
203
|
+
describe("an ambiguous name answers with EVERY entity it reaches", () => {
|
|
186
204
|
// `ds:zeta.chip` is declared BEFORE `ds:alpha.chip` in the fixture, so the
|
|
187
|
-
// store enumerates it first while IRI order puts `alpha` first.
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
205
|
+
// store enumerates it first while IRI order puts `alpha` first. Neither of
|
|
206
|
+
// these stories declares a ranking, so the order is the total `STR(?uri)`
|
|
207
|
+
// it always was — what changed is that BOTH chips come back. Both paths,
|
|
208
|
+
// because the resolve is generated SPARQL either way and only the field
|
|
209
|
+
// fetch differs.
|
|
210
|
+
it("answers with both, best first, on the sparql path", async () => {
|
|
191
211
|
const out = await lookupVia(SPQ, "Chip");
|
|
192
|
-
expect(uris(out)).toEqual([`${DS}alpha.chip`]);
|
|
212
|
+
expect(uris(out)).toEqual([`${DS}alpha.chip`, `${DS}zeta.chip`]);
|
|
193
213
|
});
|
|
194
214
|
|
|
195
|
-
it("
|
|
215
|
+
it("answers with both, best first, on the graphql path", async () => {
|
|
196
216
|
const out = await lookupVia(GQL, "Chip");
|
|
197
|
-
expect(uris(out)).toEqual([`${DS}alpha.chip`]);
|
|
217
|
+
expect(uris(out)).toEqual([`${DS}alpha.chip`, `${DS}zeta.chip`]);
|
|
198
218
|
});
|
|
199
219
|
|
|
200
220
|
it("agrees with itself across repeated resolves", async () => {
|
|
221
|
+
// Determinism was never the property in question — `LIMIT 1` over a total
|
|
222
|
+
// order was already deterministic. It is pinned because the order now
|
|
223
|
+
// carries the ranking's judgement, and an order that varied by store
|
|
224
|
+
// would make the first result mean nothing.
|
|
201
225
|
const runs = await Promise.all([
|
|
202
226
|
lookupVia(SPQ, "Chip"),
|
|
203
227
|
lookupVia(SPQ, "chip"),
|
|
204
228
|
lookupVia(GQL, "CHIP"),
|
|
205
229
|
]);
|
|
206
230
|
expect(runs.map(uris)).toEqual([
|
|
207
|
-
[`${DS}alpha.chip`],
|
|
208
|
-
[`${DS}alpha.chip`],
|
|
209
|
-
[`${DS}alpha.chip`],
|
|
231
|
+
[`${DS}alpha.chip`, `${DS}zeta.chip`],
|
|
232
|
+
[`${DS}alpha.chip`, `${DS}zeta.chip`],
|
|
233
|
+
[`${DS}alpha.chip`, `${DS}zeta.chip`],
|
|
210
234
|
]);
|
|
211
235
|
});
|
|
236
|
+
|
|
237
|
+
it("answers with ONE entity for an unambiguous name", async () => {
|
|
238
|
+
// The plural case costs the singular one nothing: a unique name still
|
|
239
|
+
// returns an array of one, which is the payload it always had.
|
|
240
|
+
const out = await lookupVia(GQL, "Modal");
|
|
241
|
+
expect(uris(out)).toEqual([`${DS}modal`]);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("answers with ONE entity for an IRI, even a shared name's IRI", async () => {
|
|
245
|
+
// An IRI is one entity by construction, so the IRI form keeps its
|
|
246
|
+
// `LIMIT 1`: there is nothing to rank and nothing else to reach.
|
|
247
|
+
const out = await lookupVia(SPQ, "ds:zeta.chip");
|
|
248
|
+
expect(uris(out)).toEqual([`${DS}zeta.chip`]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("reaches both entities when a GLOB expands onto the shared name", async () => {
|
|
252
|
+
// The glob population is DISTINCT names, so "Chip" expands to ONE
|
|
253
|
+
// candidate — a glob was never the escape a shared name needs. Live,
|
|
254
|
+
// `block lookup 'Butt*'` listed Launchpad's Button and ButtonLink while
|
|
255
|
+
// the global Button appeared nowhere. Now the shared name reaches both
|
|
256
|
+
// through the glob for the same reason it does through a plain name.
|
|
257
|
+
const out = await lookupVia(SPQ, "Chi*");
|
|
258
|
+
expect(uris(out)).toEqual([`${DS}alpha.chip`, `${DS}zeta.chip`]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("adds no notice — the payload IS the address", async () => {
|
|
262
|
+
// An earlier draft kept the arity and named the outranked IRIs in a
|
|
263
|
+
// notice. With every entity returned there is nothing set aside to name,
|
|
264
|
+
// and a sentence restating what the payload already carries is noise.
|
|
265
|
+
// The zero-record notice is untouched; only the ambiguity one is gone.
|
|
266
|
+
expect(
|
|
267
|
+
lookupNoticeVia(SPQ, await lookupVia(SPQ, "Chip")),
|
|
268
|
+
).toBeUndefined();
|
|
269
|
+
expect(
|
|
270
|
+
lookupNoticeVia(SPQ, await lookupVia(SPQ, "Modal")),
|
|
271
|
+
).toBeUndefined();
|
|
272
|
+
});
|
|
212
273
|
});
|
|
213
274
|
});
|
|
@@ -42,6 +42,7 @@ import type {
|
|
|
42
42
|
PackChildRow,
|
|
43
43
|
PackEntity,
|
|
44
44
|
PackLookup,
|
|
45
|
+
PackRow,
|
|
45
46
|
StorySource,
|
|
46
47
|
} from "./types.js";
|
|
47
48
|
|
|
@@ -53,7 +54,15 @@ export interface LookupError {
|
|
|
53
54
|
readonly suggestions?: readonly string[];
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* The result of a (possibly multi-name) lookup.
|
|
59
|
+
*
|
|
60
|
+
* `results` may be LONGER than the arguments that produced it: a name several
|
|
61
|
+
* entities share answers with all of them, ranked. There is no companion field
|
|
62
|
+
* naming the ones not answered with, because there are none — an earlier draft
|
|
63
|
+
* kept the single-entity arity and carried the rest as IRIs in a notice, which
|
|
64
|
+
* is a sentence about an address where the payload is the address.
|
|
65
|
+
*/
|
|
57
66
|
export interface LookupOutput {
|
|
58
67
|
readonly results: PackEntity[];
|
|
59
68
|
readonly errors: LookupError[];
|
|
@@ -80,6 +89,9 @@ export async function resolveLookup(
|
|
|
80
89
|
throw PragmaError.invalidInput("names", "(empty)", {
|
|
81
90
|
recovery: cliRecovery(`${noun} list`, `List available ${noun} entries.`, {
|
|
82
91
|
tool: `${noun}_list`,
|
|
92
|
+
// A concrete, valid argument bag rather than a bare tool name: an
|
|
93
|
+
// agent can call the recovery as written instead of guessing one.
|
|
94
|
+
params: {},
|
|
83
95
|
}),
|
|
84
96
|
});
|
|
85
97
|
}
|
|
@@ -103,7 +115,7 @@ export async function resolveLookup(
|
|
|
103
115
|
const query = expanded.names[index];
|
|
104
116
|
if (query === undefined) continue;
|
|
105
117
|
if (outcome.status === "fulfilled") {
|
|
106
|
-
results.push(outcome.value);
|
|
118
|
+
results.push(...outcome.value);
|
|
107
119
|
continue;
|
|
108
120
|
}
|
|
109
121
|
const error = outcome.reason;
|
|
@@ -186,7 +198,35 @@ async function expandQueries(
|
|
|
186
198
|
return { names, globErrors };
|
|
187
199
|
}
|
|
188
200
|
|
|
189
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Look up every entity a lookup argument reaches, dispatching to the pack's
|
|
203
|
+
* declared fetch source.
|
|
204
|
+
*
|
|
205
|
+
* One argument, EVERY entity it reaches — ranked, best first.
|
|
206
|
+
*
|
|
207
|
+
* `lookup` is deliberately one tool for the singular and the plural case. An
|
|
208
|
+
* agent that had to decide up front whether a name is unique would pay two
|
|
209
|
+
* round trips to find out, and the answer is not knowable from the name: 25
|
|
210
|
+
* live block names reach two or three blocks apiece, and the caller cannot
|
|
211
|
+
* tell which until it asks. Returning them all costs the caller nothing when a
|
|
212
|
+
* name is unique — the array is one long — and saves a whole exchange when it
|
|
213
|
+
* is not.
|
|
214
|
+
*
|
|
215
|
+
* That is why the `LIMIT 1` had to go rather than be compensated for. With it,
|
|
216
|
+
* `block lookup button` answered with Launchpad's Button and gave no sign the
|
|
217
|
+
* global one existed, because `apps_launchpad…` sorts before `global…`. An
|
|
218
|
+
* earlier draft of this change kept the arity and named the losers in a notice;
|
|
219
|
+
* a notice is a sentence about an address, and the payload is the address.
|
|
220
|
+
*
|
|
221
|
+
* Ranking still matters, and matters more: it is now what ORDERS the answer
|
|
222
|
+
* rather than what silently picks it. The best row leads, so a caller reading
|
|
223
|
+
* only the first gets the entity the ranking believes was meant.
|
|
224
|
+
*
|
|
225
|
+
* The rows are collapsed to one per `?uri` first. The sparql form projects its
|
|
226
|
+
* fields in the same SELECT, so a multi-valued field yields one row per value;
|
|
227
|
+
* that has always been true and `LIMIT 1` merely hid it. Without the collapse
|
|
228
|
+
* one entity would report itself as several.
|
|
229
|
+
*/
|
|
190
230
|
async function lookupOne(
|
|
191
231
|
rt: LookupRuntime,
|
|
192
232
|
lookup: PackLookup,
|
|
@@ -195,47 +235,71 @@ async function lookupOne(
|
|
|
195
235
|
source: StorySource,
|
|
196
236
|
prefixes: Readonly<Record<string, string>>,
|
|
197
237
|
level: string | undefined,
|
|
198
|
-
): Promise<PackEntity> {
|
|
238
|
+
): Promise<PackEntity[]> {
|
|
199
239
|
const graphqlSourced = lookup.source === "graphql";
|
|
200
240
|
const rows = await runSelect(
|
|
201
241
|
rt,
|
|
202
242
|
buildResolveQuery(lookup, query, prefixes, level),
|
|
203
243
|
source,
|
|
204
244
|
);
|
|
205
|
-
const
|
|
206
|
-
if (
|
|
245
|
+
const bases = firstRowPerEntity(rows);
|
|
246
|
+
if (bases.length === 0) {
|
|
207
247
|
const candidates = await listEntityNames(rt, lookup, source);
|
|
208
248
|
throw PragmaError.notFound(noun, query, {
|
|
209
249
|
suggestions: suggestNames(query, candidates),
|
|
210
250
|
recovery: cliRecovery(`${noun} list`, `List available ${noun} entries.`, {
|
|
211
251
|
tool: `${noun}_list`,
|
|
252
|
+
// A concrete, valid argument bag rather than a bare tool name: an
|
|
253
|
+
// agent can call the recovery as written instead of guessing one.
|
|
254
|
+
params: {},
|
|
212
255
|
}),
|
|
213
256
|
});
|
|
214
257
|
}
|
|
215
258
|
|
|
216
259
|
if (graphqlSourced) {
|
|
217
|
-
return
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
260
|
+
return Promise.all(
|
|
261
|
+
bases.map((base) =>
|
|
262
|
+
fetchGraphqlLookup(
|
|
263
|
+
rt,
|
|
264
|
+
lookup,
|
|
265
|
+
String(base.uri),
|
|
266
|
+
base.name ?? query,
|
|
267
|
+
// The GraphQL lane's only use of the source is CONFIG_ERROR
|
|
268
|
+
// attribution, which is right for any origin — so it takes the label,
|
|
269
|
+
// not provenance.
|
|
270
|
+
source.label,
|
|
271
|
+
prefixes,
|
|
272
|
+
level,
|
|
273
|
+
),
|
|
274
|
+
),
|
|
227
275
|
);
|
|
228
276
|
}
|
|
229
277
|
|
|
230
|
-
const
|
|
231
|
-
for (const
|
|
232
|
-
entity
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
278
|
+
const entities: PackEntity[] = [];
|
|
279
|
+
for (const base of bases) {
|
|
280
|
+
const entity: PackEntity = { ...base };
|
|
281
|
+
for (const expand of activeExpands(lookup, level)) {
|
|
282
|
+
entity[expand.name] = (await runSelect(
|
|
283
|
+
rt,
|
|
284
|
+
buildExpandQuery(expand, String(base.uri)),
|
|
285
|
+
source,
|
|
286
|
+
)) as readonly PackChildRow[];
|
|
287
|
+
}
|
|
288
|
+
entities.push(entity);
|
|
289
|
+
}
|
|
290
|
+
return entities;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Collapse a ranked resolve to one row per entity, keeping the best-ranked. */
|
|
294
|
+
function firstRowPerEntity(rows: readonly PackRow[]): PackRow[] {
|
|
295
|
+
const seen = new Set<string>();
|
|
296
|
+
const bases: PackRow[] = [];
|
|
297
|
+
for (const row of rows) {
|
|
298
|
+
if (!row.uri || seen.has(row.uri)) continue;
|
|
299
|
+
seen.add(row.uri);
|
|
300
|
+
bases.push(row);
|
|
237
301
|
}
|
|
238
|
-
return
|
|
302
|
+
return bases;
|
|
239
303
|
}
|
|
240
304
|
|
|
241
305
|
/**
|
|
@@ -19,14 +19,39 @@ import {
|
|
|
19
19
|
resolveLookup,
|
|
20
20
|
} from "./resolveEntity.js";
|
|
21
21
|
import { parseSampleCount, pickRandom } from "./sample.js";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
applyPackFilters,
|
|
24
|
+
type FilterVocabularies,
|
|
25
|
+
} from "./sparql/applyFilters.js";
|
|
23
26
|
import { applyPackSearch } from "./sparql/applySearch.js";
|
|
24
27
|
import { runSelect } from "./sparql/runSelect.js";
|
|
25
|
-
import type {
|
|
28
|
+
import type {
|
|
29
|
+
PackFilter,
|
|
30
|
+
PackList,
|
|
31
|
+
PackLookup,
|
|
32
|
+
PackRow,
|
|
33
|
+
StorySource,
|
|
34
|
+
} from "./types.js";
|
|
26
35
|
|
|
27
36
|
/** The highest canonical level — sample fetches everything for shape discovery. */
|
|
28
37
|
const HIGHEST_LEVEL = "detailed";
|
|
29
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The recovery every lookup-shaped miss carries: browse the noun's list.
|
|
41
|
+
*
|
|
42
|
+
* `mcp.params` is populated rather than left off. A bare tool NAME is only half
|
|
43
|
+
* an instruction to an agent — it still has to guess an argument bag, and a
|
|
44
|
+
* guess that misses returns `-32602 Invalid arguments`, which reads like the
|
|
45
|
+
* recovery itself was wrong. `{}` is the concrete, valid call: every compiled
|
|
46
|
+
* list verb's params are optional, so the recovery is now copy-pasteable.
|
|
47
|
+
*/
|
|
48
|
+
function listRecovery(noun: string) {
|
|
49
|
+
return cliRecovery(`${noun} list`, `List available ${noun} entries.`, {
|
|
50
|
+
tool: `${noun}_list`,
|
|
51
|
+
params: {},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
30
55
|
/** Facts a list-shaped run body needs beyond its `shape`. */
|
|
31
56
|
export interface ListRunMeta {
|
|
32
57
|
readonly source: StorySource;
|
|
@@ -45,16 +70,64 @@ export function makeListRun(
|
|
|
45
70
|
shape: PackList,
|
|
46
71
|
meta: ListRunMeta,
|
|
47
72
|
): (params: Record<string, unknown>, rt: PragmaRuntime) => Promise<PackRow[]> {
|
|
48
|
-
return async (params, rt) =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
return async (params, rt) => {
|
|
74
|
+
const rows = await runSelect(rt, shape.query, meta.source);
|
|
75
|
+
const vocabularies = await readFilterVocabularies(
|
|
76
|
+
rt,
|
|
77
|
+
shape.filters,
|
|
78
|
+
params,
|
|
79
|
+
meta.source,
|
|
80
|
+
);
|
|
81
|
+
return applyPackSearch(
|
|
82
|
+
applyPackFilters(rows, shape.filters, params, vocabularies),
|
|
55
83
|
shape.search,
|
|
56
84
|
params,
|
|
57
85
|
);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read the declared vocabulary of each value-free filter the caller actually
|
|
91
|
+
* used.
|
|
92
|
+
*
|
|
93
|
+
* WHY a second query rather than the rows already in hand: the rows are a
|
|
94
|
+
* population, not a vocabulary. A category the graph declares with no standards
|
|
95
|
+
* filed under it, or a `ds:ConceptType` no concept uses yet, is a REAL value
|
|
96
|
+
* that appears in no row — and rejecting it as `INVALID_INPUT` contradicts both
|
|
97
|
+
* `standard categories` (which lists it, with count 0) and the documented calm
|
|
98
|
+
* empty list. The query reads the same terms the enumerating surface reads, so
|
|
99
|
+
* "the graph is the vocabulary" stays true of the graph rather than of whatever
|
|
100
|
+
* the list happened to return.
|
|
101
|
+
*
|
|
102
|
+
* Only for a filter that is DECLARED with a vocabulary, carries no `values`
|
|
103
|
+
* (a declared set is already the vocabulary), and was actually PROVIDED — an
|
|
104
|
+
* unfiltered `list` runs exactly the one query it always did.
|
|
105
|
+
*/
|
|
106
|
+
async function readFilterVocabularies(
|
|
107
|
+
rt: PragmaRuntime,
|
|
108
|
+
filters: readonly PackFilter[] | undefined,
|
|
109
|
+
params: Record<string, unknown>,
|
|
110
|
+
source: StorySource,
|
|
111
|
+
): Promise<FilterVocabularies | undefined> {
|
|
112
|
+
const needed = (filters ?? []).filter(
|
|
113
|
+
(filter) =>
|
|
114
|
+
filter.vocabulary !== undefined &&
|
|
115
|
+
filter.values === undefined &&
|
|
116
|
+
params[filter.param] !== undefined,
|
|
117
|
+
);
|
|
118
|
+
if (needed.length === 0) return undefined;
|
|
119
|
+
const resolved = new Map<string, readonly string[]>();
|
|
120
|
+
for (const filter of needed) {
|
|
121
|
+
const vocabulary = filter.vocabulary;
|
|
122
|
+
if (!vocabulary) continue;
|
|
123
|
+
const variable = vocabulary.variable ?? filter.variable;
|
|
124
|
+
const rows = await runSelect(rt, vocabulary.query, source);
|
|
125
|
+
resolved.set(
|
|
126
|
+
filter.param,
|
|
127
|
+
rows.map((row) => row[variable] ?? "").filter((value) => value !== ""),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return resolved;
|
|
58
131
|
}
|
|
59
132
|
|
|
60
133
|
/** Build the run body for a lookup verb (variadic names → resolved entities). */
|
|
@@ -88,11 +161,7 @@ export function makeLookupRun(
|
|
|
88
161
|
code: first.code as PragmaError["code"],
|
|
89
162
|
message: first.message,
|
|
90
163
|
suggestions: first.suggestions ? [...first.suggestions] : undefined,
|
|
91
|
-
recovery:
|
|
92
|
-
`${noun} list`,
|
|
93
|
-
`List available ${noun} entries.`,
|
|
94
|
-
{ tool: `${noun}_list` },
|
|
95
|
-
),
|
|
164
|
+
recovery: listRecovery(noun),
|
|
96
165
|
});
|
|
97
166
|
}
|
|
98
167
|
}
|
|
@@ -119,11 +188,7 @@ export function makeSampleRun(
|
|
|
119
188
|
if (names.length === 0) {
|
|
120
189
|
throw PragmaError.emptyResults(noun, {
|
|
121
190
|
message: `No ${noun} entries to sample.`,
|
|
122
|
-
recovery:
|
|
123
|
-
`${noun} list`,
|
|
124
|
-
`List available ${noun} entries.`,
|
|
125
|
-
{ tool: `${noun}_list` },
|
|
126
|
-
),
|
|
191
|
+
recovery: listRecovery(noun),
|
|
127
192
|
});
|
|
128
193
|
}
|
|
129
194
|
const selected = pickRandom(names, count);
|