@canonical/pragma-cli 0.32.0 → 0.34.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 +72 -0
- package/dist/pragma +0 -0
- package/package.json +19 -15
- package/pragma.conf.ts +664 -0
- package/src/bin.ts +15 -6
- package/src/capabilities/block.parity.test.ts +250 -0
- package/src/capabilities/capabilities/capabilities.render.ts +3 -2
- package/src/capabilities/capabilities/capabilities.test.ts +10 -1
- package/src/capabilities/capabilities/capabilities.verb.ts +32 -14
- package/src/capabilities/capabilities/catalog.ts +20 -8
- package/src/capabilities/capabilities/hints.ts +21 -36
- package/src/capabilities/capabilities/types.ts +2 -0
- package/src/capabilities/colophon/collectColophon.ts +34 -19
- package/src/capabilities/colophon/colophon.render.test.ts +31 -4
- package/src/capabilities/colophon/colophon.render.ts +13 -4
- package/src/capabilities/colophon/colophon.test.ts +3 -3
- package/src/capabilities/colophon/colophon.verb.ts +11 -4
- package/src/capabilities/colophon/markdownTerminal.ts +33 -14
- package/src/capabilities/colophon/types.ts +16 -7
- package/src/capabilities/config/collectConfigShow.test.ts +57 -0
- package/src/capabilities/config/collectConfigShow.ts +41 -4
- package/src/capabilities/config/fields.ts +10 -108
- package/src/capabilities/config/set.test.ts +6 -6
- package/src/capabilities/config/set.verb.ts +12 -8
- package/src/capabilities/config/show.render.test.ts +137 -0
- package/src/capabilities/config/show.render.ts +57 -16
- package/src/capabilities/config/show.verb.ts +9 -8
- package/src/capabilities/config/types.ts +8 -0
- package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
- package/src/capabilities/create/byteEquality.test.ts +82 -101
- package/src/capabilities/create/compiledCreate.subprocess.test.ts +53 -16
- package/src/capabilities/create/constants.ts +75 -0
- package/src/capabilities/create/create.mcp.test.ts +2 -2
- package/src/capabilities/create/create.test.ts +192 -16
- package/src/capabilities/create/create.verb.ts +208 -79
- package/src/capabilities/create/index.ts +2 -10
- package/src/capabilities/create/pickGenerator.ts +34 -28
- package/src/capabilities/create/templates.embedded.generated.ts +4 -29
- package/src/capabilities/create/types.ts +10 -6
- package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
- package/src/capabilities/distribution.test.ts +120 -0
- package/src/capabilities/distribution.ts +59 -0
- package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
- package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
- package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
- package/src/capabilities/doctor/checks/checkMcpConfigured.ts +15 -8
- package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
- package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
- package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
- package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
- package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
- package/src/capabilities/doctor/checks/deriveBand.test.ts +42 -0
- package/src/capabilities/doctor/checks/deriveBand.ts +28 -0
- package/src/capabilities/doctor/doctor.render.test.ts +170 -0
- package/src/capabilities/doctor/doctor.render.ts +117 -34
- package/src/capabilities/doctor/doctor.test.ts +68 -9
- package/src/capabilities/doctor/doctor.verb.ts +6 -2
- package/src/capabilities/doctor/runChecks.ts +33 -9
- package/src/capabilities/doctor/types.ts +16 -1
- package/src/capabilities/emptyRecovery.test.ts +32 -15
- package/src/capabilities/graph/inspect.verb.ts +3 -2
- package/src/capabilities/graph/query.verb.ts +3 -2
- package/src/capabilities/graph/runQuery.ts +8 -5
- package/src/capabilities/index.ts +22 -18
- package/src/capabilities/info/collectInfo.ts +10 -6
- package/src/capabilities/info/info.render.ts +3 -2
- package/src/capabilities/info/info.verb.ts +3 -2
- package/src/capabilities/lazy.test.ts +93 -2
- package/src/capabilities/ontology/index.ts +10 -3
- package/src/capabilities/ontology/ontology.test.ts +157 -5
- package/src/capabilities/ontology/verbs.ts +203 -104
- package/src/capabilities/prompt/prompt.render.test.ts +94 -0
- package/src/capabilities/prompt/prompt.render.ts +23 -6
- package/src/capabilities/prompt/prompt.test.ts +68 -1
- package/src/capabilities/prompt/verbs.ts +24 -11
- package/src/capabilities/reference.test.ts +32 -0
- package/src/capabilities/resources/provider.ts +58 -25
- package/src/capabilities/resources/resources.test.ts +176 -21
- package/src/capabilities/scopeTypeSync.test.ts +37 -0
- package/src/capabilities/setup/index.ts +7 -1
- package/src/capabilities/setup/operations/setupCompletions.ts +49 -16
- package/src/capabilities/setup/operations/setupGenerator.ts +166 -63
- package/src/capabilities/setup/operations/setupLsp.ts +56 -12
- package/src/capabilities/setup/operations/setupMcp.ts +216 -39
- package/src/capabilities/setup/operations/setupSkills.ts +10 -4
- package/src/capabilities/setup/setup.render.test.ts +109 -0
- package/src/capabilities/setup/setup.render.ts +61 -14
- package/src/capabilities/setup/setup.test.ts +352 -1
- package/src/capabilities/setup/setup.verb.ts +68 -9
- package/src/capabilities/setup/shell.ts +29 -3
- package/src/capabilities/setup/types.ts +59 -2
- package/src/capabilities/shared/bands.ts +16 -0
- package/src/capabilities/skill/discover.ts +11 -5
- package/src/capabilities/skill/render.ts +4 -5
- package/src/capabilities/skill/verbs.ts +7 -7
- package/src/capabilities/sources/collectStatus.ts +69 -49
- package/src/capabilities/sources/index.ts +1 -1
- package/src/capabilities/sources/installSkills.ts +2 -2
- package/src/capabilities/sources/runUpdate.ts +146 -98
- package/src/capabilities/sources/sources.test.ts +264 -84
- package/src/capabilities/sources/status.render.test.ts +94 -0
- package/src/capabilities/sources/status.render.ts +55 -33
- package/src/capabilities/sources/status.verb.ts +15 -8
- package/src/capabilities/sources/types.ts +18 -25
- package/src/capabilities/sources/update.render.ts +9 -2
- package/src/capabilities/sources/update.verb.ts +12 -23
- package/src/capabilities/surface.test.ts +65 -17
- package/src/capabilities/tier.parity.test.ts +144 -0
- package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
- package/src/constants.test.ts +9 -7
- package/src/constants.ts +49 -23
- package/src/identity.test.ts +333 -0
- package/src/index.ts +2 -2
- package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +18 -16
- package/src/kernel/completion/complete.test.ts +3 -5
- package/src/kernel/completion/emitScripts.test.ts +1 -1
- package/src/kernel/completion/emitScripts.ts +4 -2
- package/src/kernel/completion/entitySource.test.ts +185 -41
- package/src/kernel/completion/entitySource.ts +131 -113
- package/src/kernel/completion/model.test.ts +3 -3
- package/src/kernel/completion/model.ts +11 -1
- package/src/kernel/completion/parse.test.ts +2 -4
- package/src/kernel/completion/safety.test.ts +82 -36
- package/src/kernel/completion/shellDrive.test.ts +592 -0
- package/src/kernel/completion/templates/fish.ts +28 -6
- package/src/kernel/completion/types.ts +3 -3
- package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
- package/src/kernel/config/defaults.test.ts +85 -0
- package/src/kernel/config/defaults.ts +12 -23
- package/src/kernel/config/evaluateProjectConfig.ts +2 -3
- package/src/kernel/config/findProjectConfig.ts +4 -3
- package/src/kernel/config/firstRun.ts +12 -5
- package/src/kernel/config/globalConfig.ts +2 -2
- package/src/kernel/config/index.ts +4 -5
- package/src/kernel/config/paths.ts +17 -17
- package/src/kernel/config/readConfig.test.ts +242 -6
- package/src/kernel/config/readConfig.ts +8 -8
- package/src/kernel/config/schema.test.ts +50 -0
- package/src/kernel/config/schema.ts +97 -10
- package/src/kernel/config/types.ts +73 -19
- package/src/kernel/copy.test.ts +453 -0
- package/src/kernel/error/PragmaError.test.ts +5 -5
- package/src/kernel/error/constants.ts +3 -2
- package/src/kernel/error/index.ts +1 -1
- package/src/kernel/error/recovery.test.ts +22 -45
- package/src/kernel/error/recovery.ts +22 -27
- package/src/kernel/error/renderError.ts +1 -1
- package/src/kernel/error/types.ts +5 -4
- package/src/kernel/packs/collect.test.ts +195 -11
- package/src/kernel/packs/collect.ts +213 -58
- package/src/kernel/packs/compile.test.ts +163 -7
- package/src/kernel/packs/compile.ts +15 -12
- package/src/kernel/packs/disclosure.test.ts +5 -2
- package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
- package/src/kernel/packs/graphqlSource.test.ts +6 -3
- package/src/kernel/packs/iri.ts +5 -7
- package/src/kernel/packs/renderPack.ts +5 -3
- package/src/kernel/packs/resolveEntity.ts +21 -17
- package/src/kernel/packs/runBodies.ts +15 -14
- package/src/kernel/packs/schema.ts +96 -10
- package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
- package/src/kernel/packs/sparql/runSelect.ts +73 -34
- package/src/kernel/packs/toolDescription.test.ts +10 -2
- package/src/kernel/packs/types.ts +39 -4
- package/src/kernel/packs/uniqueness.ts +4 -4
- package/src/kernel/project/cli/buildProgram.test.ts +60 -11
- package/src/kernel/project/cli/buildProgram.ts +11 -0
- package/src/kernel/project/cli/dispatch.test.ts +67 -0
- package/src/kernel/project/cli/dispatch.ts +34 -12
- package/src/kernel/project/cli/globalFlags.test.ts +12 -4
- package/src/kernel/project/cli/globalFlags.ts +14 -14
- package/src/kernel/project/cli/helpFormat.ts +1 -1
- package/src/kernel/project/cli/rootHelp.ts +109 -107
- package/src/kernel/project/mcp/buildServer.ts +11 -6
- package/src/kernel/project/mcp/instructions.ts +12 -7
- package/src/kernel/project/mcp/mcpError.ts +52 -0
- package/src/kernel/project/mcp/parity.test.ts +39 -1
- package/src/kernel/project/mcp/prompts/prompts.test.ts +128 -5
- package/src/kernel/project/mcp/prompts/provider.ts +51 -7
- package/src/kernel/project/mcp/prompts/source.ts +68 -67
- package/src/kernel/project/mcp/registerVerb.ts +25 -6
- package/src/kernel/project/mcp/serve.ts +11 -3
- package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
- package/src/kernel/render/disclosure.ts +4 -2
- package/src/kernel/render/formatters.ts +5 -4
- package/src/kernel/render/prefixes.test.ts +36 -0
- package/src/kernel/render/prefixes.ts +40 -13
- package/src/kernel/render/renderers.test.ts +35 -1
- package/src/kernel/render/renderers.ts +25 -7
- package/src/kernel/render/style.test.ts +31 -0
- package/src/kernel/render/style.ts +74 -0
- package/src/kernel/runtime/graphpack/build.ts +57 -3
- package/src/kernel/runtime/graphpack/buildIndex.test.ts +149 -0
- package/src/kernel/runtime/graphpack/buildIndex.ts +173 -50
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -8
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -4
- package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
- package/src/kernel/runtime/graphpack/embedded.ts +37 -11
- package/src/kernel/runtime/graphpack/graphpack.test.ts +274 -6
- package/src/kernel/runtime/graphpack/hash.ts +3 -2
- package/src/kernel/runtime/graphpack/index.ts +1 -1
- package/src/kernel/runtime/graphpack/manifest.ts +114 -12
- package/src/kernel/runtime/graphpack/read.ts +32 -22
- package/src/kernel/runtime/graphpack/schemas.ts +74 -0
- package/src/kernel/runtime/graphpack/stories.ts +84 -0
- package/src/kernel/runtime/graphpack/types.ts +72 -45
- package/src/kernel/runtime/loadSession.ts +4 -12
- package/src/kernel/runtime/paths.ts +64 -15
- package/src/kernel/runtime/probe.test.ts +8 -4
- package/src/kernel/runtime/readEntity.ts +6 -5
- package/src/kernel/runtime/refs/gitOps.ts +1 -19
- package/src/kernel/runtime/refs/parseRef.ts +7 -7
- package/src/kernel/runtime/refs/resolve.ts +141 -65
- package/src/kernel/runtime/resolveSources.test.ts +63 -24
- package/src/kernel/runtime/resolveSources.ts +36 -25
- package/src/kernel/runtime/storeReadiness.ts +62 -0
- package/src/kernel/runtime/storeless.test.ts +1 -1
- package/src/kernel/runtime/types.ts +1 -1
- package/src/kernel/runtime/wasmEmbed.test.ts +25 -20
- package/src/kernel/spec/emitReference.test.ts +196 -0
- package/src/kernel/spec/emitReference.ts +624 -0
- package/src/kernel/spec/emitSurface.ts +13 -7
- package/src/kernel/spec/index.ts +2 -0
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/kernel/spec/surfaceConformance.ts +8 -0
- package/src/kernel/spec/types.ts +29 -9
- package/src/kernel/spec/validate.ts +2 -1
- package/src/kernel/vocabulary.test.ts +164 -0
- package/src/kernel/vocabulary.ts +146 -0
- package/src/testing/PARITY_GAPS.ts +9 -3
- package/src/testing/behavioral/README.md +3 -3
- package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
- package/src/testing/behavioral/completion.test.ts +21 -8
- package/src/testing/behavioral/disclosure.test.ts +12 -0
- package/src/testing/behavioral/docExamples.test.ts +283 -0
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +3 -2
- package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
- package/src/testing/behavioral/harness.foundation.test.ts +14 -5
- package/src/testing/behavioral/journeys.cli.test.ts +116 -73
- package/src/testing/behavioral/journeys.defaultPack.test.ts +477 -0
- package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
- package/src/testing/behavioral/journeys.interactiveTty.e2e.test.ts +66 -0
- package/src/testing/behavioral/journeys.livePack.e2e.test.ts +129 -0
- package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +179 -0
- package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
- package/src/testing/behavioral/liveReadSurface.ts +4 -3
- package/src/testing/behavioral/mcpSurface.test.ts +10 -5
- package/src/testing/behavioral/staleCommands.test.ts +86 -0
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +9 -9
- package/src/testing/eval/cases/assembly.ts +16 -9
- package/src/testing/eval/cases/effectful.ts +31 -16
- package/src/testing/eval/cases/readNouns.ts +23 -18
- package/src/testing/eval/cases/stable.ts +5 -1
- package/src/testing/eval/eval.test.ts +7 -1
- package/src/testing/fixtures/fixtureCapability.ts +201 -1
- package/src/testing/fixtures/graph/canonical.ts +6 -3
- package/src/testing/fixtures/graph/defaultPack.ts +179 -0
- package/src/testing/fixtures/graph/owlExport.ts +97 -0
- package/src/testing/helpers/extractSnippets.ts +29 -0
- package/src/testing/helpers/fixtureGraph.ts +33 -16
- package/src/testing/helpers/golden.ts +1 -1
- package/src/testing/helpers/packRuntime.ts +2 -3
- package/src/testing/helpers/projectCli.ts +2 -1
- package/src/testing/helpers/projectMcp.ts +6 -0
- package/src/testing/perf/budgets.test.ts +4 -0
- package/src/testing/perf/budgets.ts +22 -7
- package/src/testing/perf/globalSetup.ts +121 -6
- package/src/types.ts +2 -2
- package/src/capabilities/block/blockList.verb.ts +0 -150
- package/src/capabilities/block/index.ts +0 -25
- package/src/capabilities/block/pack.ts +0 -155
- package/src/capabilities/block/parity.test.ts +0 -180
- package/src/capabilities/block/tierChain.ts +0 -78
- package/src/capabilities/colophon/pragmaColophon.ts +0 -66
- package/src/capabilities/config/field.test.ts +0 -188
- package/src/capabilities/modifier/index.ts +0 -15
- package/src/capabilities/modifier/pack.ts +0 -64
- package/src/capabilities/standard/index.ts +0 -18
- package/src/capabilities/standard/pack.ts +0 -128
- package/src/capabilities/tier/index.ts +0 -19
- package/src/capabilities/tier/lookup.render.ts +0 -34
- package/src/capabilities/tier/lookup.test.ts +0 -68
- package/src/capabilities/tier/lookup.verb.ts +0 -44
- package/src/capabilities/tier/pack.ts +0 -28
- package/src/capabilities/tier/runLookup.ts +0 -59
- package/src/capabilities/token/addConfig.render.ts +0 -27
- package/src/capabilities/token/addConfig.test.ts +0 -62
- package/src/capabilities/token/addConfig.verb.ts +0 -40
- package/src/capabilities/token/index.ts +0 -19
- package/src/capabilities/token/pack.ts +0 -58
- package/src/capabilities/token/runAddConfig.ts +0 -95
- package/src/kernel/completion/bashDrive.test.ts +0 -73
- package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
- package/src/kernel/runtime/lock.ts +0 -90
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kernel is generic machinery: it may describe itself, but it may not name
|
|
3
|
+
* the distribution in an authored string. A fork changes `pragma.conf.ts`;
|
|
4
|
+
* anything the kernel hardcodes becomes a lie it cannot fix.
|
|
5
|
+
*
|
|
6
|
+
* What this guard enforces, exactly: no string literal under `src/kernel/**`
|
|
7
|
+
* (plus `constants.ts` and `bin.ts`) contains the distribution's `name` as a
|
|
8
|
+
* word, the domain phrase "design system", or any namespace IRI the
|
|
9
|
+
* distribution declares in `prefixes`. Module specifiers are not copy and are
|
|
10
|
+
* skipped; every remaining site that legitimately carries one is listed in
|
|
11
|
+
* {@link EXEMPT} with its reason.
|
|
12
|
+
*
|
|
13
|
+
* `src/capabilities/**` and `pragma.conf.ts` are OUT of the rule above.
|
|
14
|
+
* `pragma.conf.ts` is the file a fork edits, so it is content by definition;
|
|
15
|
+
* `src/capabilities/**` carries the kernel's own nouns — their prompts,
|
|
16
|
+
* setup copy, doctor findings and other runtime copy no doc publishes — and a
|
|
17
|
+
* guard needing a 65-entry exemption list is a guard that mostly exempts. (The
|
|
18
|
+
* `ds:` residue the exclusion was originally written for is gone: L-OPEN-9
|
|
19
|
+
* left no hand-written data verb behind, and `distribution.test.ts` now proves
|
|
20
|
+
* every data noun is exactly its declared story.) Two narrower rules reach it
|
|
21
|
+
* instead, at the bottom of this
|
|
22
|
+
* file — *a command a user is told to run is never a literal*, stated once as
|
|
23
|
+
* a POSITION ({@link COMMAND_POSITIONS}, read over raw source so it holds
|
|
24
|
+
* whatever the scanner can see) and once as a SHAPE (a backticked command
|
|
25
|
+
* inside any authored literal, read through {@link readCopy} so an
|
|
26
|
+
* interpolated one leaves no matching chunk). Neither rule carries an
|
|
27
|
+
* exemption: the last one — the colophon narrative, this distribution's voice
|
|
28
|
+
* in a capability source — became content the config declares
|
|
29
|
+
* (`pragma.conf.ts#colophon`), which is what made removing it checkable.
|
|
30
|
+
*
|
|
31
|
+
* NOTE for a reader of an older revision: this docblock used to say the
|
|
32
|
+
* `examples[].cmd` sweep and the `docs/reference/*.md` regen "have to move
|
|
33
|
+
* together". They must be CONSISTENT, not simultaneous — the whole sweep was
|
|
34
|
+
* measured to move zero doc bytes, because every literal it replaced was
|
|
35
|
+
* already spelling this distribution's own name.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
39
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
40
|
+
import { fileURLToPath } from "node:url";
|
|
41
|
+
import { describe, expect, it } from "vitest";
|
|
42
|
+
import conf from "../../pragma.conf.js";
|
|
43
|
+
import { BIN_NAME } from "../constants.js";
|
|
44
|
+
|
|
45
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
46
|
+
const root = resolve(here, "..");
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Files whose strings are pinned, frozen, or owned by another lane, so changing
|
|
50
|
+
* them is not a copy edit:
|
|
51
|
+
* - `*.generated.ts` — build artifacts (the embedded pack's graph data).
|
|
52
|
+
* - `config/defaults.ts` — THE distribution seam: it imports the distribution
|
|
53
|
+
* config and names the file it imports. That is its job, not a leak.
|
|
54
|
+
* - `kernel/vocabulary.ts` — the same seam for the vocabulary declaration. Its
|
|
55
|
+
* diagnostics quote the file it imports, and five modules hardcode that
|
|
56
|
+
* specifier, so a fork cannot rename it: deriving the name from `BIN_NAME`
|
|
57
|
+
* here would name a path that does not exist.
|
|
58
|
+
* - `runtime/graphpack/hash.ts` — `<<<pragma-pack:…>>>` are hash domain
|
|
59
|
+
* separators; changing them re-mints every pack content hash. CROSS-LANE.
|
|
60
|
+
*/
|
|
61
|
+
const EXEMPT = [
|
|
62
|
+
".generated.ts",
|
|
63
|
+
"config/defaults.ts",
|
|
64
|
+
"kernel/vocabulary.ts",
|
|
65
|
+
"runtime/graphpack/hash.ts",
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Every authored `.ts` in the kernel, plus the identity module and the bin.
|
|
70
|
+
*
|
|
71
|
+
* @param dir - Directory to walk.
|
|
72
|
+
* @returns Absolute paths of the non-test TypeScript sources beneath it.
|
|
73
|
+
* @note Impure — reads the source tree.
|
|
74
|
+
*/
|
|
75
|
+
function listSources(dir: string): string[] {
|
|
76
|
+
const found: string[] = [];
|
|
77
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
78
|
+
const path = join(dir, entry.name);
|
|
79
|
+
if (entry.isDirectory()) found.push(...listSources(path));
|
|
80
|
+
else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts")) {
|
|
81
|
+
found.push(path);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return found;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const files = [
|
|
88
|
+
...listSources(join(root, "kernel")),
|
|
89
|
+
join(root, "constants.ts"),
|
|
90
|
+
join(root, "bin.ts"),
|
|
91
|
+
].filter((file) => !EXEMPT.some((suffix) => file.endsWith(suffix)));
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The authored copy in a file: every string literal that is neither a module
|
|
95
|
+
* specifier nor inside a comment.
|
|
96
|
+
*
|
|
97
|
+
* A single left-to-right pass, not a pair of regexes, because the two orderings
|
|
98
|
+
* a regex pair allows are both wrong: strip comments first and any literal
|
|
99
|
+
* containing `//` is truncated, re-pairing the quotes across the deleted tail
|
|
100
|
+
* and hiding whatever followed; match literals first and an apostrophe in prose
|
|
101
|
+
* ("the kernel's") opens a quote that never closes, which is also the shape
|
|
102
|
+
* that makes the literal pattern backtrack catastrophically. Reading the file
|
|
103
|
+
* in source order makes both cases fall out: a `//` inside a string belongs to
|
|
104
|
+
* the string, an apostrophe inside a comment belongs to the comment.
|
|
105
|
+
*
|
|
106
|
+
* A REGEX LITERAL is code, not copy, and it is the third ordering hazard: a `"`
|
|
107
|
+
* inside a regex (`/[<>"{}|\\^`\s]/`) would otherwise open a phantom string that
|
|
108
|
+
* swallows every literal until the next `"` — so the scanner reads regexes as
|
|
109
|
+
* regexes and skips them whole.
|
|
110
|
+
*
|
|
111
|
+
* A TEMPLATE SUBSTITUTION is the fourth, and the one this tree actually writes:
|
|
112
|
+
* `${…}` is code, and the code inside it may open another template. Reading a
|
|
113
|
+
* template as one flat span makes the NESTED backtick close the OUTER literal,
|
|
114
|
+
* and every quote after it in the file pairs off by one — which left the whole
|
|
115
|
+
* tail of `packs/renderPack.ts` invisible to this guard. So a substitution is
|
|
116
|
+
* handed back to the code scanner, and the literal chunks around it are the copy.
|
|
117
|
+
*
|
|
118
|
+
* Specifiers (`./x.js`, `@scope/pkg`) name modules, not users, and must stay
|
|
119
|
+
* literal, so they are not copy.
|
|
120
|
+
*
|
|
121
|
+
* @param source - The file's text.
|
|
122
|
+
* @returns The authored string literals.
|
|
123
|
+
*/
|
|
124
|
+
function readCopy(source: string): string[] {
|
|
125
|
+
const copy: string[] = [];
|
|
126
|
+
scanCode(source, 0, copy);
|
|
127
|
+
return copy;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A `/` opens a regex only where a VALUE may start; after a value it divides. */
|
|
131
|
+
const REGEX_OPENS_AFTER = "=(,:[!&|?{};+-*%~^<>";
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Read code from `from`, pushing every authored literal it quotes onto `copy`.
|
|
135
|
+
*
|
|
136
|
+
* @param source - The file's text.
|
|
137
|
+
* @param from - Index to start reading at.
|
|
138
|
+
* @param copy - Accumulator the authored literals are pushed onto.
|
|
139
|
+
* @param untilBrace - Stop at the `}` closing a template substitution.
|
|
140
|
+
* @returns The index the scan stopped at.
|
|
141
|
+
*/
|
|
142
|
+
function scanCode(
|
|
143
|
+
source: string,
|
|
144
|
+
from: number,
|
|
145
|
+
copy: string[],
|
|
146
|
+
untilBrace = false,
|
|
147
|
+
): number {
|
|
148
|
+
// One character of context decides regex-vs-division — the last significant
|
|
149
|
+
// one — which is all TypeScript sources in this tree need (no `)`/`]`-preceded
|
|
150
|
+
// regex, i.e. no `if (x) /re/.test(y)`, which would read as division).
|
|
151
|
+
let previous = "";
|
|
152
|
+
let braces = 0;
|
|
153
|
+
for (let i = from; i < source.length; i++) {
|
|
154
|
+
const char = source[i];
|
|
155
|
+
if (char === "/" && source[i + 1] === "*") {
|
|
156
|
+
const end = source.indexOf("*/", i + 2);
|
|
157
|
+
if (end === -1) return source.length;
|
|
158
|
+
i = end + 1;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (char === "/" && source[i + 1] === "/") {
|
|
162
|
+
const end = source.indexOf("\n", i + 2);
|
|
163
|
+
if (end === -1) return source.length;
|
|
164
|
+
i = end;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (
|
|
168
|
+
char === "/" &&
|
|
169
|
+
previous !== "" &&
|
|
170
|
+
REGEX_OPENS_AFTER.includes(previous)
|
|
171
|
+
) {
|
|
172
|
+
// Skip the body, honouring escapes and `[…]` classes (where an unescaped
|
|
173
|
+
// `/` is an ordinary character). An unterminated regex cannot span a line,
|
|
174
|
+
// so a newline ends the scan rather than eating the rest of the file.
|
|
175
|
+
let inClass = false;
|
|
176
|
+
i += 1;
|
|
177
|
+
while (i < source.length && source[i] !== "\n") {
|
|
178
|
+
const body = source[i];
|
|
179
|
+
if (body === "\\") i += 1;
|
|
180
|
+
else if (body === "[") inClass = true;
|
|
181
|
+
else if (body === "]") inClass = false;
|
|
182
|
+
else if (body === "/" && !inClass) break;
|
|
183
|
+
i += 1;
|
|
184
|
+
}
|
|
185
|
+
previous = "/";
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (char === "`") {
|
|
189
|
+
i = scanTemplate(source, i + 1, copy);
|
|
190
|
+
previous = char;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (char !== '"' && char !== "'") {
|
|
194
|
+
if (untilBrace && char === "{") braces += 1;
|
|
195
|
+
if (untilBrace && char === "}") {
|
|
196
|
+
if (braces === 0) return i;
|
|
197
|
+
braces -= 1;
|
|
198
|
+
}
|
|
199
|
+
if (!/\s/.test(char)) previous = char;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const start = ++i;
|
|
203
|
+
while (i < source.length && source[i] !== char) {
|
|
204
|
+
i += source[i] === "\\" ? 2 : 1;
|
|
205
|
+
}
|
|
206
|
+
pushCopy(copy, source.slice(start, i));
|
|
207
|
+
previous = char;
|
|
208
|
+
}
|
|
209
|
+
return source.length;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Read a template literal's body from `from` — just past the opening backtick —
|
|
214
|
+
* pushing its literal chunks onto `copy` and handing each `${…}` back to
|
|
215
|
+
* {@link scanCode}, which is where a nested template is read as a template.
|
|
216
|
+
*
|
|
217
|
+
* @param source - The file's text.
|
|
218
|
+
* @param from - Index of the first character after the opening backtick.
|
|
219
|
+
* @param copy - Accumulator the authored chunks are pushed onto.
|
|
220
|
+
* @returns The index of the closing backtick.
|
|
221
|
+
*/
|
|
222
|
+
function scanTemplate(source: string, from: number, copy: string[]): number {
|
|
223
|
+
let chunk = "";
|
|
224
|
+
let i = from;
|
|
225
|
+
while (i < source.length && source[i] !== "`") {
|
|
226
|
+
if (source[i] === "\\") {
|
|
227
|
+
chunk += source.slice(i, i + 2);
|
|
228
|
+
i += 2;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (source[i] === "$" && source[i + 1] === "{") {
|
|
232
|
+
i = scanCode(source, i + 2, copy, true) + 1;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
chunk += source[i];
|
|
236
|
+
i += 1;
|
|
237
|
+
}
|
|
238
|
+
pushCopy(copy, chunk);
|
|
239
|
+
return i;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Keep a literal unless it is empty or names a module rather than a user. */
|
|
243
|
+
function pushCopy(copy: string[], literal: string): void {
|
|
244
|
+
if (literal && !literal.startsWith(".") && !literal.startsWith("@")) {
|
|
245
|
+
copy.push(literal);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* `file: literal` for every authored literal matching `pattern`, so a failure
|
|
251
|
+
* names its offenders instead of only counting them.
|
|
252
|
+
*
|
|
253
|
+
* @param pattern - What kernel copy may not contain.
|
|
254
|
+
* @returns One entry per offending literal.
|
|
255
|
+
* @note Impure — reads every kernel source.
|
|
256
|
+
*/
|
|
257
|
+
function findOffenders(pattern: RegExp): string[] {
|
|
258
|
+
const found: string[] = [];
|
|
259
|
+
for (const file of files) {
|
|
260
|
+
for (const literal of readCopy(readFileSync(file, "utf-8"))) {
|
|
261
|
+
if (pattern.test(literal)) {
|
|
262
|
+
found.push(`${relative(root, file)}: ${literal}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return found;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
describe("the copy scanner (PROTECTED)", () => {
|
|
270
|
+
// The guard below is only as good as what the scanner can see, so the scanner
|
|
271
|
+
// is pinned directly on the shape that used to blind it: `src/kernel/packs/
|
|
272
|
+
// iri.ts` declares two regexes that each contain a `"`, and a leak between
|
|
273
|
+
// them was invisible. DERIVED, so the probe leaks whatever this distribution
|
|
274
|
+
// is called.
|
|
275
|
+
const leak = `${BIN_NAME} sources update`;
|
|
276
|
+
|
|
277
|
+
it("reads a literal sitting between two regexes that contain a quote", () => {
|
|
278
|
+
const source = [
|
|
279
|
+
'const UNSAFE = /[<>"{}|\\\\^`\\s]/;',
|
|
280
|
+
`const LEAK = "${leak}";`,
|
|
281
|
+
'const EMBEDDABLE = /^[A-Za-z][\\w+.-]*:\\/\\/[^<>"\\s]+$/;',
|
|
282
|
+
].join("\n");
|
|
283
|
+
expect(readCopy(source)).toEqual([leak]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("reads the same literal with no regex around it", () => {
|
|
287
|
+
expect(readCopy(`const LEAK = "${leak}";`)).toEqual([leak]);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("reads a dividing slash as division, not as an opening regex", () => {
|
|
291
|
+
// The division and the leak share a LINE: a scanner that read every `/` as
|
|
292
|
+
// a regex opener would skip to the next `/` or the newline, whichever comes
|
|
293
|
+
// first, and swallow the leak. On separate lines the regex skip stops at the
|
|
294
|
+
// newline before it reaches the literal, so both scanners agree and the case
|
|
295
|
+
// asserts nothing.
|
|
296
|
+
const source = `const rate = done / all; const LEAK = "${leak}";`;
|
|
297
|
+
expect(readCopy(source)).toEqual([leak]);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("reads a template substitution as code, not as the end of the literal", () => {
|
|
301
|
+
// `packs/renderPack.ts` writes exactly this: a template whose substitution
|
|
302
|
+
// contains another template, itself containing an ESCAPED backtick. Read
|
|
303
|
+
// flat, the nested backtick closes the outer literal and every quote after
|
|
304
|
+
// it in the file pairs off by one — so the leak below, and the whole tail of
|
|
305
|
+
// any file shaped like this, became invisible.
|
|
306
|
+
const source = [
|
|
307
|
+
"const hint = shape.emptyRecovery",
|
|
308
|
+
" ? `${shape.emptyRecovery.message}${",
|
|
309
|
+
" shape.emptyRecovery.cli",
|
|
310
|
+
" ? ` Run \\`${PREFIX}${shape.emptyRecovery.cli}\\`.`",
|
|
311
|
+
' : ""',
|
|
312
|
+
" }`",
|
|
313
|
+
" : DEFAULT_HINT;",
|
|
314
|
+
`const LEAK = "${leak}";`,
|
|
315
|
+
].join("\n");
|
|
316
|
+
expect(readCopy(source)).toContain(leak);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe("kernel copy (PROTECTED)", () => {
|
|
321
|
+
it("no kernel string names the distribution", () => {
|
|
322
|
+
// DERIVED from the shipped `name`, and ESCAPED: this guard hardcodes no
|
|
323
|
+
// name, and a fork called `my.cli` or `c++tool` neither over-matches nor
|
|
324
|
+
// dies compiling its own name as a pattern. The word boundaries keep
|
|
325
|
+
// identifiers like `PragmaError` out while still catching the forms the
|
|
326
|
+
// name actually leaks in — `pragma.config.ts`, `pragma-pack`, `pragma `.
|
|
327
|
+
const name = BIN_NAME.replace(/[-.*+?^${}()|[\]\\]/g, "\\$&");
|
|
328
|
+
expect(findOffenders(new RegExp(`\\b${name}\\b`, "i"))).toEqual([]);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("no kernel string names a domain", () => {
|
|
332
|
+
expect(findOffenders(/design[- ]system/i)).toEqual([]);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("no kernel string hardcodes a namespace the distribution declares", () => {
|
|
336
|
+
// The behavioural half of the same rule. A kernel module that spells out a
|
|
337
|
+
// declared namespace IRI has decided what the domain is, so a fork editing
|
|
338
|
+
// `pragma.conf.ts` changes the store and not the code reading it.
|
|
339
|
+
//
|
|
340
|
+
// DERIVED from the declaration, one pattern per namespace rather than one
|
|
341
|
+
// alternation: a distribution that declares none then yields no offenders,
|
|
342
|
+
// instead of an empty alternation that matches every literal.
|
|
343
|
+
const offenders = Object.values(conf.prefixes ?? {}).flatMap((namespace) =>
|
|
344
|
+
findOffenders(
|
|
345
|
+
new RegExp(namespace.replace(/[-.*+?^${}()|[\]\\]/g, "\\$&")),
|
|
346
|
+
),
|
|
347
|
+
);
|
|
348
|
+
expect(offenders).toEqual([]);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("no kernel string writes a term in a namespace the distribution declares", () => {
|
|
352
|
+
// The form the kernel actually leaked the domain in. Every coupling this
|
|
353
|
+
// guard exists to keep out — `ds:Prompt`, `ds:Tier`, `ds:name` — was a
|
|
354
|
+
// PREFIXED NAME, which contains no namespace IRI and so passes the rule
|
|
355
|
+
// above. DERIVED from the same declaration, by KEY this time.
|
|
356
|
+
//
|
|
357
|
+
// A leading non-word character keeps `https://…` and `foo.ds:x` out while
|
|
358
|
+
// still catching a bare term, one inside a query template, and one in
|
|
359
|
+
// user-facing copy.
|
|
360
|
+
const offenders = Object.keys(conf.prefixes ?? {}).flatMap((prefix) =>
|
|
361
|
+
findOffenders(
|
|
362
|
+
new RegExp(
|
|
363
|
+
`(^|[^A-Za-z0-9_])${prefix.replace(/[-.*+?^${}()|[\]\\]/g, "\\$&")}:[A-Za-z_]`,
|
|
364
|
+
),
|
|
365
|
+
),
|
|
366
|
+
);
|
|
367
|
+
expect(offenders).toEqual([]);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* The grammar positions that quote a command a user is told to RUN: an example
|
|
373
|
+
* (`cmd:`), a recovery hint (`cli:`) and a doctor fix (`remedy:`).
|
|
374
|
+
*/
|
|
375
|
+
const COMMAND_POSITIONS = ["cmd", "cli", "remedy"];
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Every authored `.ts` under `src/capabilities/**` — no exemptions. The one
|
|
379
|
+
* file these rules used to exempt, `colophon/pragmaColophon.ts`, carried the
|
|
380
|
+
* distribution's own narrative in a capability source while the owner had not
|
|
381
|
+
* decided whether a fork inherits, rewrites, or declares it. The ruling
|
|
382
|
+
* (PRA-107): the narrative is DECLARED — it lives in `pragma.conf.ts` as
|
|
383
|
+
* `colophon: { markdown, summary }`, the file a fork edits — and the module is
|
|
384
|
+
* deleted. This filter-free list is the machine-checkable proof that the seam
|
|
385
|
+
* exists: a narrative reintroduced as a capability source falls under the two
|
|
386
|
+
* rules below like any other copy.
|
|
387
|
+
*/
|
|
388
|
+
const capabilitySources = listSources(join(root, "capabilities"));
|
|
389
|
+
|
|
390
|
+
describe("capability commands (PROTECTED)", () => {
|
|
391
|
+
it("no `cmd:`, `cli:` or `remedy:` value is a bare literal", () => {
|
|
392
|
+
// The rule that reaches `src/capabilities/**` without exempting most of it.
|
|
393
|
+
// The wider "name nothing" rule cannot: that tree legitimately carries the
|
|
394
|
+
// domain in runtime copy and in wire identifiers. But a command string is
|
|
395
|
+
// never content — it is the installed binary's own name plus a grammar
|
|
396
|
+
// path, and both are derived. So this is stated as a POSITION rule, not a
|
|
397
|
+
// file rule, and carries no exemptions.
|
|
398
|
+
//
|
|
399
|
+
// Raw source, not `readCopy`: the position is what makes it an offence, and
|
|
400
|
+
// reading the text directly means this guard holds whatever the scanner can
|
|
401
|
+
// see. DERIVED and ESCAPED from the shipped `name`, as above.
|
|
402
|
+
//
|
|
403
|
+
// A BACKTICK is a quote here. Every swept site is a template literal, so a
|
|
404
|
+
// regression that keeps the backtick and drops the interpolation is the ONE
|
|
405
|
+
// shape this rule has to catch — and it was the one shape the class `["']`
|
|
406
|
+
// could not see. The opening quote is captured and the span runs to it, so
|
|
407
|
+
// a name inside backticks in a double-quoted sentence still counts while
|
|
408
|
+
// the match cannot run past the literal that opened it.
|
|
409
|
+
//
|
|
410
|
+
// The scan is over the whole file, not line by line, because the formatter
|
|
411
|
+
// wraps a long value onto the line after its key: that is how `doctor`'s
|
|
412
|
+
// ke-store remedy sat in a `remedy:` position, naming this distribution,
|
|
413
|
+
// under a guard that reported zero exemptions.
|
|
414
|
+
const name = BIN_NAME.replace(/[-.*+?^${}()|[\]\\]/g, "\\$&");
|
|
415
|
+
const pattern = new RegExp(
|
|
416
|
+
`(?:${COMMAND_POSITIONS.join("|")}):\\s*(["'\`])(?:(?!\\1)[^])*?\\b${name}\\b`,
|
|
417
|
+
"gi",
|
|
418
|
+
);
|
|
419
|
+
const offenders: string[] = [];
|
|
420
|
+
for (const file of capabilitySources) {
|
|
421
|
+
for (const match of readFileSync(file, "utf-8").matchAll(pattern)) {
|
|
422
|
+
offenders.push(`${relative(root, file)}: ${match[0].trim()}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
expect(offenders).toEqual([]);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("no command quoted in capability prose is a bare literal", () => {
|
|
429
|
+
// The other half of the same promise, and the half the position rule cannot
|
|
430
|
+
// reach: an empty-state hint, a recovery MESSAGE (the second argument to
|
|
431
|
+
// `cliRecovery`, beside a first argument the position rule already covers),
|
|
432
|
+
// a `sources status` headline, a wizard step's title. They reach the same
|
|
433
|
+
// user with the same instruction, and seven of them named this distribution
|
|
434
|
+
// while the rule above reported zero exemptions.
|
|
435
|
+
//
|
|
436
|
+
// A command in prose is a backticked one: `` run `pragma sources update` ``.
|
|
437
|
+
// `readCopy` reads a template's substitutions as code and its literal chunks
|
|
438
|
+
// as copy, so an interpolated `\`${BIN_NAME} …\`` leaves no chunk that
|
|
439
|
+
// begins with the name — which is exactly the difference this asserts.
|
|
440
|
+
// DERIVED and ESCAPED from the shipped `name`, as above.
|
|
441
|
+
const name = BIN_NAME.replace(/[-.*+?^${}()|[\]\\]/g, "\\$&");
|
|
442
|
+
const pattern = new RegExp(`\`${name}\\b`, "i");
|
|
443
|
+
const offenders: string[] = [];
|
|
444
|
+
for (const file of capabilitySources) {
|
|
445
|
+
for (const literal of readCopy(readFileSync(file, "utf-8"))) {
|
|
446
|
+
if (pattern.test(literal)) {
|
|
447
|
+
offenders.push(`${relative(root, file)}: ${literal}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
expect(offenders).toEqual([]);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
@@ -26,10 +26,10 @@ describe("PragmaError factories", () => {
|
|
|
26
26
|
|
|
27
27
|
it("invalidInput enumerates valid options", () => {
|
|
28
28
|
const error = PragmaError.invalidInput("format", "yaml", {
|
|
29
|
-
validOptions: ["plain", "json"],
|
|
29
|
+
validOptions: ["plain", "llm", "json"],
|
|
30
30
|
});
|
|
31
31
|
expect(error.code).toBe("INVALID_INPUT");
|
|
32
|
-
expect(error.validOptions).toEqual(["plain", "json"]);
|
|
32
|
+
expect(error.validOptions).toEqual(["plain", "llm", "json"]);
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
it("unknownVerb names the token", () => {
|
|
@@ -73,7 +73,7 @@ describe("serializeError", () => {
|
|
|
73
73
|
it("includes suggestions, recovery, validOptions, and filters when present", () => {
|
|
74
74
|
const error = PragmaError.notFound("block", "Buton", {
|
|
75
75
|
suggestions: ["Button"],
|
|
76
|
-
recovery: cliRecovery("
|
|
76
|
+
recovery: cliRecovery("block list", "List blocks."),
|
|
77
77
|
});
|
|
78
78
|
expect(serializeError(error)).toEqual({
|
|
79
79
|
code: "ENTITY_NOT_FOUND",
|
|
@@ -105,14 +105,14 @@ describe("envelope builders", () => {
|
|
|
105
105
|
|
|
106
106
|
it("wraps an error into the failure envelope", () => {
|
|
107
107
|
const error = PragmaError.invalidInput("format", "yaml", {
|
|
108
|
-
validOptions: ["plain", "json"],
|
|
108
|
+
validOptions: ["plain", "llm", "json"],
|
|
109
109
|
});
|
|
110
110
|
expect(errorEnvelope(error)).toEqual({
|
|
111
111
|
ok: false,
|
|
112
112
|
error: {
|
|
113
113
|
code: "INVALID_INPUT",
|
|
114
114
|
message: 'Invalid format "yaml".',
|
|
115
|
-
validOptions: ["plain", "json"],
|
|
115
|
+
validOptions: ["plain", "llm", "json"],
|
|
116
116
|
},
|
|
117
117
|
});
|
|
118
118
|
});
|
|
@@ -27,8 +27,9 @@ const ERROR_CODES = [
|
|
|
27
27
|
// A runtime condition that is NOT a bug and NOT a usage mistake, so it must
|
|
28
28
|
// NOT collapse to INTERNAL_ERROR's "please report this issue": a capability
|
|
29
29
|
// genuinely unavailable in this build/environment (`create package` /
|
|
30
|
-
// `create application` in the compiled binary, whose
|
|
31
|
-
//
|
|
30
|
+
// `create application` in the compiled binary, whose generators read their
|
|
31
|
+
// templates from disk rather than through the embedded manifest —
|
|
32
|
+
// `create component` reads through it and runs), OR an external command
|
|
32
33
|
// that RAN and failed for a fixable environment reason (a denied global
|
|
33
34
|
// `npm i -g`, a network/registry failure — see `shared/assertExecOk`). Both
|
|
34
35
|
// carry an actionable recovery and map to the generic runtime exit 1.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
export { ERROR_CODES } from "./constants.js";
|
|
10
10
|
export { PragmaError } from "./PragmaError.js";
|
|
11
|
-
export {
|
|
11
|
+
export { cliRecovery } from "./recovery.js";
|
|
12
12
|
export {
|
|
13
13
|
renderErrorJson,
|
|
14
14
|
renderErrorLlm,
|
|
@@ -1,60 +1,37 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { RECOVERY_CLI_PREFIX } from "../../constants.js";
|
|
3
|
-
import {
|
|
4
|
-
import { assertRecoveryCli, cliRecovery } from "./recovery.js";
|
|
3
|
+
import { cliRecovery } from "./recovery.js";
|
|
5
4
|
|
|
6
5
|
describe("recovery.cli invariant (D5)", () => {
|
|
7
|
-
it("
|
|
8
|
-
//
|
|
9
|
-
//
|
|
6
|
+
it("carries the shipped distribution's recovery prefix", () => {
|
|
7
|
+
// The prefix is DERIVED from the distribution's `name` (pragma.conf.ts), so
|
|
8
|
+
// recovery hints quote a command the installed binary actually answers to.
|
|
9
|
+
// This pins the value THIS distribution ships; `src/identity.test.ts` proves
|
|
10
|
+
// the derivation by varying the config.
|
|
10
11
|
expect(RECOVERY_CLI_PREFIX).toBe("pragma ");
|
|
11
12
|
});
|
|
12
13
|
|
|
13
|
-
it("
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
expect((
|
|
20
|
-
expect(() => assertRecoveryCli("pragmatic info")).toThrow(
|
|
21
|
-
/must start with/,
|
|
22
|
-
);
|
|
23
|
-
expect(() => assertRecoveryCli("pragmainfo")).toThrow(/must start with/);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("builds a validated recovery whose cli carries the prefix", () => {
|
|
27
|
-
const recovery = cliRecovery(
|
|
28
|
-
"pragma config show",
|
|
29
|
-
"See the resolved config.",
|
|
30
|
-
);
|
|
31
|
-
expect(recovery).toEqual({
|
|
14
|
+
it("prepends the distribution's prefix to the command it is given", () => {
|
|
15
|
+
// The caller passes the SUFFIX, so a hint naming the wrong binary is
|
|
16
|
+
// unwritable rather than merely asserted against. What replaced the deleted
|
|
17
|
+
// `assertRecoveryCli` is `kernel/copy.test.ts`'s position rule, which sees
|
|
18
|
+
// the raw `cli:` literals that never reached this function at all — the only
|
|
19
|
+
// place a wrong prefix was ever actually written.
|
|
20
|
+
expect(cliRecovery("config show", "See the resolved config.")).toEqual({
|
|
32
21
|
cli: "pragma config show",
|
|
33
22
|
message: "See the resolved config.",
|
|
34
23
|
});
|
|
35
|
-
expect(recovery.cli?.startsWith(RECOVERY_CLI_PREFIX)).toBe(true);
|
|
36
24
|
});
|
|
37
25
|
|
|
38
|
-
it("
|
|
39
|
-
expect(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
it("every PragmaError.recovery.cli in the kernel carries the prefix", () => {
|
|
43
|
-
// The factories that ship a recovery hint must honour the invariant. Only
|
|
44
|
-
// internalError seeds one today (message-only, no cli), so assert the
|
|
45
|
-
// guard holds for any cli that is present.
|
|
46
|
-
const errors = [
|
|
47
|
-
PragmaError.internalError("boom"),
|
|
48
|
-
PragmaError.notFound("block", "Nope", {
|
|
49
|
-
recovery: cliRecovery("pragma block list", "List blocks."),
|
|
26
|
+
it("carries an MCP tool alongside the command when one is given", () => {
|
|
27
|
+
expect(
|
|
28
|
+
cliRecovery("sources update", "Build the store.", {
|
|
29
|
+
tool: "sources_update",
|
|
50
30
|
}),
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
).not.toThrow();
|
|
57
|
-
}
|
|
58
|
-
}
|
|
31
|
+
).toEqual({
|
|
32
|
+
cli: "pragma sources update",
|
|
33
|
+
message: "Build the store.",
|
|
34
|
+
mcp: { tool: "sources_update" },
|
|
35
|
+
});
|
|
59
36
|
});
|
|
60
37
|
});
|
|
@@ -1,49 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The recovery-string invariant (D5).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* must begin with
|
|
6
|
-
*
|
|
7
|
-
* so
|
|
8
|
-
*
|
|
4
|
+
* A `recovery.cli` hint quotes a command the installed binary answers to, so it
|
|
5
|
+
* must begin with {@link RECOVERY_CLI_PREFIX} — the distribution's own `name`
|
|
6
|
+
* plus a space. {@link cliRecovery} takes the command SUFFIX and prepends the
|
|
7
|
+
* prefix itself, so a hint that names the wrong binary is unwritable rather
|
|
8
|
+
* than merely detected. That is why there is no assertion here: an assertion
|
|
9
|
+
* over a string the caller composed from the prefix cannot fail, and the hints
|
|
10
|
+
* that were actually wrong were raw `cli:` object literals that never reached
|
|
11
|
+
* this function at all. `kernel/copy.test.ts` covers those, at the position.
|
|
12
|
+
*
|
|
13
|
+
* `kernel/packs/schema.ts` holds a user-authored pack's `emptyRecovery.cli` to
|
|
14
|
+
* the same shape from the other side — it must NOT carry a prefix, because the
|
|
15
|
+
* consuming distribution's renderer supplies one.
|
|
9
16
|
*/
|
|
10
17
|
|
|
11
18
|
import { RECOVERY_CLI_PREFIX } from "../../constants.js";
|
|
12
19
|
import type { Recovery } from "./types.js";
|
|
13
20
|
|
|
14
21
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* @param cli - The candidate CLI recovery command.
|
|
18
|
-
* @throws Error when the command does not begin with `pragma `.
|
|
19
|
-
*/
|
|
20
|
-
export function assertRecoveryCli(cli: string): void {
|
|
21
|
-
if (!cli.startsWith(RECOVERY_CLI_PREFIX)) {
|
|
22
|
-
throw new Error(
|
|
23
|
-
`recovery.cli must start with "${RECOVERY_CLI_PREFIX}" (D5); got "${cli}"`,
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Build a {@link Recovery} whose `cli` command is guaranteed to carry the
|
|
30
|
-
* canonical prefix, optionally pairing it with an MCP tool an agent can call.
|
|
22
|
+
* Build a {@link Recovery} whose `cli` command carries the distribution's own
|
|
23
|
+
* prefix, optionally pairing it with an MCP tool an agent can call.
|
|
31
24
|
*
|
|
32
25
|
* A recovery may need to speak to BOTH surfaces: the `cli` string guides a human,
|
|
33
26
|
* while `mcp` names the tool an agent invokes (an agent cannot run a shell
|
|
34
27
|
* command). The single human `message` is shared by both.
|
|
35
28
|
*
|
|
36
|
-
* @param
|
|
29
|
+
* @param command - The recovery command WITHOUT the binary name (`sources update`).
|
|
37
30
|
* @param message - Human-readable guidance shown alongside the command.
|
|
38
31
|
* @param mcp - The MCP tool (and optional params) an agent calls to recover.
|
|
39
|
-
* @returns A
|
|
40
|
-
* @throws Error when `cli` does not begin with `pragma `.
|
|
32
|
+
* @returns A recovery hint whose `cli` names this distribution's binary.
|
|
41
33
|
*/
|
|
42
34
|
export function cliRecovery(
|
|
43
|
-
|
|
35
|
+
command: string,
|
|
44
36
|
message: string,
|
|
45
37
|
mcp?: Recovery["mcp"],
|
|
46
38
|
): Recovery {
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
return {
|
|
40
|
+
message,
|
|
41
|
+
cli: `${RECOVERY_CLI_PREFIX}${command}`,
|
|
42
|
+
...(mcp ? { mcp } : {}),
|
|
43
|
+
};
|
|
49
44
|
}
|