@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,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fixture graph authored to exercise `graph connect`'s traversal POLICY and
|
|
3
|
+
* every one of its five negative answers.
|
|
4
|
+
*
|
|
5
|
+
* This is where exact numbers belong. The shipped pack is about to change —
|
|
6
|
+
* new channel symbols and hundreds of binding records will move every hop count
|
|
7
|
+
* and path set in it — so a test that pinned "these two are exactly 2 hops
|
|
8
|
+
* apart" against real data would fail on a correct change and teach a
|
|
9
|
+
* maintainer to update the fixture rather than to think. Here the data is
|
|
10
|
+
* authored, so the numbers ARE the specification: this graph exists to make one
|
|
11
|
+
* statement per shape, and if a count moves, the behaviour moved.
|
|
12
|
+
*
|
|
13
|
+
* The roster sizes are GENERATED from the kernel's own threshold rather than
|
|
14
|
+
* written out, for the reason `blockGraph.ts` gives for the same move: the count
|
|
15
|
+
* is the whole point of the case, and a hand-copied number invites someone to
|
|
16
|
+
* tidy one line away and quietly turn a roster back into a relation. They are
|
|
17
|
+
* also derived from the constant rather than a copy of it, so a change to the
|
|
18
|
+
* threshold moves this fixture with it.
|
|
19
|
+
*
|
|
20
|
+
* What each region is for:
|
|
21
|
+
*
|
|
22
|
+
* - **the chain** `ds:a1 … ds:a8`, one predicate, one edge per node: hop
|
|
23
|
+
* counting, the default limit, the `--hops` ceiling, and the "no path within
|
|
24
|
+
* the limit" answer. Deliberately UNTYPED — a class with eight members is a
|
|
25
|
+
* relation under the fan-in rule, so typing them would connect every pair of
|
|
26
|
+
* them through their class in two hops and there would be no unconnected pair
|
|
27
|
+
* left to test.
|
|
28
|
+
* - **the reversed pair** `ds:r1 … ds:r3`: a path that has to be walked against
|
|
29
|
+
* the direction of one of its edges.
|
|
30
|
+
* - **the fan of three routes** `ds:m1 … ds:m2`: several shortest paths, both
|
|
31
|
+
* ranking tiebreaks, and the path cap.
|
|
32
|
+
* - **`ds:shares` twice over**: ONE predicate carrying a roster group and a
|
|
33
|
+
* relation group, which is what a rule keyed on a predicate NAME could never
|
|
34
|
+
* get right.
|
|
35
|
+
* - **`ds:fansTo`**: the other direction of the same rule — one subject fanning
|
|
36
|
+
* out past the threshold.
|
|
37
|
+
* - **`ds:isolated`**: in the graph, but with nothing walkable on it at all.
|
|
38
|
+
* - **the blank-node bridge**: a path THROUGH a record, which is the traversal
|
|
39
|
+
* a per-layer SPARQL query cannot do because a blank node cannot be re-bound
|
|
40
|
+
* by name.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { ROSTER_THRESHOLD } from "../../kernel/runtime/readEntity.js";
|
|
44
|
+
|
|
45
|
+
/** The prefixes the fixture store is built and queried with. */
|
|
46
|
+
export const CONNECT_PREFIXES: Readonly<Record<string, string>> = {
|
|
47
|
+
ds: "https://ds.canonical.com/",
|
|
48
|
+
owl: "http://www.w3.org/2002/07/owl#",
|
|
49
|
+
rdfs: "http://www.w3.org/2000/01/rdf-schema#",
|
|
50
|
+
xsd: "http://www.w3.org/2001/XMLSchema#",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** How long the hop-counting chain is — long enough to outrun the ceiling. */
|
|
54
|
+
export const CHAIN_LENGTH = 8;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* How many members a group needs to be a roster: one clear of the threshold,
|
|
58
|
+
* plus one so that removing a line does not silently reclassify the group.
|
|
59
|
+
*/
|
|
60
|
+
export const ROSTER_SIZE = ROSTER_THRESHOLD + 2;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The true size of the `(ds:shares, ds:hub)` group: the generated members plus
|
|
64
|
+
* `ds:isolated`, which shares the hub precisely so that it has nothing walkable
|
|
65
|
+
* of its own. Stated rather than left to be recounted, because it is the number
|
|
66
|
+
* the answer reports.
|
|
67
|
+
*/
|
|
68
|
+
export const HUB_ROSTER_SIZE = ROSTER_SIZE + 1;
|
|
69
|
+
|
|
70
|
+
/** `ds:a1 -> ds:a2 -> … -> ds:a8`, one admitted edge per link. */
|
|
71
|
+
const CHAIN = Array.from(
|
|
72
|
+
{ length: CHAIN_LENGTH - 1 },
|
|
73
|
+
(_, index) => `ds:a${index + 1} ds:linksTo ds:a${index + 2} .`,
|
|
74
|
+
).join("\n");
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* `ROSTER_SIZE` subjects all asserting `ds:shares ds:hub` — so the group
|
|
78
|
+
* `(ds:shares, ds:hub)` fans in past the threshold and every one of those edges
|
|
79
|
+
* is refused. Each member also holds ONE private relation, so it is still a
|
|
80
|
+
* node the walk can start from: an endpoint with no relation at all is a
|
|
81
|
+
* different answer, and `ds:isolated` is where that one is tested.
|
|
82
|
+
*/
|
|
83
|
+
const ROSTER = Array.from(
|
|
84
|
+
{ length: ROSTER_SIZE },
|
|
85
|
+
(_, index) =>
|
|
86
|
+
`ds:member${index} a ds:Probe ; ds:shares ds:hub ; ds:owns ds:private${index} .`,
|
|
87
|
+
).join("\n");
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* One subject asserting `ds:fansTo` at `ROSTER_SIZE` objects — the OTHER
|
|
91
|
+
* direction of the same rule. Its objects share a roster; none of the edges is
|
|
92
|
+
* walkable. Each object holds one private relation, for the same reason the
|
|
93
|
+
* roster members do.
|
|
94
|
+
*/
|
|
95
|
+
const FAN = `ds:fanHub ${Array.from(
|
|
96
|
+
{ length: ROSTER_SIZE },
|
|
97
|
+
(_, index) => `ds:fansTo ds:leaf${index}`,
|
|
98
|
+
).join(" ; ")} .
|
|
99
|
+
${Array.from(
|
|
100
|
+
{ length: ROSTER_SIZE },
|
|
101
|
+
(_, index) => `ds:leaf${index} ds:owns ds:leafPrivate${index} .`,
|
|
102
|
+
).join("\n")}`;
|
|
103
|
+
|
|
104
|
+
/** The fixture ontology + individuals as Turtle. */
|
|
105
|
+
export const CONNECT_TTL = `
|
|
106
|
+
@prefix ds: <https://ds.canonical.com/> .
|
|
107
|
+
@prefix owl: <http://www.w3.org/2002/07/owl#> .
|
|
108
|
+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
|
109
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
110
|
+
|
|
111
|
+
# ---- Ontology (TBox) ----
|
|
112
|
+
ds:Probe a owl:Class ; rdfs:label "Probe" .
|
|
113
|
+
ds:Record a owl:Class ; rdfs:label "Record" .
|
|
114
|
+
|
|
115
|
+
ds:linksTo a owl:ObjectProperty ; rdfs:label "linksTo" .
|
|
116
|
+
ds:pointsAt a owl:ObjectProperty ; rdfs:label "pointsAt" .
|
|
117
|
+
ds:alpha a owl:ObjectProperty ; rdfs:label "alpha" .
|
|
118
|
+
ds:beta a owl:ObjectProperty ; rdfs:label "beta" .
|
|
119
|
+
ds:gamma a owl:ObjectProperty ; rdfs:label "gamma" .
|
|
120
|
+
ds:shares a owl:ObjectProperty ; rdfs:label "shares" .
|
|
121
|
+
ds:fansTo a owl:ObjectProperty ; rdfs:label "fansTo" .
|
|
122
|
+
ds:owns a owl:ObjectProperty ; rdfs:label "owns" .
|
|
123
|
+
ds:holds a owl:ObjectProperty ; rdfs:label "holds" .
|
|
124
|
+
ds:refers a owl:ObjectProperty ; rdfs:label "refers" .
|
|
125
|
+
|
|
126
|
+
# ---- The chain: hop counting, the limit, and the ceiling ----
|
|
127
|
+
${CHAIN}
|
|
128
|
+
|
|
129
|
+
# ---- The reversed pair: r1 <-pointsAt- r2 -pointsAt-> r3 ----
|
|
130
|
+
ds:r2 ds:pointsAt ds:r1 , ds:r3 .
|
|
131
|
+
|
|
132
|
+
# ---- Three shortest routes from m1 to m2, for ranking and the path cap ----
|
|
133
|
+
# via1 stays inside ds:alpha; via3 stays inside ds:gamma; via2 mixes the two.
|
|
134
|
+
# So the ranking's second key (fewest distinct predicates) puts via1 and via3
|
|
135
|
+
# ahead of via2, and its third key (lexicographic) settles alpha before gamma.
|
|
136
|
+
ds:m1 ds:alpha ds:via1 , ds:via2 .
|
|
137
|
+
ds:via1 ds:alpha ds:m2 .
|
|
138
|
+
ds:via2 ds:beta ds:m2 .
|
|
139
|
+
ds:m1 ds:gamma ds:via3 .
|
|
140
|
+
ds:via3 ds:gamma ds:m2 .
|
|
141
|
+
|
|
142
|
+
# ---- ds:shares, as a ROSTER: ${ROSTER_SIZE} subjects on one object ----
|
|
143
|
+
${ROSTER}
|
|
144
|
+
|
|
145
|
+
# ---- ds:shares, as a RELATION: two subjects on one object ----
|
|
146
|
+
# The SAME predicate as the roster above. Nothing about the name distinguishes
|
|
147
|
+
# these edges; only the fan-in does.
|
|
148
|
+
ds:relA a ds:Probe ; ds:shares ds:small .
|
|
149
|
+
ds:relB a ds:Probe ; ds:shares ds:small .
|
|
150
|
+
|
|
151
|
+
# ---- ds:fansTo: one subject past the threshold, the other direction ----
|
|
152
|
+
${FAN}
|
|
153
|
+
|
|
154
|
+
# ---- In the graph, but nothing walkable on it ----
|
|
155
|
+
ds:isolated a ds:Probe ; ds:shares ds:hub .
|
|
156
|
+
|
|
157
|
+
# ---- A path THROUGH a blank-node record ----
|
|
158
|
+
ds:bridgeStart ds:holds [ a ds:Record ; ds:refers ds:bridgeEnd ] .
|
|
159
|
+
`;
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*
|
|
30
30
|
* Two of those anchors exist to pin an ABSENCE, and are load-bearing for the
|
|
31
31
|
* "declared, not inferred" halves of the read grammar:
|
|
32
|
-
* - `
|
|
32
|
+
* - `dt:legacy.borderRadius` — a `dt:TokenSymbol` with no `rdfs:label`, which
|
|
33
33
|
* `token list` therefore never publishes and `token lookup`/`token sample`
|
|
34
34
|
* must never reach (`PackLookup.nameFallback` is declared by `standard`
|
|
35
35
|
* alone).
|
|
@@ -42,6 +42,10 @@ import { BLOCK_PREFIXES, BLOCK_TTL } from "../blockGraph.js";
|
|
|
42
42
|
|
|
43
43
|
/** Extra `ds:` individuals: a tier-chain + a beta-only block, tokens, tiers. */
|
|
44
44
|
const DS_EXTRA_TTL = `
|
|
45
|
+
@prefix dt: <https://dt.canonical.com/> .
|
|
46
|
+
@prefix w3c-tokens: <https://dt.canonical.com/w3c-tokens/> .
|
|
47
|
+
@prefix dt-web: <https://dt.canonical.com/platform/web/> .
|
|
48
|
+
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
|
45
49
|
# ---- Tiers beyond the global BLOCK_TTL already declares ----
|
|
46
50
|
ds:apps a ds:Tier ; ds:name "apps" .
|
|
47
51
|
ds:apps_lxd a ds:Tier ; ds:name "apps/lxd" .
|
|
@@ -66,37 +70,125 @@ ds:betaWidget a ds:Component ;
|
|
|
66
70
|
ds:summary "An experimental widget gated to the prerelease channel." .
|
|
67
71
|
|
|
68
72
|
# ---- Tokens ----
|
|
69
|
-
ds:Token
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
73
|
+
# The token STRATA, not the retired ds:Token class: the story addresses
|
|
74
|
+
# dt:TokenSymbol, keyed on the rdfs:label the token-ontology pack publishes,
|
|
75
|
+
# with type and description hanging on the DEFINITIONS behind each symbol
|
|
76
|
+
# rather than on the symbol itself. The story reaches the one definition that
|
|
77
|
+
# speaks for the symbol through the symbol's BASE resolved value — the head of
|
|
78
|
+
# its resolution chain — so every labelled symbol here carries one, which is
|
|
79
|
+
# the shipped graph's own shape.
|
|
80
|
+
dt:TokenSymbol a owl:Class .
|
|
81
|
+
w3c-tokens:Token a owl:Class .
|
|
82
|
+
w3c-tokens:TokenType a owl:Class .
|
|
83
|
+
dt:symbol a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
|
|
84
|
+
dt:tokenType a owl:ObjectProperty ; rdfs:range w3c-tokens:TokenType .
|
|
85
|
+
dt:channelOf a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
|
|
86
|
+
dt:covers a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
|
|
87
|
+
dt:forSymbol a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
|
|
88
|
+
dt:resolvesTo a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
89
|
+
dt:resolutionChain a owl:ObjectProperty .
|
|
90
|
+
dt:derivedFrom a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
|
|
91
|
+
dt:coordinate a owl:ObjectProperty .
|
|
92
|
+
w3c-tokens:description a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
93
|
+
w3c-tokens:inFile a owl:ObjectProperty .
|
|
94
|
+
w3c-tokens:path a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
95
|
+
|
|
96
|
+
w3c-tokens:color a w3c-tokens:TokenType ; rdfs:label "color" .
|
|
97
|
+
w3c-tokens:dimension a w3c-tokens:TokenType ; rdfs:label "dimension" .
|
|
98
|
+
|
|
99
|
+
dt:color.primary a dt:TokenSymbol ; rdfs:label "color.primary" .
|
|
100
|
+
dt:spacing.medium a dt:TokenSymbol ; rdfs:label "spacing.medium" .
|
|
101
|
+
|
|
102
|
+
<https://dt.canonical.com/file/light.json#color.primary> a w3c-tokens:Token ;
|
|
103
|
+
dt:symbol dt:color.primary ;
|
|
104
|
+
dt:tokenType w3c-tokens:color ;
|
|
105
|
+
w3c-tokens:inFile <https://dt.canonical.com/file/light.json> ;
|
|
106
|
+
w3c-tokens:description "The primary brand colour." .
|
|
107
|
+
<https://dt.canonical.com/file/light.json#spacing.medium> a w3c-tokens:Token ;
|
|
108
|
+
dt:symbol dt:spacing.medium ;
|
|
109
|
+
dt:tokenType w3c-tokens:dimension ;
|
|
110
|
+
w3c-tokens:inFile <https://dt.canonical.com/file/light.json> ;
|
|
111
|
+
w3c-tokens:description "A medium spacing step." .
|
|
112
|
+
<https://dt.canonical.com/file/light.json> w3c-tokens:path "light.json" .
|
|
113
|
+
|
|
114
|
+
[] a dt:ResolvedValue ; dt:forSymbol dt:color.primary ;
|
|
115
|
+
dt:resolvesTo "#0066CC" ;
|
|
116
|
+
dt:resolutionChain ( <https://dt.canonical.com/file/light.json#color.primary> ) .
|
|
117
|
+
[] a dt:ResolvedValue ; dt:forSymbol dt:spacing.medium ;
|
|
118
|
+
dt:resolvesTo "1rem" ;
|
|
119
|
+
dt:resolutionChain ( <https://dt.canonical.com/file/light.json#spacing.medium> ) .
|
|
120
|
+
|
|
121
|
+
# ---- Platform variables ----
|
|
122
|
+
# The second stratum-shaped noun needs a population here for the same reason the
|
|
123
|
+
# symbols do: the cross-noun sweeps derive their cases from the declared stories,
|
|
124
|
+
# so a noun with no rows in this graph makes every filter's roster empty and
|
|
125
|
+
# every refusal unable to name what it would accept.
|
|
126
|
+
dt:Variable a owl:Class .
|
|
127
|
+
dt:Declaration a owl:Class .
|
|
128
|
+
dt:Condition a owl:Class .
|
|
129
|
+
dt:Coordinate a owl:Class .
|
|
130
|
+
dt:Tier a owl:Class .
|
|
131
|
+
dt:Visibility a owl:Class .
|
|
132
|
+
dt:ofSymbol a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
|
|
133
|
+
dt:tier a owl:ObjectProperty ; rdfs:range dt:Tier .
|
|
134
|
+
dt:visibility a owl:ObjectProperty ; rdfs:range dt:Visibility .
|
|
135
|
+
dt:declaredAt a owl:ObjectProperty ; rdfs:range dt:Declaration .
|
|
136
|
+
dt:under a owl:ObjectProperty ; rdfs:range dt:Condition .
|
|
137
|
+
dt:emits a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
138
|
+
dt:at a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
139
|
+
dt:alsoAt a owl:ObjectProperty ; rdfs:range dt:Coordinate .
|
|
140
|
+
dt:derives a owl:ObjectProperty .
|
|
141
|
+
dt:references a owl:ObjectProperty .
|
|
142
|
+
dt:selectsCoordinate a owl:ObjectProperty ; rdfs:range dt:Coordinate .
|
|
143
|
+
dt-web:selector a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
144
|
+
dt-web:inAtRule a owl:ObjectProperty .
|
|
145
|
+
|
|
146
|
+
dt:tier.semantic a dt:Tier .
|
|
147
|
+
dt:tier.primitive a dt:Tier .
|
|
148
|
+
dt:visibility.public a dt:Visibility .
|
|
149
|
+
dt:visibility.internal a dt:Visibility .
|
|
150
|
+
dt:coordinate.mode.dark a dt:Coordinate .
|
|
151
|
+
|
|
152
|
+
<https://dt.canonical.com/s4/web/cond/root> a dt:Condition ;
|
|
153
|
+
dt-web:selector ":root" ;
|
|
154
|
+
dt-web:inAtRule ( "@layer ds.tokens" ) .
|
|
155
|
+
|
|
156
|
+
# Published WITHOUT its leading dashes, which is the only form typable as a
|
|
157
|
+
# positional argument.
|
|
158
|
+
<https://dt.canonical.com/s4/web/--color-primary> a dt:Variable ;
|
|
159
|
+
rdfs:label "color-primary" ;
|
|
160
|
+
dt:ofSymbol dt:color.primary ;
|
|
161
|
+
dt:tier dt:tier.semantic ;
|
|
162
|
+
dt:visibility dt:visibility.public ;
|
|
163
|
+
dt:declaredAt [
|
|
164
|
+
a dt:Declaration ;
|
|
165
|
+
dt:under <https://dt.canonical.com/s4/web/cond/root> ;
|
|
166
|
+
dt:emits "#0066CC" ;
|
|
167
|
+
dt:at "tokens.css:4" ;
|
|
168
|
+
dt:alsoAt dt:coordinate.mode.dark
|
|
169
|
+
] .
|
|
170
|
+
# Stands for NO symbol — the population the token noun cannot reach.
|
|
171
|
+
<https://dt.canonical.com/s4/web/--legacy-radius> a dt:Variable ;
|
|
172
|
+
rdfs:label "legacy-radius" ;
|
|
173
|
+
dt:tier dt:tier.primitive ;
|
|
174
|
+
dt:visibility dt:visibility.internal ;
|
|
175
|
+
dt:declaredAt [
|
|
176
|
+
a dt:Declaration ;
|
|
177
|
+
dt:under <https://dt.canonical.com/s4/web/cond/root> ;
|
|
178
|
+
dt:emits "var(--color-primary)" ;
|
|
179
|
+
dt:at "tokens.css:9" ;
|
|
180
|
+
dt:references ( <https://dt.canonical.com/s4/web/--color-primary> )
|
|
181
|
+
] .
|
|
182
|
+
|
|
183
|
+
# A dt:TokenSymbol carrying NO rdfs:label — the entity "token list" does not
|
|
184
|
+
# publish, because its query REQUIRES the literal. It is here to pin what a
|
|
185
|
+
# lookup may NOT reach: "token lookup" addresses by rdfs:label, and a story
|
|
186
|
+
# whose list requires its "by" property must not become addressable (or
|
|
187
|
+
# sampleable) under a name derived from its IRI. See PackLookup.nameFallback,
|
|
188
|
+
# declared by the "standard" story alone — the one story whose list DOES
|
|
189
|
+
# publish such a name. The token noun deliberately declares none, because the
|
|
190
|
+
# kernel's derivation would publish these dots as slashes.
|
|
191
|
+
dt:legacy.borderRadius a dt:TokenSymbol .
|
|
100
192
|
`;
|
|
101
193
|
|
|
102
194
|
/**
|
|
@@ -336,6 +428,10 @@ ds:implementation.svelte-ds-global.button a ds:ImplementationObject ;
|
|
|
336
428
|
/** The prefixes the canonical store is built and queried with. */
|
|
337
429
|
export const CANONICAL_PREFIXES: Readonly<Record<string, string>> = {
|
|
338
430
|
...BLOCK_PREFIXES,
|
|
431
|
+
dt: "https://dt.canonical.com/",
|
|
432
|
+
"w3c-tokens": "https://dt.canonical.com/w3c-tokens/",
|
|
433
|
+
"dt-web": "https://dt.canonical.com/platform/web/",
|
|
434
|
+
rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
339
435
|
cs: "http://pragma.canonical.com/codestandards#",
|
|
340
436
|
skos: "http://www.w3.org/2004/02/skos/core#",
|
|
341
437
|
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem fixtures for `probeWritable`'s seam — the two machines no CI host
|
|
3
|
+
* is.
|
|
4
|
+
*
|
|
5
|
+
* `probeWritable` decides whether a row REPORTS a named skip or attempts a
|
|
6
|
+
* write that fails with a raw fs message, and its two blocking answers cannot
|
|
7
|
+
* be arranged for real here: no CI host has a `/nix/store`, and a test process
|
|
8
|
+
* running as root can write to a `0o500` directory whatever its mode bits say.
|
|
9
|
+
* So the filesystem is three injected functions.
|
|
10
|
+
*
|
|
11
|
+
* Two factories, because the suites want different things. {@link storeProbe}
|
|
12
|
+
* keeps the REAL filesystem and rewrites one subtree's realpath into the
|
|
13
|
+
* store, so an end-to-end case can build a genuine project and still be a Nix
|
|
14
|
+
* machine for the one path that matters. {@link fakeFsProbe} invents the
|
|
15
|
+
* filesystem outright and RECORDS every call, so a unit case can assert the
|
|
16
|
+
* walk itself and not only its answer.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { accessSync, existsSync, realpathSync } from "node:fs";
|
|
20
|
+
import type { FsProbe } from "../../capabilities/setup/operations/writability.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A probe over the real filesystem whose `realpathSync` maps everything under
|
|
24
|
+
* `root` into `/nix/store/<hash>-…`.
|
|
25
|
+
*
|
|
26
|
+
* @param root - The subtree that resolves into the store.
|
|
27
|
+
* @param hash - The store path's name, so two suites can tell theirs apart.
|
|
28
|
+
* @returns The probe.
|
|
29
|
+
* @note Impure — `existsSync`/`accessSync` are the real ones.
|
|
30
|
+
*/
|
|
31
|
+
export const storeProbe = (root: string, hash = "1a2b3c-vscode"): FsProbe => ({
|
|
32
|
+
existsSync,
|
|
33
|
+
accessSync,
|
|
34
|
+
realpathSync: (path) =>
|
|
35
|
+
path.startsWith(root)
|
|
36
|
+
? `/nix/store/${hash}${path.slice(root.length)}`
|
|
37
|
+
: realpathSync(path),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** What a {@link fakeFsProbe} filesystem consists of. */
|
|
41
|
+
export interface FakeFsSpec {
|
|
42
|
+
/** The paths that exist. */
|
|
43
|
+
readonly present?: readonly string[];
|
|
44
|
+
/** Each path's realpath, for the ones that resolve elsewhere. */
|
|
45
|
+
readonly realpath?: Readonly<Record<string, string>>;
|
|
46
|
+
/** The RESOLVED paths that refuse `W_OK`. */
|
|
47
|
+
readonly denied?: readonly string[];
|
|
48
|
+
/** Whether `realpathSync` throws (a race, or a symlink loop). */
|
|
49
|
+
readonly realpathThrows?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A {@link fakeFsProbe}, plus the calls it saw. */
|
|
53
|
+
export type RecordingFsProbe = FsProbe & {
|
|
54
|
+
/** Every path `existsSync` was asked about, in order — the walk. */
|
|
55
|
+
readonly seen: string[];
|
|
56
|
+
/** Every path `accessSync` was asked about, in order. */
|
|
57
|
+
readonly accessed: string[];
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* An invented filesystem that records what it was asked.
|
|
62
|
+
*
|
|
63
|
+
* @param spec - The filesystem to present.
|
|
64
|
+
* @returns The probe, carrying `seen` and `accessed`.
|
|
65
|
+
* @note Pure — it touches no real path; the arrays are its own state.
|
|
66
|
+
*/
|
|
67
|
+
export const fakeFsProbe = (spec: FakeFsSpec): RecordingFsProbe => {
|
|
68
|
+
const seen: string[] = [];
|
|
69
|
+
const accessed: string[] = [];
|
|
70
|
+
return {
|
|
71
|
+
seen,
|
|
72
|
+
accessed,
|
|
73
|
+
existsSync: (path) => {
|
|
74
|
+
seen.push(path);
|
|
75
|
+
return (spec.present ?? []).includes(path);
|
|
76
|
+
},
|
|
77
|
+
realpathSync: (path) => {
|
|
78
|
+
if (spec.realpathThrows === true) throw new Error("ELOOP");
|
|
79
|
+
return spec.realpath?.[path] ?? path;
|
|
80
|
+
},
|
|
81
|
+
accessSync: (path) => {
|
|
82
|
+
accessed.push(path);
|
|
83
|
+
if ((spec.denied ?? []).includes(path)) {
|
|
84
|
+
throw new Error("EACCES: permission denied");
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins how `runCli` reports a child that does not finish.
|
|
3
|
+
*
|
|
4
|
+
* `spawnSync` reports a timeout the same way it reports a failure to spawn —
|
|
5
|
+
* through `error` — and a caller that does not look will see only
|
|
6
|
+
* `status: null`, which reads downstream as an ordinary non-zero exit or, worse,
|
|
7
|
+
* as a content mismatch. These tests hold the boundary where that distinction
|
|
8
|
+
* is made, because the cost of losing it is paid in diagnosis rather than in a
|
|
9
|
+
* red test.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
import { runCli } from "./runCli.js";
|
|
14
|
+
|
|
15
|
+
describe("runCli — a child that does not finish", () => {
|
|
16
|
+
it("reports a timeout as a timeout, with the duration", async () => {
|
|
17
|
+
// `--help` on a 1ms budget cannot complete; any real CLI start-up exceeds it.
|
|
18
|
+
await expect(async () =>
|
|
19
|
+
runCli(["--help"], { timeoutMs: 1 }),
|
|
20
|
+
).rejects.toThrow(/timed out after 1ms/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("names the signal that killed it, so a hang is distinguishable", async () => {
|
|
24
|
+
await expect(async () =>
|
|
25
|
+
runCli(["--help"], { timeoutMs: 1 }),
|
|
26
|
+
).rejects.toThrow(/killed by SIG/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("does not describe a timeout as a failure to spawn", async () => {
|
|
30
|
+
// The previous message said "failed to spawn", which sent a reader looking
|
|
31
|
+
// for a missing binary or a bad path instead of a slow or hung command.
|
|
32
|
+
await expect(async () =>
|
|
33
|
+
runCli(["--help"], { timeoutMs: 1 }),
|
|
34
|
+
).rejects.not.toThrow(/failed to spawn/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("attaches whatever the child managed to write", async () => {
|
|
38
|
+
// Empty streams are still reported explicitly: "(empty)" is evidence, an
|
|
39
|
+
// absent section is an omission the reader has to interpret.
|
|
40
|
+
await expect(async () =>
|
|
41
|
+
runCli(["--help"], { timeoutMs: 1 }),
|
|
42
|
+
).rejects.toThrow(/stdout:[\s\S]*stderr:/);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -46,10 +46,17 @@ export interface RunCliOptions {
|
|
|
46
46
|
* test the shipped-entry boundary itself.
|
|
47
47
|
*/
|
|
48
48
|
readonly mode?: "shipped" | "source";
|
|
49
|
-
/** Spawn timeout in milliseconds (default
|
|
49
|
+
/** Spawn timeout in milliseconds (default {@link DEFAULT_TIMEOUT_MS}). */
|
|
50
50
|
readonly timeoutMs?: number;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* How long a spawned CLI may run before it is killed. Twenty seconds is far
|
|
55
|
+
* past any assertion these tests make; a run that reaches it has hung, not
|
|
56
|
+
* been slow.
|
|
57
|
+
*/
|
|
58
|
+
const DEFAULT_TIMEOUT_MS = 20_000;
|
|
59
|
+
|
|
53
60
|
/** The captured outcome of a `runCli` invocation. */
|
|
54
61
|
export interface RunCliResult {
|
|
55
62
|
readonly stdout: string;
|
|
@@ -114,12 +121,29 @@ export function runCli(
|
|
|
114
121
|
cwd: options.cwd,
|
|
115
122
|
env: { ...env, ...options.env },
|
|
116
123
|
encoding: "utf-8",
|
|
117
|
-
timeout: options.timeoutMs ??
|
|
124
|
+
timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
118
125
|
});
|
|
119
126
|
|
|
120
127
|
if (result.error) {
|
|
128
|
+
// A timeout is not a spawn failure, and saying so costs real diagnosis
|
|
129
|
+
// time: `spawnSync` reports both through `error`, but a timed-out child
|
|
130
|
+
// has already produced output worth seeing, and the duration is the whole
|
|
131
|
+
// finding. Separated here so the message names what happened, and carries
|
|
132
|
+
// the evidence rather than making the reader go and get it.
|
|
133
|
+
const timedOutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
134
|
+
const invocation = `${command} ${spawnArgs.join(" ")}`;
|
|
135
|
+
const captured = [
|
|
136
|
+
result.stdout ? `stdout:\n${result.stdout}` : "stdout: (empty)",
|
|
137
|
+
result.stderr ? `stderr:\n${result.stderr}` : "stderr: (empty)",
|
|
138
|
+
].join("\n");
|
|
139
|
+
|
|
140
|
+
if ((result.error as NodeJS.ErrnoException).code === "ETIMEDOUT") {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`runCli: ${invocation} timed out after ${timedOutMs}ms (killed by ${result.signal ?? "signal"}).\n${captured}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
121
145
|
throw new Error(
|
|
122
|
-
`runCli: failed to spawn ${
|
|
146
|
+
`runCli: failed to spawn ${invocation} — ${result.error.message}.\n${captured}`,
|
|
123
147
|
);
|
|
124
148
|
}
|
|
125
149
|
|
|
@@ -77,7 +77,9 @@ describe("perf budgets (PROTECTED)", () => {
|
|
|
77
77
|
// so it flaked red under whole-suite CPU contention on slower-than-reference
|
|
78
78
|
// hardware even though the median sat comfortably under budget (see
|
|
79
79
|
// BUDGETS.md). p95 is kept as a SOFT signal against gross regressions, with
|
|
80
|
-
// headroom rather than at the bare ceiling.
|
|
80
|
+
// headroom rather than at the bare ceiling. That statistic change left the
|
|
81
|
+
// ceiling of the day (100 ms) untouched; BUDGET_COMPLETE_MS has been
|
|
82
|
+
// re-derived twice since, most recently to 240 on 2026-09-10.
|
|
81
83
|
expect(result.trimmedMeanMs).toBeLessThanOrEqual(BUDGET_COMPLETE_MS);
|
|
82
84
|
expect(result.p95Ms).toBeLessThanOrEqual(BUDGET_COMPLETE_MS * 1.5);
|
|
83
85
|
});
|