@catheadowl/ukp 0.1.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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +208 -0
  3. package/bun.lock +35 -0
  4. package/package.json +51 -0
  5. package/src/artifacts.ts +75 -0
  6. package/src/capabilities/nav.ts +734 -0
  7. package/src/capabilities/propose.ts +456 -0
  8. package/src/capabilities/qmd.ts +174 -0
  9. package/src/capabilities/read.ts +1040 -0
  10. package/src/capabilities/remote-client.ts +856 -0
  11. package/src/capabilities/rename-recovery.ts +294 -0
  12. package/src/capabilities/rg.ts +505 -0
  13. package/src/capabilities/search.ts +1105 -0
  14. package/src/capabilities/tls-identity.ts +87 -0
  15. package/src/capabilities/update.ts +385 -0
  16. package/src/cli.ts +425 -0
  17. package/src/commands/diagnose.ts +383 -0
  18. package/src/commands/flags.ts +49 -0
  19. package/src/commands/guide-content/client.txt +36 -0
  20. package/src/commands/guide-content/propose.txt +38 -0
  21. package/src/commands/guide-content/service-qmd.txt +44 -0
  22. package/src/commands/guide-content/service.txt +114 -0
  23. package/src/commands/guide.ts +121 -0
  24. package/src/commands/init.ts +245 -0
  25. package/src/commands/inspect.ts +182 -0
  26. package/src/commands/inventory.ts +570 -0
  27. package/src/commands/kit.ts +269 -0
  28. package/src/commands/nav.ts +280 -0
  29. package/src/commands/propose.ts +257 -0
  30. package/src/commands/read.ts +558 -0
  31. package/src/commands/rg.ts +355 -0
  32. package/src/commands/search.ts +325 -0
  33. package/src/commands/serve.ts +221 -0
  34. package/src/commands/update.ts +99 -0
  35. package/src/config/client.ts +55 -0
  36. package/src/config/external-tool.ts +58 -0
  37. package/src/config/file-native.ts +111 -0
  38. package/src/config/manifest.ts +188 -0
  39. package/src/fslock.ts +95 -0
  40. package/src/path-safety.ts +65 -0
  41. package/src/registry.ts +436 -0
  42. package/src/scope.ts +78 -0
  43. package/src/server.ts +1011 -0
  44. package/tsconfig.json +12 -0
