@canonical/pragma-cli 0.35.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bundled-skills/add-standard/SKILL.md +338 -0
- package/bundled-skills/adoption-a1-styles/SKILL.md +269 -0
- package/bundled-skills/adoption-a2-components/SKILL.md +291 -0
- package/bundled-skills/adoption-a3-forms/SKILL.md +260 -0
- package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +406 -0
- package/bundled-skills/anatomy-author/SKILL.md +1226 -0
- package/bundled-skills/design-auditor/SKILL.md +455 -0
- package/bundled-skills/implement-component/SKILL.md +333 -0
- package/bundled-skills/specify-component/SKILL.md +331 -0
- package/bundled-skills/specify-pattern/SKILL.md +292 -0
- package/dist/package.json +16 -15
- package/dist/pragma.conf.js +236 -34
- package/dist/pragma.conf.js.map +1 -1
- package/dist/src/bin.js +1 -1
- package/dist/src/bin.js.map +1 -1
- package/dist/src/capabilities/colophon/colophon.render.js +1 -1
- package/dist/src/capabilities/colophon/colophon.render.js.map +1 -1
- package/dist/src/capabilities/create/mount.js +4 -3
- package/dist/src/capabilities/create/mount.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkPackageRefs.js +60 -4
- package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkShellCompletions.js +8 -8
- package/dist/src/capabilities/doctor/checks/checkShellCompletions.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js +195 -0
- package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -0
- package/dist/src/capabilities/doctor/checks/index.js +6 -6
- package/dist/src/capabilities/doctor/checks/targetHealth.js +194 -50
- package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
- package/dist/src/capabilities/doctor/doctor.render.js +43 -64
- package/dist/src/capabilities/doctor/doctor.render.js.map +1 -1
- package/dist/src/capabilities/doctor/doctor.verb.js +6 -2
- package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -1
- package/dist/src/capabilities/doctor/runChecks.js +14 -11
- package/dist/src/capabilities/doctor/runChecks.js.map +1 -1
- package/dist/src/capabilities/graph/query.render.js +2 -2
- package/dist/src/capabilities/graph/query.render.js.map +1 -1
- package/dist/src/capabilities/info/collectInfo.js +8 -2
- package/dist/src/capabilities/info/collectInfo.js.map +1 -1
- package/dist/src/capabilities/info/info.render.js +6 -2
- package/dist/src/capabilities/info/info.render.js.map +1 -1
- package/dist/src/capabilities/prompt/prompt.render.js +2 -2
- package/dist/src/capabilities/prompt/prompt.render.js.map +1 -1
- package/dist/src/capabilities/setup/buildPlan.js +43 -39
- package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
- package/dist/src/capabilities/setup/mode.js +2 -2
- package/dist/src/capabilities/setup/operations/index.js +4 -4
- package/dist/src/capabilities/setup/operations/index.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupGenerator.js +92 -45
- package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupLsp.js +88 -36
- package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupMcp.js +97 -51
- package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupSkills.js +325 -47
- package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
- package/dist/src/capabilities/setup/plan.js.map +1 -1
- package/dist/src/capabilities/setup/plan.render.js +144 -66
- package/dist/src/capabilities/setup/plan.render.js.map +1 -1
- package/dist/src/capabilities/setup/setup.render.js +1 -1
- package/dist/src/capabilities/setup/setup.verb.js +86 -33
- package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
- package/dist/src/capabilities/setup/targets.js +110 -45
- package/dist/src/capabilities/setup/targets.js.map +1 -1
- package/dist/src/capabilities/shared/index.js +6 -5
- package/dist/src/capabilities/shared/index.js.map +1 -1
- package/dist/src/capabilities/shared/packageManager.js +300 -54
- package/dist/src/capabilities/shared/packageManager.js.map +1 -1
- package/dist/src/capabilities/skill/discover.js +97 -11
- package/dist/src/capabilities/skill/discover.js.map +1 -1
- package/dist/src/capabilities/skill/render.js +2 -2
- package/dist/src/capabilities/skill/render.js.map +1 -1
- package/dist/src/capabilities/sources/installSkills.js +145 -3
- package/dist/src/capabilities/sources/installSkills.js.map +1 -1
- package/dist/src/capabilities/sources/runUpdate.js +68 -2
- package/dist/src/capabilities/sources/runUpdate.js.map +1 -1
- package/dist/src/capabilities/sources/update.verb.js +3 -3
- package/dist/src/capabilities/sources/update.verb.js.map +1 -1
- package/dist/src/capabilities/upgrade/runUpgrade.js +43 -7
- package/dist/src/capabilities/upgrade/runUpgrade.js.map +1 -1
- package/dist/src/capabilities/upgrade/upgrade.render.js +10 -2
- package/dist/src/capabilities/upgrade/upgrade.render.js.map +1 -1
- package/dist/src/constants.js +6 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/kernel/index.js +11 -2
- package/dist/src/kernel/index.js.map +1 -1
- package/dist/src/kernel/interactivity.js +68 -0
- package/dist/src/kernel/interactivity.js.map +1 -0
- package/dist/src/kernel/packs/renderPack.js +3 -3
- package/dist/src/kernel/packs/renderPack.js.map +1 -1
- package/dist/src/kernel/packs/resolveEntity.js +63 -12
- package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
- package/dist/src/kernel/packs/runBodies.js +57 -4
- package/dist/src/kernel/packs/runBodies.js.map +1 -1
- package/dist/src/kernel/packs/schema.js +54 -2
- package/dist/src/kernel/packs/schema.js.map +1 -1
- package/dist/src/kernel/packs/sparql/applyFilters.js +104 -5
- package/dist/src/kernel/packs/sparql/applyFilters.js.map +1 -1
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js +306 -43
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
- package/dist/src/kernel/packs/types.js.map +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
- package/dist/src/kernel/project/cli/dispatch.js +23 -23
- package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
- package/dist/src/kernel/project/cli/globalFlags.js +4 -1
- package/dist/src/kernel/project/cli/globalFlags.js.map +1 -1
- package/dist/src/kernel/project/cli/helpFormat.js +1 -2
- package/dist/src/kernel/project/cli/helpFormat.js.map +1 -1
- package/dist/src/kernel/project/cli/rootHelp.js +34 -4
- package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
- package/dist/src/kernel/project/mcp/registerVerb.js +16 -1
- package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
- package/dist/src/kernel/render/renderers.js +1 -1
- package/dist/src/kernel/render/style.js +29 -9
- package/dist/src/kernel/render/style.js.map +1 -1
- package/dist/src/kernel/render/vocabulary.js +174 -0
- package/dist/src/kernel/render/vocabulary.js.map +1 -0
- package/dist/src/kernel/spec/emitReference.js +1 -1
- package/dist/src/kernel/spec/emitReference.js.map +1 -1
- package/package.json +16 -15
- package/pragma.conf.ts +241 -34
- package/src/bin.ts +8 -1
- package/src/capabilities/block.parity.test.ts +25 -11
- package/src/capabilities/block.shipped.exec.test.ts +282 -0
- package/src/capabilities/block.tierRank.exec.test.ts +300 -0
- package/src/capabilities/colophon/colophon.render.ts +1 -2
- package/src/capabilities/colophon/colophon.test.ts +2 -2
- package/src/capabilities/create/createGrammar.test.ts +1 -1
- package/src/capabilities/create/mount.ts +3 -3
- package/src/capabilities/distribution.parity.test.ts +4 -0
- package/src/capabilities/doctor/checks/checkPackageRefs.ts +69 -4
- package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +6 -4
- package/src/capabilities/doctor/checks/checkShellCompletions.ts +8 -8
- package/src/capabilities/doctor/checks/harnessInventory.test.ts +219 -0
- package/src/capabilities/doctor/checks/harnessInventory.ts +298 -0
- package/src/capabilities/doctor/checks/index.ts +6 -6
- package/src/capabilities/doctor/checks/targetHealth.ts +229 -59
- package/src/capabilities/doctor/doctor.render.test.ts +108 -32
- package/src/capabilities/doctor/doctor.render.ts +56 -80
- package/src/capabilities/doctor/doctor.test.ts +247 -35
- package/src/capabilities/doctor/doctor.verb.ts +7 -2
- package/src/capabilities/doctor/runChecks.ts +14 -11
- package/src/capabilities/doctor/types.ts +15 -24
- package/src/capabilities/emptyRecovery.test.ts +10 -2
- package/src/capabilities/graph/query.render.ts +2 -2
- package/src/capabilities/info/collectInfo.ts +8 -1
- package/src/capabilities/info/info.render.ts +6 -2
- package/src/capabilities/info/info.test.ts +37 -1
- package/src/capabilities/info/types.ts +10 -2
- package/src/capabilities/prompt/prompt.render.ts +2 -2
- package/src/capabilities/renderVocabularySync.test.ts +82 -0
- package/src/capabilities/scopeTypeSync.test.ts +29 -15
- package/src/capabilities/setup/buildPlan.ts +45 -41
- package/src/capabilities/setup/index.ts +1 -1
- package/src/capabilities/setup/mode.ts +2 -2
- package/src/capabilities/setup/operations/index.ts +6 -1
- package/src/capabilities/setup/operations/setupGenerator.ts +144 -70
- package/src/capabilities/setup/operations/setupLsp.ts +129 -37
- package/src/capabilities/setup/operations/setupMcp.ts +132 -61
- package/src/capabilities/setup/operations/setupSkills.ts +392 -58
- package/src/capabilities/setup/plan.render.ts +171 -76
- package/src/capabilities/setup/plan.ts +17 -12
- package/src/capabilities/setup/setup.mcp.correctness.test.ts +9 -9
- package/src/capabilities/setup/setup.render.test.ts +50 -36
- package/src/capabilities/setup/setup.render.ts +1 -1
- package/src/capabilities/setup/setup.test.ts +881 -50
- package/src/capabilities/setup/setup.verb.ts +91 -33
- package/src/capabilities/setup/targets.ts +120 -52
- package/src/capabilities/setup/types.ts +10 -18
- package/src/capabilities/shared/index.ts +17 -6
- package/src/capabilities/shared/packageManager.test.ts +421 -6
- package/src/capabilities/shared/packageManager.ts +394 -59
- package/src/capabilities/skill/bundledSkills.test.ts +347 -0
- package/src/capabilities/skill/discover.ts +100 -11
- package/src/capabilities/skill/render.ts +2 -2
- package/src/capabilities/skill/skill.test.ts +17 -4
- package/src/capabilities/sources/installSkills.ts +181 -3
- package/src/capabilities/sources/runUpdate.ts +78 -2
- package/src/capabilities/sources/sources.test.ts +228 -0
- package/src/capabilities/sources/update.verb.ts +3 -3
- package/src/capabilities/surface.test.ts +2 -2
- package/src/capabilities/upgrade/runUpgrade.ts +45 -6
- package/src/capabilities/upgrade/types.ts +10 -2
- package/src/capabilities/upgrade/upgrade.render.ts +9 -2
- package/src/capabilities/upgrade/upgrade.test.ts +72 -0
- package/src/constants.ts +6 -1
- package/src/identity.test.ts +18 -5
- package/src/kernel/completion/entitySource.test.ts +15 -2
- package/src/kernel/completion/safety.test.ts +16 -9
- package/src/kernel/config/defaults.test.ts +2 -1
- package/src/kernel/index.ts +11 -2
- package/src/kernel/interactivity.test.ts +76 -0
- package/src/kernel/interactivity.ts +69 -0
- package/src/kernel/packs/compile.test.ts +92 -0
- package/src/kernel/packs/renderPack.ts +3 -3
- package/src/kernel/packs/resolveEntity.test.ts +75 -14
- package/src/kernel/packs/resolveEntity.ts +88 -24
- package/src/kernel/packs/runBodies.ts +84 -19
- package/src/kernel/packs/schema.ts +60 -4
- package/src/kernel/packs/sparql/applyFilters.ts +125 -4
- package/src/kernel/packs/sparql/buildLookupQuery.test.ts +290 -0
- package/src/kernel/packs/sparql/buildLookupQuery.ts +323 -45
- package/src/kernel/packs/toolDescription.test.ts +35 -1
- package/src/kernel/packs/types.ts +125 -2
- package/src/kernel/project/cli/buildProgram.test.ts +73 -2
- package/src/kernel/project/cli/buildProgram.ts +8 -1
- package/src/kernel/project/cli/dispatch.test.ts +5 -4
- package/src/kernel/project/cli/dispatch.ts +25 -24
- package/src/kernel/project/cli/globalFlags.ts +4 -1
- package/src/kernel/project/cli/helpFormat.ts +1 -2
- package/src/kernel/project/cli/rootHelp.ts +36 -3
- package/src/kernel/project/mcp/registerVerb.ts +20 -1
- package/src/kernel/render/renderers.ts +1 -1
- package/src/kernel/render/style.ts +32 -9
- package/src/kernel/render/vocabulary.ts +276 -0
- package/src/kernel/spec/emitReference.ts +1 -1
- package/src/kernel/spec/types.ts +35 -6
- package/src/testing/PARITY_GAPS.ts +9 -3
- package/src/testing/behavioral/docExamples.test.ts +4 -2
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +110 -7
- package/src/testing/behavioral/journeys.firstInstall.test.ts +7 -2
- package/src/testing/behavioral/journeys.packageStories.test.ts +6 -3
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +26 -1
- package/src/testing/eval/cases/readNouns.ts +245 -9
- package/src/testing/fixtures/blockGraph.ts +28 -13
- package/src/testing/fixtures/graph/canonical.ts +95 -12
- package/dist/src/capabilities/shared/bands.js +0 -16
- package/dist/src/capabilities/shared/bands.js.map +0 -1
- package/src/capabilities/shared/bands.ts +0 -16
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rendering vocabulary — the ONE place a state and the word for it meet.
|
|
3
|
+
*
|
|
4
|
+
* `doctor` reports states, `setup` plans and applies them, and both speak to
|
|
5
|
+
* the same reader about the same machine. For as long as each surface spelled
|
|
6
|
+
* the states itself — three glyph tables, two scope-label maps, two indent
|
|
7
|
+
* constants — the type layer and the copy layer were free to disagree, and a
|
|
8
|
+
* copy pass could rename a scope in every sentence while the model underneath
|
|
9
|
+
* kept the old word. This module ends that freedom: the statuses, their
|
|
10
|
+
* glyphs, the scope names, and the shared layout markers are each defined
|
|
11
|
+
* once, here, and every renderer reads them.
|
|
12
|
+
*
|
|
13
|
+
* The SCOPES are **global** and **local project**, chosen because they are the
|
|
14
|
+
* words the user already typed: `--global` and `--local`. Reading `Local
|
|
15
|
+
* project` above a row whose fix is `pragma setup mcp --local` is the whole
|
|
16
|
+
* point — the report names the flag that repairs it. (Earlier these surfaces
|
|
17
|
+
* said MACHINE/PROJECT, and later "global band"/"project band": a third and a
|
|
18
|
+
* fourth term for the same two things, and "band" is a word nobody outside
|
|
19
|
+
* this repository has ever used for a config scope. The type layer carried
|
|
20
|
+
* "band" longest — `ScopeBand`, `PlanRow.band`, a `bands.ts` — and the render
|
|
21
|
+
* tests now guard against either spelling coming back.)
|
|
22
|
+
*
|
|
23
|
+
* The GLYPHS are one small alphabet with fixed meanings, so the same mark
|
|
24
|
+
* never says two things: `✓` is work done or verified, `✗` is a real fault,
|
|
25
|
+
* `◇` is available-but-not-set-up, `○` is inert (nothing to act on), and `·`
|
|
26
|
+
* is neutral punctuation — a sub-item bullet, or a row that never ran. Doctor
|
|
27
|
+
* and setup map their own status tiers onto that alphabet below, which is what
|
|
28
|
+
* keeps a failed setup row and a failed doctor row — the same finding seen
|
|
29
|
+
* twice — wearing the same mark.
|
|
30
|
+
*
|
|
31
|
+
* This is the RENDERING vocabulary; the graph-domain vocabulary a distribution
|
|
32
|
+
* declares (`kernel/vocabulary.ts`) is a different thing with the same good
|
|
33
|
+
* reason for existing. Like the rest of `kernel/render/`, this module is
|
|
34
|
+
* statically reachable from the `--help`/`__complete` fast paths, so it
|
|
35
|
+
* imports NOTHING — it is inert data plus two pure one-line functions.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// Scopes
|
|
40
|
+
// =============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* One of the two config scopes: the per-user/home `global` scope or the
|
|
44
|
+
* per-repository `project` scope. Mirrors `@canonical/harnesses`' `ScopeBand`
|
|
45
|
+
* STRUCTURALLY (pinned by `scopeTypeSync.test.ts`) but is declared here so the
|
|
46
|
+
* statically-reachable type modules never pull the harnesses runtime onto the
|
|
47
|
+
* fast-path module graph — and under pragma's own name for the concept, the
|
|
48
|
+
* one the `--global`/`--local` flags and every rendered sentence use.
|
|
49
|
+
*/
|
|
50
|
+
export type Scope = "project" | "global";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The resolved scope selection: which scope(s) a run touches. `global` is the
|
|
54
|
+
* DEFAULT — the user/home scope a machine-level installer configures;
|
|
55
|
+
* `project` is the opt-in per-repository scope; `both` is the explicit "run
|
|
56
|
+
* each scope in one invocation". Structurally mirrors the harnesses
|
|
57
|
+
* `ScopeSelection` (same pin).
|
|
58
|
+
*/
|
|
59
|
+
export type ScopeSelection = Scope | "both";
|
|
60
|
+
|
|
61
|
+
/** The user-facing display label for each of the two config scopes. */
|
|
62
|
+
export const SCOPE_LABELS: Record<Scope, string> = {
|
|
63
|
+
global: "Global",
|
|
64
|
+
project: "Local project",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The scope phrase in running text — the lowercase form of
|
|
69
|
+
* {@link SCOPE_LABELS}, plus the word for "both". Authored here rather than
|
|
70
|
+
* lowercased at the call site so a reader grepping for either spelling lands
|
|
71
|
+
* in one file.
|
|
72
|
+
*/
|
|
73
|
+
export const SCOPE_PHRASES: Record<ScopeSelection, string> = {
|
|
74
|
+
global: "global",
|
|
75
|
+
project: "local project",
|
|
76
|
+
both: "global and local project",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// =============================================================================
|
|
80
|
+
// The glyph alphabet
|
|
81
|
+
// =============================================================================
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The five marks, by meaning. Uncolored — every glyph is tinted at render
|
|
85
|
+
* time, never baked at module load. `summon-core`'s Ink wizard renders the
|
|
86
|
+
* same `success`/`failure` marks for the same meanings; the two packages
|
|
87
|
+
* cannot share this module (the dependency runs the other way, and the
|
|
88
|
+
* fast-path covenants forbid the import), so `renderVocabularySync.test.ts`
|
|
89
|
+
* pins the two alphabets to byte equality instead.
|
|
90
|
+
*/
|
|
91
|
+
export const GLYPHS = {
|
|
92
|
+
/** Work done, or a state verified healthy. */
|
|
93
|
+
success: "✓",
|
|
94
|
+
/** A real fault — something set up that no longer works, or a failed row. */
|
|
95
|
+
failure: "✗",
|
|
96
|
+
/** Detected and installable, but not set up — an invitation, not a fault. */
|
|
97
|
+
available: "◇",
|
|
98
|
+
/** Inert: nothing to act on here, and nothing wrong either. */
|
|
99
|
+
inert: "○",
|
|
100
|
+
/** Neutral punctuation — a sub-item bullet, or a row that never ran. */
|
|
101
|
+
neutral: "·",
|
|
102
|
+
} as const;
|
|
103
|
+
|
|
104
|
+
/** The arrow in front of an inline instruction (`↳ fix: …`). */
|
|
105
|
+
export const FIX_ARROW = "↳";
|
|
106
|
+
|
|
107
|
+
/** The bullet a check's indented sub-items carry. */
|
|
108
|
+
export const SUB_BULLET = GLYPHS.neutral;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The marker for a row that carries NO outcome — one the user deselected, so
|
|
112
|
+
* it was neither done nor skipped-for-cause. It must not be a ✓: a green check
|
|
113
|
+
* against work that never ran is the same lie the old recap told when it
|
|
114
|
+
* reported "Setup complete" over a target it had dropped.
|
|
115
|
+
*/
|
|
116
|
+
export const NOT_RUN_GLYPH = GLYPHS.neutral;
|
|
117
|
+
|
|
118
|
+
// =============================================================================
|
|
119
|
+
// Indents
|
|
120
|
+
// =============================================================================
|
|
121
|
+
|
|
122
|
+
/** One indent step — every list body row starts two spaces in. */
|
|
123
|
+
export const INDENT = " ";
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The indent that aligns a check's sub-lines under its name: the row's own
|
|
127
|
+
* {@link INDENT}, plus one glyph column, plus the two-space gap after it.
|
|
128
|
+
*/
|
|
129
|
+
export const SUB_INDENT = " ";
|
|
130
|
+
|
|
131
|
+
// =============================================================================
|
|
132
|
+
// Doctor's status tiers
|
|
133
|
+
// =============================================================================
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Status of a doctor check or one of its sub-items.
|
|
137
|
+
*
|
|
138
|
+
* `available` is the tier between fail and skip that keeps the report honest:
|
|
139
|
+
* an opt-in integration (MCP registration, skills symlinks, a completion
|
|
140
|
+
* script) that is detected and installable but that the user has not set up.
|
|
141
|
+
* Nothing is broken — a fresh install is HEALTHY with several availables — so
|
|
142
|
+
* reporting these as `fail` would teach users that the failure count is noise.
|
|
143
|
+
* `fail` stays reserved for a real fault: something set up that no longer
|
|
144
|
+
* works, or an environment the CLI cannot run correctly in. `skip` remains
|
|
145
|
+
* "nothing to check here" (no shell detected, no harnesses present).
|
|
146
|
+
*/
|
|
147
|
+
export type CheckStatus = "pass" | "fail" | "available" | "skip";
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Each check status's mark. `available` gets its own glyph, distinct from ✗:
|
|
151
|
+
* an opt-in integration that is not set up is not a fault, and rendering it as
|
|
152
|
+
* one would train users to ignore the failure count. The glyph pairs with the
|
|
153
|
+
* word in the tally, so the distinction survives with color stripped.
|
|
154
|
+
*/
|
|
155
|
+
export const CHECK_GLYPHS: Record<CheckStatus, string> = {
|
|
156
|
+
pass: GLYPHS.success,
|
|
157
|
+
fail: GLYPHS.failure,
|
|
158
|
+
available: GLYPHS.available,
|
|
159
|
+
skip: GLYPHS.inert,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The word in front of a check row's instruction, chosen by what the row IS.
|
|
164
|
+
*
|
|
165
|
+
* A `fail` and an `available` are things to repair or switch on, so they read
|
|
166
|
+
* `fix`. A `skip` is neither — nothing is wrong and nothing is off; the row
|
|
167
|
+
* simply has nothing to act on yet — so labelling its instruction `fix` would
|
|
168
|
+
* be the same "a skip is a fault" reading the `available` glyph exists to
|
|
169
|
+
* prevent. It reads `next`, which is what the line actually is.
|
|
170
|
+
*/
|
|
171
|
+
export const checkRemedyWord = (status: CheckStatus): "fix" | "next" =>
|
|
172
|
+
status === "skip" ? "next" : "fix";
|
|
173
|
+
|
|
174
|
+
// =============================================================================
|
|
175
|
+
// Setup's outcome tiers
|
|
176
|
+
// =============================================================================
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* How one setup row ended. `skipped` is NOT a failure — see
|
|
180
|
+
* `setup/plan.ts#planExitFailed`, which states the exit rule once.
|
|
181
|
+
*/
|
|
182
|
+
export type OutcomeStatus =
|
|
183
|
+
| "done"
|
|
184
|
+
| "noop"
|
|
185
|
+
| "skipped"
|
|
186
|
+
| "failed"
|
|
187
|
+
| "removed"
|
|
188
|
+
| "kept";
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Each outcome's mark. The three ways work can be complete — `done`, `noop`
|
|
192
|
+
* (already current), `removed` — all read ✓; the two ways a row can stand
|
|
193
|
+
* aside — `kept`, `skipped` — read the inert ○; only `failed` earns ✗.
|
|
194
|
+
*/
|
|
195
|
+
export const OUTCOME_GLYPHS: Record<OutcomeStatus, string> = {
|
|
196
|
+
done: GLYPHS.success,
|
|
197
|
+
noop: GLYPHS.success,
|
|
198
|
+
removed: GLYPHS.success,
|
|
199
|
+
kept: GLYPHS.inert,
|
|
200
|
+
skipped: GLYPHS.inert,
|
|
201
|
+
failed: GLYPHS.failure,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The word in front of a row outcome's instruction — `fix` for a row that
|
|
206
|
+
* failed, `next` for one that skipped. Same rule as {@link checkRemedyWord}: a
|
|
207
|
+
* skip is not a fault, and labelling its instruction as a repair is what
|
|
208
|
+
* teaches a reader to treat skips as failures.
|
|
209
|
+
*/
|
|
210
|
+
export const outcomeRemedyWord = (status: OutcomeStatus): "fix" | "next" =>
|
|
211
|
+
status === "failed" ? "fix" : "next";
|
|
212
|
+
|
|
213
|
+
// =============================================================================
|
|
214
|
+
// Install-source states
|
|
215
|
+
// =============================================================================
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* How the running CLI got onto the machine — the state `info`, `upgrade`, and
|
|
219
|
+
* doctor's `pragma version` row all report, so its words live here with the
|
|
220
|
+
* other shared states. Only a `global` install has a sanctioned update
|
|
221
|
+
* command; the type layer in `capabilities/shared/packageManager.ts` makes the
|
|
222
|
+
* others structurally command-less, and {@link INSTALL_UPDATE_GUIDANCE} is
|
|
223
|
+
* what those states say instead. `linked` in particular must never be handed
|
|
224
|
+
* an install command: `npm i -g` against a linked checkout would overwrite the
|
|
225
|
+
* link to the development tree the user is working in.
|
|
226
|
+
*/
|
|
227
|
+
export type InstallKind =
|
|
228
|
+
| "global"
|
|
229
|
+
| "workspace"
|
|
230
|
+
| "linked"
|
|
231
|
+
| "ephemeral"
|
|
232
|
+
| "unknown";
|
|
233
|
+
|
|
234
|
+
/** The install kinds that carry NO update command (everything but `global`). */
|
|
235
|
+
export type NoCommandInstallKind = Exclude<InstallKind, "global">;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The display label for an install source, e.g. `npm (global)`, `bun link
|
|
239
|
+
* (development checkout)`, `npx (ephemeral)`, `unknown (node runtime)`. The
|
|
240
|
+
* `via` word is the manager/runner/runtime name the detector resolved —
|
|
241
|
+
* absent only for a `workspace`/`linked` install whose manager could not be
|
|
242
|
+
* told, which then reads as the bare state.
|
|
243
|
+
*/
|
|
244
|
+
export const installSourceLabel = (kind: InstallKind, via?: string): string => {
|
|
245
|
+
switch (kind) {
|
|
246
|
+
case "global":
|
|
247
|
+
return `${via} (global)`;
|
|
248
|
+
case "workspace":
|
|
249
|
+
return via ? `${via} (local project)` : "local project dependency";
|
|
250
|
+
case "linked":
|
|
251
|
+
return via
|
|
252
|
+
? `${via} link (development checkout)`
|
|
253
|
+
: "linked (development checkout)";
|
|
254
|
+
case "ephemeral":
|
|
255
|
+
return `${via} (ephemeral)`;
|
|
256
|
+
case "unknown":
|
|
257
|
+
return `unknown (${via} runtime)`;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* What each command-less install state says when a newer release exists —
|
|
263
|
+
* honest guidance in place of a command that would be wrong to run. The
|
|
264
|
+
* `linked` sentence is the load-bearing one: it replaces the `npm i -g` that
|
|
265
|
+
* would clobber the development link.
|
|
266
|
+
*/
|
|
267
|
+
export const INSTALL_UPDATE_GUIDANCE: Record<NoCommandInstallKind, string> = {
|
|
268
|
+
workspace:
|
|
269
|
+
"Installed as a project dependency — update it through the project's package.json.",
|
|
270
|
+
linked:
|
|
271
|
+
"Linked development checkout — update it from its source tree, or unlink it before installing a release.",
|
|
272
|
+
ephemeral:
|
|
273
|
+
"Ephemeral run — invoke it again with an explicit @latest version to pick up the newest release.",
|
|
274
|
+
unknown:
|
|
275
|
+
"The install method cannot be determined — update it the same way it was installed.",
|
|
276
|
+
};
|
|
@@ -541,7 +541,7 @@ const CONFIG_FIELD_DOCS: Record<keyof RawConfig, ConfigFieldDoc> = {
|
|
|
541
541
|
logo: {
|
|
542
542
|
type: "string[] (optional)",
|
|
543
543
|
notes:
|
|
544
|
-
"Distribution-only — see below. ASCII-art wordmark lines shown above the header on root `--help`, tinted with the terminal palette's orange slot. Declared content, not code: a wordmark spells the distribution's name, so a fork ships its own art or omits the field and gets no wordmark. Omit or leave empty for a bare header.",
|
|
544
|
+
"Distribution-only — see below. ASCII-art wordmark lines shown above the header on root `--help`, tinted with the terminal palette's orange slot. Shown to a HUMAN reader only: the front door omits the art for MACHINE-ORIENTED output — `--format llm` or `--format json`, plus the `llm` a non-interactive stdout auto-infers — so a piped `--help` spends none of an agent's budget on a picture of the name it just typed. The gate is the output SHAPE, not whether stdout is a terminal, and explicit beats inferred in both directions: `--format plain` keeps the art down a pipe (as does `PRAGMA_NO_AUTO_LLM`, which turns the inference off), and `--format llm` drops it on a TTY. Declared content, not code: a wordmark spells the distribution's name, so a fork ships its own art or omits the field and gets no wordmark. Omit or leave empty for a bare header.",
|
|
545
545
|
},
|
|
546
546
|
colophon: {
|
|
547
547
|
type: "object (optional)",
|
package/src/kernel/spec/types.ts
CHANGED
|
@@ -149,11 +149,40 @@ export type ParamSpec =
|
|
|
149
149
|
* list-shaped formatters read it; `llm` (the byte-frozen agent contract) and
|
|
150
150
|
* `json` (the envelope) never see it.
|
|
151
151
|
*
|
|
152
|
-
* `
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
152
|
+
* `notice` is the seam for what the DATA cannot say about itself. Return the
|
|
153
|
+
* calm sentence when the payload would otherwise misrepresent the read; return
|
|
154
|
+
* `undefined` (or omit the member) when it speaks for itself.
|
|
155
|
+
*
|
|
156
|
+
* Two shapes qualify, and they are the same failure. A zero-record result
|
|
157
|
+
* ("empty ≠ silence"): an unbuilt store, a mistyped filter and a genuinely empty
|
|
158
|
+
* result were all `{"ok":true,"data":[],"meta":{}}`. And a result that is one of
|
|
159
|
+
* several it could have been — a `lookup` whose name reached more entities than
|
|
160
|
+
* the one it answers with. A caller shown Launchpad's `Button` has no way to
|
|
161
|
+
* discover the global one from a payload that looks exactly like an unambiguous
|
|
162
|
+
* hit; the notice names the IRIs the answer did not take, which is the only
|
|
163
|
+
* address that reaches them.
|
|
164
|
+
*
|
|
165
|
+
* It was called `emptyNotice` while emptiness was the only case. The channel is
|
|
166
|
+
* unchanged — same key, same three surfaces — so this is one seam covering both,
|
|
167
|
+
* not a second one grown beside it.
|
|
168
|
+
*
|
|
169
|
+
* Three surfaces route it, each in its own register — it is one seam, not a
|
|
170
|
+
* plain-mode courtesy:
|
|
171
|
+
* - `plain`: the dispatcher writes it to STDERR with exit 0, keeping the stdout
|
|
172
|
+
* data stream free of human sentences a pipe would read as records. `--quiet`
|
|
173
|
+
* mutes it (success-path guidance).
|
|
174
|
+
* - `--format json` AND the MCP tool result: it rides the envelope as
|
|
175
|
+
* `meta.notice` (`project/cli/dispatch.ts#renderData`,
|
|
176
|
+
* `project/mcp/registerVerb.ts#emptyMeta` — the same key from the same seam,
|
|
177
|
+
* so the two MACHINE surfaces stay byte-equal). `data` keeps its uniform empty
|
|
178
|
+
* shape; `[]` stays `[]`. Without it an agent could not tell an unbuilt store,
|
|
179
|
+
* a mistyped filter and a genuinely empty result apart — all three were
|
|
180
|
+
* `{"ok":true,"data":[],"meta":{}}`.
|
|
181
|
+
* - `llm` (the byte-frozen agent contract) is the ONE mode that still ignores
|
|
182
|
+
* it: it renders one stream and its bytes are frozen.
|
|
183
|
+
*
|
|
184
|
+
* The formatters themselves never see the notice: `json(d)` renders the data
|
|
185
|
+
* alone, and the ENVELOPE layer carries the notice beside it.
|
|
157
186
|
*/
|
|
158
187
|
export interface Formatters<T> {
|
|
159
188
|
readonly plain: (
|
|
@@ -162,7 +191,7 @@ export interface Formatters<T> {
|
|
|
162
191
|
) => string;
|
|
163
192
|
readonly llm: (d: T) => string;
|
|
164
193
|
readonly json: (d: T) => string;
|
|
165
|
-
readonly
|
|
194
|
+
readonly notice?: (d: T) => string | undefined;
|
|
166
195
|
}
|
|
167
196
|
|
|
168
197
|
/** A usage example shown in verb help. */
|
|
@@ -42,13 +42,13 @@ export const PARITY_GAPS: readonly ParityGapEntry[] = [
|
|
|
42
42
|
id: "read-meta-always-empty",
|
|
43
43
|
area: "envelope",
|
|
44
44
|
description:
|
|
45
|
-
"
|
|
45
|
+
"NARROWED: a read envelope still carries no `meta.count`/`meta.total` — the only way to know a list is empty is `data.length === 0`, not a count field. But `meta` is no longer ALWAYS `{}` on a read. A zero-record read now carries the verb's own `notice` as `meta.notice` on BOTH machine surfaces (`project/cli/dispatch.ts#renderData` for `--format json`, `project/mcp/registerVerb.ts#emptyMeta` for MCP — one seam, the same key, so the two stay byte-equal). `data` keeps its uniform empty shape; `[]` is still `[]`. The divergence this entry records is the ABSENCE of counts, not the emptiness of `meta`: without the notice an agent could not tell an unbuilt store, a mistyped filter and a genuinely empty result apart, all three being `{ok:true,data:[],meta:{}}`.",
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
id: "no-empty-hook-on-free-filter",
|
|
49
49
|
area: "pack list / empty-state",
|
|
50
50
|
description:
|
|
51
|
-
"A zero-row pack list —
|
|
51
|
+
"STILL TRUE OF EMPTINESS, NARROWED ON BAD ARGUMENTS. A zero-row pack list — narrowed by a declared filter, by an unmatched `--search`, or unconditionally empty — is a SUCCESS: `ok:true, data:[]` at exit 0 with a calm notice (stderr in plain mode, `meta.notice` on the machine surfaces). pragma v2 still never raises EMPTY_RESULTS for an empty list, and U5's removal of the enum-filter-only `buildListEmptyError` hook stands. What is no longer true is that an UNMATCHED free-filter value stays successful: a value-free filter now rejects a value its vocabulary does not admit with INVALID_INPUT and the admissible values as `validOptions`, the same courtesy a declared-`values` filter always extended (`packs/sparql/applyFilters.ts`). The vocabulary is the graph's, not the returned rows' — a filter declares where it lives (`PackFilter.vocabulary`), so a real value that happens to match no row (a category with zero standards, a `ds:ConceptType` no concept uses) is a calm empty list, and only a value the graph does not know is an error.",
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
54
|
id: "raw-iri-in-data",
|
|
@@ -84,7 +84,7 @@ export const PARITY_GAPS: readonly ParityGapEntry[] = [
|
|
|
84
84
|
id: "block-lookup-not-tier-scoped",
|
|
85
85
|
area: "block",
|
|
86
86
|
description:
|
|
87
|
-
"`block lookup` resolves by `ds:name`
|
|
87
|
+
"CLOSED, by REVERSAL of the entry it used to be. This gap read: `block lookup` resolves by `ds:name` globally, a same-named block at two tiers cannot be disambiguated by plain name, address it by prefixed IRI instead. That was accepted while the answer was merely arbitrary; it stopped being acceptable once measured — 25 live block names are carried by 2–3 blocks, and EVERY one of them was answered with whichever IRI sorted first and no sign the others existed (`block lookup button` returned Launchpad's Button, because `apps_launchpad…` precedes `global…`). 'Address it by IRI' is not a recovery an agent can reach when nothing tells it there is a second block to address. Two things replace it, and neither is a change of arity — one name still answers with one entity, and a glob is still the declared multi-entity form. (1) The discard is no longer SILENT: the resolve is unlimited, the winner is answered with, and the IRIs it outranked ride the verb's `notice` — the same seam a zero-record read uses (stderr in plain mode, `meta.notice` on `--format json` and MCP), because an ambiguous hit is the same failure as an empty one, a result that misrepresents itself. `LookupOutput.ambiguous` carries them in `data` for the machine surfaces, and is ABSENT (not `[]`) when nothing was ambiguous, so every unambiguous read keeps the payload it had. Note the recovery is not reachable any other way: a glob expands over DISTINCT names, so `block lookup 'Butt*'` returned Launchpad's Button and ButtonLink and the global Button appeared nowhere. (2) The order is a declared, derived RANKING (`PackLookup.scopeWeight` in `pragma.conf.ts`, generated by `packs/sparql/buildLookupQuery.ts#rankingClause`): a tier's depth comes from the `/` count in its OWN `ds:name` (`\"Apps/Launchpad\"` → 2, never from the `_` in `ds:apps_launchpad`), and it MULTIPLIES the existing type weight rather than tiebreaking above it, so a whole component in a nested tier still outranks a global SUBcomponent (`TextInput`). Nothing is enumerated, so a tier added upstream is placed without an edit here; an asserted `ds:tierRank` would take precedence over the derived depth and retire the config declaration with no code change. WHAT REMAINS a gap: `lookup` is still not tier-SCOPED — there is no way to ask for one tier's answer only, the ranking merely decides which comes first — and since L-OPEN-9 nothing is tier/channel-FILTERED (the hand-written `block list` went with its `tierChain.ts`; see `block-list-unfiltered`). Two top-level tiers still cannot be ordered against each other by depth alone, which today leaves `Navigation` (`global` vs `apps`) and `InfoTable` (`global` vs `sites`) decided by the IRI fallback; naming a precedence among the depth-1 tiers is an ontology question, deliberately NOT invented here.",
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
id: "block-list-unfiltered",
|
|
@@ -182,4 +182,10 @@ export const PARITY_GAPS: readonly ParityGapEntry[] = [
|
|
|
182
182
|
description:
|
|
183
183
|
"The native MCP `prompts/list` (and the `prompt_list`/`prompt_lookup` content tools) project `ds:Prompt` KG entities read from the ACTIVE pack index (`kernel/project/mcp/prompts/source.ts#listPromptSummaries`) — project-supplied data, NOT shipped in the embedded/bundled pack. A bare install (no project-authored `ds:Prompt`) advertises the prompts capability but lists ZERO prompts, never booting the store on the list path. Non-empty listings are exercised only against the seeded fixture graph (`testing/fixtures/graph/canonical.ts`); the storeless empty-by-default case is pinned by `prompts.test.ts` ('lists zero prompts … when no prompt entities exist').",
|
|
184
184
|
},
|
|
185
|
+
{
|
|
186
|
+
id: "scope-not-band-in-machine-output",
|
|
187
|
+
area: "setup / doctor `--format json`",
|
|
188
|
+
description:
|
|
189
|
+
"A setup plan row and a doctor check carry their config scope under the key `scope` (values `global`/`project`), not `band`. The old shell's type layer said `band` (`ScopeBand`, `PlanRow.band`, `CheckResult.band`) while every user-facing string had already moved to the scope vocabulary the `--global`/`--local` flags use; the type-layer rename (kernel/render/vocabulary.ts) finished the move, and `--format json`/MCP emit the model verbatim, so the machine key moved with it. Any consumer reading `.band` off `setup --format json` or `doctor --format json` reads `.scope` now — `setup.render.test.ts` pins that neither spelling of `band` can return through any render, JSON included.",
|
|
190
|
+
},
|
|
185
191
|
] as const;
|
|
@@ -164,9 +164,11 @@ const READ_CASES: readonly ReadCase[] = [
|
|
|
164
164
|
},
|
|
165
165
|
{ command: "pragma standard list", key: "standard list", params: {} },
|
|
166
166
|
{
|
|
167
|
-
|
|
167
|
+
// The doc's headline example, and the one the roll-up is about: a parent
|
|
168
|
+
// category answering for its whole branch.
|
|
169
|
+
command: "pragma standard list --category testing",
|
|
168
170
|
key: "standard list",
|
|
169
|
-
params: { category: ["
|
|
171
|
+
params: { category: ["testing"] },
|
|
170
172
|
},
|
|
171
173
|
{
|
|
172
174
|
command: "pragma standard categories",
|
|
@@ -11,10 +11,16 @@
|
|
|
11
11
|
* a total miss; only a PARTIAL batch reports the miss while staying
|
|
12
12
|
* `ok:true` (that shape is B1's job, `agentSession.mcp.test.ts`). See
|
|
13
13
|
* PARITY_GAPS `single-lookup-miss-fails-batch-partial-reports`.
|
|
14
|
-
* 2. A filtered list narrowed to zero rows is `{ok:true, data:[]
|
|
15
|
-
*
|
|
16
|
-
* always
|
|
17
|
-
* `read
|
|
14
|
+
* 2. A filtered list narrowed to zero rows is `{ok:true, data:[]}` — there is
|
|
15
|
+
* still no `meta.count` field on any read envelope, which is the divergence
|
|
16
|
+
* `read-meta-always-empty` now records (it no longer claims `meta` is always
|
|
17
|
+
* `{}`: a zero-record read carries the list's own `notice` as
|
|
18
|
+
* `meta.notice` on both machine surfaces, asserted below).
|
|
19
|
+
* 3. A value-free filter given a value its vocabulary does not admit is
|
|
20
|
+
* INVALID_INPUT, not a successful empty list — the narrowing recorded in
|
|
21
|
+
* PARITY_GAPS `no-empty-hook-on-free-filter`. Emptiness itself is still a
|
|
22
|
+
* success: a value the GRAPH admits but no row carries (a category with zero
|
|
23
|
+
* standards) is a calm empty list, asserted below.
|
|
18
24
|
*/
|
|
19
25
|
|
|
20
26
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
@@ -88,7 +94,22 @@ describe("lookup miss — total miss fails the call (B3, adapted)", () => {
|
|
|
88
94
|
});
|
|
89
95
|
});
|
|
90
96
|
|
|
91
|
-
|
|
97
|
+
/**
|
|
98
|
+
* CHANGED DELIBERATELY. This used to assert that ANY filter value narrowed to a
|
|
99
|
+
* plain `{"ok":true,"data":[],"meta":{}}`, which was written when no value-free
|
|
100
|
+
* filter could tell a typo from an empty answer. It could: the rows it is
|
|
101
|
+
* filtering carry the vocabulary. Over MCP the old shape made a mistyped
|
|
102
|
+
* `--category`, an unbuilt store and a genuinely empty category byte-identical —
|
|
103
|
+
* the silence a model has no way to recover from.
|
|
104
|
+
*
|
|
105
|
+
* A value the graph does not know is now INVALID_INPUT with the admissible
|
|
106
|
+
* values as `validOptions`, exactly as a filter declaring `values` has always
|
|
107
|
+
* behaved; the difference is that the vocabulary is READ FROM THE GRAPH rather
|
|
108
|
+
* than copied into the story. Zero rows for a value that DOES exist stays a calm
|
|
109
|
+
* empty list — that is a real answer, not a bad argument — and "exists" means
|
|
110
|
+
* the graph declares it, not that some returned row happened to carry it.
|
|
111
|
+
*/
|
|
112
|
+
describe("list — a filter value the graph does not carry is INVALID_INPUT (B3, adapted)", () => {
|
|
92
113
|
it("has at least one filtered list verb to sweep", () => {
|
|
93
114
|
// A guard, not a no-op: if this ever goes empty the sweep below silently
|
|
94
115
|
// covers nothing, which would be a silent coverage loss worth noticing.
|
|
@@ -97,16 +118,98 @@ describe("list — a narrowing filter to zero rows is a plain empty list (B3, ad
|
|
|
97
118
|
|
|
98
119
|
it.each(
|
|
99
120
|
filteredListVerbs,
|
|
100
|
-
)("$tool: an
|
|
121
|
+
)("$tool: an unobserved filter value is rejected, naming the observed ones", async ({
|
|
101
122
|
tool,
|
|
102
123
|
param,
|
|
103
124
|
}) => {
|
|
104
125
|
const result = await mcp.callTool(tool, {
|
|
105
126
|
[param]: "zzz-definitely-not-a-real-value",
|
|
106
127
|
});
|
|
128
|
+
expect(result.ok).toBe(false);
|
|
129
|
+
const error = result.error as { code: string; validOptions?: string[] };
|
|
130
|
+
expect(error.code).toBe("INVALID_INPUT");
|
|
131
|
+
expect(error.validOptions?.length).toBeGreaterThan(0);
|
|
132
|
+
expect(error.validOptions).not.toContain("zzz-definitely-not-a-real-value");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it.each(
|
|
136
|
+
filteredListVerbs,
|
|
137
|
+
)("$tool: every value the graph DOES carry narrows to a calm list", async ({
|
|
138
|
+
tool,
|
|
139
|
+
param,
|
|
140
|
+
}) => {
|
|
141
|
+
const rejected = await mcp.callTool(tool, { [param]: "zzz-nope" });
|
|
142
|
+
const admissible = (rejected.error as { validOptions: string[] })
|
|
143
|
+
.validOptions;
|
|
144
|
+
expect(admissible.length).toBeGreaterThan(0);
|
|
145
|
+
// EVERY admissible value is accepted, and at least one is populated. Not
|
|
146
|
+
// "the first one returns rows": the vocabulary is the graph's, so it can
|
|
147
|
+
// legitimately contain a value no row carries (a category with zero
|
|
148
|
+
// standards) — that value is a calm empty list, not a rejection, and the
|
|
149
|
+
// sweep would otherwise read a real answer as a failure.
|
|
150
|
+
let populated = 0;
|
|
151
|
+
for (const value of admissible) {
|
|
152
|
+
const result = await mcp.callTool(tool, { [param]: value });
|
|
153
|
+
expect(result.ok).toBe(true);
|
|
154
|
+
if ((result.data as unknown[]).length > 0) populated += 1;
|
|
155
|
+
}
|
|
156
|
+
expect(populated).toBeGreaterThan(0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("standard_list: a declared category with NO standards is a calm empty list", async () => {
|
|
160
|
+
// The case that separates "the graph is the vocabulary" from "the returned
|
|
161
|
+
// rows are the vocabulary". `standard_categories` publishes every declared
|
|
162
|
+
// slug with its count, zeros included, and tells the agent to pick one —
|
|
163
|
+
// so rejecting a zero-count slug as INVALID_INPUT tells it the slug the
|
|
164
|
+
// tool just handed it was a typo. Validity read off the rows did exactly
|
|
165
|
+
// that, because a category with no standards appears in no row.
|
|
166
|
+
const categories = await mcp.callTool("standard_categories");
|
|
167
|
+
const zero = (categories.data as { name: string; count: string }[]).filter(
|
|
168
|
+
(row) => Number(row.count) === 0,
|
|
169
|
+
);
|
|
170
|
+
expect(zero.length).toBeGreaterThan(0);
|
|
171
|
+
for (const row of zero) {
|
|
172
|
+
const result = await mcp.callTool("standard_list", {
|
|
173
|
+
category: row.name,
|
|
174
|
+
});
|
|
175
|
+
expect(result.ok).toBe(true);
|
|
176
|
+
expect(result.data).toEqual([]);
|
|
177
|
+
// And it is a real empty answer, so it carries the calm notice rather
|
|
178
|
+
// than passing in silence.
|
|
179
|
+
expect((result.meta as { notice?: string }).notice ?? "").not.toBe("");
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("standard_list: the rejection names the declared slugs, not just the used ones", async () => {
|
|
184
|
+
const result = await mcp.callTool("standard_list", {
|
|
185
|
+
category: "zzz-definitely-not-a-slug",
|
|
186
|
+
});
|
|
187
|
+
expect(result.ok).toBe(false);
|
|
188
|
+
const error = result.error as { code: string; validOptions: string[] };
|
|
189
|
+
expect(error.code).toBe("INVALID_INPUT");
|
|
190
|
+
const categories = await mcp.callTool("standard_categories");
|
|
191
|
+
const declared = (categories.data as { name: string }[]).map((r) => r.name);
|
|
192
|
+
// Same vocabulary, both surfaces: whatever `standard_categories` lists is
|
|
193
|
+
// what `--category` accepts. Read from `cs:Category`/`cs:slug` on both
|
|
194
|
+
// sides, so the two cannot drift.
|
|
195
|
+
expect([...error.validOptions].sort()).toEqual([...declared].sort());
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("empty ≠ silence on the machine surfaces", () => {
|
|
200
|
+
it("carries the list's empty-state guidance in the envelope meta", async () => {
|
|
201
|
+
// `notice` had exactly one consumer — the CLI dispatcher's stderr — so
|
|
202
|
+
// an agent received `{"ok":true,"data":[],"meta":{}}` and nothing else. The
|
|
203
|
+
// search path is the honest way to reach an empty list without a bad
|
|
204
|
+
// argument.
|
|
205
|
+
const result = await mcp.callTool("standard_list", {
|
|
206
|
+
search: "zzz-nothing-matches-this",
|
|
207
|
+
});
|
|
107
208
|
expect(result.ok).toBe(true);
|
|
108
209
|
expect(result.data).toEqual([]);
|
|
109
|
-
|
|
210
|
+
const notice = (result.meta as { notice?: string }).notice ?? "";
|
|
211
|
+
expect(notice).toContain("No standard entries found.");
|
|
212
|
+
expect(notice).toContain("pragma standard categories");
|
|
110
213
|
});
|
|
111
214
|
});
|
|
112
215
|
|
|
@@ -158,10 +158,15 @@ describe("first install — the surfaces say where the answers come from", () =>
|
|
|
158
158
|
expect(status.entityCount).toBeGreaterThan(0);
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
-
it("doctor passes,
|
|
161
|
+
it("doctor passes, saying the packs came with the CLI rather than from the config", async () => {
|
|
162
162
|
const result = await checkPackageRefs(bootRuntime(JSON_FLAGS, emptyCwd()));
|
|
163
163
|
expect(result.status).toBe("pass");
|
|
164
|
-
|
|
164
|
+
// What is pinned is the DISTINCTION — a fresh install reads packs it
|
|
165
|
+
// shipped with, not packs this project configured — and that the row says
|
|
166
|
+
// how to change that. The wording is the reader's ("shipped with the CLI"),
|
|
167
|
+
// not the build's ("embedded snapshot"); the per-pack revisions moved to
|
|
168
|
+
// the row's sub-items when each pack got its own line.
|
|
169
|
+
expect(result.detail).toContain("shipped with the CLI");
|
|
165
170
|
expect(result.detail).toContain("pragma sources update");
|
|
166
171
|
});
|
|
167
172
|
});
|
|
@@ -138,12 +138,15 @@ describe("the two recoveries still run with bad stories in the pack", () => {
|
|
|
138
138
|
// The pack DOES answer reads, so the check passes…
|
|
139
139
|
expect(check?.status).toBe("pass");
|
|
140
140
|
expect(check?.detail).toContain("2 stories ignored");
|
|
141
|
-
// …with the unusable stories named underneath it.
|
|
142
|
-
|
|
141
|
+
// …with the unusable stories named underneath it. The items also carry
|
|
142
|
+
// one PASSING row per pack the answer was built from, so this pins the
|
|
143
|
+
// FAILING ones: what a story problem contributes is a failing item, and
|
|
144
|
+
// that claim must not break every time provenance gains a row.
|
|
145
|
+
const failing = (check?.items ?? []).filter((i) => i.status === "fail");
|
|
146
|
+
expect(failing.map((item) => item.label).sort()).toEqual([
|
|
143
147
|
"fixture/stories/broken.json",
|
|
144
148
|
"fixture/stories/invalid.json",
|
|
145
149
|
]);
|
|
146
|
-
for (const item of check?.items ?? []) expect(item.status).toBe("fail");
|
|
147
150
|
});
|
|
148
151
|
});
|
|
149
152
|
|
|
@@ -63,7 +63,12 @@ exports[`eval harness — seed gate > runs the full seed (stable + read-noun) ca
|
|
|
63
63
|
"passed": true,
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
|
-
"id": "content-standard-
|
|
66
|
+
"id": "content-standard-category-counts-match-filtered-list",
|
|
67
|
+
"kind": "content",
|
|
68
|
+
"passed": true,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"id": "content-standard-category-rollup-keeps-the-direct-member",
|
|
67
72
|
"kind": "content",
|
|
68
73
|
"passed": true,
|
|
69
74
|
},
|
|
@@ -172,6 +177,21 @@ exports[`eval harness — seed gate > runs the full seed (stable + read-noun) ca
|
|
|
172
177
|
"kind": "tool",
|
|
173
178
|
"passed": true,
|
|
174
179
|
},
|
|
180
|
+
{
|
|
181
|
+
"id": "tool-standard-list-empty-category-is-a-calm-empty-list",
|
|
182
|
+
"kind": "tool",
|
|
183
|
+
"passed": true,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"id": "tool-standard-list-names-are-addressable-by-lookup",
|
|
187
|
+
"kind": "tool",
|
|
188
|
+
"passed": true,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"id": "tool-standard-lookup-advertised-glob-matches",
|
|
192
|
+
"kind": "tool",
|
|
193
|
+
"passed": true,
|
|
194
|
+
},
|
|
175
195
|
{
|
|
176
196
|
"id": "tool-standard-lookup-has-dos-and-donts",
|
|
177
197
|
"kind": "tool",
|
|
@@ -182,6 +202,11 @@ exports[`eval harness — seed gate > runs the full seed (stable + read-noun) ca
|
|
|
182
202
|
"kind": "tool",
|
|
183
203
|
"passed": true,
|
|
184
204
|
},
|
|
205
|
+
{
|
|
206
|
+
"id": "tool-token-lookup-addresses-only-what-list-publishes",
|
|
207
|
+
"kind": "tool",
|
|
208
|
+
"passed": true,
|
|
209
|
+
},
|
|
185
210
|
{
|
|
186
211
|
"id": "tool-token-noun-is-read-only",
|
|
187
212
|
"kind": "tool",
|