@canonical/pragma-cli 0.36.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/bundled-skills/adoption-a1-styles/SKILL.md +24 -0
- package/bundled-skills/adoption-a2-components/SKILL.md +22 -6
- package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +1397 -327
- package/bundled-skills/anatomy-author/SKILL.md +840 -137
- package/bundled-skills/design-auditor/SKILL.md +133 -9
- package/bundled-skills/implement-component/SKILL.md +192 -23
- package/bundled-skills/specify-component/SKILL.md +124 -12
- package/bundled-skills/specify-pattern/SKILL.md +47 -8
- package/dist/package.json +23 -20
- package/dist/pragma.conf.js +963 -40
- package/dist/pragma.conf.js.map +1 -1
- package/dist/src/capabilities/capabilities/catalog.js +14 -5
- package/dist/src/capabilities/capabilities/catalog.js.map +1 -1
- package/dist/src/capabilities/capabilities/hints.js +33 -5
- package/dist/src/capabilities/capabilities/hints.js.map +1 -1
- package/dist/src/capabilities/config/show.render.js +30 -2
- package/dist/src/capabilities/config/show.render.js.map +1 -1
- package/dist/src/capabilities/create/create.verb.js +2 -1
- package/dist/src/capabilities/create/create.verb.js.map +1 -1
- package/dist/src/capabilities/create/createSurface.generated.js +23 -1
- package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
- package/dist/src/capabilities/doctor/checks/targetHealth.js +104 -14
- package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
- package/dist/src/capabilities/graph/connect.render.js +101 -0
- package/dist/src/capabilities/graph/connect.render.js.map +1 -0
- package/dist/src/capabilities/graph/connect.types.js +21 -0
- package/dist/src/capabilities/graph/connect.types.js.map +1 -0
- package/dist/src/capabilities/graph/connect.verb.js +147 -0
- package/dist/src/capabilities/graph/connect.verb.js.map +1 -0
- package/dist/src/capabilities/graph/index.js +5 -3
- package/dist/src/capabilities/graph/index.js.map +1 -1
- package/dist/src/capabilities/graph/queryText.js +206 -0
- package/dist/src/capabilities/graph/queryText.js.map +1 -0
- package/dist/src/capabilities/graph/relationIndex.js +302 -0
- package/dist/src/capabilities/graph/relationIndex.js.map +1 -0
- package/dist/src/capabilities/graph/runConnect.js +456 -0
- package/dist/src/capabilities/graph/runConnect.js.map +1 -0
- package/dist/src/capabilities/graph/runQuery.js +21 -4
- package/dist/src/capabilities/graph/runQuery.js.map +1 -1
- package/dist/src/capabilities/setup/buildPlan.js +6 -2
- package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
- package/dist/src/capabilities/setup/operations/index.js +7 -2
- package/dist/src/capabilities/setup/operations/index.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
- package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
- package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupGenerator.js +72 -14
- package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupLsp.js +372 -62
- package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupMcp.js +150 -5
- package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupSkills.js +4 -2
- package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
- package/dist/src/capabilities/setup/operations/writability.js +114 -0
- package/dist/src/capabilities/setup/operations/writability.js.map +1 -0
- package/dist/src/capabilities/setup/plan.js.map +1 -1
- package/dist/src/capabilities/setup/setup.verb.js +80 -31
- package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
- package/dist/src/capabilities/setup/targets.js +124 -42
- package/dist/src/capabilities/setup/targets.js.map +1 -1
- package/dist/src/capabilities/shared/index.js +1 -1
- package/dist/src/capabilities/shared/index.js.map +1 -1
- package/dist/src/constants.js +7 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/kernel/error/renderError.js +3 -2
- package/dist/src/kernel/error/renderError.js.map +1 -1
- package/dist/src/kernel/error/validOptions.js +41 -0
- package/dist/src/kernel/error/validOptions.js.map +1 -0
- package/dist/src/kernel/packs/compile.js +90 -6
- package/dist/src/kernel/packs/compile.js.map +1 -1
- package/dist/src/kernel/packs/cursor.js +135 -0
- package/dist/src/kernel/packs/cursor.js.map +1 -0
- package/dist/src/kernel/packs/graphql/buildLookupDocument.js +9 -2
- package/dist/src/kernel/packs/graphql/buildLookupDocument.js.map +1 -1
- package/dist/src/kernel/packs/paging.js +108 -0
- package/dist/src/kernel/packs/paging.js.map +1 -0
- package/dist/src/kernel/packs/renderPack.js +154 -5
- package/dist/src/kernel/packs/renderPack.js.map +1 -1
- package/dist/src/kernel/packs/resolveEntity.js +118 -19
- package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
- package/dist/src/kernel/packs/runBodies.js +156 -18
- package/dist/src/kernel/packs/runBodies.js.map +1 -1
- package/dist/src/kernel/packs/schema.js +160 -12
- package/dist/src/kernel/packs/schema.js.map +1 -1
- package/dist/src/kernel/packs/sparql/authorQuery.js +227 -0
- package/dist/src/kernel/packs/sparql/authorQuery.js.map +1 -0
- package/dist/src/kernel/packs/sparql/buildListQuery.js +251 -0
- package/dist/src/kernel/packs/sparql/buildListQuery.js.map +1 -0
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js +125 -22
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
- package/dist/src/kernel/packs/sparql/filterValues.js +145 -0
- package/dist/src/kernel/packs/sparql/filterValues.js.map +1 -0
- package/dist/src/kernel/packs/sparql/index.js +24 -14
- package/dist/src/kernel/packs/sparql/index.js.map +1 -1
- package/dist/src/kernel/packs/sparql/runSelect.js +10 -4
- package/dist/src/kernel/packs/sparql/runSelect.js.map +1 -1
- package/dist/src/kernel/packs/sparql/searchTerm.js +25 -0
- package/dist/src/kernel/packs/sparql/searchTerm.js.map +1 -0
- package/dist/src/kernel/packs/storyRules.js +151 -0
- package/dist/src/kernel/packs/storyRules.js.map +1 -0
- package/dist/src/kernel/packs/tierScope.js +310 -0
- package/dist/src/kernel/packs/tierScope.js.map +1 -0
- package/dist/src/kernel/packs/types.js +80 -0
- package/dist/src/kernel/packs/types.js.map +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js +10 -1
- package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
- package/dist/src/kernel/project/cli/dispatch.js +104 -3
- package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
- package/dist/src/kernel/project/cli/rootHelp.js +8 -5
- package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
- package/dist/src/kernel/project/cli/suggestNames.js +31 -7
- package/dist/src/kernel/project/cli/suggestNames.js.map +1 -1
- package/dist/src/kernel/project/mcp/registerVerb.js +5 -1
- package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
- package/dist/src/kernel/render/renderers.js +34 -2
- package/dist/src/kernel/render/renderers.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
- package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
- package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/read.js +17 -10
- package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
- package/dist/src/kernel/runtime/loadSession.js +3 -1
- package/dist/src/kernel/runtime/loadSession.js.map +1 -1
- package/dist/src/kernel/runtime/probe.js +1 -1
- package/dist/src/kernel/runtime/probe.js.map +1 -1
- package/dist/src/kernel/runtime/readEntity.js +19 -4
- package/dist/src/kernel/runtime/readEntity.js.map +1 -1
- package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
- package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
- package/dist/src/kernel/runtime/refs/resolve.js +38 -5
- package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
- package/dist/src/kernel/spec/emitReference.js +1 -1
- package/dist/src/kernel/spec/emitReference.js.map +1 -1
- package/package.json +23 -20
- package/pragma.conf.ts +983 -40
- package/src/capabilities/block.parity.test.ts +207 -27
- package/src/capabilities/block.shipped.exec.test.ts +156 -14
- package/src/capabilities/block.tierRank.exec.test.ts +46 -9
- package/src/capabilities/capabilities/catalog.ts +14 -5
- package/src/capabilities/capabilities/hints.ts +40 -5
- package/src/capabilities/config/collectConfigShow.test.ts +1 -0
- package/src/capabilities/config/set.test.ts +24 -25
- package/src/capabilities/config/show.render.test.ts +9 -4
- package/src/capabilities/config/show.render.ts +30 -2
- package/src/capabilities/create/byteEquality.test.ts +28 -1
- package/src/capabilities/create/create.test.ts +1 -0
- package/src/capabilities/create/create.verb.ts +2 -1
- package/src/capabilities/create/createGrammar.test.ts +4 -1
- package/src/capabilities/create/createSurface.generated.ts +23 -1
- package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
- package/src/capabilities/distribution.parity.test.ts +12 -11
- package/src/capabilities/distribution.test.ts +77 -7
- package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
- package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
- package/src/capabilities/doctor/checks/harnessInventory.test.ts +13 -6
- package/src/capabilities/doctor/checks/harnessInventory.ts +8 -4
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
- package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
- package/src/capabilities/doctor/checks/targetHealth.ts +114 -13
- package/src/capabilities/doctor/doctor.test.ts +327 -36
- package/src/capabilities/emptyRecovery.test.ts +84 -28
- package/src/capabilities/graph/connect.render.ts +141 -0
- package/src/capabilities/graph/connect.shipped.exec.test.ts +370 -0
- package/src/capabilities/graph/connect.test.ts +361 -0
- package/src/capabilities/graph/connect.types.ts +136 -0
- package/src/capabilities/graph/connect.verb.ts +165 -0
- package/src/capabilities/graph/index.ts +5 -3
- package/src/capabilities/graph/query.shipped.exec.test.ts +66 -0
- package/src/capabilities/graph/query.test.ts +17 -0
- package/src/capabilities/graph/queryText.test.ts +121 -0
- package/src/capabilities/graph/queryText.ts +240 -0
- package/src/capabilities/graph/relationIndex.ts +372 -0
- package/src/capabilities/graph/runConnect.ts +577 -0
- package/src/capabilities/graph/runQuery.ts +21 -6
- package/src/capabilities/lazy.test.ts +32 -2
- package/src/capabilities/listBudget.shipped.exec.test.ts +239 -0
- package/src/capabilities/listLookup.shipped.exec.test.ts +387 -0
- package/src/capabilities/listQuery.shipped.exec.test.ts +400 -0
- package/src/capabilities/resources/resources.test.ts +38 -2
- package/src/capabilities/setup/buildPlan.ts +7 -1
- package/src/capabilities/setup/operations/index.ts +14 -0
- package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
- package/src/capabilities/setup/operations/setupConfig.ts +7 -2
- package/src/capabilities/setup/operations/setupGenerator.ts +93 -15
- package/src/capabilities/setup/operations/setupLsp.ts +544 -85
- package/src/capabilities/setup/operations/setupMcp.ts +212 -4
- package/src/capabilities/setup/operations/setupSkills.ts +7 -2
- package/src/capabilities/setup/operations/writability.test.ts +151 -0
- package/src/capabilities/setup/operations/writability.ts +135 -0
- package/src/capabilities/setup/plan.ts +11 -0
- package/src/capabilities/setup/setup.test.ts +992 -16
- package/src/capabilities/setup/setup.verb.ts +84 -36
- package/src/capabilities/setup/targets.ts +152 -48
- package/src/capabilities/setup/types.ts +41 -5
- package/src/capabilities/shared/index.ts +1 -0
- package/src/capabilities/sources/sources.test.ts +55 -0
- package/src/capabilities/surface.test.ts +59 -11
- package/src/capabilities/tier.parity.test.ts +2 -2
- package/src/capabilities/token.parity.test.ts +784 -0
- package/src/capabilities/tokenGraph.cli.test.ts +279 -0
- package/src/capabilities/tokenGraph.shipped.exec.test.ts +653 -0
- package/src/capabilities/variable.parity.test.ts +448 -0
- package/src/constants.ts +8 -0
- package/src/kernel/completion/shellDrive.test.ts +32 -30
- package/src/kernel/config/defaults.test.ts +21 -2
- package/src/kernel/config/readConfig.test.ts +1 -1
- package/src/kernel/error/renderError.ts +3 -2
- package/src/kernel/error/validOptions.test.ts +57 -0
- package/src/kernel/error/validOptions.ts +45 -0
- package/src/kernel/packs/collect.test.ts +2 -0
- package/src/kernel/packs/compile.test.ts +188 -15
- package/src/kernel/packs/compile.ts +109 -14
- package/src/kernel/packs/cursor.ts +135 -0
- package/src/kernel/packs/filteredEmpty.test.ts +213 -0
- package/src/kernel/packs/graphql/buildLookupDocument.ts +16 -9
- package/src/kernel/packs/graphqlSource.test.ts +152 -0
- package/src/kernel/packs/listNotice.test.ts +156 -0
- package/src/kernel/packs/paging.ts +110 -0
- package/src/kernel/packs/renderPack.ts +186 -13
- package/src/kernel/packs/resolveEntity.test.ts +62 -3
- package/src/kernel/packs/resolveEntity.ts +186 -26
- package/src/kernel/packs/runBodies.ts +196 -26
- package/src/kernel/packs/schema.ts +180 -13
- package/src/kernel/packs/sparql/authorQuery.test.ts +179 -0
- package/src/kernel/packs/sparql/authorQuery.ts +257 -0
- package/src/kernel/packs/sparql/buildListQuery.test.ts +344 -0
- package/src/kernel/packs/sparql/buildListQuery.ts +344 -0
- package/src/kernel/packs/sparql/buildLookupQuery.test.ts +138 -0
- package/src/kernel/packs/sparql/buildLookupQuery.ts +140 -20
- package/src/kernel/packs/sparql/filterValues.test.ts +86 -0
- package/src/kernel/packs/sparql/filterValues.ts +182 -0
- package/src/kernel/packs/sparql/index.ts +30 -14
- package/src/kernel/packs/sparql/runSelect.test.ts +63 -0
- package/src/kernel/packs/sparql/runSelect.ts +42 -2
- package/src/kernel/packs/sparql/searchTerm.ts +29 -0
- package/src/kernel/packs/storyRules.ts +180 -0
- package/src/kernel/packs/tierScope.test.ts +487 -0
- package/src/kernel/packs/tierScope.ts +409 -0
- package/src/kernel/packs/toolDescription.test.ts +13 -10
- package/src/kernel/packs/types.ts +311 -9
- package/src/kernel/project/cli/buildProgram.ts +17 -1
- package/src/kernel/project/cli/dispatch.test.ts +87 -0
- package/src/kernel/project/cli/dispatch.ts +114 -4
- package/src/kernel/project/cli/rootHelp.ts +8 -5
- package/src/kernel/project/cli/suggest.test.ts +35 -0
- package/src/kernel/project/cli/suggestNames.ts +34 -7
- package/src/kernel/project/mcp/registerVerb.ts +5 -1
- package/src/kernel/render/contracts.ts +2 -0
- package/src/kernel/render/prefixes.test.ts +2 -0
- package/src/kernel/render/renderers.test.ts +43 -0
- package/src/kernel/render/renderers.ts +34 -1
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
- package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
- package/src/kernel/runtime/graphpack/embedded.ts +14 -6
- package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
- package/src/kernel/runtime/graphpack/read.ts +17 -13
- package/src/kernel/runtime/loadSession.ts +3 -1
- package/src/kernel/runtime/probe.ts +1 -1
- package/src/kernel/runtime/readEntity.ts +19 -4
- package/src/kernel/runtime/refs/parseRef.test.ts +49 -1
- package/src/kernel/runtime/refs/parseRef.ts +25 -1
- package/src/kernel/runtime/refs/resolve.ts +39 -5
- package/src/kernel/runtime/types.ts +30 -0
- package/src/kernel/spec/emitReference.ts +1 -1
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/kernel/spec/types.ts +17 -0
- package/src/testing/PARITY_GAPS.ts +4 -4
- package/src/testing/behavioral/agentSession.mcp.test.ts +43 -39
- package/src/testing/behavioral/completion.test.ts +7 -6
- package/src/testing/behavioral/disclosure.test.ts +39 -37
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +82 -64
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +11 -10
- package/src/testing/behavioral/journeys.cli.test.ts +71 -32
- package/src/testing/behavioral/journeys.defaultPack.test.ts +6 -4
- package/src/testing/behavioral/journeys.firstInstall.test.ts +70 -5
- package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +4 -3
- package/src/testing/behavioral/journeys.packageStories.test.ts +2 -1
- package/src/testing/behavioral/mcpSurface.test.ts +28 -25
- package/src/testing/behavioral/parity.test.ts +25 -23
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +20 -0
- package/src/testing/eval/cases/effectful.ts +7 -1
- package/src/testing/eval/cases/readNouns.ts +100 -11
- package/src/testing/fixtures/blockGraph.ts +89 -5
- package/src/testing/fixtures/connectGraph.ts +159 -0
- package/src/testing/fixtures/graph/canonical.ts +128 -32
- package/src/testing/helpers/fsProbe.ts +88 -0
- package/src/testing/helpers/runCli.test.ts +44 -0
- package/src/testing/helpers/runCli.ts +27 -3
- package/src/testing/perf/budgets.test.ts +3 -1
- package/src/testing/perf/budgets.ts +138 -54
- package/dist/src/kernel/packs/sparql/applyFilters.js +0 -167
- package/dist/src/kernel/packs/sparql/applyFilters.js.map +0 -1
- package/dist/src/kernel/packs/sparql/applySearch.js +0 -25
- package/dist/src/kernel/packs/sparql/applySearch.js.map +0 -1
- package/src/kernel/packs/sparql/applyFilters.ts +0 -203
- package/src/kernel/packs/sparql/applySearch.ts +0 -33
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The response-size budget for a list-shaped answer, measured over the SHIPPED
|
|
3
|
+
* pack (PROTECTED).
|
|
4
|
+
*
|
|
5
|
+
* No list had a size budget before this. The five enforced performance
|
|
6
|
+
* constants are all latency, none of them runs a story's list query, and the
|
|
7
|
+
* only size ceiling in the package covered the MCP resource listing rather than
|
|
8
|
+
* a verb — so a story could grow its payload without limit and nothing turned
|
|
9
|
+
* red. The largest answer the distribution ships today is 78 KB from one call,
|
|
10
|
+
* and an agent pays for it twice: once in transfer, once in the context window
|
|
11
|
+
* it can no longer spend on the task.
|
|
12
|
+
*
|
|
13
|
+
* WHY A BUDGET AND NOT JUST PAGINATION. `--limit` gives a caller a way to ask
|
|
14
|
+
* for less. A budget is what fails the build when a story forgets that its
|
|
15
|
+
* DEFAULT answer is the one every speculative call gets. The two are
|
|
16
|
+
* complementary: the default page (500 rows) bounds the row count, and this
|
|
17
|
+
* bounds the bytes those rows turn into — because a row's width is a story's
|
|
18
|
+
* choice and no row count implies a payload size.
|
|
19
|
+
*
|
|
20
|
+
* WHERE IT IS ENFORCED. Where the payload is BUILT — the `json` formatter, the
|
|
21
|
+
* one seam both machine surfaces serialise through (`--format json` parses it
|
|
22
|
+
* into the envelope; the MCP tool result parses it into `toolSuccess`). Not in
|
|
23
|
+
* the timed perf pass: that pass is deliberately not run by CI (owner ruling
|
|
24
|
+
* 2026-08-30), and a budget nothing runs is not a gate. This is the same choice
|
|
25
|
+
* the resource listing's ceiling made, for the same reason.
|
|
26
|
+
*
|
|
27
|
+
* WHAT IT DOES NOT COVER. Latency. Declaring stories is measurably free — when
|
|
28
|
+
* five read stories moved into the distribution's configuration and grew it
|
|
29
|
+
* from 55 to 513 lines, the fast-path deltas straddled zero — and no latency
|
|
30
|
+
* constant moves for this either.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
34
|
+
import { compileStoryModule } from "../kernel/packs/compile.js";
|
|
35
|
+
import { DEFAULT_LIST_LIMIT, MAX_LIST_WINDOW } from "../kernel/packs/paging.js";
|
|
36
|
+
import {
|
|
37
|
+
distributionSource,
|
|
38
|
+
type PackList,
|
|
39
|
+
type PackPage,
|
|
40
|
+
} from "../kernel/packs/types.js";
|
|
41
|
+
import { verbKey } from "../kernel/packs/uniqueness.js";
|
|
42
|
+
import { bootRuntime } from "../kernel/runtime/boot.js";
|
|
43
|
+
import type { PragmaRuntime } from "../kernel/runtime/types.js";
|
|
44
|
+
import type { VerbSpec } from "../kernel/spec/types.js";
|
|
45
|
+
import { TEST_FLAGS } from "../testing/helpers/projectCli.js";
|
|
46
|
+
import { declaredStories } from "./distribution.js";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The ceiling on ONE list-shaped answer, in bytes of the serialised payload.
|
|
50
|
+
*
|
|
51
|
+
* **Derived from measurement, 2026-09-10**, over the pack the distribution
|
|
52
|
+
* actually ships: the largest answer is `standard list` at 147 rows and 78,129
|
|
53
|
+
* bytes, whose rows are fat rather than many, and the most ROWS is `block list`
|
|
54
|
+
* at 252 for 32,956 bytes. 1.6 × 78,129 = 125,006, rounded to 125,000 — the
|
|
55
|
+
* same shape of derivation the resource listing used (60,000 → 100,000 at a
|
|
56
|
+
* measured 65,119, i.e. 1.54×), and headroom enough that ordinary upstream
|
|
57
|
+
* growth in the code-standards pack does not turn this red on a pack bump.
|
|
58
|
+
* Every figure and the full table are in BUDGETS.md.
|
|
59
|
+
*
|
|
60
|
+
* **When this is next reached, the fix is not a bigger number.** A ceiling
|
|
61
|
+
* raised on demand is a formality. The fix is either a narrower default page
|
|
62
|
+
* for the story that blew it — which is the kernel decision K-1 deliberately
|
|
63
|
+
* left un-taken, there being no story that needs it yet — or narrower columns,
|
|
64
|
+
* which trades an honest surface for an arithmetic one and is the worse of the
|
|
65
|
+
* two. Whichever is chosen, it is a decision with a reason, which is what a
|
|
66
|
+
* budget exists to force.
|
|
67
|
+
*/
|
|
68
|
+
const LIST_PAYLOAD_BUDGET_BYTES = 125_000;
|
|
69
|
+
|
|
70
|
+
const SOURCE = distributionSource("pragma.conf.ts");
|
|
71
|
+
|
|
72
|
+
/** One list-shaped body of a declared story, with the verb that runs it. */
|
|
73
|
+
interface Body {
|
|
74
|
+
readonly label: string;
|
|
75
|
+
readonly verb: VerbSpec;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Every list-shaped body the distribution declares, DERIVED from the config —
|
|
80
|
+
* a story declared tomorrow is held to this budget by the author who declares
|
|
81
|
+
* it, without their help and without an entry in a list here.
|
|
82
|
+
*/
|
|
83
|
+
const BODIES: readonly Body[] = [...declaredStories].flatMap(
|
|
84
|
+
([noun, story]) => {
|
|
85
|
+
const module = compileStoryModule(story, SOURCE, {});
|
|
86
|
+
const find = (verb: string): Body => {
|
|
87
|
+
const found = module.verbs.find(
|
|
88
|
+
(v) => verbKey(v.path) === `${noun} ${verb}`,
|
|
89
|
+
);
|
|
90
|
+
if (!found) throw new Error(`no compiled "${noun} ${verb}" verb`);
|
|
91
|
+
return { label: `${noun} ${verb}`, verb: found };
|
|
92
|
+
};
|
|
93
|
+
const bodies: Body[] = [];
|
|
94
|
+
if ((story.list as PackList | undefined) !== undefined) {
|
|
95
|
+
bodies.push(find("list"));
|
|
96
|
+
}
|
|
97
|
+
for (const verb of story.verbs ?? []) bodies.push(find(verb.verb));
|
|
98
|
+
return bodies;
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
let rt: PragmaRuntime;
|
|
103
|
+
beforeAll(async () => {
|
|
104
|
+
rt = bootRuntime(TEST_FLAGS);
|
|
105
|
+
await rt.store.get();
|
|
106
|
+
}, 60_000);
|
|
107
|
+
|
|
108
|
+
afterAll(async () => {
|
|
109
|
+
(await rt.store.get()).store.dispose();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The payload one call transmits, in bytes.
|
|
114
|
+
*
|
|
115
|
+
* The `json` formatter's own output is indented — it is read by humans through
|
|
116
|
+
* `--format json | jq` as often as by machines — but neither machine surface
|
|
117
|
+
* sends those spaces: both parse it and re-serialise into their envelope. So
|
|
118
|
+
* the budget measures what crosses the wire, not what the formatter's string
|
|
119
|
+
* happens to weigh.
|
|
120
|
+
*/
|
|
121
|
+
async function payloadBytes(
|
|
122
|
+
body: Body,
|
|
123
|
+
params: Record<string, unknown>,
|
|
124
|
+
): Promise<{ rows: number; bytes: number }> {
|
|
125
|
+
const page = (await body.verb.run(params, rt)) as { rows: unknown[] };
|
|
126
|
+
const built = body.verb.output.formatters.json(page as never);
|
|
127
|
+
return {
|
|
128
|
+
rows: page.rows.length,
|
|
129
|
+
bytes: Buffer.byteLength(JSON.stringify(JSON.parse(built))),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
describe("list response budget, shipped pack (PROTECTED)", () => {
|
|
134
|
+
it("derives every list-shaped body from the config", () => {
|
|
135
|
+
// A guard, not a no-op: an empty derivation would make the sweep below
|
|
136
|
+
// vacuously green, which is the failure mode a budget can least afford.
|
|
137
|
+
expect(BODIES.map((body) => body.label)).toContain("standard list");
|
|
138
|
+
expect(BODIES.length).toBeGreaterThanOrEqual(9);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it.each(BODIES.map((body) => [body.label, body] as const))(
|
|
142
|
+
"%s: the default answer is inside the budget",
|
|
143
|
+
async (_label, body) => {
|
|
144
|
+
const { rows, bytes } = await payloadBytes(body, {});
|
|
145
|
+
expect(bytes).toBeLessThanOrEqual(LIST_PAYLOAD_BUDGET_BYTES);
|
|
146
|
+
// The row half of the budget: the default page is the ceiling, and a
|
|
147
|
+
// body answering more of them would mean the page was not applied.
|
|
148
|
+
expect(rows).toBeLessThanOrEqual(DEFAULT_LIST_LIMIT);
|
|
149
|
+
},
|
|
150
|
+
60_000,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
it.each(BODIES.map((body) => [body.label, body] as const))(
|
|
154
|
+
"%s: a population past the budget is PAGED, and each page is inside it",
|
|
155
|
+
async (_label, body) => {
|
|
156
|
+
// This case used to assert that the whole population also fitted one
|
|
157
|
+
// answer, and recorded that the two measurements were the same number
|
|
158
|
+
// because every story fitted one default page. Its own note said the day
|
|
159
|
+
// a story outgrew the page they would diverge and that this assertion
|
|
160
|
+
// would be what said so. They have diverged: the token-graph stories
|
|
161
|
+
// brought populations of 745, 1,156, 1,237 and 1,746 rows, and asking one
|
|
162
|
+
// of them for everything at once now measures 158 KB to 273 KB.
|
|
163
|
+
//
|
|
164
|
+
// So the claim moves to the one that survives, and it is the stronger of
|
|
165
|
+
// the two for exactly the bodies that broke the old one. The budget
|
|
166
|
+
// bounds an ANSWER, never a population — a population outgrowing it is
|
|
167
|
+
// what pagination is for, and the promise is that every page a caller can
|
|
168
|
+
// reach is inside the budget and that the pages together are the whole
|
|
169
|
+
// answer. A story still inside the budget is held to the old assertion
|
|
170
|
+
// unchanged, so nothing is given up where nothing had to be.
|
|
171
|
+
const whole = await payloadBytes(body, { limit: MAX_LIST_WINDOW });
|
|
172
|
+
if (whole.bytes <= LIST_PAYLOAD_BUDGET_BYTES) {
|
|
173
|
+
expect(whole.bytes).toBeLessThanOrEqual(LIST_PAYLOAD_BUDGET_BYTES);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// Past the budget: every page inside it, and the pages exhaust the
|
|
177
|
+
// population rather than stopping short of it.
|
|
178
|
+
let after: string | undefined;
|
|
179
|
+
let seen = 0;
|
|
180
|
+
let pages = 0;
|
|
181
|
+
do {
|
|
182
|
+
const page = (await body.verb.run(
|
|
183
|
+
after === undefined ? {} : { after },
|
|
184
|
+
rt,
|
|
185
|
+
)) as PackPage;
|
|
186
|
+
const bytes = Buffer.byteLength(
|
|
187
|
+
JSON.stringify(
|
|
188
|
+
JSON.parse(body.verb.output.formatters.json(page as never)),
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
expect(
|
|
192
|
+
bytes,
|
|
193
|
+
`page ${pages + 1} of ${body.label} is outside the budget`,
|
|
194
|
+
).toBeLessThanOrEqual(LIST_PAYLOAD_BUDGET_BYTES);
|
|
195
|
+
expect(page.rows.length).toBeLessThanOrEqual(DEFAULT_LIST_LIMIT);
|
|
196
|
+
seen += page.rows.length;
|
|
197
|
+
after = page.nextAfter;
|
|
198
|
+
pages += 1;
|
|
199
|
+
// A guard against a cursor that never terminates: the kernel's own row
|
|
200
|
+
// ceiling bounds how far a caller may walk, so a walk needing more
|
|
201
|
+
// pages than that is a defect in the cursor rather than a big corpus.
|
|
202
|
+
expect(pages).toBeLessThan(MAX_LIST_WINDOW / DEFAULT_LIST_LIMIT);
|
|
203
|
+
} while (after !== undefined);
|
|
204
|
+
expect(pages).toBeGreaterThan(1);
|
|
205
|
+
expect(seen).toBe(whole.rows);
|
|
206
|
+
},
|
|
207
|
+
120_000,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
it("the budget has headroom over the largest answer, and is not slack", async () => {
|
|
211
|
+
// A ceiling that no longer bounds anything catches nothing, and one sitting
|
|
212
|
+
// on the measurement flakes. Both halves are asserted so a future edit to
|
|
213
|
+
// the constant has to face them.
|
|
214
|
+
//
|
|
215
|
+
// The effective headroom is the 0.8 factor, not the 1.6 the ceiling was
|
|
216
|
+
// derived from: the largest answer may grow by 28 per cent before this
|
|
217
|
+
// turns red, and BUDGETS.md says 28 per cent for that reason. The two
|
|
218
|
+
// numbers answer different questions — 1.6 is how the ceiling was chosen,
|
|
219
|
+
// 0.8 is what the suite enforces — and the tighter one is the gate.
|
|
220
|
+
const measured = await Promise.all(
|
|
221
|
+
BODIES.map((body) => payloadBytes(body, {})),
|
|
222
|
+
);
|
|
223
|
+
const largest = Math.max(...measured.map((entry) => entry.bytes));
|
|
224
|
+
expect(largest).toBeGreaterThan(LIST_PAYLOAD_BUDGET_BYTES / 4);
|
|
225
|
+
expect(largest).toBeLessThan(LIST_PAYLOAD_BUDGET_BYTES * 0.8);
|
|
226
|
+
}, 60_000);
|
|
227
|
+
|
|
228
|
+
it("the kernel's row ceiling cannot ask for an answer above the budget", () => {
|
|
229
|
+
// The arithmetic that makes `MAX_LIST_WINDOW` a derived number rather than
|
|
230
|
+
// a taste: the narrowest row a story can serialise is `{}` and its
|
|
231
|
+
// separating comma, three bytes, so a limit above BUDGET/3 could not
|
|
232
|
+
// produce a legal answer whatever the story's columns are — and a limit
|
|
233
|
+
// that cannot produce a legal answer is not a legal limit. Asserted here
|
|
234
|
+
// because the ceiling lives in `paging.ts`, which cannot import a test.
|
|
235
|
+
expect(MAX_LIST_WINDOW * 3).toBeLessThanOrEqual(LIST_PAYLOAD_BUDGET_BYTES);
|
|
236
|
+
// And it is not so tight that it refuses a caller who wants everything.
|
|
237
|
+
expect(MAX_LIST_WINDOW).toBeGreaterThan(DEFAULT_LIST_LIMIT);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The list→lookup grammar, EXECUTED against the shipped pack, for every noun
|
|
3
|
+
* that has both halves (PROTECTED).
|
|
4
|
+
*
|
|
5
|
+
* The tool descriptions instruct an agent to take a row's `name` VERBATIM from
|
|
6
|
+
* `<noun>_list` to `<noun>_lookup`. That promise broke for `standard` when the
|
|
7
|
+
* shipped data and the story keyed the two halves on different properties:
|
|
8
|
+
* `standard list` published names synthesized from the IRI while
|
|
9
|
+
* `standard lookup` answered only the ~13% of standards carrying an asserted
|
|
10
|
+
* `cs:name` — a closed loop where every published address missed (#1047). The
|
|
11
|
+
* fixture suites could not see it, because a fixture obligingly asserts
|
|
12
|
+
* whatever the story reads; only the corpus the release actually ships can
|
|
13
|
+
* falsify the promise the release actually makes.
|
|
14
|
+
*
|
|
15
|
+
* So this file boots the SHIPPED graph and asserts the round trip over the
|
|
16
|
+
* WHOLE corpus, not a sample, for every noun with a list+lookup pair — the
|
|
17
|
+
* defect class is not standard-specific, since every pair shares this
|
|
18
|
+
* machinery. Three guarantees per noun:
|
|
19
|
+
*
|
|
20
|
+
* - every name `list` publishes resolves through `lookup` (executed through
|
|
21
|
+
* the real verb bodies, so glob/IRI argument-shape dispatch is exercised
|
|
22
|
+
* with the very strings an agent would paste);
|
|
23
|
+
* - every published name is in the addressable population (`listEntityNames`
|
|
24
|
+
* — the ONE pool that feeds miss-suggestions, glob expansion AND `sample`'s
|
|
25
|
+
* draw), so a suggestion can never offer a name the resolve cannot answer;
|
|
26
|
+
* - for a story that derives names (`nameFallback: "iri"`), that pool covers
|
|
27
|
+
* the WHOLE class population — `sample` is presented as "see real data
|
|
28
|
+
* shapes", and drawing only from the subset carrying an asserted name would
|
|
29
|
+
* be the #1047 defect wearing sample's clothes.
|
|
30
|
+
*
|
|
31
|
+
* The final describe is the negative control: the same assertions run against
|
|
32
|
+
* a fixture compiled in the exact pre-#1047 shape (a list that synthesizes
|
|
33
|
+
* names over a lookup that requires the asserted property) and are shown to
|
|
34
|
+
* FAIL. A guard whose failing case no longer fails is worse than no guard.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
38
|
+
import { compileStoryModule } from "../kernel/packs/compile.js";
|
|
39
|
+
import { DEFAULT_LIST_LIMIT, MAX_LIST_WINDOW } from "../kernel/packs/paging.js";
|
|
40
|
+
import { listEntityNames } from "../kernel/packs/resolveEntity.js";
|
|
41
|
+
import {
|
|
42
|
+
distributionSource,
|
|
43
|
+
type PackDefinition,
|
|
44
|
+
type PackPage,
|
|
45
|
+
} from "../kernel/packs/types.js";
|
|
46
|
+
import { verbKey } from "../kernel/packs/uniqueness.js";
|
|
47
|
+
import { bootRuntime } from "../kernel/runtime/boot.js";
|
|
48
|
+
import type { PragmaRuntime } from "../kernel/runtime/types.js";
|
|
49
|
+
import type { CapabilityModule, VerbSpec } from "../kernel/spec/types.js";
|
|
50
|
+
import { buildFixtureRuntime } from "../testing/helpers/packRuntime.js";
|
|
51
|
+
import { TEST_FLAGS } from "../testing/helpers/projectCli.js";
|
|
52
|
+
import { declaredStories, storyModules } from "./distribution.js";
|
|
53
|
+
|
|
54
|
+
/** The provenance label the distribution's own compiled stories carry. */
|
|
55
|
+
const SOURCE = distributionSource("pragma.conf.ts");
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Every noun declaring BOTH halves of the grammar, derived from the config
|
|
59
|
+
* rather than listed — a story added tomorrow is covered the moment it is
|
|
60
|
+
* declared, by the author who declares it and without their help.
|
|
61
|
+
*/
|
|
62
|
+
const NOUNS: readonly string[] = [...declaredStories]
|
|
63
|
+
.filter(([, story]) => story.list !== undefined && story.lookup !== undefined)
|
|
64
|
+
.map(([noun]) => noun);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Nouns whose shipped corpus is empty TODAY — a data-content gap this suite
|
|
68
|
+
* does not own.
|
|
69
|
+
*
|
|
70
|
+
* EMPTY, and that is the allowlist working as designed rather than a list
|
|
71
|
+
* nobody maintains. `token` was its only entry, because the noun addressed
|
|
72
|
+
* `ds:Token` — a class no shipped graph asserted. Repointed at the token
|
|
73
|
+
* SYMBOLS, and with the token-ontology pack now publishing the name literal
|
|
74
|
+
* both halves of the grammar key on, the noun answers 745 rows and the entry
|
|
75
|
+
* had to go: an entry here is ASSERTED to have zero rows, so it goes red the
|
|
76
|
+
* moment the corpus arrives.
|
|
77
|
+
*
|
|
78
|
+
* The allowlist EXPIRES BY CONSTRUCTION, which is why it is kept rather than
|
|
79
|
+
* deleted. A list that merely skipped the non-empty assertion would be a
|
|
80
|
+
* permanent blind spot — a noun gains data, nobody removes the entry, a later
|
|
81
|
+
* regression back to zero rows stays vacuously green — which is exactly the
|
|
82
|
+
* hidden-empty case this guard exists to prevent, sitting inside the guard
|
|
83
|
+
* itself.
|
|
84
|
+
*/
|
|
85
|
+
const EMPTY_CORPUS_TODAY: readonly string[] = [];
|
|
86
|
+
|
|
87
|
+
/** The verb `<noun> <verb>` from a compiled module, or throw naming the gap. */
|
|
88
|
+
function verbOf(
|
|
89
|
+
module: CapabilityModule,
|
|
90
|
+
noun: string,
|
|
91
|
+
verb: string,
|
|
92
|
+
): VerbSpec {
|
|
93
|
+
const found = module.verbs.find((v) => verbKey(v.path) === `${noun} ${verb}`);
|
|
94
|
+
if (!found) throw new Error(`no compiled "${noun} ${verb}" verb`);
|
|
95
|
+
return found;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Run `<noun> list` and return every name it publishes, verbatim, deduped.
|
|
100
|
+
*
|
|
101
|
+
* WALKS THE PAGES, and it has to. This file's promise is the round trip over the
|
|
102
|
+
* WHOLE corpus rather than a sample, and that used to come free: every declared
|
|
103
|
+
* population fitted inside one default page, so one call was the corpus. Three
|
|
104
|
+
* populations now exceed it — the token symbols, the platform variables and the
|
|
105
|
+
* chain — so reading one page would quietly turn this suite into a test of the
|
|
106
|
+
* first 300 names in alphabetical order, which is the sampling it exists to
|
|
107
|
+
* refuse.
|
|
108
|
+
*
|
|
109
|
+
* The walk is bounded by the kernel's own row ceiling for the same reason the
|
|
110
|
+
* budget sweep's is: a list needing more pages than that is a cursor defect, not
|
|
111
|
+
* a large corpus, and an unbounded `while` over a broken cursor would hang the
|
|
112
|
+
* suite instead of failing it.
|
|
113
|
+
*/
|
|
114
|
+
async function publishedNames(
|
|
115
|
+
rt: PragmaRuntime,
|
|
116
|
+
module: CapabilityModule,
|
|
117
|
+
noun: string,
|
|
118
|
+
): Promise<string[]> {
|
|
119
|
+
const verb = verbOf(module, noun, "list");
|
|
120
|
+
const names = new Set<string>();
|
|
121
|
+
let after: string | undefined;
|
|
122
|
+
let pages = 0;
|
|
123
|
+
do {
|
|
124
|
+
const page = (await verb.run(
|
|
125
|
+
after === undefined ? {} : { after },
|
|
126
|
+
rt,
|
|
127
|
+
)) as PackPage;
|
|
128
|
+
for (const row of page.rows) {
|
|
129
|
+
const name = row.name ?? "";
|
|
130
|
+
if (name !== "") names.add(name);
|
|
131
|
+
}
|
|
132
|
+
after = page.nextAfter;
|
|
133
|
+
pages += 1;
|
|
134
|
+
if (pages > MAX_LIST_WINDOW / DEFAULT_LIST_LIMIT) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`${noun} list did not terminate after ${pages} pages — the cursor is not advancing`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
} while (after !== undefined);
|
|
140
|
+
return [...names];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Take every published name VERBATIM through the real lookup verb, exactly as
|
|
145
|
+
* the tool description instructs an agent to, and return the per-name errors.
|
|
146
|
+
* One batched call: the resolver treats each name independently, and a total
|
|
147
|
+
* miss throws — surfaced as one synthetic error so the assertion that reads
|
|
148
|
+
* this stays a set comparison rather than a try/catch.
|
|
149
|
+
*/
|
|
150
|
+
async function lookupErrors(
|
|
151
|
+
rt: PragmaRuntime,
|
|
152
|
+
module: CapabilityModule,
|
|
153
|
+
noun: string,
|
|
154
|
+
names: readonly string[],
|
|
155
|
+
): Promise<{ query: string; code: string }[]> {
|
|
156
|
+
if (names.length === 0) return [];
|
|
157
|
+
try {
|
|
158
|
+
const output = (await verbOf(module, noun, "lookup").run(
|
|
159
|
+
{ name: [...names] },
|
|
160
|
+
rt,
|
|
161
|
+
)) as { errors: { query: string; code: string }[] };
|
|
162
|
+
return output.errors;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return [
|
|
165
|
+
{
|
|
166
|
+
query: `(all ${names.length})`,
|
|
167
|
+
code: error instanceof Error ? error.message : String(error),
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ONE store for the file — booting the shipped pack is the expensive part, and
|
|
174
|
+
// this package already contends under parallel runs.
|
|
175
|
+
let rt: PragmaRuntime;
|
|
176
|
+
beforeAll(async () => {
|
|
177
|
+
rt = bootRuntime(TEST_FLAGS);
|
|
178
|
+
await rt.store.get();
|
|
179
|
+
}, 60_000);
|
|
180
|
+
afterAll(async () => {
|
|
181
|
+
(await rt.store.get()).store.dispose();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
/** One scalar from the shipped graph, queried directly. */
|
|
185
|
+
async function scalar(query: string): Promise<string | undefined> {
|
|
186
|
+
const result = await rt.query.sparql(query);
|
|
187
|
+
if (result.type !== "select") return undefined;
|
|
188
|
+
const row = result.bindings[0];
|
|
189
|
+
return row === undefined ? undefined : Object.values(row)[0];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
describe("every list-published name resolves through lookup, whole corpus (PROTECTED)", () => {
|
|
193
|
+
it("derives at least one list+lookup noun from the config", () => {
|
|
194
|
+
expect(NOUNS.length).toBeGreaterThan(0);
|
|
195
|
+
// The two nouns this guarantee has already been broken for / leaned on.
|
|
196
|
+
expect(NOUNS).toContain("standard");
|
|
197
|
+
expect(NOUNS).toContain("block");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it.each(NOUNS.map((noun) => [noun] as const))(
|
|
201
|
+
"%s: every published name round-trips, and its corpus is not silently empty",
|
|
202
|
+
async (noun) => {
|
|
203
|
+
const module = storyModules.get(noun) as CapabilityModule;
|
|
204
|
+
const names = await publishedNames(rt, module, noun);
|
|
205
|
+
if (EMPTY_CORPUS_TODAY.includes(noun)) {
|
|
206
|
+
// Asserted empty, not skipped: this is what expires the allowlist.
|
|
207
|
+
expect(
|
|
208
|
+
names,
|
|
209
|
+
`${noun} is allowlisted as an empty corpus but published rows — ` +
|
|
210
|
+
"delete it from EMPTY_CORPUS_TODAY so a later regression to zero rows cannot hide behind the entry",
|
|
211
|
+
).toEqual([]);
|
|
212
|
+
} else {
|
|
213
|
+
expect(
|
|
214
|
+
names.length,
|
|
215
|
+
`${noun} list published no rows at all from the shipped pack`,
|
|
216
|
+
).toBeGreaterThan(0);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const errors = await lookupErrors(rt, module, noun, names);
|
|
220
|
+
expect(
|
|
221
|
+
errors,
|
|
222
|
+
`${noun} list published ${errors.length} name(s) its own lookup cannot resolve — ` +
|
|
223
|
+
"the two-step grammar the tool descriptions document is broken for every agent following it",
|
|
224
|
+
).toEqual([]);
|
|
225
|
+
},
|
|
226
|
+
120_000,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
it.each(NOUNS.map((noun) => [noun] as const))(
|
|
230
|
+
"%s: every published name is in the pool that feeds suggestions, globs and sample",
|
|
231
|
+
async (noun) => {
|
|
232
|
+
const story = declaredStories.get(noun) as PackDefinition;
|
|
233
|
+
const lookup = story.lookup;
|
|
234
|
+
if (!lookup) throw new Error(`no "${noun}" lookup`);
|
|
235
|
+
const module = storyModules.get(noun) as CapabilityModule;
|
|
236
|
+
|
|
237
|
+
const names = await publishedNames(rt, module, noun);
|
|
238
|
+
const pool = await listEntityNames(rt, lookup, SOURCE);
|
|
239
|
+
// The resolve matches case-insensitively; hold the pool to the same bar.
|
|
240
|
+
const addressable = new Set(pool.map((name) => name.toLowerCase()));
|
|
241
|
+
const missing = names.filter(
|
|
242
|
+
(name) => !addressable.has(name.toLowerCase()),
|
|
243
|
+
);
|
|
244
|
+
expect(
|
|
245
|
+
missing,
|
|
246
|
+
`${noun}: published but not addressable — a miss-suggestion or glob over this pool can hand out none of these, and sample can never draw them`,
|
|
247
|
+
).toEqual([]);
|
|
248
|
+
},
|
|
249
|
+
60_000,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
it.each(
|
|
253
|
+
NOUNS.filter(
|
|
254
|
+
(noun) => declaredStories.get(noun)?.lookup?.nameFallback === "iri",
|
|
255
|
+
).map((noun) => [noun] as const),
|
|
256
|
+
)(
|
|
257
|
+
"%s: the derived-name pool covers the WHOLE class population (sample is representative)",
|
|
258
|
+
async (noun) => {
|
|
259
|
+
const story = declaredStories.get(noun) as PackDefinition;
|
|
260
|
+
const lookup = story.lookup;
|
|
261
|
+
if (!lookup?.type && !lookup?.types?.length) {
|
|
262
|
+
throw new Error(`"${noun}" derives names without a class constraint`);
|
|
263
|
+
}
|
|
264
|
+
const types = (lookup.types ?? [lookup.type as string]).join(" ");
|
|
265
|
+
// The pool is DISTINCT NAMES, and a name may be legitimately shared (two
|
|
266
|
+
// shipped standards carry the same upstream rdfs:label; the ranked
|
|
267
|
+
// resolve answers a shared name with every entity it reaches). So the
|
|
268
|
+
// full-population claim is counted the way the pool is built: one name
|
|
269
|
+
// per asserted `by` value, plus one derived name per instance carrying
|
|
270
|
+
// none. A pool smaller than that is the #1047 defect — sample and
|
|
271
|
+
// suggestions drawing from the labelled subset only. (A prose label
|
|
272
|
+
// colliding with a slash-derived name would make this one too strict;
|
|
273
|
+
// that collision would itself be a data defect worth the red bar.)
|
|
274
|
+
const labelled = Number(
|
|
275
|
+
await scalar(
|
|
276
|
+
[
|
|
277
|
+
"SELECT (COUNT(DISTINCT ?name) AS ?n) WHERE {",
|
|
278
|
+
` VALUES ?class { ${types} }`,
|
|
279
|
+
` ?uri a ?class ; ${lookup.by} ?name .`,
|
|
280
|
+
"}",
|
|
281
|
+
].join("\n"),
|
|
282
|
+
),
|
|
283
|
+
);
|
|
284
|
+
const bare = Number(
|
|
285
|
+
await scalar(
|
|
286
|
+
[
|
|
287
|
+
"SELECT (COUNT(DISTINCT ?uri) AS ?n) WHERE {",
|
|
288
|
+
` VALUES ?class { ${types} }`,
|
|
289
|
+
" ?uri a ?class .",
|
|
290
|
+
` FILTER NOT EXISTS { ?uri ${lookup.by} ?any . }`,
|
|
291
|
+
"}",
|
|
292
|
+
].join("\n"),
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
const pool = await listEntityNames(rt, lookup, SOURCE);
|
|
296
|
+
|
|
297
|
+
expect(labelled + bare).toBeGreaterThan(0);
|
|
298
|
+
expect(
|
|
299
|
+
pool.length,
|
|
300
|
+
`${noun}: ${labelled} asserted name(s) + ${bare} instance(s) with none, but only ` +
|
|
301
|
+
`${pool.length} addressable names — sample's draw pool is an unrepresentative slice of the corpus`,
|
|
302
|
+
).toBe(labelled + bare);
|
|
303
|
+
},
|
|
304
|
+
60_000,
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe("negative control: the pre-#1047 shape fails these assertions", () => {
|
|
309
|
+
// The exact divergence that shipped: a list that COALESCEs an OPTIONAL
|
|
310
|
+
// display name with an IRI-derived one, over a lookup that requires the
|
|
311
|
+
// asserted property (no nameFallback). One fixture standard carries the
|
|
312
|
+
// property; the other is published under a name the lookup cannot reach.
|
|
313
|
+
const PREFIXES = {
|
|
314
|
+
rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
315
|
+
cs: "http://pragma.canonical.com/codestandards#",
|
|
316
|
+
};
|
|
317
|
+
const TTL = `
|
|
318
|
+
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
|
319
|
+
@prefix owl: <http://www.w3.org/2002/07/owl#> .
|
|
320
|
+
@prefix cs: <http://pragma.canonical.com/codestandards#> .
|
|
321
|
+
|
|
322
|
+
cs:name a owl:DatatypeProperty .
|
|
323
|
+
cs:description a owl:DatatypeProperty .
|
|
324
|
+
|
|
325
|
+
cs:react.component.tsdoc a cs:CodeStandard ;
|
|
326
|
+
cs:description "Published under a derived name; carries no cs:name." .
|
|
327
|
+
cs:code.function.purity a cs:CodeStandard ;
|
|
328
|
+
cs:name "Function purity" ;
|
|
329
|
+
cs:description "Carries an asserted cs:name." .
|
|
330
|
+
`;
|
|
331
|
+
const BROKEN_STORY: PackDefinition = {
|
|
332
|
+
noun: "standard",
|
|
333
|
+
description: "pre-#1047 shape",
|
|
334
|
+
list: {
|
|
335
|
+
query: [
|
|
336
|
+
"SELECT ?uri ?name ?description WHERE {",
|
|
337
|
+
" ?uri a cs:CodeStandard ;",
|
|
338
|
+
" cs:description ?description .",
|
|
339
|
+
" OPTIONAL { ?uri cs:name ?n . }",
|
|
340
|
+
' BIND(COALESCE(?n, REPLACE(STRAFTER(STR(?uri), "#"), "\\\\.", "/")) AS ?name)',
|
|
341
|
+
"}",
|
|
342
|
+
"ORDER BY ?name",
|
|
343
|
+
].join("\n"),
|
|
344
|
+
columns: [
|
|
345
|
+
{ field: "name", label: "Name" },
|
|
346
|
+
{ field: "description", label: "Description" },
|
|
347
|
+
],
|
|
348
|
+
},
|
|
349
|
+
lookup: {
|
|
350
|
+
source: "sparql",
|
|
351
|
+
by: "cs:name", // required — no nameFallback: the pre-#1047 population
|
|
352
|
+
type: "cs:CodeStandard",
|
|
353
|
+
fields: [
|
|
354
|
+
{
|
|
355
|
+
name: "description",
|
|
356
|
+
property: "cs:description",
|
|
357
|
+
label: "Description",
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
it("publishes a name its own lookup cannot resolve, and these assertions catch it", async () => {
|
|
364
|
+
const { rt: fixtureRt } = await buildFixtureRuntime({
|
|
365
|
+
ttl: TTL,
|
|
366
|
+
prefixes: PREFIXES,
|
|
367
|
+
});
|
|
368
|
+
const module = compileStoryModule(BROKEN_STORY, SOURCE, PREFIXES);
|
|
369
|
+
|
|
370
|
+
const names = await publishedNames(fixtureRt, module, "standard");
|
|
371
|
+
expect(names).toContain("react/component/tsdoc");
|
|
372
|
+
expect(names).toContain("Function purity");
|
|
373
|
+
|
|
374
|
+
// The round-trip assertion goes RED on the synthesized name…
|
|
375
|
+
const errors = await lookupErrors(fixtureRt, module, "standard", names);
|
|
376
|
+
expect(errors.map((error) => error.query)).toEqual([
|
|
377
|
+
"react/component/tsdoc",
|
|
378
|
+
]);
|
|
379
|
+
|
|
380
|
+
// …and so does the pool-inclusion assertion: the addressable population
|
|
381
|
+
// is only the labelled subset.
|
|
382
|
+
const lookup = BROKEN_STORY.lookup;
|
|
383
|
+
if (!lookup) throw new Error("fixture story has no lookup");
|
|
384
|
+
const pool = await listEntityNames(fixtureRt, lookup, SOURCE);
|
|
385
|
+
expect(pool).toEqual(["Function purity"]);
|
|
386
|
+
}, 60_000);
|
|
387
|
+
});
|