@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.
- package/README.md +72 -0
- package/dist/pragma +0 -0
- package/package.json +14 -13
- package/src/bin.ts +2 -2
- package/src/capabilities/block/blockList.test.ts +79 -0
- package/src/capabilities/block/blockList.verb.ts +18 -5
- package/src/capabilities/block/pack.ts +12 -1
- package/src/capabilities/block/parity.test.ts +9 -2
- package/src/capabilities/capabilities/capabilities.test.ts +10 -1
- package/src/capabilities/capabilities/capabilities.verb.ts +1 -1
- package/src/capabilities/capabilities/catalog.ts +7 -3
- package/src/capabilities/capabilities/hints.ts +8 -17
- package/src/capabilities/capabilities/types.ts +2 -0
- 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 +4 -1
- package/src/capabilities/colophon/markdownTerminal.ts +33 -14
- package/src/capabilities/colophon/pragmaColophon.ts +4 -4
- package/src/capabilities/colophon/types.ts +1 -1
- package/src/capabilities/config/fields.ts +21 -92
- package/src/capabilities/config/set.test.ts +6 -6
- package/src/capabilities/config/set.verb.ts +5 -5
- package/src/capabilities/config/show.render.test.ts +65 -0
- package/src/capabilities/config/show.render.ts +56 -10
- package/src/capabilities/config/show.verb.ts +5 -5
- package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
- package/src/capabilities/create/byteEquality.test.ts +7 -4
- package/src/capabilities/create/compiledCreate.subprocess.test.ts +1 -1
- package/src/capabilities/create/create.test.ts +32 -3
- package/src/capabilities/create/create.verb.ts +69 -11
- package/src/capabilities/doctor/checks/checkConfigFile.ts +1 -1
- package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
- package/src/capabilities/doctor/checks/checkMcpConfigured.ts +10 -4
- 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 +116 -34
- package/src/capabilities/doctor/doctor.test.ts +31 -1
- package/src/capabilities/doctor/runChecks.ts +28 -4
- package/src/capabilities/doctor/types.ts +15 -0
- package/src/capabilities/ontology/index.ts +10 -3
- package/src/capabilities/ontology/ontology.test.ts +157 -5
- package/src/capabilities/ontology/verbs.ts +192 -93
- 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 +61 -0
- package/src/capabilities/reference.test.ts +32 -0
- package/src/capabilities/resources/provider.ts +28 -14
- package/src/capabilities/resources/resources.test.ts +99 -0
- 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 +157 -61
- 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 +7 -1
- 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 +57 -5
- package/src/capabilities/setup/types.ts +59 -2
- package/src/capabilities/shared/bands.ts +16 -0
- package/src/capabilities/sources/collectStatus.ts +19 -4
- package/src/capabilities/sources/runUpdate.ts +58 -1
- package/src/capabilities/sources/sources.test.ts +187 -1
- package/src/capabilities/sources/status.render.test.ts +65 -0
- package/src/capabilities/sources/status.render.ts +50 -23
- package/src/capabilities/surface.test.ts +34 -9
- package/src/capabilities/tier/lookup.render.test.ts +72 -0
- package/src/capabilities/tier/lookup.render.ts +33 -8
- package/src/capabilities/tier/lookup.test.ts +15 -0
- package/src/capabilities/tier/runLookup.ts +28 -0
- package/src/constants.test.ts +1 -1
- package/src/constants.ts +2 -2
- package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +13 -14
- package/src/kernel/completion/bashDrive.test.ts +3 -3
- package/src/kernel/completion/complete.test.ts +3 -5
- package/src/kernel/completion/entitySource.ts +14 -6
- package/src/kernel/completion/model.test.ts +2 -2
- package/src/kernel/completion/model.ts +0 -1
- package/src/kernel/completion/parse.test.ts +2 -4
- package/src/kernel/error/PragmaError.test.ts +4 -4
- package/src/kernel/error/renderError.ts +1 -1
- package/src/kernel/packs/compile.ts +1 -1
- package/src/kernel/project/cli/buildProgram.test.ts +51 -7
- package/src/kernel/project/cli/buildProgram.ts +11 -0
- package/src/kernel/project/cli/globalFlags.test.ts +12 -4
- package/src/kernel/project/cli/globalFlags.ts +14 -14
- package/src/kernel/project/cli/rootHelp.ts +4 -2
- package/src/kernel/project/mcp/instructions.ts +1 -1
- package/src/kernel/project/mcp/mcpError.ts +52 -0
- package/src/kernel/project/mcp/prompts/prompts.test.ts +8 -0
- package/src/kernel/project/mcp/prompts/provider.ts +36 -6
- package/src/kernel/render/formatters.ts +5 -4
- package/src/kernel/render/renderers.test.ts +34 -0
- 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 +28 -0
- package/src/kernel/runtime/graphpack/buildIndex.test.ts +103 -0
- package/src/kernel/runtime/graphpack/buildIndex.ts +124 -36
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +3 -6
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -2
- package/src/kernel/runtime/graphpack/graphpack.test.ts +69 -1
- package/src/kernel/runtime/graphpack/read.ts +28 -13
- package/src/kernel/runtime/graphpack/types.ts +14 -0
- package/src/kernel/runtime/loadSession.ts +4 -12
- package/src/kernel/runtime/refs/resolve.ts +63 -3
- package/src/kernel/runtime/storeReadiness.ts +61 -0
- package/src/kernel/runtime/types.ts +1 -1
- package/src/kernel/runtime/wasmEmbed.test.ts +3 -1
- package/src/kernel/spec/emitReference.test.ts +196 -0
- package/src/kernel/spec/emitReference.ts +491 -0
- package/src/kernel/spec/emitSurface.ts +4 -2
- package/src/kernel/spec/index.ts +2 -0
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/testing/PARITY_GAPS.ts +1 -1
- package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
- package/src/testing/behavioral/completion.test.ts +2 -2
- package/src/testing/behavioral/disclosure.test.ts +12 -0
- package/src/testing/behavioral/docExamples.test.ts +283 -0
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +2 -1
- package/src/testing/behavioral/journeys.cli.test.ts +3 -1
- package/src/testing/behavioral/journeys.defaultPack.test.ts +571 -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 +176 -0
- package/src/testing/behavioral/mcpSurface.test.ts +1 -1
- package/src/testing/behavioral/staleCommands.test.ts +76 -0
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +2 -2
- package/src/testing/eval/cases/assembly.ts +3 -3
- package/src/testing/eval/cases/effectful.ts +6 -3
- package/src/testing/eval/cases/readNouns.ts +4 -1
- package/src/testing/fixtures/fixtureCapability.ts +156 -0
- package/src/testing/fixtures/graph/defaultPack.ts +178 -0
- package/src/testing/fixtures/graph/owlExport.ts +95 -0
- package/src/testing/helpers/extractSnippets.ts +29 -0
- package/src/testing/helpers/golden.ts +1 -1
- package/src/capabilities/config/field.test.ts +0 -188
|
@@ -4,11 +4,24 @@
|
|
|
4
4
|
* A lean port of the v1 `buildGraphIndex` (#856): three bulk SPARQL queries —
|
|
5
5
|
* types, labels, and instance counts — joined in memory, rather than a query
|
|
6
6
|
* per subject. Only typed named-node subjects are indexed (untyped subjects and
|
|
7
|
-
* blank
|
|
8
|
-
* carries the FROZEN `{ name, type }` minimum plus the enrichment fields
|
|
7
|
+
* blank-node subjects are reachable via `graph query` but never listed). Each
|
|
8
|
+
* entity carries the FROZEN `{ name, type }` minimum plus the enrichment fields
|
|
9
9
|
* (`uri`, `prefixed`, `types`, `label`, `box`) the completion tier and reads
|
|
10
10
|
* use. The output is pure JSON — no store handle survives — so the completion
|
|
11
11
|
* tier reads it without ever booting oxigraph.
|
|
12
|
+
*
|
|
13
|
+
* Real-data correctness (things a single-typed toy graph never exercises):
|
|
14
|
+
* - Blank-node `rdf:type` objects (SHACL shapes, RDF lists) are ignored — a
|
|
15
|
+
* `_:b0` is never a domain class and must not become a `type:"_:b0"` key.
|
|
16
|
+
* - The abox primary type is the LEXICALLY-SMALLEST domain class, a
|
|
17
|
+
* deterministic tie-break: SPARQL returns a subject's types in unspecified
|
|
18
|
+
* order, so a multi-domain individual would otherwise get an arbitrary
|
|
19
|
+
* completion key that flips across builds/engines.
|
|
20
|
+
* - Multilingual labels prefer untagged / `@en`, then the lexically smallest —
|
|
21
|
+
* never the store's arbitrary first row.
|
|
22
|
+
* - An OWL-punned subject (a class/property IRI ALSO asserted as a domain
|
|
23
|
+
* individual) emits BOTH its tbox and abox facet, so its abox membership
|
|
24
|
+
* stays completable instead of vanishing behind the tbox facet.
|
|
12
25
|
*/
|
|
13
26
|
|
|
14
27
|
import { compactUri } from "../../render/compactUri.js";
|
|
@@ -73,51 +86,109 @@ const ALT_NAME_PREDICATES = ["https://ds.canonical.com/name"];
|
|
|
73
86
|
const isStdVocab = (uri: string): boolean =>
|
|
74
87
|
STD_VOCAB_PREFIXES.some((ns) => uri.startsWith(ns));
|
|
75
88
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
89
|
+
/** One classification facet: which box a subject occupies + its primary type. */
|
|
90
|
+
interface Facet {
|
|
91
|
+
readonly box: "tbox" | "abox";
|
|
92
|
+
readonly primary: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Classify a subject's full types into its facets — one per box it occupies.
|
|
97
|
+
*
|
|
98
|
+
* A plain individual or a plain class yields ONE facet. An OWL-punned subject
|
|
99
|
+
* (a class/property IRI ALSO asserted as a domain individual) yields BOTH its
|
|
100
|
+
* tbox and abox facet (A8), so its abox membership stays completable rather than
|
|
101
|
+
* disappearing behind the tbox one. The abox primary is the lexically-smallest
|
|
102
|
+
* domain class — a deterministic tie-break (A3), not the store's first row.
|
|
103
|
+
*
|
|
104
|
+
* @param fullTypes - The subject's asserted `rdf:type` values (blank nodes
|
|
105
|
+
* already dropped), in store order.
|
|
106
|
+
* @returns The subject's facets (empty when it has no usable type).
|
|
107
|
+
*/
|
|
108
|
+
function classify(fullTypes: readonly string[]): Facet[] {
|
|
109
|
+
if (fullTypes.length === 0) return [];
|
|
110
|
+
const facets: Facet[] = [];
|
|
81
111
|
const classMeta = fullTypes.find((t) => CLASS_METATYPES.has(t));
|
|
82
|
-
if (classMeta) return { box: "tbox", primary: classMeta };
|
|
83
112
|
const propMeta = fullTypes.find((t) => PROPERTY_METATYPES.has(t));
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
113
|
+
if (classMeta !== undefined) facets.push({ box: "tbox", primary: classMeta });
|
|
114
|
+
else if (propMeta !== undefined)
|
|
115
|
+
facets.push({ box: "tbox", primary: propMeta });
|
|
116
|
+
|
|
117
|
+
// A subject's domain classes: its own types minus the structural std vocab
|
|
118
|
+
// and the `owl:NamedIndividual` typing artifact. Sorted, so the abox primary
|
|
119
|
+
// (the smallest) is stable across builds/engines regardless of store order.
|
|
120
|
+
const domainClasses = fullTypes
|
|
121
|
+
.filter((t) => t !== NAMED_INDIVIDUAL && !isStdVocab(t))
|
|
122
|
+
.sort();
|
|
123
|
+
const primaryDomain = domainClasses.at(0);
|
|
124
|
+
if (primaryDomain !== undefined) {
|
|
125
|
+
facets.push({ box: "abox", primary: primaryDomain });
|
|
126
|
+
} else if (facets.length === 0) {
|
|
127
|
+
// No schema facet and no domain class — a bare individual (only
|
|
128
|
+
// `owl:NamedIndividual`, or only std-vocab types). Keep it indexed under a
|
|
129
|
+
// deterministic primary rather than dropping it silently.
|
|
130
|
+
const fallback =
|
|
131
|
+
fullTypes
|
|
132
|
+
.filter((t) => t !== NAMED_INDIVIDUAL)
|
|
133
|
+
.sort()
|
|
134
|
+
.at(0) ?? [...fullTypes].sort().at(0);
|
|
135
|
+
if (fallback !== undefined) facets.push({ box: "abox", primary: fallback });
|
|
136
|
+
}
|
|
137
|
+
return facets;
|
|
91
138
|
}
|
|
92
139
|
|
|
93
140
|
/** A `VALUES ?p { … }` body from a predicate list. */
|
|
94
141
|
const valuesList = (uris: readonly string[]): string =>
|
|
95
142
|
uris.map((uri) => `<${uri}>`).join(" ");
|
|
96
143
|
|
|
144
|
+
/** Language rank for label selection: untagged (0) < `@en` (1) < any other (2). */
|
|
145
|
+
function rankLanguage(language: string): number {
|
|
146
|
+
if (language === "") return 0;
|
|
147
|
+
const lower = language.toLowerCase();
|
|
148
|
+
return lower === "en" || lower.startsWith("en-") ? 1 : 2;
|
|
149
|
+
}
|
|
150
|
+
|
|
97
151
|
/**
|
|
98
152
|
* Per subject, keep the object whose predicate ranks HIGHEST (lowest index) in
|
|
99
153
|
* `preference`. The `?p` binding is projected so the winner is chosen by declared
|
|
100
154
|
* preference order: SPARQL returns solutions in an unspecified order, so keeping
|
|
101
155
|
* the first row per subject would be store-order-dependent (an entity carrying
|
|
102
156
|
* both `rdfs:label` and `skos:prefLabel` could index either), not ranked.
|
|
157
|
+
*
|
|
158
|
+
* Ties within a predicate (a subject with several `rdfs:label` values — the
|
|
159
|
+
* multilingual case) are broken deterministically: untagged / `@en` first, then
|
|
160
|
+
* the lexically smallest value. So `"Button"@en`/`"Bouton"@fr` always resolves
|
|
161
|
+
* to `Button`, never whichever row the store happened to return first (A7).
|
|
103
162
|
*/
|
|
104
163
|
function preferredBySubject(
|
|
105
164
|
result: import("@canonical/ke").SelectResult,
|
|
106
165
|
valueVar: string,
|
|
107
166
|
preference: readonly string[],
|
|
108
167
|
): Map<string, string> {
|
|
109
|
-
const best = new Map<
|
|
168
|
+
const best = new Map<
|
|
169
|
+
string,
|
|
170
|
+
{ value: string; rank: number; langRank: number }
|
|
171
|
+
>();
|
|
110
172
|
for (const term of result.termBindings) {
|
|
111
173
|
if (term.s?.termType !== "NamedNode") continue;
|
|
112
|
-
const
|
|
174
|
+
const valueTerm = term[valueVar];
|
|
113
175
|
const predicate = term.p?.value;
|
|
114
|
-
if (
|
|
176
|
+
if (valueTerm === undefined || predicate === undefined) continue;
|
|
177
|
+
const value = valueTerm.value;
|
|
178
|
+
if (!value) continue;
|
|
115
179
|
const rank = preference.indexOf(predicate);
|
|
116
180
|
if (rank === -1) continue;
|
|
181
|
+
const language =
|
|
182
|
+
valueTerm.termType === "Literal" ? (valueTerm.language ?? "") : "";
|
|
183
|
+
const langRank = rankLanguage(language);
|
|
117
184
|
const current = best.get(term.s.value);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
185
|
+
const better =
|
|
186
|
+
current === undefined ||
|
|
187
|
+
rank < current.rank ||
|
|
188
|
+
(rank === current.rank &&
|
|
189
|
+
(langRank < current.langRank ||
|
|
190
|
+
(langRank === current.langRank && value < current.value)));
|
|
191
|
+
if (better) best.set(term.s.value, { value, rank, langRank });
|
|
121
192
|
}
|
|
122
193
|
const picked = new Map<string, string>();
|
|
123
194
|
for (const [subject, entry] of best) picked.set(subject, entry.value);
|
|
@@ -179,8 +250,13 @@ export async function buildIndex(
|
|
|
179
250
|
const instanceCountByType: Record<string, number> = {};
|
|
180
251
|
for (const term of typesResult.termBindings) {
|
|
181
252
|
if (term.s?.termType !== "NamedNode") continue;
|
|
253
|
+
const typeTerm = term.type;
|
|
254
|
+
// Ignore blank-node `rdf:type` objects (SHACL shapes / RDF lists): a `_:b0`
|
|
255
|
+
// is never a domain class, so it must not become a garbage primary type or
|
|
256
|
+
// inflate the per-type instance counts (A6).
|
|
257
|
+
if (typeTerm === undefined || typeTerm.termType === "BlankNode") continue;
|
|
182
258
|
const subject = term.s.value;
|
|
183
|
-
const typeUri =
|
|
259
|
+
const typeUri = typeTerm.value;
|
|
184
260
|
const bucket = typesBySubject.get(subject) ?? [];
|
|
185
261
|
bucket.push(typeUri);
|
|
186
262
|
typesBySubject.set(subject, bucket);
|
|
@@ -201,23 +277,35 @@ export async function buildIndex(
|
|
|
201
277
|
|
|
202
278
|
const entities: PackIndexEntity[] = [];
|
|
203
279
|
for (const [subject, fullTypes] of typesBySubject) {
|
|
204
|
-
const
|
|
205
|
-
if (
|
|
280
|
+
const facets = classify(fullTypes);
|
|
281
|
+
if (facets.length === 0) continue;
|
|
206
282
|
const prefixed = compactUri(subject, prefixes);
|
|
207
283
|
const altNames = altNamesBySubject.get(subject);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
284
|
+
const typesCompact = fullTypes.map((t) => compactUri(t, prefixes));
|
|
285
|
+
const label = labelBySubject.get(subject) ?? null;
|
|
286
|
+
const description = descBySubject.get(subject) ?? null;
|
|
287
|
+
// A punned subject emits one entity per facet (tbox class + abox individual).
|
|
288
|
+
for (const facet of facets) {
|
|
289
|
+
entities.push({
|
|
290
|
+
name: prefixed,
|
|
291
|
+
type: compactUri(facet.primary, prefixes),
|
|
292
|
+
uri: subject,
|
|
293
|
+
prefixed,
|
|
294
|
+
types: typesCompact,
|
|
295
|
+
label,
|
|
296
|
+
...(altNames && altNames.length > 0 ? { altNames } : {}),
|
|
297
|
+
box: facet.box,
|
|
298
|
+
description,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
219
301
|
}
|
|
220
|
-
|
|
302
|
+
// Sort by name, then a deterministic tie-break (box, then type) so a punned
|
|
303
|
+
// subject's two facets keep a stable order across builds.
|
|
304
|
+
entities.sort((a, b) => {
|
|
305
|
+
if (a.name !== b.name) return a.name < b.name ? -1 : 1;
|
|
306
|
+
if (a.box !== b.box) return a.box === "tbox" ? -1 : 1;
|
|
307
|
+
return a.type < b.type ? -1 : a.type > b.type ? 1 : 0;
|
|
308
|
+
});
|
|
221
309
|
|
|
222
310
|
return {
|
|
223
311
|
version: 2,
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
// AUTO-GENERATED by scripts/genEmbedded.ts — do not edit by hand.
|
|
2
2
|
// Regenerate after changing sample.ttl: `bun run scripts/genEmbedded.ts`.
|
|
3
|
-
export const dataNq =
|
|
4
|
-
|
|
5
|
-
export const
|
|
6
|
-
'{"version":1,"sourcesHash":"8a510cc1fb8c950e","classes":[{"uri":"https://pragma.canonical.com/sample#Component","superclasses":[],"label":"Component","definition":"A reusable design-system building block."}],"properties":[{"uri":"https://pragma.canonical.com/sample#dependsOn","kind":"object","domains":["https://pragma.canonical.com/sample#Component"],"ranges":["https://pragma.canonical.com/sample#Component"],"label":"depends on"},{"uri":"https://pragma.canonical.com/sample#componentName","kind":"datatype","domains":["https://pragma.canonical.com/sample#Component"],"ranges":["http://www.w3.org/2001/XMLSchema#string"],"label":"component name"}],"inverses":[],"functionals":[],"datatypes":[],"namespaces":[["https://pragma.canonical.com/sample#","ex"]],"shaclConstraints":[],"unions":[],"instanceStats":[["http://www.w3.org/2002/07/owl#ObjectProperty",{"total":1,"named":1}],["https://pragma.canonical.com/sample#Component",{"total":3,"named":3}],["http://www.w3.org/2002/07/owl#DatatypeProperty",{"total":1,"named":1}],["http://www.w3.org/2002/07/owl#Class",{"total":1,"named":1}]],"selfReferential":[],"functionalViolations":[],"undeclaredPredicates":[],"annotations":[],"deepBlankNesting":false}';
|
|
7
|
-
export const manifestJson =
|
|
8
|
-
'{"name":"pragma-sample","version":"0.31.0","sourceRef":"embedded:sample.ttl","contentHash":"bfe536dc9afc625502a8f578aadf4eb9927b55260951bc61f58cf07219593034","prefixes":{"ex":"https://pragma.canonical.com/sample#","rdf":"http://www.w3.org/1999/02/22-rdf-syntax-ns#","rdfs":"http://www.w3.org/2000/01/rdf-schema#","owl":"http://www.w3.org/2002/07/owl#","xsd":"http://www.w3.org/2001/XMLSchema#"},"createdAt":"2026-07-18T04:52:05.250Z"}';
|
|
3
|
+
export const dataNq = "<https://pragma.canonical.com/sample#Dialog> <https://pragma.canonical.com/sample#dependsOn> <https://pragma.canonical.com/sample#Card> .\n<https://pragma.canonical.com/sample#Dialog> <https://pragma.canonical.com/sample#dependsOn> <https://pragma.canonical.com/sample#Button> .\n<https://pragma.canonical.com/sample#Dialog> <https://pragma.canonical.com/sample#componentName> \"Dialog\" .\n<https://pragma.canonical.com/sample#Dialog> <http://www.w3.org/2000/01/rdf-schema#label> \"Dialog\" .\n<https://pragma.canonical.com/sample#Dialog> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://pragma.canonical.com/sample#Component> .\n<https://pragma.canonical.com/sample#Card> <https://pragma.canonical.com/sample#dependsOn> <https://pragma.canonical.com/sample#Button> .\n<https://pragma.canonical.com/sample#Card> <https://pragma.canonical.com/sample#componentName> \"Card\" .\n<https://pragma.canonical.com/sample#Card> <http://www.w3.org/2000/01/rdf-schema#label> \"Card\" .\n<https://pragma.canonical.com/sample#Card> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://pragma.canonical.com/sample#Component> .\n<https://pragma.canonical.com/sample#Button> <https://pragma.canonical.com/sample#componentName> \"Button\" .\n<https://pragma.canonical.com/sample#Button> <http://www.w3.org/2000/01/rdf-schema#label> \"Button\" .\n<https://pragma.canonical.com/sample#Button> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://pragma.canonical.com/sample#Component> .\n<https://pragma.canonical.com/sample#dependsOn> <http://www.w3.org/2000/01/rdf-schema#range> <https://pragma.canonical.com/sample#Component> .\n<https://pragma.canonical.com/sample#dependsOn> <http://www.w3.org/2000/01/rdf-schema#domain> <https://pragma.canonical.com/sample#Component> .\n<https://pragma.canonical.com/sample#dependsOn> <http://www.w3.org/2000/01/rdf-schema#label> \"depends on\" .\n<https://pragma.canonical.com/sample#dependsOn> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#ObjectProperty> .\n<https://pragma.canonical.com/sample#componentName> <http://www.w3.org/2000/01/rdf-schema#range> <http://www.w3.org/2001/XMLSchema#string> .\n<https://pragma.canonical.com/sample#componentName> <http://www.w3.org/2000/01/rdf-schema#domain> <https://pragma.canonical.com/sample#Component> .\n<https://pragma.canonical.com/sample#componentName> <http://www.w3.org/2000/01/rdf-schema#label> \"component name\" .\n<https://pragma.canonical.com/sample#componentName> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#DatatypeProperty> .\n<https://pragma.canonical.com/sample#Component> <http://www.w3.org/2000/01/rdf-schema#comment> \"A reusable design-system building block.\" .\n<https://pragma.canonical.com/sample#Component> <http://www.w3.org/2000/01/rdf-schema#label> \"Component\" .\n<https://pragma.canonical.com/sample#Component> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> .\n";
|
|
4
|
+
export const schemaJson = "{\"version\":1,\"sourcesHash\":\"8a510cc1fb8c950e\",\"classes\":[{\"uri\":\"https://pragma.canonical.com/sample#Component\",\"superclasses\":[],\"label\":\"Component\",\"definition\":\"A reusable design-system building block.\"}],\"properties\":[{\"uri\":\"https://pragma.canonical.com/sample#dependsOn\",\"kind\":\"object\",\"domains\":[\"https://pragma.canonical.com/sample#Component\"],\"ranges\":[\"https://pragma.canonical.com/sample#Component\"],\"label\":\"depends on\"},{\"uri\":\"https://pragma.canonical.com/sample#componentName\",\"kind\":\"datatype\",\"domains\":[\"https://pragma.canonical.com/sample#Component\"],\"ranges\":[\"http://www.w3.org/2001/XMLSchema#string\"],\"label\":\"component name\"}],\"inverses\":[],\"functionals\":[],\"datatypes\":[],\"namespaces\":[[\"https://pragma.canonical.com/sample#\",\"ex\"]],\"shaclConstraints\":[],\"unions\":[],\"instanceStats\":[[\"http://www.w3.org/2002/07/owl#ObjectProperty\",{\"total\":1,\"named\":1}],[\"https://pragma.canonical.com/sample#Component\",{\"total\":3,\"named\":3}],[\"http://www.w3.org/2002/07/owl#DatatypeProperty\",{\"total\":1,\"named\":1}],[\"http://www.w3.org/2002/07/owl#Class\",{\"total\":1,\"named\":1}]],\"selfReferential\":[],\"functionalViolations\":[],\"undeclaredPredicates\":[],\"annotations\":[],\"deepBlankNesting\":false}";
|
|
5
|
+
export const manifestJson = "{\"name\":\"pragma-sample\",\"version\":\"0.31.0\",\"sourceRef\":\"embedded:sample.ttl\",\"contentHash\":\"bfe536dc9afc625502a8f578aadf4eb9927b55260951bc61f58cf07219593034\",\"prefixes\":{\"ex\":\"https://pragma.canonical.com/sample#\",\"rdf\":\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\",\"rdfs\":\"http://www.w3.org/2000/01/rdf-schema#\",\"owl\":\"http://www.w3.org/2002/07/owl#\",\"xsd\":\"http://www.w3.org/2001/XMLSchema#\"},\"createdAt\":\"1970-01-01T00:00:00.000Z\",\"tripleCount\":23,\"entityCount\":3}";
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
// AUTO-GENERATED by scripts/genEmbedded.ts — do not edit by hand.
|
|
2
2
|
// Regenerate after changing sample.ttl: `bun run scripts/genEmbedded.ts`.
|
|
3
|
-
export const indexJson =
|
|
4
|
-
'{"version":2,"contentHash":"bfe536dc9afc625502a8f578aadf4eb9927b55260951bc61f58cf07219593034","prefixes":{"ex":"https://pragma.canonical.com/sample#","rdf":"http://www.w3.org/1999/02/22-rdf-syntax-ns#","rdfs":"http://www.w3.org/2000/01/rdf-schema#","owl":"http://www.w3.org/2002/07/owl#","xsd":"http://www.w3.org/2001/XMLSchema#"},"entities":[{"name":"ex:Button","type":"ex:Component","uri":"https://pragma.canonical.com/sample#Button","prefixed":"ex:Button","types":["ex:Component"],"label":"Button","box":"abox","description":null},{"name":"ex:Card","type":"ex:Component","uri":"https://pragma.canonical.com/sample#Card","prefixed":"ex:Card","types":["ex:Component"],"label":"Card","box":"abox","description":null},{"name":"ex:Component","type":"owl:Class","uri":"https://pragma.canonical.com/sample#Component","prefixed":"ex:Component","types":["owl:Class"],"label":"Component","box":"tbox","description":"A reusable design-system building block."},{"name":"ex:Dialog","type":"ex:Component","uri":"https://pragma.canonical.com/sample#Dialog","prefixed":"ex:Dialog","types":["ex:Component"],"label":"Dialog","box":"abox","description":null},{"name":"ex:componentName","type":"owl:DatatypeProperty","uri":"https://pragma.canonical.com/sample#componentName","prefixed":"ex:componentName","types":["owl:DatatypeProperty"],"label":"component name","box":"tbox","description":null},{"name":"ex:dependsOn","type":"owl:ObjectProperty","uri":"https://pragma.canonical.com/sample#dependsOn","prefixed":"ex:dependsOn","types":["owl:ObjectProperty"],"label":"depends on","box":"tbox","description":null}],"instanceCountByType":{"https://pragma.canonical.com/sample#Component":3,"http://www.w3.org/2002/07/owl#ObjectProperty":1,"http://www.w3.org/2002/07/owl#DatatypeProperty":1,"http://www.w3.org/2002/07/owl#Class":1}}';
|
|
3
|
+
export const indexJson = "{\"version\":2,\"contentHash\":\"bfe536dc9afc625502a8f578aadf4eb9927b55260951bc61f58cf07219593034\",\"prefixes\":{\"ex\":\"https://pragma.canonical.com/sample#\",\"rdf\":\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\",\"rdfs\":\"http://www.w3.org/2000/01/rdf-schema#\",\"owl\":\"http://www.w3.org/2002/07/owl#\",\"xsd\":\"http://www.w3.org/2001/XMLSchema#\"},\"entities\":[{\"name\":\"ex:Button\",\"type\":\"ex:Component\",\"uri\":\"https://pragma.canonical.com/sample#Button\",\"prefixed\":\"ex:Button\",\"types\":[\"ex:Component\"],\"label\":\"Button\",\"box\":\"abox\",\"description\":null},{\"name\":\"ex:Card\",\"type\":\"ex:Component\",\"uri\":\"https://pragma.canonical.com/sample#Card\",\"prefixed\":\"ex:Card\",\"types\":[\"ex:Component\"],\"label\":\"Card\",\"box\":\"abox\",\"description\":null},{\"name\":\"ex:Component\",\"type\":\"owl:Class\",\"uri\":\"https://pragma.canonical.com/sample#Component\",\"prefixed\":\"ex:Component\",\"types\":[\"owl:Class\"],\"label\":\"Component\",\"box\":\"tbox\",\"description\":\"A reusable design-system building block.\"},{\"name\":\"ex:Dialog\",\"type\":\"ex:Component\",\"uri\":\"https://pragma.canonical.com/sample#Dialog\",\"prefixed\":\"ex:Dialog\",\"types\":[\"ex:Component\"],\"label\":\"Dialog\",\"box\":\"abox\",\"description\":null},{\"name\":\"ex:componentName\",\"type\":\"owl:DatatypeProperty\",\"uri\":\"https://pragma.canonical.com/sample#componentName\",\"prefixed\":\"ex:componentName\",\"types\":[\"owl:DatatypeProperty\"],\"label\":\"component name\",\"box\":\"tbox\",\"description\":null},{\"name\":\"ex:dependsOn\",\"type\":\"owl:ObjectProperty\",\"uri\":\"https://pragma.canonical.com/sample#dependsOn\",\"prefixed\":\"ex:dependsOn\",\"types\":[\"owl:ObjectProperty\"],\"label\":\"depends on\",\"box\":\"tbox\",\"description\":null}],\"instanceCountByType\":{\"https://pragma.canonical.com/sample#Component\":3,\"http://www.w3.org/2002/07/owl#ObjectProperty\":1,\"http://www.w3.org/2002/07/owl#DatatypeProperty\":1,\"http://www.w3.org/2002/07/owl#Class\":1}}";
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdtempSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
2
8
|
import { tmpdir } from "node:os";
|
|
3
9
|
import { join } from "node:path";
|
|
4
10
|
import { executeLocal } from "@canonical/ke-graphql";
|
|
@@ -128,6 +134,68 @@ describe("graphpack hash stability (PROTECTED)", () => {
|
|
|
128
134
|
});
|
|
129
135
|
});
|
|
130
136
|
|
|
137
|
+
describe("graphpack manifest — persisted counts (A9/A10)", () => {
|
|
138
|
+
it("records tripleCount and a distinct-abox entityCount", async () => {
|
|
139
|
+
const { dir } = await build([{ path: "a.ttl", content: TTL }]);
|
|
140
|
+
const manifest = JSON.parse(
|
|
141
|
+
readFileSync(join(dir, MANIFEST_FILE), "utf-8"),
|
|
142
|
+
) as { tripleCount?: number; entityCount?: number };
|
|
143
|
+
expect(typeof manifest.tripleCount).toBe("number");
|
|
144
|
+
expect(manifest.tripleCount ?? 0).toBeGreaterThan(0);
|
|
145
|
+
// TTL declares two individuals (ex:Button, ex:Card) → two abox subjects.
|
|
146
|
+
expect(manifest.entityCount).toBe(2);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("graphpack read — truncated data cache (A9)", () => {
|
|
151
|
+
it("a truncated-but-non-empty data.nq surfaces STORE_UNAVAILABLE", async () => {
|
|
152
|
+
// A UNIQUE graph so corrupting its cache never poisons the shared TTL pack.
|
|
153
|
+
const uniqueTtl = `${TTL}\nex:Truncated a ex:Component ; rdfs:label "Truncated" .\n`;
|
|
154
|
+
const { dir } = await build([{ path: "trunc.ttl", content: uniqueTtl }]);
|
|
155
|
+
const dataPath = join(dir, DATA_FILE);
|
|
156
|
+
const lines = readFileSync(dataPath, "utf-8")
|
|
157
|
+
.split("\n")
|
|
158
|
+
.filter((line) => line.trim() !== "");
|
|
159
|
+
expect(lines.length).toBeGreaterThan(1);
|
|
160
|
+
// Drop the last statement: the dump is now a PARTIAL graph — still
|
|
161
|
+
// non-empty (so it passes the size>0 completeness gate) but fewer triples
|
|
162
|
+
// than the manifest recorded, which the boot cross-check must catch.
|
|
163
|
+
writeFileSync(dataPath, `${lines.slice(0, -1).join("\n")}\n`);
|
|
164
|
+
|
|
165
|
+
let caught: unknown;
|
|
166
|
+
try {
|
|
167
|
+
const session = await readPack(dir);
|
|
168
|
+
session.store.dispose();
|
|
169
|
+
} catch (error) {
|
|
170
|
+
caught = error;
|
|
171
|
+
}
|
|
172
|
+
expect(caught).toMatchObject({ code: "STORE_UNAVAILABLE" });
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("tolerates a benign superset (actual > recorded), not corruption", async () => {
|
|
176
|
+
// A future ke counting change could load MORE triples than the manifest
|
|
177
|
+
// recorded. That is not truncation, so boot must NOT reject it — otherwise
|
|
178
|
+
// one counting change trips a fleet-wide false STORE_UNAVAILABLE. Simulate by
|
|
179
|
+
// lowering the manifest's tripleCount below the dump's actual count.
|
|
180
|
+
const uniqueTtl = `${TTL}\nex:Superset a ex:Component ; rdfs:label "Superset" .\n`;
|
|
181
|
+
const { dir } = await build([{ path: "superset.ttl", content: uniqueTtl }]);
|
|
182
|
+
const manifestPath = join(dir, MANIFEST_FILE);
|
|
183
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as {
|
|
184
|
+
tripleCount: number;
|
|
185
|
+
};
|
|
186
|
+
writeFileSync(
|
|
187
|
+
manifestPath,
|
|
188
|
+
JSON.stringify({ ...manifest, tripleCount: manifest.tripleCount - 1 }),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// The booted store holds one MORE triple than the (lowered) manifest — a
|
|
192
|
+
// superset. On the pre-fix exact-equality guard this threw; now it boots.
|
|
193
|
+
const session = await readPack(dir);
|
|
194
|
+
expect(session.index.entities.length).toBeGreaterThan(0);
|
|
195
|
+
session.store.dispose();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
131
199
|
describe("graphpack read — incomplete pack recovery (CLI + MCP)", () => {
|
|
132
200
|
it("an incomplete pack is STORE_UNAVAILABLE with a `sources_update` tool recovery", async () => {
|
|
133
201
|
// A dir with no manifest is an incomplete pack — readPack rejects before any
|
|
@@ -73,13 +73,28 @@ export async function readPack(dir: string): Promise<StoreSession> {
|
|
|
73
73
|
JSON.parse(readFileSync(join(dir, INDEX_FILE), "utf-8")),
|
|
74
74
|
);
|
|
75
75
|
|
|
76
|
-
// Guard against a corrupt
|
|
77
|
-
// ke silently
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
76
|
+
// Guard against a corrupt or TRUNCATED `data.nq` beside an intact manifest:
|
|
77
|
+
// ke silently serves whatever loaded (falling back to an EMPTY store on a
|
|
78
|
+
// parse failure, and rewriting an empty dump). When the manifest records the
|
|
79
|
+
// build-time triple count (A9), require the booted store to hold AT LEAST it:
|
|
80
|
+
// a SHORTFALL means the dump was truncated (fewer triples than were built) —
|
|
81
|
+
// it passes the mere size>0 completeness gate yet would serve a partial graph
|
|
82
|
+
// silently, so it must surface STORE_UNAVAILABLE here. A benign SUPERSET (more
|
|
83
|
+
// triples than recorded — e.g. a future ke counting change) is TOLERATED
|
|
84
|
+
// rather than treated as corruption, so such a change can't trip a fleet-wide
|
|
85
|
+
// false STORE_UNAVAILABLE. Older packs without the count fall back to the
|
|
86
|
+
// "populated index but empty store" check (via the now-empty dump,
|
|
87
|
+
// `packIsComplete` turns false so recovery rebuilds) rather than serving an
|
|
88
|
+
// empty graph as the pack.
|
|
89
|
+
const actualTriples = await countTriples(store);
|
|
90
|
+
if (manifest.tripleCount !== undefined) {
|
|
91
|
+
if (actualTriples < manifest.tripleCount) {
|
|
92
|
+
store.dispose();
|
|
93
|
+
throw packUnavailable(
|
|
94
|
+
`The pack at ${dir} has a corrupt data cache (expected at least ${manifest.tripleCount} triples, loaded ${actualTriples}).`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
} else if (index.entities.length > 0 && actualTriples === 0) {
|
|
83
98
|
store.dispose();
|
|
84
99
|
throw packUnavailable(`The pack at ${dir} has a corrupt data cache.`);
|
|
85
100
|
}
|
|
@@ -93,10 +108,10 @@ export async function readPack(dir: string): Promise<StoreSession> {
|
|
|
93
108
|
};
|
|
94
109
|
}
|
|
95
110
|
|
|
96
|
-
/**
|
|
97
|
-
async function
|
|
98
|
-
const result = (await store.query(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return result.
|
|
111
|
+
/** Count the booted store's triples (a cheap aggregate over the union graph). */
|
|
112
|
+
async function countTriples(store: StoreSession["store"]): Promise<number> {
|
|
113
|
+
const result = (await store.query(
|
|
114
|
+
"SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }" as never,
|
|
115
|
+
)) as import("@canonical/ke").SelectResult;
|
|
116
|
+
return Number(result.bindings.at(0)?.n ?? 0);
|
|
102
117
|
}
|
|
@@ -105,6 +105,20 @@ export const manifestSchema = z.object({
|
|
|
105
105
|
contentHash: z.string(),
|
|
106
106
|
prefixes: z.record(z.string(), z.string()),
|
|
107
107
|
createdAt: z.string(),
|
|
108
|
+
/**
|
|
109
|
+
* The store's triple count at build time. Cross-checked against the booted
|
|
110
|
+
* store so a truncated-but-non-empty `data.nq` (a partial graph that passes
|
|
111
|
+
* the mere size>0 completeness gate) surfaces as STORE_UNAVAILABLE rather
|
|
112
|
+
* than being served silently (A9). Optional — packs built before this field
|
|
113
|
+
* skip the check.
|
|
114
|
+
*/
|
|
115
|
+
tripleCount: z.number().optional(),
|
|
116
|
+
/**
|
|
117
|
+
* The distinct abox entity count (matches {@link entityTotal}). Lets
|
|
118
|
+
* `sources status` report the figure without parsing the whole `index.json`
|
|
119
|
+
* (A10). Optional — packs built before this field fall back to the index read.
|
|
120
|
+
*/
|
|
121
|
+
entityCount: z.number().optional(),
|
|
108
122
|
});
|
|
109
123
|
|
|
110
124
|
/** Pack provenance and the prefixes the store was built with. */
|
|
@@ -9,13 +9,11 @@
|
|
|
9
9
|
* than silently reaching out.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { RECOVERY_CLI_PREFIX } from "../../constants.js";
|
|
13
12
|
import type { ConfigLayers } from "../config/types.js";
|
|
14
|
-
import { PragmaError } from "../error/PragmaError.js";
|
|
15
|
-
import { cliRecovery } from "../error/recovery.js";
|
|
16
13
|
import { materializeEmbeddedPack } from "./graphpack/embedded.js";
|
|
17
14
|
import { readPack } from "./graphpack/read.js";
|
|
18
15
|
import { resolveSources } from "./resolveSources.js";
|
|
16
|
+
import { storeUnavailable } from "./storeReadiness.js";
|
|
19
17
|
import type { StoreSession } from "./types.js";
|
|
20
18
|
|
|
21
19
|
/** What {@link loadStoreSession} needs from the runtime. */
|
|
@@ -39,15 +37,9 @@ export async function loadStoreSession(
|
|
|
39
37
|
const decision = resolveSources(layers, ctx.cwd);
|
|
40
38
|
|
|
41
39
|
if (decision.kind === "unavailable") {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"Build the local store from the configured packages.",
|
|
46
|
-
// An agent recovers by calling the tool (then retrying — PR9's C1 cold-
|
|
47
|
-
// store retry makes a post-update retry succeed).
|
|
48
|
-
{ tool: "sources_update" },
|
|
49
|
-
),
|
|
50
|
-
});
|
|
40
|
+
// The SAME cold-store diagnostic the native MCP surfaces raise (shared via
|
|
41
|
+
// storeReadiness), so a cold store reads identically on every surface.
|
|
42
|
+
throw storeUnavailable(decision.reason);
|
|
51
43
|
}
|
|
52
44
|
|
|
53
45
|
const dir =
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* machine-independent. Reached only from the `sources update` Task body.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
12
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
13
13
|
import { createRequire } from "node:module";
|
|
14
14
|
import { dirname, join, relative } from "node:path";
|
|
15
15
|
import { RECOVERY_CLI_PREFIX } from "../../../constants.js";
|
|
@@ -84,9 +84,28 @@ function walkTtl(
|
|
|
84
84
|
// build (a `.`-prefixed Turtle local name isn't even valid).
|
|
85
85
|
if (entry.name.startsWith(".")) continue;
|
|
86
86
|
const full = join(dir, entry.name);
|
|
87
|
-
|
|
87
|
+
// Resolve symlinks: pnpm / workspace trees link their sources, but
|
|
88
|
+
// `Dirent.isFile`/`isDirectory` are BOTH false for a symlink — so a
|
|
89
|
+
// symlinked `.ttl` was silently skipped (L6). Follow the link to recover a
|
|
90
|
+
// symlinked FILE, but NEVER recurse into a symlinked DIRECTORY: a symlink
|
|
91
|
+
// cycle (a link to an ancestor dir) would recurse without bound — a
|
|
92
|
+
// stack-overflow RangeError surfacing as an INTERNAL "please report" — and a
|
|
93
|
+
// linked directory could pull `.ttl` from OUTSIDE the package root. Linking
|
|
94
|
+
// individual source files (all L6 needs) stays supported; a dangling link is
|
|
95
|
+
// skipped, not fatal.
|
|
96
|
+
let isDir = entry.isDirectory();
|
|
97
|
+
let isFile = entry.isFile();
|
|
98
|
+
if (entry.isSymbolicLink()) {
|
|
99
|
+
try {
|
|
100
|
+
isFile = statSync(full).isFile();
|
|
101
|
+
isDir = false;
|
|
102
|
+
} catch {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (isDir) {
|
|
88
107
|
walkTtl(full, base, label, out);
|
|
89
|
-
} else if (
|
|
108
|
+
} else if (isFile && entry.name.endsWith(".ttl")) {
|
|
90
109
|
out.push({
|
|
91
110
|
path: `${label}/${relative(base, full)}`,
|
|
92
111
|
content: readFileSync(full, "utf-8"),
|
|
@@ -143,6 +162,47 @@ export function harvestPrefixes(
|
|
|
143
162
|
return prefixes;
|
|
144
163
|
}
|
|
145
164
|
|
|
165
|
+
/** One `@prefix` clash: a label declared with two or more distinct IRIs. */
|
|
166
|
+
export interface PrefixClash {
|
|
167
|
+
readonly label: string;
|
|
168
|
+
/** The distinct IRIs bound to the label, in first-seen order. */
|
|
169
|
+
readonly iris: readonly string[];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Detect `@prefix` label clashes across a set of sources.
|
|
174
|
+
*
|
|
175
|
+
* {@link harvestPrefixes} resolves clashes last-wins (Turtle's own rule),
|
|
176
|
+
* silently compacting the losing package's URIs to the wrong prefix (or to full
|
|
177
|
+
* IRIs). This surfaces the clashes so `sources update` can warn (A5): a label
|
|
178
|
+
* bound to two or more distinct IRIs. Harmless same-label/same-IRI
|
|
179
|
+
* redeclarations (the common case) are NOT clashes. Result is sorted by label
|
|
180
|
+
* so the warnings are deterministic.
|
|
181
|
+
*
|
|
182
|
+
* @param sources - The packages' labelled RDF sources.
|
|
183
|
+
* @returns One entry per clashing label, each with its distinct IRIs.
|
|
184
|
+
*/
|
|
185
|
+
export function detectPrefixClashes(
|
|
186
|
+
sources: readonly { readonly content: string }[],
|
|
187
|
+
): PrefixClash[] {
|
|
188
|
+
const byLabel = new Map<string, string[]>();
|
|
189
|
+
for (const { content } of sources) {
|
|
190
|
+
for (const match of content.matchAll(PREFIX_DECL)) {
|
|
191
|
+
const [, label, iri] = match;
|
|
192
|
+
if (label === undefined || iri === undefined) continue;
|
|
193
|
+
const iris = byLabel.get(label) ?? [];
|
|
194
|
+
if (!iris.includes(iri)) iris.push(iri);
|
|
195
|
+
byLabel.set(label, iris);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const clashes: PrefixClash[] = [];
|
|
199
|
+
for (const [label, iris] of byLabel) {
|
|
200
|
+
if (iris.length > 1) clashes.push({ label, iris });
|
|
201
|
+
}
|
|
202
|
+
clashes.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0));
|
|
203
|
+
return clashes;
|
|
204
|
+
}
|
|
205
|
+
|
|
146
206
|
/**
|
|
147
207
|
* Locate a package's `package.json` from the project.
|
|
148
208
|
*
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The STORELESS store-readiness check — the ONE source of the cold-store
|
|
3
|
+
* `STORE_UNAVAILABLE` error (its message AND its `pragma sources update` /
|
|
4
|
+
* `sources_update` recovery), shared by the lazy store boot ({@link
|
|
5
|
+
* loadStoreSession}) and the native MCP discovery surfaces (the `prompts/*`
|
|
6
|
+
* provider, the `pragma:{+uri}` resource browser).
|
|
7
|
+
*
|
|
8
|
+
* A cold store must present the SAME diagnostic on every surface: an agent that
|
|
9
|
+
* lists prompts over the `prompt_list` tool (store-backed, so its `needsStore`
|
|
10
|
+
* pre-check throws) and one that lists them over native `prompts/list` must see
|
|
11
|
+
* the identical `STORE_UNAVAILABLE` + `sources_update` recovery, not one error
|
|
12
|
+
* and one silently-empty list. Centralizing the construction here is what keeps
|
|
13
|
+
* those surfaces from drifting apart.
|
|
14
|
+
*
|
|
15
|
+
* It reaches only {@link resolveSources} (a lock + config probe — no store, no
|
|
16
|
+
* ke), so a caller on the storeless fast path can pre-check availability without
|
|
17
|
+
* ever booting the store.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { RECOVERY_CLI_PREFIX } from "../../constants.js";
|
|
21
|
+
import type { ConfigLayers } from "../config/types.js";
|
|
22
|
+
import { PragmaError } from "../error/PragmaError.js";
|
|
23
|
+
import { cliRecovery } from "../error/recovery.js";
|
|
24
|
+
import { resolveSources } from "./resolveSources.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build the canonical cold-store `STORE_UNAVAILABLE` error: the reason plus the
|
|
28
|
+
* single `pragma sources update` recovery that also names the `sources_update`
|
|
29
|
+
* tool an agent calls (then retries — PR9's C1 cold-store retry succeeds).
|
|
30
|
+
*
|
|
31
|
+
* @param reason - The boot decision's reason (e.g. "the locked pack is missing").
|
|
32
|
+
* @returns The structured STORE_UNAVAILABLE error.
|
|
33
|
+
*/
|
|
34
|
+
export function storeUnavailable(reason: string): PragmaError {
|
|
35
|
+
return PragmaError.storeUnavailable(`${reason}.`, {
|
|
36
|
+
recovery: cliRecovery(
|
|
37
|
+
`${RECOVERY_CLI_PREFIX}sources update`,
|
|
38
|
+
"Build the local store from the configured packages.",
|
|
39
|
+
{ tool: "sources_update" },
|
|
40
|
+
),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The STORE_UNAVAILABLE error to surface when the store cannot boot from the
|
|
46
|
+
* current config/lock, or `undefined` when a store read may proceed. STORELESS —
|
|
47
|
+
* probes the decision table only, never constructs the store.
|
|
48
|
+
*
|
|
49
|
+
* @param layers - The resolved config layers.
|
|
50
|
+
* @param cwd - The project directory the boot resolves against.
|
|
51
|
+
* @returns The error to surface cold, or `undefined` when the store is available.
|
|
52
|
+
*/
|
|
53
|
+
export function checkStoreAvailable(
|
|
54
|
+
layers: ConfigLayers,
|
|
55
|
+
cwd: string,
|
|
56
|
+
): PragmaError | undefined {
|
|
57
|
+
const decision = resolveSources(layers, cwd);
|
|
58
|
+
return decision.kind === "unavailable"
|
|
59
|
+
? storeUnavailable(decision.reason)
|
|
60
|
+
: undefined;
|
|
61
|
+
}
|
|
@@ -17,7 +17,7 @@ import type { ConfigLayers } from "../config/types.js";
|
|
|
17
17
|
* separately so inferred mode can shape output without switching behaviour.
|
|
18
18
|
*/
|
|
19
19
|
export interface GlobalFlags {
|
|
20
|
-
/** Condensed Markdown output
|
|
20
|
+
/** Condensed Markdown output, from `--format llm` or auto-detection. */
|
|
21
21
|
readonly llm: boolean;
|
|
22
22
|
/** True when `llm` was inferred from a non-interactive stdout, not requested. */
|
|
23
23
|
readonly autoLlm?: boolean;
|