@@ -0,0 +1,383 @@
1
+ import { loadManifest, type LoadedManifest, type ManifestCapability } from "../config/manifest.ts";
2
+ import { FILE_NATIVE_CAPABILITIES, isFileNativeCapability } from "../config/file-native.ts";
3
+ import { EXTERNAL_TOOL_CAPABILITIES, EXTERNAL_PROVIDER, isExternalToolCapability } from "../config/external-tool.ts";
4
+ import { isRemoteBinding, localPathOf, readRegistry, type RegistryBinding } from "../registry.ts";
5
+ import { resolveScope } from "../scope.ts";
6
+ import { KitUsageError, parseKitArgs, renderKitHelp, renderKitUsageError, type UkpCommandSpec } from "./kit.ts";
7
+ import { HelpRequestError, isHelpRequest } from "./flags.ts";
8
+ import { defaultQmdCommand } from "../capabilities/qmd.ts";
9
+ import { resolveProposeFolder } from "../capabilities/propose.ts";
10
+ import { rgExecutableAvailable } from "../capabilities/rg.ts";
11
+
12
+ export interface ProviderCheck {
13
+ supported: boolean;
14
+ reason?: string;
15
+ }
16
+
17
+ export type ProviderResolver = (provider: string, capability?: string) => ProviderCheck;
18
+
19
+ export interface DiagnoseReport {
20
+ service: LoadedManifest;
21
+ capabilities: Array<{
22
+ name: string;
23
+ provider: string;
24
+ source: "manifest" | "derived-local" | "base-tier";
25
+ status: "ok" | "warning";
26
+ reason?: string;
27
+ }>;
28
+ }
29
+
30
+ export interface DiagnoseCommandContext {
31
+ currentDirectory: string;
32
+ registryPath: string;
33
+ resolveProvider?: ProviderResolver;
34
+ }
35
+
36
+ export interface DiagnoseCommandResult {
37
+ exitCode: number;
38
+ stdout: string;
39
+ stderr: string;
40
+ }
41
+
42
+ export interface RenderDiagnoseOptions {
43
+ includeSearchabilityHint?: boolean;
44
+ }
45
+
46
+ const SEARCHABILITY_HINT = "hint: diagnose checks wiring, not indexed content; content is searchable after provider setup (see 'ukp guide service qmd').";
47
+
48
+ /** Single-source command spec (ADR 0024): summary feeds the root help via
49
+ * cli.ts; usage feeds the help header and the usage-error line; the scope
50
+ * family, singleton detection, and help-intent triage live in kit.ts. */
51
+ export const DIAGNOSE_SPEC: UkpCommandSpec = {
52
+ name: "diagnose",
53
+ summary: "check a Service or endpoints for wiring problems (manifest, provider setup)",
54
+ group: "operations",
55
+ description: "Validate a Service folder or selected registered Service endpoints.",
56
+ usage: "[--endpoint <name> ... | -g]",
57
+ scope: {
58
+ endpointHelp: "validate one registered endpoint; repeat to validate multiple endpoints",
59
+ globalHelp: "validate every endpoint in the Host Registry; takes no value",
60
+ },
61
+ helpSuffix: [
62
+ "",
63
+ "Scope:",
64
+ " With no --endpoint or -g, diagnose validates the current folder as a",
65
+ " Service (Manifest and provider wiring). -g validates every registered",
66
+ " endpoint; -g cannot be combined with --endpoint.",
67
+ "",
68
+ ].join("\n"),
69
+ };
70
+
71
+ // Provider registration point for this UKP build: every provider that can
72
+ // back a capability must have its availability check here. Adding a provider
73
+ // means (1) a branch in this resolver and (2) a capability adapter under
74
+ // capabilities/ that the command layer dispatches to.
75
+ export function defaultProviderResolver(provider: string, capability = "search"): ProviderCheck {
76
+ if (isFileNativeCapability(capability)) {
77
+ // Only the UKP-native file provider exists for the file-native set;
78
+ // Manifest load already defaults bare declarations to "file".
79
+ return provider === "file"
80
+ ? { supported: true }
81
+ : {
82
+ supported: false,
83
+ reason: `provider '${provider}' is not supported for capability '${capability}' by this UKP build`,
84
+ };
85
+ }
86
+ if (isExternalToolCapability(capability)) {
87
+ // External-tool base tier (ADR-RG-003): the provider marker is fixed;
88
+ // availability is the base tool itself, degrading to a warning row.
89
+ if (provider !== EXTERNAL_PROVIDER) {
90
+ return { supported: false, reason: `provider '${provider}' is not supported for capability '${capability}' by this UKP build` };
91
+ }
92
+ return rgExecutableAvailable()
93
+ ? { supported: true }
94
+ : { supported: false, reason: "rg executable is not available" };
95
+ }
96
+ if (capability === "update") {
97
+ if (provider !== "qmd") {
98
+ return { supported: false, reason: `provider '${provider}' is not supported for capability 'update' by this UKP build` };
99
+ }
100
+ return defaultQmdCommand()
101
+ ? { supported: true }
102
+ : { supported: false, reason: "qmd executable is not available" };
103
+ }
104
+ if (capability !== "search") {
105
+ return { supported: false, reason: `capability '${capability}' is not implemented by this UKP build` };
106
+ }
107
+ if (provider !== "qmd") {
108
+ return { supported: false, reason: `provider '${provider}' is not supported by this UKP build` };
109
+ }
110
+ return defaultQmdCommand()
111
+ ? { supported: true }
112
+ : { supported: false, reason: "qmd executable is not available" };
113
+ }
114
+
115
+ function proposeFolderError(serviceFolder: string, declaration: ManifestCapability): string | undefined {
116
+ try {
117
+ resolveProposeFolder(serviceFolder, declaration);
118
+ return undefined;
119
+ } catch (error) {
120
+ return error instanceof Error ? error.message : String(error);
121
+ }
122
+ }
123
+
124
+ export function evaluateServiceCapabilities(
125
+ service: LoadedManifest,
126
+ resolveProvider: ProviderResolver = defaultProviderResolver,
127
+ ): DiagnoseReport["capabilities"] {
128
+ const manifestCapabilities = Object.entries(service.manifest.capabilities).map(([name, declaration]) => {
129
+ // Provider is guaranteed post-load for valid Manifests (file-native bare
130
+ // declarations default to "file"; others fail at load). The "(none)"
131
+ // arm is defensive depth for direct API callers.
132
+ const provider = declaration.provider ?? "(none)";
133
+ const check = resolveProvider(provider, name);
134
+ // Front-load propose folder validation so config typos surface in the
135
+ // Service-side self-check instead of the first `ukp propose` run.
136
+ const folderError = name === "propose"
137
+ ? proposeFolderError(service.folder, declaration)
138
+ : undefined;
139
+ const supported = check.supported && folderError === undefined;
140
+ const reason = !check.supported ? check.reason : folderError;
141
+ return {
142
+ name,
143
+ provider,
144
+ source: "manifest" as const,
145
+ status: supported ? "ok" as const : "warning" as const,
146
+ ...(reason ? { reason } : {}),
147
+ };
148
+ });
149
+
150
+ // Derived file-native defaults (ADR 0016 rule 2): every read-side
151
+ // file-native capability not declared by the Manifest is still effectively
152
+ // present. The table is the single source — no per-capability rows here.
153
+ const derived = Object.entries(FILE_NATIVE_CAPABILITIES)
154
+ .filter(([name, spec]) => spec.derived && !Object.hasOwn(service.manifest.capabilities, name))
155
+ .map(([name]) => {
156
+ const check = resolveProvider("file", name);
157
+ return {
158
+ name,
159
+ provider: "file",
160
+ source: "derived-local" as const,
161
+ status: check.supported ? "ok" as const : "warning" as const,
162
+ ...(check.reason ? { reason: check.reason } : {}),
163
+ };
164
+ });
165
+
166
+ // External-tool base tier (ADR-RG-003): base-tier members are effectively
167
+ // present on every registered local Service without declaration; a missing
168
+ // base tool degrades the row to a warning, never hides it.
169
+ const baseTier = Object.entries(EXTERNAL_TOOL_CAPABILITIES)
170
+ .filter(([name, spec]) => spec.baseTier && !Object.hasOwn(service.manifest.capabilities, name))
171
+ .map(([name]) => {
172
+ const check = resolveProvider(EXTERNAL_PROVIDER, name);
173
+ return {
174
+ name,
175
+ provider: EXTERNAL_PROVIDER,
176
+ source: "base-tier" as const,
177
+ status: check.supported ? "ok" as const : "warning" as const,
178
+ ...(check.reason ? { reason: check.reason } : {}),
179
+ };
180
+ });
181
+ return [...manifestCapabilities, ...derived, ...baseTier];
182
+ }
183
+
184
+ export function diagnoseService(
185
+ serviceFolder: string,
186
+ resolveProvider: ProviderResolver = defaultProviderResolver,
187
+ ): DiagnoseReport {
188
+ const service = loadManifest(serviceFolder);
189
+ const capabilities = evaluateServiceCapabilities(service, resolveProvider);
190
+
191
+ if (!capabilities.some((capability) => capability.status === "ok")) {
192
+ throw new Error("NO_SUPPORTED_CAPABILITY");
193
+ }
194
+ return { service, capabilities };
195
+ }
196
+
197
+ export function renderDependencyRegistryWarnings(
198
+ service: LoadedManifest,
199
+ registry: readonly RegistryBinding[],
200
+ ): string[] {
201
+ if (!service.manifest.dependencies || service.manifest.dependencies.length === 0) return [];
202
+ const registeredEndpoints = new Set(registry.map((binding) => binding.name));
203
+ return service.manifest.dependencies
204
+ .filter((dependency) => !registeredEndpoints.has(dependency.endpoint))
205
+ .map((dependency) =>
206
+ `dependency target '${dependency.endpoint}' is not registered (declared by ${service.effectiveName})`
207
+ );
208
+ }
209
+
210
+ function parseDiagnoseArgs(args: readonly string[]): {
211
+ explicitEndpoints?: string[];
212
+ global: boolean;
213
+ warnings: string[];
214
+ } {
215
+ const parsed = parseKitArgs(DIAGNOSE_SPEC, args);
216
+ return {
217
+ explicitEndpoints: parsed.scope.explicitEndpoints,
218
+ global: parsed.scope.global,
219
+ warnings: parsed.scope.warnings,
220
+ };
221
+ }
222
+
223
+ function diagnoseBinding(
224
+ binding: RegistryBinding,
225
+ resolveProvider: ProviderResolver | undefined,
226
+ ): { status: "ok"; report: DiagnoseReport } | { status: "failed"; message: string } {
227
+ if (isRemoteBinding(binding)) {
228
+ return {
229
+ status: "failed",
230
+ message: `remote endpoint '${binding.name}' (${binding.url}): diagnose is local-only in ukp_remote W2; reachability shows in 'ukp list'`,
231
+ };
232
+ }
233
+ try {
234
+ const report = diagnoseService(localPathOf(binding), resolveProvider);
235
+ if (report.service.effectiveName !== binding.name) {
236
+ return {
237
+ status: "failed",
238
+ message: `endpoint '${binding.name}' no longer matches Service effective name '${report.service.effectiveName}'`,
239
+ };
240
+ }
241
+ return { status: "ok", report };
242
+ } catch (error) {
243
+ return { status: "failed", message: error instanceof Error ? error.message : String(error) };
244
+ }
245
+ }
246
+
247
+ export function executeDiagnoseCommand(
248
+ args: readonly string[],
249
+ context: DiagnoseCommandContext,
250
+ ): DiagnoseCommandResult {
251
+ if (isHelpRequest(args)) {
252
+ return { exitCode: 0, stdout: renderDiagnoseHelp(), stderr: "" };
253
+ }
254
+
255
+ try {
256
+ const parsed = parseDiagnoseArgs(args);
257
+ if (!parsed.explicitEndpoints && !parsed.global) {
258
+ try {
259
+ const report = diagnoseService(context.currentDirectory, context.resolveProvider);
260
+ let dependencyWarnings: string[] = [];
261
+ try {
262
+ dependencyWarnings = renderDependencyRegistryWarnings(report.service, readRegistry(context.registryPath));
263
+ } catch (error) {
264
+ if (report.service.manifest.dependencies && report.service.manifest.dependencies.length > 0) {
265
+ dependencyWarnings = [
266
+ `dependency registry check skipped: ${error instanceof Error ? error.message : String(error)}`,
267
+ ];
268
+ }
269
+ }
270
+ const warnings = [...parsed.warnings, ...dependencyWarnings];
271
+ return {
272
+ exitCode: 0,
273
+ stdout: renderDiagnose(report, {
274
+ includeSearchabilityHint: true,
275
+ }),
276
+ stderr: warnings.length > 0 ? `${warnings.join("\n")}\n` : "",
277
+ };
278
+ } catch (error) {
279
+ return {
280
+ exitCode: 1,
281
+ stdout: "",
282
+ stderr: renderLocalDiagnoseError(error instanceof Error ? error.message : String(error)),
283
+ };
284
+ }
285
+ }
286
+
287
+ const registry = readRegistry(context.registryPath);
288
+ const scope = resolveScope({
289
+ currentDirectory: context.currentDirectory,
290
+ registry,
291
+ explicitEndpoints: parsed.explicitEndpoints,
292
+ global: parsed.global,
293
+ });
294
+ const warnings = [...parsed.warnings, ...scope.warnings];
295
+ const output: string[] = [];
296
+ let failed = false;
297
+
298
+ for (const binding of scope.bindings) {
299
+ output.push(`== ${binding.name} ==`);
300
+ const result = diagnoseBinding(binding, context.resolveProvider);
301
+ if (result.status === "ok") {
302
+ output.push(renderDiagnose(result.report, { includeSearchabilityHint: true }).trimEnd());
303
+ warnings.push(...renderDependencyRegistryWarnings(result.report.service, registry));
304
+ } else {
305
+ failed = true;
306
+ output.push("status: failed");
307
+ output.push(`error: ${result.message}`);
308
+ }
309
+ }
310
+
311
+ if (scope.bindings.length === 0) {
312
+ failed = true;
313
+ warnings.push("no endpoints selected: the Host Registry is empty; run 'ukp register' from a Service folder, then retry");
314
+ }
315
+
316
+ return {
317
+ exitCode: failed ? 1 : 0,
318
+ stdout: output.length > 0 ? `${output.join("\n")}\n` : "",
319
+ stderr: warnings.length > 0 ? `${warnings.join("\n")}\n` : "",
320
+ };
321
+ } catch (error) {
322
+ if (error instanceof HelpRequestError) {
323
+ return { exitCode: 0, stdout: renderDiagnoseHelp(), stderr: "" };
324
+ }
325
+ if (error instanceof KitUsageError) {
326
+ return { exitCode: 2, stdout: "", stderr: renderDiagnoseUsageError(error.message) };
327
+ }
328
+ return {
329
+ exitCode: 1,
330
+ stdout: "",
331
+ stderr: `ukp diagnose: ${error instanceof Error ? error.message : String(error)}\n`,
332
+ };
333
+ }
334
+ }
335
+
336
+ export function renderLocalDiagnoseError(message: string): string {
337
+ return [
338
+ `ukp diagnose: ${message}`,
339
+ "Hint: 'ukp diagnose' checks the current folder as a Service.",
340
+ "Run 'ukp guide service' for the short onboarding guide.",
341
+ "Use 'ukp diagnose -g' to validate every registered endpoint, or 'ukp diagnose --endpoint <name>' for one endpoint.",
342
+ ].join("\n") + "\n";
343
+ }
344
+
345
+ export function renderDiagnose(report: DiagnoseReport, options: RenderDiagnoseOptions = {}): string {
346
+ const lines = [
347
+ `endpoint: ${report.service.effectiveName} (source: ${report.service.nameSource})`,
348
+ ];
349
+ if (report.service.manifest.description) {
350
+ lines.push(`description: ${report.service.manifest.description}`);
351
+ }
352
+ if (report.service.manifest.dependencies && report.service.manifest.dependencies.length > 0) {
353
+ for (const dependency of report.service.manifest.dependencies) {
354
+ lines.push(`dependency: depends_on -> ${dependency.endpoint} (kind: ${dependency.kind})`);
355
+ if (dependency.reason) {
356
+ lines.push(`dependency_reason: ${dependency.reason}`);
357
+ }
358
+ }
359
+ }
360
+ lines.push(`location: ${report.service.folder}`);
361
+ const showSearchabilityHint = options.includeSearchabilityHint
362
+ && report.capabilities.some((capability) => capability.name === "search" && capability.status === "ok");
363
+ for (const capability of report.capabilities) {
364
+ lines.push(capability.source === "derived-local"
365
+ ? `capability: ${capability.name} (derived local baseline)`
366
+ : capability.source === "base-tier"
367
+ ? `capability: ${capability.name} (external-tool base tier)`
368
+ : `capability: ${capability.name}`);
369
+ lines.push(`provider: ${capability.provider}`);
370
+ lines.push(`status: ${capability.status}`);
371
+ if (capability.reason) lines.push(`warning: ${capability.reason}`);
372
+ }
373
+ if (showSearchabilityHint) lines.push(SEARCHABILITY_HINT);
374
+ return `${lines.join("\n")}\n`;
375
+ }
376
+
377
+ export function renderDiagnoseHelp(): string {
378
+ return renderKitHelp(DIAGNOSE_SPEC);
379
+ }
380
+
381
+ export function renderDiagnoseUsageError(message: string): string {
382
+ return renderKitUsageError(DIAGNOSE_SPEC, message);
383
+ }
@@ -0,0 +1,49 @@
1
+ import { CommanderError } from "commander";
2
+
3
+ export function countFlagOccurrences(args: readonly string[], flag: string): number {
4
+ if (flag.startsWith("--")) {
5
+ return args.filter((argument) => argument === flag || argument.startsWith(`${flag}=`)).length;
6
+ }
7
+
8
+ if (!flag.startsWith("-") || flag.length !== 2) {
9
+ return args.filter((argument) => argument === flag).length;
10
+ }
11
+
12
+ const shortFlag = flag[1];
13
+ return args.reduce((count, argument) => {
14
+ if (argument === flag) return count + 1;
15
+ if (!argument.startsWith("-") || argument.length <= 2) return count;
16
+
17
+ const bundle = argument.slice(1);
18
+ if (!/^[A-Za-z]+$/.test(bundle)) return count;
19
+ if (!bundle.split("").every((character) => character === shortFlag)) return count;
20
+ return count + bundle.length;
21
+ }, 0);
22
+ }
23
+
24
+ export function isHelpRequest(args: readonly string[]): boolean {
25
+ return args.length === 1 && (args[0] === "-h" || args[0] === "--help");
26
+ }
27
+
28
+ /** commander reports help/version intent through exitOverride's
29
+ * CommanderError (codes `commander.help` / `commander.helpDisplayed` /
30
+ * `commander.version`); the `(outputHelp)` message is an internal sentinel,
31
+ * not user-facing text. Options are order-independent, so `--help` must win
32
+ * wherever it appears, and the parse layer must not translate these into
33
+ * usage errors. Call inside an `instanceof CommanderError` guard — a type
34
+ * predicate here would narrow the sibling branch to `never`. */
35
+ export function isCommanderHelpIntent(error: CommanderError): boolean {
36
+ return error.code === "commander.help"
37
+ || error.code === "commander.helpDisplayed"
38
+ || error.code === "commander.version";
39
+ }
40
+
41
+ /** Flows from the parse layer (whose configureOutput silencing swallowed
42
+ * commander's own help rendering) to the execute layer, which re-renders
43
+ * help and returns exit 0. */
44
+ export class HelpRequestError extends Error {
45
+ constructor() {
46
+ super("help requested");
47
+ this.name = "HelpRequestError";
48
+ }
49
+ }
@@ -0,0 +1,36 @@
1
+ UKP Client quickstart
2
+
3
+ Goal: use registered Services from a workspace by default, without naming an endpoint on every call.
4
+
5
+ Model:
6
+ - Client path is separate from the provider path: it uses Services; it does not declare one.
7
+ - A folder may hold both roles at once (dual-role): a Service folder may also carry its own .ukp/client.toml to set its default scope. Each file is read independently.
8
+ - A workspace .ukp/client.toml default_endpoints list selects which registered endpoints you mean when no selector is given.
9
+ - UKP walks up from the current directory to find the nearest .ukp/client.toml.
10
+ - Client Config stores endpoint names only; bindings live in the Host Registry.
11
+ - Explicit --endpoint <name> or -g overrides the client defaults. With no Client Config, most commands fall back to every registered endpoint.
12
+ - ukp update is stricter: it does not fall back to the Registry. It uses your Client Config defaults, or requires --endpoint <name> / -g.
13
+ - ukp register does not edit .ukp/client.toml.
14
+
15
+ 1. Create a workspace Client Config
16
+ .ukp/client.toml at the workspace root:
17
+ default_endpoints = ["endpoint-a", "endpoint-b"]
18
+ Each name must match a registered endpoint; run 'ukp list' to see what is registered.
19
+
20
+ 2. Use Services without a selector
21
+ ukp search "keyword"
22
+ ukp inspect
23
+ ukp read --endpoint endpoint-a docs/example.md (read still names a path, and --endpoint here wins)
24
+
25
+ 3. Check which scope is active
26
+ ukp inspect
27
+ It prints scope: explicit / global / client-config / registry-fallback and the source.
28
+
29
+ 4. Dangling defaults warn instead of failing silently
30
+ A default_endpoints entry not registered yet is reported as a warning by inspect, and commands still run on the resolvable ones.
31
+
32
+ Remember:
33
+ - Client path consumes Services; provider path (ukp guide service) creates them.
34
+ - Client Config lists names, not paths or provider details.
35
+ - Explicit selectors always beat Client Config defaults.
36
+ - Register does not edit Client Config, and Client Config does not register endpoints.
@@ -0,0 +1,38 @@
1
+ UKP propose quickstart
2
+
3
+ Goal: submit a change proposal to a Service endpoint as an idempotent, adjudicable document — not a file write.
4
+
5
+ Model:
6
+ - A proposal is a pending change the Service owner will adjudicate; the submitter proposes, the owner decides.
7
+ - Identity is client-owned: you pick the id (a slug), and resubmitting the same id updates the same proposal.
8
+ - Submission is an upsert with three outcomes: created (new, revision 1), unchanged (identical content, no write), updated (replaced, revision +1).
9
+ - revision is an audit counter and the history of your edits; it never resets.
10
+ - ukp propose is the only documented write path. Never edit Service-side proposal files directly: a later submission overwrites them and your edits bypass revision tracking.
11
+
12
+ 1. Declare the capability (Service side)
13
+ .ukp/service.toml:
14
+ [capabilities.propose]
15
+ provider = "file"
16
+ The provider line is optional (a bare [capabilities.propose] means the file provider).
17
+ Optional [capabilities.propose.config] folder = "inbox" (default) sets where proposals land.
18
+
19
+ 2. Submit a proposal
20
+ ukp propose --endpoint <name> --file draft.md
21
+ --id defaults to the draft basename (extension stripped); the basename becomes the proposal's persistent identity.
22
+ Renaming the file and resubmitting creates a new proposal, not a revision.
23
+ Pass --id <slug> to pin the identity explicitly.
24
+ Add --json for a machine-readable envelope (id, status, revision).
25
+
26
+ 3. Revise a proposal
27
+ Edit your local draft, then run the same command with the same id.
28
+ The Service replaces the stored proposal, keeps created, bumps revision, refreshes updated.
29
+ Identical resubmission is a no-op (exit 0, revision unchanged).
30
+
31
+ 4. Read proposals back
32
+ Reading is owned by the Service: if it indexes its proposal folder into its search surface,
33
+ ukp search / ukp read reach proposals like any other content.
34
+
35
+ Remember:
36
+ - Same id, same proposal: do not fork ids (add-topic, add-topic-v2) for revisions of one idea.
37
+ - Frontmatter service fields (id, status, revision, created, updated) are Service-maintained and stripped from submissions; your subject/kind display fields pass through.
38
+ - status stays "proposed" until adjudication exists; there is no accept/reject command yet.
@@ -0,0 +1,44 @@
1
+ UKP Service quickstart - QMD provider setup
2
+
3
+ Goal: make a registered Service's content searchable through the default QMD provider.
4
+ Run these from the Service folder root.
5
+
6
+ Boundary:
7
+ - These are provider-owned steps. QMD owns collection, index, ranking, and config.
8
+ - UKP only routes capability requests. This topic carries the journey; exact
9
+ flags and arguments belong to the relevant QMD subcommand help.
10
+
11
+ Fast path for a simple knowledge folder:
12
+ 1. qmd init
13
+ 2. qmd collection add .
14
+ 3. qmd update
15
+ 4. qmd search "keyword"
16
+ 5. qmd get <reference> --no-line-numbers
17
+
18
+ 1. Initialize QMD (qmd init)
19
+ 2. Add the searchable collections (qmd collection add)
20
+ For a pure knowledge base, add the folder root; otherwise add the folder(s)
21
+ whose content should be searchable.
22
+ 3. Use a stable short collection name (qmd collection rename)
23
+ QMD names a collection from the indexed folder; rename it to a short
24
+ stable name so provider-native artifact URIs stay clean.
25
+ 4. Update the index (qmd update, qmd status)
26
+ 5. Verify the provider before returning to UKP (qmd search / qmd get)
27
+ If a search returns matches, the provider is ready; return to
28
+ 'ukp guide service' step 4.
29
+ A read smoke ('qmd get <reference> --no-line-numbers') is required for
30
+ observe mode (external or nested collection root): it confirms the provider
31
+ can resolve the provider references that ukp read will delegate to it
32
+ (bare docid handoff keys and qmd:// references).
33
+
34
+ Exact flags and arguments come from the relevant QMD subcommand help, for example:
35
+ qmd init --help
36
+ qmd collection add --help
37
+ qmd update --help
38
+ qmd search --help
39
+ qmd get --help
40
+
41
+ Remember:
42
+ - Provider config is not stored in the UKP Registry.
43
+ - A registered Service is addressable; it is searchable only after provider setup.
44
+ - Keep the local index out of version control: ignore .qmd/*.sqlite and .qmd/*.sqlite-*.
@@ -0,0 +1,114 @@
1
+ UKP Service quickstart
2
+
3
+ Goal: make a folder a named Knowledge Service, register it, inspect the route, search it, and read an endpoint-scoped reference.
4
+ Run the setup commands from the Service folder root.
5
+
6
+ Fast path:
7
+ 1. Create the UKP Service Manifest
8
+ ukp init service --name your-endpoint-name
9
+ Optional: add --description "Short boundary note"
10
+
11
+ 2. Make content searchable through the provider
12
+ ukp guide service qmd
13
+
14
+ 3. Validate and register the Service
15
+ ukp diagnose
16
+ ukp register
17
+ ukp list
18
+
19
+ 4. Inspect, search, then read
20
+ ukp inspect --endpoint your-endpoint-name
21
+ ukp search "keyword" --endpoint your-endpoint-name --limit 3
22
+ ukp read --endpoint your-endpoint-name <reference>
23
+
24
+ Key boundary:
25
+ - init/register makes the folder addressable as a Service.
26
+ - provider setup makes the Service's content searchable.
27
+ - Both are needed for an end-to-end search path.
28
+
29
+ Model:
30
+ - A UKP Service is a knowledge endpoint that declares capabilities.
31
+ - Four separate journeys:
32
+ - provider path: init service + register make a folder an addressable Service;
33
+ - content-searchable: provider setup decides what content is indexed (provider-owned; see 'ukp guide service qmd');
34
+ - declared dependencies: a Service Manifest can declare [[dependencies]] entries with endpoint + kind; this is declarative only;
35
+ - client path: a workspace .ukp/client.toml default scope lets you use Services by default instead of naming one each call.
36
+ - Registered as a Service does not mean its content is searchable; both steps are needed.
37
+ - QMD is the current default search/update provider, not the definition of a Service.
38
+ - Provider setup is provider-owned: the provider owns collection, index, ranking, and config.
39
+
40
+ Details:
41
+
42
+ 1. Choose a stable Service folder
43
+ Use the project or knowledge-domain root. Limit searchable subfolders in the provider, not in the UKP Registry.
44
+
45
+ 2. Create the UKP Service Manifest
46
+ ukp init service --name your-endpoint-name
47
+ Optional: add --description "Short boundary note"
48
+ Name must be a lowercase endpoint slug, for example: cad-notes
49
+ This writes .ukp/service.toml with the current minimal provider declaration:
50
+ [capabilities.search]
51
+ provider = "qmd"
52
+
53
+ 3. Make content searchable through the provider (provider-owned)
54
+ Provider setup is not a UKP step. For the default QMD provider:
55
+ ukp guide service qmd
56
+ That topic points exact flags and arguments to the relevant QMD subcommand help.
57
+
58
+ If this Service depends on other externally consumed knowledge endpoints,
59
+ declare them in the Manifest with repeated --dependency <name> entries;
60
+ each entry becomes [[dependencies]] with kind = "context" and can be edited later.
61
+
62
+ 4. Validate and register
63
+ ukp diagnose
64
+ ukp register
65
+ ukp list
66
+ At this point the folder is a registered Service: addressable and callable.
67
+ Its content is searchable only because step 3 configured the provider.
68
+
69
+ 5. Inspect the effective route
70
+ ukp inspect --endpoint your-endpoint-name
71
+ Use inspect when you need to explain scope, Registry binding, Manifest capability, or provider availability.
72
+
73
+ 6. Search
74
+ ukp search "keyword" --endpoint your-endpoint-name --limit 3
75
+ ukp search "keyword" --endpoint your-endpoint-name --limit 3 --json
76
+ ukp search "keyword" --endpoint your-endpoint-name --recursive
77
+ In a workspace with .ukp/client.toml default_endpoints, you can omit --endpoint for search.
78
+
79
+ 7. Read an endpoint-scoped reference
80
+ Every registered local Service has the derived read/file baseline; no Manifest get entry is needed.
81
+ ukp read --endpoint your-endpoint-name docs/example.md
82
+ ukp read --endpoint your-endpoint-name docs/example.md --lines 10:20
83
+ Search results carry copyable handoff lines you can paste straight into read:
84
+ - a ukp://<endpoint>/<path> URI is the durable, endpoint-scoped reference;
85
+ - a bare docid is the provider's short-lived document id (QMD handoff key).
86
+ On a QMD-backed Service (including observe mode), provider content is read with
87
+ 'ukp read --endpoint <name> <docid handoff key or qmd:// reference>'; do not
88
+ manually convert qmd:// provider locations into Service-folder paths.
89
+
90
+ 8. Update provider-owned state
91
+ This is optional for the first search path. Add this capability to .ukp/service.toml when provider maintenance should be exposed:
92
+ [capabilities.update]
93
+ provider = "qmd"
94
+ ukp update --endpoint your-endpoint-name
95
+ In a workspace with .ukp/client.toml default_endpoints, you can omit --endpoint for update too.
96
+ Use ukp update -g only when you explicitly want to update every registered endpoint.
97
+
98
+ Advanced:
99
+ - Declared dependencies belong in the Service Manifest as [[dependencies]] entries.
100
+ - ukp init service --dependency <name> emits a context dependency; edit the Manifest later for authority, implementation, evidence, or a reason.
101
+ - Dependencies do not change ordinary search/read. Only explicit search --recursive traverses direct authority/context dependencies.
102
+ - Workspace defaults belong to the client path; run 'ukp guide client' from the consuming workspace.
103
+ - update is a provider-owned maintenance route, not part of the first search smoke.
104
+
105
+ Remember:
106
+ - Endpoint name identifies the Service.
107
+ - Host Registry stores endpoint name -> Service folder path.
108
+ - Provider collection decides what content is indexed; folder-external authoritative knowledge should be modeled as its own endpoint when UKP ownership matters.
109
+ - Register makes the folder addressable; it does not make content searchable (provider indexing does).
110
+ - Register does not edit .ukp/client.toml or provider configuration.
111
+ - Declarative dependencies live in the Service Manifest as [[dependencies]] entries; they do not change ordinary search/read, while explicit search --recursive traverses direct authority/context dependencies.
112
+ - Future providers should add provider adapters instead of turning provider internals into UKP rules.
113
+ - To register an already-used name at a new location, unregister the old binding first:
114
+ ukp unregister --endpoint <name>