@canonical/pragma-cli 0.32.0 → 0.33.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.
Files changed (140) hide show
  1. package/README.md +72 -0
  2. package/dist/pragma +0 -0
  3. package/package.json +14 -13
  4. package/src/bin.ts +2 -2
  5. package/src/capabilities/block/blockList.test.ts +79 -0
  6. package/src/capabilities/block/blockList.verb.ts +18 -5
  7. package/src/capabilities/block/pack.ts +12 -1
  8. package/src/capabilities/block/parity.test.ts +9 -2
  9. package/src/capabilities/capabilities/capabilities.test.ts +10 -1
  10. package/src/capabilities/capabilities/capabilities.verb.ts +1 -1
  11. package/src/capabilities/capabilities/catalog.ts +7 -3
  12. package/src/capabilities/capabilities/hints.ts +8 -17
  13. package/src/capabilities/capabilities/types.ts +2 -0
  14. package/src/capabilities/colophon/colophon.render.test.ts +31 -4
  15. package/src/capabilities/colophon/colophon.render.ts +13 -4
  16. package/src/capabilities/colophon/colophon.test.ts +3 -3
  17. package/src/capabilities/colophon/colophon.verb.ts +4 -1
  18. package/src/capabilities/colophon/markdownTerminal.ts +33 -14
  19. package/src/capabilities/colophon/pragmaColophon.ts +4 -4
  20. package/src/capabilities/colophon/types.ts +1 -1
  21. package/src/capabilities/config/fields.ts +21 -92
  22. package/src/capabilities/config/set.test.ts +6 -6
  23. package/src/capabilities/config/set.verb.ts +5 -5
  24. package/src/capabilities/config/show.render.test.ts +65 -0
  25. package/src/capabilities/config/show.render.ts +56 -10
  26. package/src/capabilities/config/show.verb.ts +5 -5
  27. package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
  28. package/src/capabilities/create/byteEquality.test.ts +7 -4
  29. package/src/capabilities/create/compiledCreate.subprocess.test.ts +1 -1
  30. package/src/capabilities/create/create.test.ts +32 -3
  31. package/src/capabilities/create/create.verb.ts +69 -11
  32. package/src/capabilities/doctor/checks/checkConfigFile.ts +1 -1
  33. package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
  34. package/src/capabilities/doctor/checks/checkMcpConfigured.ts +10 -4
  35. package/src/capabilities/doctor/checks/deriveBand.test.ts +42 -0
  36. package/src/capabilities/doctor/checks/deriveBand.ts +28 -0
  37. package/src/capabilities/doctor/doctor.render.test.ts +170 -0
  38. package/src/capabilities/doctor/doctor.render.ts +116 -34
  39. package/src/capabilities/doctor/doctor.test.ts +31 -1
  40. package/src/capabilities/doctor/runChecks.ts +28 -4
  41. package/src/capabilities/doctor/types.ts +15 -0
  42. package/src/capabilities/ontology/index.ts +10 -3
  43. package/src/capabilities/ontology/ontology.test.ts +157 -5
  44. package/src/capabilities/ontology/verbs.ts +192 -93
  45. package/src/capabilities/prompt/prompt.render.test.ts +94 -0
  46. package/src/capabilities/prompt/prompt.render.ts +23 -6
  47. package/src/capabilities/prompt/prompt.test.ts +61 -0
  48. package/src/capabilities/reference.test.ts +32 -0
  49. package/src/capabilities/resources/provider.ts +28 -14
  50. package/src/capabilities/resources/resources.test.ts +99 -0
  51. package/src/capabilities/scopeTypeSync.test.ts +37 -0
  52. package/src/capabilities/setup/index.ts +7 -1
  53. package/src/capabilities/setup/operations/setupCompletions.ts +49 -16
  54. package/src/capabilities/setup/operations/setupGenerator.ts +157 -61
  55. package/src/capabilities/setup/operations/setupLsp.ts +56 -12
  56. package/src/capabilities/setup/operations/setupMcp.ts +216 -39
  57. package/src/capabilities/setup/operations/setupSkills.ts +7 -1
  58. package/src/capabilities/setup/setup.render.test.ts +109 -0
  59. package/src/capabilities/setup/setup.render.ts +61 -14
  60. package/src/capabilities/setup/setup.test.ts +352 -1
  61. package/src/capabilities/setup/setup.verb.ts +57 -5
  62. package/src/capabilities/setup/types.ts +59 -2
  63. package/src/capabilities/shared/bands.ts +16 -0
  64. package/src/capabilities/sources/collectStatus.ts +19 -4
  65. package/src/capabilities/sources/runUpdate.ts +58 -1
  66. package/src/capabilities/sources/sources.test.ts +187 -1
  67. package/src/capabilities/sources/status.render.test.ts +65 -0
  68. package/src/capabilities/sources/status.render.ts +50 -23
  69. package/src/capabilities/surface.test.ts +34 -9
  70. package/src/capabilities/tier/lookup.render.test.ts +72 -0
  71. package/src/capabilities/tier/lookup.render.ts +33 -8
  72. package/src/capabilities/tier/lookup.test.ts +15 -0
  73. package/src/capabilities/tier/runLookup.ts +28 -0
  74. package/src/constants.test.ts +1 -1
  75. package/src/constants.ts +2 -2
  76. package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +13 -14
  77. package/src/kernel/completion/bashDrive.test.ts +3 -3
  78. package/src/kernel/completion/complete.test.ts +3 -5
  79. package/src/kernel/completion/entitySource.ts +14 -6
  80. package/src/kernel/completion/model.test.ts +2 -2
  81. package/src/kernel/completion/model.ts +0 -1
  82. package/src/kernel/completion/parse.test.ts +2 -4
  83. package/src/kernel/error/PragmaError.test.ts +4 -4
  84. package/src/kernel/error/renderError.ts +1 -1
  85. package/src/kernel/packs/compile.ts +1 -1
  86. package/src/kernel/project/cli/buildProgram.test.ts +51 -7
  87. package/src/kernel/project/cli/buildProgram.ts +11 -0
  88. package/src/kernel/project/cli/globalFlags.test.ts +12 -4
  89. package/src/kernel/project/cli/globalFlags.ts +14 -14
  90. package/src/kernel/project/cli/rootHelp.ts +4 -2
  91. package/src/kernel/project/mcp/instructions.ts +1 -1
  92. package/src/kernel/project/mcp/mcpError.ts +52 -0
  93. package/src/kernel/project/mcp/prompts/prompts.test.ts +8 -0
  94. package/src/kernel/project/mcp/prompts/provider.ts +36 -6
  95. package/src/kernel/render/formatters.ts +5 -4
  96. package/src/kernel/render/renderers.test.ts +34 -0
  97. package/src/kernel/render/renderers.ts +25 -7
  98. package/src/kernel/render/style.test.ts +31 -0
  99. package/src/kernel/render/style.ts +74 -0
  100. package/src/kernel/runtime/graphpack/build.ts +28 -0
  101. package/src/kernel/runtime/graphpack/buildIndex.test.ts +103 -0
  102. package/src/kernel/runtime/graphpack/buildIndex.ts +124 -36
  103. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +3 -6
  104. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -2
  105. package/src/kernel/runtime/graphpack/graphpack.test.ts +69 -1
  106. package/src/kernel/runtime/graphpack/read.ts +28 -13
  107. package/src/kernel/runtime/graphpack/types.ts +14 -0
  108. package/src/kernel/runtime/loadSession.ts +4 -12
  109. package/src/kernel/runtime/refs/resolve.ts +63 -3
  110. package/src/kernel/runtime/storeReadiness.ts +61 -0
  111. package/src/kernel/runtime/types.ts +1 -1
  112. package/src/kernel/runtime/wasmEmbed.test.ts +3 -1
  113. package/src/kernel/spec/emitReference.test.ts +196 -0
  114. package/src/kernel/spec/emitReference.ts +491 -0
  115. package/src/kernel/spec/emitSurface.ts +4 -2
  116. package/src/kernel/spec/index.ts +2 -0
  117. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  118. package/src/testing/PARITY_GAPS.ts +1 -1
  119. package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
  120. package/src/testing/behavioral/completion.test.ts +2 -2
  121. package/src/testing/behavioral/disclosure.test.ts +12 -0
  122. package/src/testing/behavioral/docExamples.test.ts +283 -0
  123. package/src/testing/behavioral/errorMatrix.storeless.test.ts +2 -1
  124. package/src/testing/behavioral/journeys.cli.test.ts +3 -1
  125. package/src/testing/behavioral/journeys.defaultPack.test.ts +571 -0
  126. package/src/testing/behavioral/journeys.interactiveTty.e2e.test.ts +66 -0
  127. package/src/testing/behavioral/journeys.livePack.e2e.test.ts +129 -0
  128. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +176 -0
  129. package/src/testing/behavioral/mcpSurface.test.ts +1 -1
  130. package/src/testing/behavioral/staleCommands.test.ts +76 -0
  131. package/src/testing/eval/__snapshots__/eval.test.ts.snap +2 -2
  132. package/src/testing/eval/cases/assembly.ts +3 -3
  133. package/src/testing/eval/cases/effectful.ts +6 -3
  134. package/src/testing/eval/cases/readNouns.ts +4 -1
  135. package/src/testing/fixtures/fixtureCapability.ts +156 -0
  136. package/src/testing/fixtures/graph/defaultPack.ts +178 -0
  137. package/src/testing/fixtures/graph/owlExport.ts +95 -0
  138. package/src/testing/helpers/extractSnippets.ts +29 -0
  139. package/src/testing/helpers/golden.ts +1 -1
  140. package/src/capabilities/config/field.test.ts +0 -188
@@ -0,0 +1,178 @@
1
+ /**
2
+ * The hermetic, vendored "default-pack" fixture — a SMALL but REAL multi-typed
3
+ * design-system pack, shaped the way a shipped default pack actually is, not the
4
+ * way the 6-entity single-typed canonical graph was.
5
+ *
6
+ * WHY A SECOND GRAPH (the R1 "one graph" discipline notwithstanding): the
7
+ * canonical fixture (`canonical.ts`) is deliberately clean — every individual is
8
+ * single-typed (`a ds:Component`), every block carries a tier, every label is a
9
+ * bare string. That cleanliness is exactly what masked the real-data counter and
10
+ * classifier bugs the journey harness (AV-231, Backlog E) exists to catch. This
11
+ * fixture is the ADVERSARIAL default pack: it reproduces the four shapes a real
12
+ * vendored pack has that the clean graph does not —
13
+ *
14
+ * 1. `owl:NamedIndividual` co-typing — every block is
15
+ * `a owl:NamedIndividual, ds:<Class>`, the way exported OWL individuals are.
16
+ * This is what doubles the per-type instance tally (A1 `entityTotal` 2×): a
17
+ * block counts once under `owl:NamedIndividual` AND once under its domain
18
+ * class.
19
+ * 2. Two+ domain classes (`ds:Component` and `ds:Pattern`) so a read that keys
20
+ * off the primary domain type is exercised across more than one class.
21
+ * 3. An UNTIERED block (`ds:orphanWidget` — a `ds:Component` with a `ds:name`
22
+ * but NO `ds:tier`). `block list`'s SELECT inner-joins `?c ds:tier ?t`, so an
23
+ * untiered block is silently dropped from every listing (A2), even though
24
+ * `graph query` finds it — the fixture pins both halves of that divergence.
25
+ * 4. A multilingual `rdfs:label "Button"@en, "Bouton"@fr` — two same-predicate
26
+ * labels whose winner in the single-valued index is store-order-arbitrary
27
+ * (A7), a shape a bare-string fixture can never produce.
28
+ *
29
+ * The vocabulary is the live `ds:` design-system namespace (same as
30
+ * `blockGraph.ts`/`canonical.ts`), so `block list`, `ontology list/show`, and
31
+ * `graph query` all run against real terms — the hand-written `block list` verb
32
+ * hard-codes `ds:Component`/`ds:name`/`ds:tier`/`ds:release`.
33
+ *
34
+ * Stable anchors this fixture guarantees (pinned by the journey suite):
35
+ * - Exactly 4 blocks in the graph: Button (`ds:Component`, global), Card
36
+ * (`ds:Pattern`, global), Beta Badge (`ds:Component`, apps tier, beta
37
+ * channel), Orphan Widget (`ds:Component`, UNTIERED).
38
+ * - 5 TBox classes, 4 TBox properties, 2 tiers (`global`, `apps`), 2 release
39
+ * channels (`stable`, `beta`).
40
+ * - `ds:button` carries the multilingual label; every other subject is a bare
41
+ * string, so the multilingual path is isolated to one entity.
42
+ */
43
+
44
+ /** The prefixes the default-pack fixture store is built and queried with. */
45
+ export const DEFAULT_PACK_PREFIXES: Readonly<Record<string, string>> = {
46
+ ds: "https://ds.canonical.com/",
47
+ owl: "http://www.w3.org/2002/07/owl#",
48
+ rdfs: "http://www.w3.org/2000/01/rdf-schema#",
49
+ xsd: "http://www.w3.org/2001/XMLSchema#",
50
+ };
51
+
52
+ /**
53
+ * The vendored default pack as one Turtle document: TBox (5 classes, 4
54
+ * properties) + ABox (2 tiers, 2 channels, 4 blocks). Every block is co-typed
55
+ * `owl:NamedIndividual` — the real-export shape the clean canonical graph omits.
56
+ */
57
+ export const DEFAULT_PACK_TTL = `
58
+ @prefix ds: <https://ds.canonical.com/> .
59
+ @prefix owl: <http://www.w3.org/2002/07/owl#> .
60
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
61
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
62
+
63
+ # ---- Ontology (TBox): 5 classes, 4 properties ----
64
+ ds:UIBlock a owl:Class ; rdfs:label "UI Block" .
65
+ ds:Component a owl:Class ; rdfs:subClassOf ds:UIBlock ; rdfs:label "Component" .
66
+ ds:Pattern a owl:Class ; rdfs:subClassOf ds:UIBlock ; rdfs:label "Pattern" .
67
+ ds:Tier a owl:Class ; rdfs:label "Tier" .
68
+ ds:ReleaseChannel a owl:Class ; rdfs:label "Release Channel" .
69
+
70
+ ds:name a owl:DatatypeProperty ; rdfs:domain ds:UIBlock ; rdfs:range xsd:string .
71
+ # Functional (one tier per block), matching the live ontology — a non-functional
72
+ # ds:tier compiles to a multi-valued GraphQL field the bundled block lookup rejects.
73
+ ds:tier a owl:ObjectProperty, owl:FunctionalProperty ; rdfs:domain ds:UIBlock ; rdfs:range ds:Tier .
74
+ ds:release a owl:ObjectProperty, owl:FunctionalProperty ; rdfs:domain ds:UIBlock ; rdfs:range ds:ReleaseChannel .
75
+ ds:summary a owl:DatatypeProperty ; rdfs:domain ds:UIBlock ; rdfs:range xsd:string .
76
+
77
+ # ---- Tiers (a real parent chain: apps -> global) ----
78
+ ds:global a ds:Tier ; ds:name "global" .
79
+ ds:apps a ds:Tier ; ds:name "apps" .
80
+
81
+ # ---- Release channels ----
82
+ ds:stable a ds:ReleaseChannel ; ds:name "stable" .
83
+ ds:beta a ds:ReleaseChannel ; ds:name "beta" .
84
+
85
+ # ---- Blocks (ABox) — every one co-typed owl:NamedIndividual (the export shape) ----
86
+
87
+ # A component on the global tier, carrying a MULTILINGUAL label (A7 shape).
88
+ ds:button a owl:NamedIndividual, ds:Component ;
89
+ ds:name "Button" ;
90
+ rdfs:label "Button"@en, "Bouton"@fr ;
91
+ ds:tier ds:global ;
92
+ ds:summary "A clickable button control." .
93
+
94
+ # A second DOMAIN CLASS (ds:Pattern), on the global tier.
95
+ ds:card a owl:NamedIndividual, ds:Pattern ;
96
+ ds:name "Card" ;
97
+ rdfs:label "Card" ;
98
+ ds:tier ds:global ;
99
+ ds:summary "A content card layout pattern." .
100
+
101
+ # A component gated to the BETA channel and the non-global apps tier — exercises
102
+ # both channel visibility and tier-chain inheritance.
103
+ ds:betaBadge a owl:NamedIndividual, ds:Component ;
104
+ ds:name "Beta Badge" ;
105
+ rdfs:label "Beta Badge" ;
106
+ ds:tier ds:apps ;
107
+ ds:release ds:beta ;
108
+ ds:summary "An experimental badge, gated to the prerelease channel." .
109
+
110
+ # An UNTIERED component — a real ds:Component with a ds:name but NO ds:tier.
111
+ # It IS in the graph (graph query finds it) but block list's mandatory
112
+ # tier join (?c ds:tier ?t) drops it (A2).
113
+ ds:orphanWidget a owl:NamedIndividual, ds:Component ;
114
+ ds:name "Orphan Widget" ;
115
+ rdfs:label "Orphan Widget" ;
116
+ ds:summary "A component shipped without a tier assignment." .
117
+ `;
118
+
119
+ /** No-tier viewing config, `normal` channel — hides the beta-only Beta Badge. */
120
+ export const DEFAULT_PACK_CONFIG = { channel: "normal" as const };
121
+
122
+ /** `prerelease` channel, no tier — the config where every channel-gated block shows. */
123
+ export const DEFAULT_PACK_ALL_VISIBLE_CONFIG = {
124
+ channel: "prerelease" as const,
125
+ };
126
+
127
+ /** The names of the 4 blocks the fixture declares, sorted (SPARQL `ORDER BY ?name`). */
128
+ export const DEFAULT_PACK_BLOCK_NAMES = [
129
+ "Beta Badge",
130
+ "Button",
131
+ "Card",
132
+ "Orphan Widget",
133
+ ] as const;
134
+
135
+ /** The block excluded from `block list` for want of a `ds:tier` (A2 anchor). */
136
+ export const UNTIERED_BLOCK_NAME = "Orphan Widget";
137
+
138
+ /**
139
+ * A second, ontology-only pack with NO tiered blocks — drives the EMPTY
140
+ * `block list` (a calm exit-0 empty result, not an error). It still declares the
141
+ * `ds:` TBox, so `ontology list` is POPULATED here (the populated/empty pair for
142
+ * the two list verbs is split across this and {@link INSTANCE_ONLY_TTL}).
143
+ */
144
+ export const NO_BLOCKS_TTL = `
145
+ @prefix ds: <https://ds.canonical.com/> .
146
+ @prefix owl: <http://www.w3.org/2002/07/owl#> .
147
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
148
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
149
+
150
+ ds:UIBlock a owl:Class .
151
+ ds:Component a owl:Class ; rdfs:subClassOf ds:UIBlock .
152
+ ds:Tier a owl:Class .
153
+ ds:name a owl:DatatypeProperty ; rdfs:domain ds:UIBlock ; rdfs:range xsd:string .
154
+ ds:tier a owl:ObjectProperty ; rdfs:domain ds:UIBlock ; rdfs:range ds:Tier .
155
+
156
+ ds:global a ds:Tier ; ds:name "global" .
157
+ `;
158
+
159
+ /**
160
+ * A pack of pure instance data with NO `owl:Class`/`owl:*Property` declarations —
161
+ * drives the EMPTY `ontology list` (no namespace has any classes or properties
162
+ * to group), while `graph query` still resolves its triples.
163
+ */
164
+ export const INSTANCE_ONLY_TTL = `
165
+ @prefix ex: <https://ex.test/#> .
166
+ ex:alpha ex:relatesTo ex:beta .
167
+ ex:beta ex:name "Beta" .
168
+ `;
169
+
170
+ /**
171
+ * A malformed Turtle source — a subject/predicate with no object — drives the
172
+ * `sources update` data-error classification path (a NAMED `CONFIG_ERROR`, not
173
+ * an INTERNAL "please report this issue").
174
+ */
175
+ export const MALFORMED_TTL = `@prefix ex: <https://ex.test/#> .
176
+ ex:One a ex:Widget .
177
+ ex:Two ex:danglingPredicate .
178
+ `;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * A realistic multi-typed OWL / Protégé-export fixture graph.
3
+ *
4
+ * The embedded `sample.ttl` (Button/Card/Dialog, each typed by exactly ONE
5
+ * domain class, every block tiered, every label untagged) is a TOY: it never
6
+ * exercises the counters/classifiers the way a real vocabulary does. This graph
7
+ * deliberately carries the shapes that only bite real packs (audit backlog A):
8
+ *
9
+ * - Protégé DUAL-typing: an individual asserted as its domain class AND
10
+ * `owl:NamedIndividual` — the multiset that made `entityTotal` run ~2× (A1).
11
+ * - A MULTI-DOMAIN individual (`ex:toggle`: two domain classes) — so the
12
+ * primary-type tie-break must be deterministic, not store-order (A3).
13
+ * - An UNTIERED block (`ds:datePicker`: a `ds:Component` with no `ds:tier`) —
14
+ * which `block list --all-tiers` must surface, not hide (A2).
15
+ * - A blank-node `rdf:type` (`ex:field`'s anonymous SHACL-style class) — which
16
+ * must NOT become a `type:"_:b0"` primary key (A6).
17
+ * - An OWL-PUNNED subject (`ex:Slider`: a class that is ALSO a metaclass
18
+ * individual) — whose abox facet must survive alongside its tbox one (A8).
19
+ * - MULTILINGUAL labels (`"Button"@en` / `"Bouton"@fr`) — resolved to a stable
20
+ * language, never whichever row the store returned first (A7).
21
+ *
22
+ * The `ds:` half is a compact but faithful design-system slice (so `block list`
23
+ * runs against it); the `ex:` half is generic OWL, proving the index is
24
+ * ontology-agnostic. One document, so a single store answers every read.
25
+ */
26
+
27
+ /** The prefixes the OWL-export fixture store is built and queried with. */
28
+ export const OWL_EXPORT_PREFIXES: Readonly<Record<string, string>> = {
29
+ ds: "https://ds.canonical.com/",
30
+ ex: "https://ex.test/vocab#",
31
+ rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
32
+ rdfs: "http://www.w3.org/2000/01/rdf-schema#",
33
+ owl: "http://www.w3.org/2002/07/owl#",
34
+ xsd: "http://www.w3.org/2001/XMLSchema#",
35
+ };
36
+
37
+ /** The multi-typed OWL-export fixture ontology + individuals as Turtle. */
38
+ export const OWL_EXPORT_TTL = `
39
+ @prefix ds: <https://ds.canonical.com/> .
40
+ @prefix ex: <https://ex.test/vocab#> .
41
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
42
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
43
+ @prefix owl: <http://www.w3.org/2002/07/owl#> .
44
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
45
+
46
+ # ---- Design-system TBox (so \`block list\` has real block types) ----
47
+ ds:UIBlock a owl:Class .
48
+ ds:Component a owl:Class ; rdfs:subClassOf ds:UIBlock ; rdfs:label "Component" .
49
+ ds:Tier a owl:Class .
50
+ ds:name a owl:DatatypeProperty ; rdfs:domain ds:UIBlock ; rdfs:range xsd:string .
51
+ ds:tier a owl:ObjectProperty ; rdfs:domain ds:UIBlock ; rdfs:range ds:Tier .
52
+ ds:global a owl:NamedIndividual, ds:Tier ; ds:name "global" .
53
+
54
+ # ---- Generic OWL domain classes (multi-typing lives here) ----
55
+ ex:Interactive a owl:Class ; rdfs:label "Interactive" .
56
+ ex:FormControl a owl:Class ; rdfs:label "Form Control" .
57
+ ex:Category a owl:Class ; rdfs:label "Category" .
58
+
59
+ # ---- A tiered block, Protégé-style DUAL-typed (domain class + owl:NamedIndividual),
60
+ # with a multilingual label (A1 + A7). ----
61
+ ds:button a owl:NamedIndividual, ds:Component ;
62
+ ds:name "Button" ;
63
+ ds:tier ds:global ;
64
+ rdfs:label "Button"@en , "Bouton"@fr .
65
+
66
+ # ---- An UNTIERED block: a ds:Component with NO ds:tier (A2). ----
67
+ ds:datePicker a owl:NamedIndividual, ds:Component ;
68
+ ds:name "Date Picker" ;
69
+ rdfs:label "Date Picker"@en , "Sélecteur de date"@fr .
70
+
71
+ # ---- A MULTI-DOMAIN individual: two domain classes + owl:NamedIndividual (A1 + A3).
72
+ # The abox primary must be the lexically-smallest: ex:FormControl < ex:Interactive. ----
73
+ ex:toggle a owl:NamedIndividual, ex:Interactive, ex:FormControl ;
74
+ rdfs:label "Toggle"@en , "Bascule"@fr .
75
+
76
+ # ---- An OWL-PUNNED subject: a class that is ALSO a metaclass individual (A8).
77
+ # Must emit BOTH a tbox facet (owl:Class) and an abox facet (ex:Category). ----
78
+ ex:Slider a owl:Class, owl:NamedIndividual, ex:Category ;
79
+ rdfs:label "Slider" .
80
+
81
+ # ---- A subject with a BLANK-NODE rdf:type: an anonymous class (A6).
82
+ # The blank node must NOT become ex:field's primary type. ----
83
+ ex:field a owl:NamedIndividual, ex:FormControl, [ a owl:Class ] ;
84
+ rdfs:label "Field" .
85
+ `;
86
+
87
+ /** The distinct abox individuals this fixture declares (the `entityTotal` oracle). */
88
+ export const OWL_EXPORT_ABOX_SUBJECTS: readonly string[] = [
89
+ "ds:button",
90
+ "ds:datePicker",
91
+ "ds:global",
92
+ "ex:Slider",
93
+ "ex:field",
94
+ "ex:toggle",
95
+ ];
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Pull the runnable `pragma` command lines out of a Markdown document.
3
+ *
4
+ * Scans fenced `bash` / `sh` / `console` blocks, strips a leading shell prompt
5
+ * (`$ `), and keeps the lines that invoke `pragma`. Used by the doc-example
6
+ * tests to prove every command shown in the docs is real grammar and — for the
7
+ * curated read set — actually runs.
8
+ */
9
+
10
+ /** Fenced `bash`/`sh`/`console` blocks, capturing the body between the fences. */
11
+ const FENCE_RE = /```(?:bash|sh|console)\r?\n(?<body>[\s\S]*?)```/g;
12
+
13
+ /**
14
+ * Extract every `pragma …` command line from a Markdown string.
15
+ *
16
+ * @param markdown - The Markdown source to scan.
17
+ * @returns The `pragma` command lines, in document order, prompt-stripped.
18
+ */
19
+ export function extractPragmaCommands(markdown: string): string[] {
20
+ const commands: string[] = [];
21
+ for (const match of markdown.matchAll(FENCE_RE)) {
22
+ const body = match.groups?.body ?? "";
23
+ for (const rawLine of body.split("\n")) {
24
+ const line = rawLine.replace(/^\s*\$\s+/, "").trim();
25
+ if (line.startsWith("pragma ")) commands.push(line);
26
+ }
27
+ }
28
+ return commands;
29
+ }
@@ -14,7 +14,7 @@ import { fileURLToPath } from "node:url";
14
14
  // biome-ignore lint/suspicious/noControlCharactersInRegex: matching the literal ESC byte is the point
15
15
  const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
16
16
 
17
- /** The package repo root (`packages/cli/next`), for tokenizing absolute paths. */
17
+ /** The package repo root (`packages/cli/pragma`), for tokenizing absolute paths. */
18
18
  const REPO_ROOT = fileURLToPath(new URL("../../../", import.meta.url)).replace(
19
19
  /\/$/,
20
20
  "",
@@ -1,188 +0,0 @@
1
- /**
2
- * `config tier` / `channel` / `detail` — storeless, covenant-exact setters.
3
- *
4
- * Pins the emitted shape against the golden slice, exercises set / reset-sentinel
5
- * / enum-reject / `--dry-run` plan / MCP plan-first-vs-confirm, and holds the
6
- * storeless invariant (a config write never boots the store).
7
- */
8
-
9
- import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
10
- import { tmpdir } from "node:os";
11
- import { join } from "node:path";
12
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
13
- import { globalConfigPath } from "../../kernel/config/paths.js";
14
- import { CHANNELS } from "../../kernel/config/types.js";
15
- import { PragmaError } from "../../kernel/error/PragmaError.js";
16
- import {
17
- executeVerb,
18
- extractParams,
19
- } from "../../kernel/project/cli/dispatch.js";
20
- import { bootRuntime } from "../../kernel/runtime/boot.js";
21
- import type { GlobalFlags, PragmaRuntime } from "../../kernel/runtime/types.js";
22
- import { emitVerb } from "../../kernel/spec/emitSurface.js";
23
- import type { VerbSpec } from "../../kernel/spec/types.js";
24
- import { projectMcp } from "../../testing/helpers/projectMcp.js";
25
- import { CONFIG_FIELDS } from "./fields.js";
26
- import { configModule } from "./index.js";
27
- import { runField } from "./runField.js";
28
-
29
- const FLAGS: GlobalFlags = {
30
- llm: false,
31
- autoLlm: false,
32
- format: "plain",
33
- verbose: false,
34
- };
35
- const REAL = { dryRun: false, undo: false, yes: false };
36
- const DRY = { dryRun: true, undo: false, yes: false };
37
-
38
- const verbOf = (field: string): VerbSpec =>
39
- configModule.verbs.find((v) => v.path[1] === field) as VerbSpec;
40
- const tierVerb = verbOf("tier");
41
- const channelVerb = verbOf("channel");
42
- const detailVerb = verbOf("detail");
43
-
44
- let prevXdg: string | undefined;
45
- const roots: string[] = [];
46
- const tmp = (prefix: string): string => {
47
- const dir = mkdtempSync(join(tmpdir(), prefix));
48
- roots.push(dir);
49
- return dir;
50
- };
51
-
52
- beforeEach(() => {
53
- prevXdg = process.env.XDG_CONFIG_HOME;
54
- process.env.XDG_CONFIG_HOME = tmp("pragma-cfgfield-xdg-");
55
- });
56
- afterEach(() => {
57
- process.env.XDG_CONFIG_HOME = prevXdg;
58
- for (const dir of roots) rmSync(dir, { recursive: true, force: true });
59
- roots.length = 0;
60
- });
61
-
62
- function readGlobal(): Record<string, unknown> {
63
- return JSON.parse(readFileSync(globalConfigPath(), "utf-8"));
64
- }
65
-
66
- describe("config setters — covenant-exact emission (PROTECTED)", () => {
67
- it("emits the golden slice for each field", () => {
68
- expect(emitVerb(tierVerb)).toEqual({
69
- v: "tier",
70
- args: ["<path>"],
71
- mutates: true,
72
- mcp: "config_tier",
73
- });
74
- expect(emitVerb(channelVerb)).toEqual({
75
- v: "channel",
76
- args: ["<name>"],
77
- mutates: true,
78
- mcp: "config_channel",
79
- });
80
- expect(emitVerb(detailVerb)).toEqual({
81
- v: "detail",
82
- args: ["<level>"],
83
- mutates: true,
84
- mcp: "config_detail",
85
- });
86
- });
87
- });
88
-
89
- describe("config setters — write & reset", () => {
90
- it("set writes the field to the GLOBAL config and never boots the store", async () => {
91
- const rt: PragmaRuntime = bootRuntime(FLAGS, tmp("pragma-proj-"));
92
- const outcome = await executeVerb(tierVerb, { path: "apps/lxd" }, REAL, rt);
93
-
94
- expect(outcome.exitCode).toBe(0);
95
- expect(outcome.stdout).toContain("Set tier = apps/lxd");
96
- expect(readGlobal().tier).toBe("apps/lxd");
97
- // Storeless invariant: a config write never touches the store factory.
98
- expect(rt.store.booted).toBe(false);
99
- });
100
-
101
- it("channel/detail write their enum value", async () => {
102
- const rt = bootRuntime(FLAGS, tmp("pragma-proj-"));
103
- await executeVerb(channelVerb, { name: "experimental" }, REAL, rt);
104
- await executeVerb(detailVerb, { level: "detailed" }, REAL, rt);
105
- const written = readGlobal();
106
- expect(written.channel).toBe("experimental");
107
- expect(written.detail).toBe("detailed");
108
- });
109
-
110
- it("a reset sentinel removes the tier field", async () => {
111
- const cwd = tmp("pragma-proj-");
112
- await executeVerb(
113
- tierVerb,
114
- { path: "apps/lxd" },
115
- REAL,
116
- bootRuntime(FLAGS, cwd),
117
- );
118
- expect(readGlobal().tier).toBe("apps/lxd");
119
-
120
- const outcome = await executeVerb(
121
- tierVerb,
122
- { path: "none" },
123
- REAL,
124
- bootRuntime(FLAGS, cwd),
125
- );
126
- expect(outcome.stdout).toContain("Reset tier");
127
- expect("tier" in readGlobal()).toBe(false);
128
- });
129
- });
130
-
131
- describe("config setters — enum rejection", () => {
132
- it("the CLI coerce layer rejects a non-member with INVALID_INPUT + validOptions", () => {
133
- let caught: unknown;
134
- try {
135
- extractParams(channelVerb.params, ["bogus"], {});
136
- } catch (error) {
137
- caught = error;
138
- }
139
- expect(caught).toBeInstanceOf(PragmaError);
140
- expect((caught as PragmaError).code).toBe("INVALID_INPUT");
141
- expect((caught as PragmaError).validOptions).toEqual([...CHANNELS]);
142
- });
143
-
144
- it("runField backstops a bad enum value before it reaches disk", () => {
145
- const channelSpec = CONFIG_FIELDS.find((s) => s.field === "channel");
146
- expect(() =>
147
- runField(channelSpec as (typeof CONFIG_FIELDS)[number], {
148
- name: "bogus",
149
- }),
150
- ).toThrow(/Invalid channel/);
151
- });
152
- });
153
-
154
- describe("config setters — dry-run plan", () => {
155
- it("--dry-run shows the WriteFile plan and writes nothing", async () => {
156
- const rt = bootRuntime(FLAGS, tmp("pragma-proj-"));
157
- const outcome = await executeVerb(tierVerb, { path: "apps/lxd" }, DRY, rt);
158
-
159
- expect(outcome.exitCode).toBe(0);
160
- expect(outcome.stdout).toContain("Write file");
161
- expect(outcome.stdout).toContain(globalConfigPath());
162
- expect(existsSync(globalConfigPath())).toBe(false);
163
- });
164
- });
165
-
166
- describe("config setters — MCP plan-first / confirm parity", () => {
167
- it("config_tier returns a plan without confirm, writes with confirm:true", async () => {
168
- const cwd = tmp("pragma-proj-");
169
- const mcp = await projectMcp([configModule], cwd);
170
-
171
- const plan = await mcp.callTool("config_tier", { path: "apps/lxd" });
172
- expect(plan.ok).toBe(true);
173
- expect(plan.meta).toMatchObject({ planOnly: true, confirmRequired: true });
174
- const planLines = (plan.data as { plan: string[] }).plan;
175
- expect(planLines.some((line) => line.includes("Write file"))).toBe(true);
176
- expect(existsSync(globalConfigPath())).toBe(false);
177
-
178
- const done = await mcp.callTool("config_tier", {
179
- path: "apps/lxd",
180
- confirm: true,
181
- });
182
- await mcp.cleanup();
183
-
184
- expect(done.ok).toBe(true);
185
- expect(done.data).toMatchObject({ field: "tier", value: "apps/lxd" });
186
- expect(readGlobal().tier).toBe("apps/lxd");
187
- });
188
- });