@okfit/cli 0.6.11 → 0.7.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @okfit/cli
2
2
 
3
- The `okfit` command line for [Open Knowledge Format (OKF)](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md) v0.2 bundles: `okfit validate`, `okfit init`, `okfit context`, `okfit verify`, and `okfit sync`. The full subcommand list is `okf/interfaces/cli-commands.md`'s to keep, not this sentence's to count.
3
+ The `okfit` command line for [Open Knowledge Format (OKF)](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md) v0.2 bundles: `okfit validate`, `okfit init`, `okfit context`, `okfit verify`, `okfit query`, and `okfit sync`. The full subcommand list is `okf/interfaces/cli-commands.md`'s to keep, not this sentence's to count.
4
4
 
5
5
  > **Part of the okfit kit.** Most users want **[@okfit/plugin](https://www.npmjs.com/package/@okfit/plugin)**, which pulls this package in automatically.
6
6
 
@@ -11,7 +11,10 @@ okfit [--help] [--version]
11
11
  okfit validate [path] [--config <file>] [--format human|json] [--skip-provenance] [--document <bundle-path>] [--help]
12
12
  okfit init [path] [--profile <name>] [--config <file>] [--help]
13
13
  okfit context [path] [--config <file>] [--format human|json] [--help]
14
- okfit verify <id> [path] [--config <file>] [--at <iso>] [--dry-run] [--format human|json] [--help]
14
+ okfit verify [<id>] [path] [--all] [--type <Type>]... [--stable|--draft] [--config <file>] [--at <iso>] [--dry-run] [--format human|json] [--help]
15
+ okfit query list [path] [--type <Type>]... [--tag <tag>]... [--status draft|stable|deprecated]... [--verified|--unverified] [--config <file>] [--format human|json]
16
+ okfit query get <id> [path] [--config <file>] [--format human|json]
17
+ okfit query neighbors <id> [path] [--config <file>] [--format human|json]
15
18
  ```
16
19
 
17
20
  `okfit --version` prints `okfit <cli> (engine <engine>, okf <okf>,
@@ -220,9 +223,43 @@ file and an atomic rename, and the target's mode is preserved on the
220
223
  replacement, but the file is not skipped just because it is `chmod`-ed
221
224
  read-only.
222
225
 
226
+ `--stable` or `--draft` sets the concept's `status` in the same write as the
227
+ attestation, so settling a reviewed draft is one command:
228
+ `okfit verify <id> --stable`. Passing both is a usage error (exit `64`), as
229
+ is passing either with `--all` or `--type`: promotion is a per-concept
230
+ decision. A concept already at the requested status gets no status edit. The
231
+ human line ends `; status <from> -> <to>`, `; status already <to>` or
232
+ `; status (absent) -> <to>`, and a dry run adds a `would set status:` line
233
+ with the exact fragment.
234
+
235
+ `--all` and `--type <Type>` (repeatable) attest in batch: every concept whose
236
+ type sets `require_verified` (or of the named types) that you have not
237
+ already verified. A batch skips a concept and reports why: `draft`,
238
+ `deprecated`, or `already-verified`. A batch never re-attests a
239
+ `deprecated` concept; verifying one by id is still allowed.
240
+
223
241
  This is a human-run command: it records **your** attestation that you
224
242
  reviewed the concept, so no agent, hook, or MCP tool ever invokes it.
225
243
 
244
+ ### `okfit query`
245
+
246
+ Read-only lookups over the bundle, backed by the engine's `ConceptQuery`
247
+ layer (the same one the MCP `list_concepts`, `get_concept` and
248
+ `concept_neighbors` tools use). Bare `okfit query` prints help.
249
+
250
+ - `okfit query list [path]` lists concepts as summaries (id, type, title,
251
+ description, status, tags, path). Filters: `--type <Type>` and
252
+ `--tag <tag>` (repeatable, each must be declared in the config),
253
+ `--status draft|stable|deprecated` (repeatable), and `--verified` or
254
+ `--unverified`.
255
+ - `okfit query get <id> [path]` prints one concept: the summary fields and its
256
+ outgoing links. `--format json` also includes the raw frontmatter.
257
+ - `okfit query neighbors <id> [path]` prints a concept's outgoing and
258
+ incoming links.
259
+
260
+ Exit `0` on success, `3` for an unknown id, `64` for an undeclared type or
261
+ tag or for `--verified` together with `--unverified`.
262
+
226
263
  ## Config discovery
227
264
 
228
265
  With no `--config` flag, `okfit` walks upward from `[path]` (default:
@@ -347,6 +384,19 @@ envelope to stdout and exits `3`:
347
384
 
348
385
  `init` has no `--format`; it is human output only.
349
386
 
387
+ `okfit verify --format json` prints `VerifyEnvelope` (`schema`, `okfit_version`,
388
+ `engine_version`, `distribution`, `id`, `path`, `verified`, `status`,
389
+ `dry_run`, `exit_code`), where `status` is `{ "from": ..., "to": ... }` when
390
+ `--stable` or `--draft` was given and `null` otherwise. With `--all` or
391
+ `--type` it prints `VerifyBatchEnvelope`, whose `skipped[].reason` is one of
392
+ `draft`, `deprecated` or `already-verified`.
393
+
394
+ `okfit query list --format json` prints `QueryListEnvelope` (`total`, `items`);
395
+ `query get` prints `QueryGetEnvelope` (`concept`: the summary fields plus
396
+ `frontmatter` and `links`); `query neighbors` prints `QueryNeighborsEnvelope`
397
+ (`id`, `outgoing`, `incoming`). Each carries `schema`, `okfit_version`,
398
+ `engine_version` and `distribution`.
399
+
350
400
  `okfit context --format json` prints its own envelope, distinct from the
351
401
  one above:
352
402
 
@@ -0,0 +1,174 @@
1
+ import { setExitCode } from "../internal/exit.js";
2
+ import { CLI_VERSION } from "../version.js";
3
+ import { displayRoot } from "../render/human.js";
4
+ import { humanQueryGet, humanQueryList, humanQueryNeighbors, queryListSummary } from "../render/query.js";
5
+ import { Argument, Command, Flag } from "effect/cli";
6
+ import { CurrentDistribution } from "@effected/engine";
7
+ import { ConceptQuery, QueryGetEnvelope, QueryListEnvelope, QueryNeighborsEnvelope, QuerySelectionError, jsonError, provideConfig, queryGetEnvelope, queryListEnvelope, queryNeighborsEnvelope, resolveProjectConfig, toConceptSummary } from "@okfit/engine";
8
+ import { Console, Effect, Option, Path, Schema } from "effect";
9
+ import { Bundle } from "@okfit/core";
10
+
11
+ //#region src/commands/query.ts
12
+ /** `[path]` is the PROJECT root (K-2), never the bundle root. Absolute at parse time (K-50). */
13
+ const pathArg = Argument.Path("path", { pathType: "directory" }).pipe(Argument.optional, Argument.withDescription("project root to start config discovery from (default: current directory); never the bundle root"));
14
+ const idArg = Argument.String("id").pipe(Argument.withDescription("concept id, with or without a leading slash or trailing .md"));
15
+ /** K-1: no `mustExist` — the handler stats the path itself, before building any layer. */
16
+ const configFlag = Flag.File("config").pipe(Flag.optional, Flag.withDescription("explicit config file; skips discovery"));
17
+ const formatFlag = Flag.Literals("format", ["human", "json"]).pipe(Flag.withDefault("human"), Flag.withDescription("output format: human (default) or json"));
18
+ const typeFlag = Flag.String("type").pipe(Flag.atLeast(0), Flag.withDescription("only concepts of this type (repeatable; any match)"));
19
+ const tagFlag = Flag.String("tag").pipe(Flag.atLeast(0), Flag.withDescription("only concepts carrying this tag (repeatable; all must match)"));
20
+ const statusFlag = Flag.Literals("status", [
21
+ "draft",
22
+ "stable",
23
+ "deprecated"
24
+ ]).pipe(Flag.atLeast(0), Flag.withDescription("only concepts with this status (repeatable; any match)"));
25
+ const verifiedFlag = Flag.Boolean("verified").pipe(Flag.withDefault(false), Flag.withDescription("only concepts with at least one verified entry"));
26
+ const unverifiedFlag = Flag.Boolean("unverified").pipe(Flag.withDefault(false), Flag.withDescription("only concepts with no verified entry"));
27
+ /**
28
+ * Shared skeleton: discover config, load the bundle, run `use`, and — under
29
+ * `--format json` — apply the K-22 stdout error envelope. `precheck` runs
30
+ * first, before any config discovery, so an invalid flag combination fails fast.
31
+ */
32
+ const run$1 = (input, use, precheck) => Effect.gen(function* () {
33
+ const cwd = process.cwd();
34
+ const discoveryCwd = Option.getOrElse(input.path, () => cwd);
35
+ const path = yield* Path.Path;
36
+ const distribution = yield* CurrentDistribution;
37
+ const body = Effect.gen(function* () {
38
+ if (precheck !== void 0) yield* precheck;
39
+ const resolved = yield* resolveProjectConfig({
40
+ pathArg: input.path,
41
+ explicitConfigPath: input.config,
42
+ cwd
43
+ });
44
+ yield* use({
45
+ bundle: yield* Bundle.load({ root: resolved.bundleRoot }),
46
+ config: resolved.config,
47
+ root: displayRoot(cwd, resolved.bundleRoot, path),
48
+ distribution
49
+ });
50
+ setExitCode(0);
51
+ }).pipe(provideConfig({
52
+ explicitConfigPath: input.config,
53
+ discoveryCwd
54
+ }));
55
+ if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION, Option.getOrUndefined(distribution))))));
56
+ return yield* body;
57
+ });
58
+ const distributionOf = (distribution) => Option.isSome(distribution) ? { distribution: distribution.value } : {};
59
+ /**
60
+ * `okfit query list [path] [--type <T>]... [--tag <t>]... [--status <s>]...
61
+ * [--verified|--unverified] [--config <file>] [--format human|json]`.
62
+ *
63
+ * @public
64
+ */
65
+ const queryListCommand = Command.make("list", {
66
+ path: pathArg,
67
+ config: configFlag,
68
+ type: typeFlag,
69
+ tag: tagFlag,
70
+ status: statusFlag,
71
+ verified: verifiedFlag,
72
+ unverified: unverifiedFlag,
73
+ format: formatFlag
74
+ }, (input) => {
75
+ const filter = {
76
+ ...input.type.length > 0 ? { types: input.type } : {},
77
+ ...input.tag.length > 0 ? { tags: input.tag } : {},
78
+ ...input.status.length > 0 ? { statuses: input.status } : {},
79
+ ...input.verified ? { verified: true } : {},
80
+ ...input.unverified ? { verified: false } : {}
81
+ };
82
+ return run$1(input, ({ bundle, config, root, distribution }) => Effect.gen(function* () {
83
+ const items = (yield* ConceptQuery.list(bundle, config, filter)).map(toConceptSummary);
84
+ if (input.format === "json") {
85
+ const envelope = queryListEnvelope({
86
+ okfitVersion: CLI_VERSION,
87
+ total: items.length,
88
+ items,
89
+ ...distributionOf(distribution)
90
+ });
91
+ yield* Console.log(JSON.stringify(Schema.encodeSync(QueryListEnvelope)(envelope)));
92
+ } else {
93
+ for (const line of humanQueryList(items)) yield* Console.log(line);
94
+ yield* Console.error(queryListSummary(items.length, root));
95
+ }
96
+ }), input.verified && input.unverified ? Effect.fail(new QuerySelectionError({ reason: "verified-conflict" })) : void 0);
97
+ }).pipe(Command.withDescription("List concepts, sorted by id, optionally filtered by type, tag, status, or verified state."));
98
+ /**
99
+ * `okfit query get <id> [path] [--config <file>] [--format human|json]`.
100
+ *
101
+ * @public
102
+ */
103
+ const queryGetCommand = Command.make("get", {
104
+ id: idArg,
105
+ path: pathArg,
106
+ config: configFlag,
107
+ format: formatFlag
108
+ }, (input) => run$1(input, ({ bundle, distribution }) => Effect.gen(function* () {
109
+ const { concept, links } = yield* ConceptQuery.get(bundle, input.id);
110
+ const summary = toConceptSummary(concept);
111
+ if (input.format === "json") {
112
+ const envelope = queryGetEnvelope({
113
+ okfitVersion: CLI_VERSION,
114
+ concept: summary,
115
+ frontmatter: concept.frontmatter.raw,
116
+ links,
117
+ ...distributionOf(distribution)
118
+ });
119
+ yield* Console.log(JSON.stringify(Schema.encodeSync(QueryGetEnvelope)(envelope)));
120
+ } else for (const line of humanQueryGet({
121
+ summary,
122
+ verified: concept.frontmatter.verified ?? [],
123
+ links
124
+ })) yield* Console.log(line);
125
+ }))).pipe(Command.withDescription("Show one concept: its fields, verified entries, and outgoing links."));
126
+ /**
127
+ * `okfit query neighbors <id> [path] [--config <file>] [--format human|json]`.
128
+ *
129
+ * @public
130
+ */
131
+ const queryNeighborsCommand = Command.make("neighbors", {
132
+ id: idArg,
133
+ path: pathArg,
134
+ config: configFlag,
135
+ format: formatFlag
136
+ }, (input) => run$1(input, ({ bundle, distribution }) => Effect.gen(function* () {
137
+ const result = yield* ConceptQuery.neighbors(bundle, input.id);
138
+ const project = (n) => ({
139
+ id: n.id,
140
+ kind: n.kind,
141
+ summary: n.concept === null ? null : toConceptSummary(n.concept)
142
+ });
143
+ const outgoing = result.outgoing.map(project);
144
+ const incoming = result.incoming.map(project);
145
+ if (input.format === "json") {
146
+ const envelope = queryNeighborsEnvelope({
147
+ okfitVersion: CLI_VERSION,
148
+ id: result.id,
149
+ outgoing,
150
+ incoming,
151
+ ...distributionOf(distribution)
152
+ });
153
+ yield* Console.log(JSON.stringify(Schema.encodeSync(QueryNeighborsEnvelope)(envelope)));
154
+ } else for (const line of humanQueryNeighbors({
155
+ outgoing,
156
+ incoming
157
+ })) yield* Console.log(line);
158
+ }))).pipe(Command.withDescription("Show the concepts a concept links to and the ones that link to it."));
159
+ /**
160
+ * `okfit query`: read-only questions about the bundle, one subcommand per
161
+ * MCP query tool (`list_concepts`, `get_concept`, `concept_neighbors`) over
162
+ * the same `@okfit/engine` `ConceptQuery`. No handler: bare `okfit query`
163
+ * prints help and exits 0, the root command's own K-5 behaviour.
164
+ *
165
+ * @public
166
+ */
167
+ const queryCommand = Command.make("query", {}).pipe(Command.withDescription("Read-only queries over the bundle: list, get, neighbors."), Command.withSubcommands([
168
+ queryListCommand,
169
+ queryGetCommand,
170
+ queryNeighborsCommand
171
+ ]));
172
+
173
+ //#endregion
174
+ export { queryCommand, queryGetCommand, queryListCommand, queryNeighborsCommand };
package/commands/root.js CHANGED
@@ -2,6 +2,7 @@ import { contextCommand } from "./context.js";
2
2
  import { graphCommand } from "./graph.js";
3
3
  import { initCommand } from "./init.js";
4
4
  import { lintCommand } from "./lint.js";
5
+ import { queryCommand } from "./query.js";
5
6
  import { staleCommand } from "./stale.js";
6
7
  import { syncCommand } from "./sync.js";
7
8
  import { validateCommand } from "./validate.js";
@@ -17,8 +18,9 @@ import { Command } from "effect/cli";
17
18
  * `okfit` therefore prints the root help and exits `0` with no code in this
18
19
  * package at all.
19
20
  *
20
- * `validate`, `init`, `context`, `verify`, `sync`, `lint`, `graph`, and
21
- * `stale` are the whole command tree; nothing else is registered here.
21
+ * `validate`, `init`, `context`, `verify`, `sync`, `lint`, `graph`,
22
+ * `stale`, and `query` (with its `list`/`get`/`neighbors`) are the whole
23
+ * command tree; nothing else is registered here.
22
24
  * Each is appended in introduction order, never reordered in, so
23
25
  * `--help`'s subcommand list reads that way too (contract §4.2). The
24
26
  * top-level description is left unchanged: neither `context` nor `verify`
@@ -35,7 +37,8 @@ const rootCommand = Command.make("okfit", {}).pipe(Command.withDescription("Open
35
37
  syncCommand,
36
38
  lintCommand,
37
39
  graphCommand,
38
- staleCommand
40
+ staleCommand,
41
+ queryCommand
39
42
  ]));
40
43
 
41
44
  //#endregion
@@ -20,6 +20,10 @@ const idArg = Argument.String("id").pipe(Argument.optional, Argument.withDescrip
20
20
  const allFlag = Flag.Boolean("all").pipe(Flag.withDefault(false), Flag.withDescription("verify every concept whose type sets require_verified and that you have not verified yet; drafts are skipped"));
21
21
  /** Issue #138: narrow (or, without --all, define) the batch to these types. */
22
22
  const typeFlag = Flag.String("type").pipe(Flag.atLeast(0), Flag.withDescription("verify every unverified concept of this type (repeatable); implies --all's selection rule for the named types"));
23
+ /** Issue #185: settle the concept in the same write as the attestation. */
24
+ const stableFlag = Flag.Boolean("stable").pipe(Flag.withDefault(false), Flag.withDescription("also set status: stable in the same write; needs a concept id"));
25
+ /** Issue #185: record a review without settling. */
26
+ const draftFlag = Flag.Boolean("draft").pipe(Flag.withDefault(false), Flag.withDescription("also set (or keep) status: draft in the same write; needs a concept id"));
23
27
  /** K-2: `[path]` is the PROJECT root, byte-identical to validate/init/context's. */
24
28
  const pathArg = Argument.Path("path", { pathType: "directory" }).pipe(Argument.optional, Argument.withDescription("project root to start config discovery from (default: current directory); never the bundle root"));
25
29
  /** K-1: no `mustExist`; the handler stats it via `provideConfig`. */
@@ -30,8 +34,8 @@ const atFlag = Flag.String("at").pipe(Flag.optional, Flag.withDescription("ISO 8
30
34
  const dryRunFlag = Flag.Boolean("dry-run").pipe(Flag.withDefault(false), Flag.withDescription("print the exact fragment a real run would splice in; write nothing"));
31
35
  const formatFlag = Flag.Literals("format", ["human", "json"]).pipe(Flag.withDefault("human"), Flag.withDescription("output format: human (default) or json"));
32
36
  /**
33
- * `okfit verify [<id>] [path] [--all] [--type <Type>]... [--config <file>]
34
- * [--at <iso>] [--dry-run] [--format human|json]`.
37
+ * `okfit verify [<id>] [path] [--all] [--type <Type>]... [--stable|--draft]
38
+ * [--config <file>] [--at <iso>] [--dry-run] [--format human|json]`.
35
39
  *
36
40
  * Handler order fixed by contract §2.3. Steps 1–3 are `context`'s handler
37
41
  * in substance — stat `--config` (K-1) via `provideConfig`, discover,
@@ -51,6 +55,8 @@ const verifyCommand = Command.make("verify", {
51
55
  config: configFlag,
52
56
  all: allFlag,
53
57
  type: typeFlag,
58
+ stable: stableFlag,
59
+ draft: draftFlag,
54
60
  at: atFlag,
55
61
  dryRun: dryRunFlag,
56
62
  format: formatFlag
@@ -70,6 +76,9 @@ const verifyCommand = Command.make("verify", {
70
76
  const at = Option.isNone(input.at) ? DateTime.startOf(yield* Now, "second") : yield* Schema.decodeUnknownEffect(Timestamp)(input.at.value);
71
77
  if (Option.isSome(input.id) && Option.isSome(input.path) && batch) return yield* new VerifySelectionError({ reason: "id-and-batch" });
72
78
  if (Option.isNone(input.id) && !batch) return yield* new VerifySelectionError({ reason: "no-selection" });
79
+ if (input.stable && input.draft) return yield* new VerifySelectionError({ reason: "status-conflict" });
80
+ const status = input.stable ? "stable" : input.draft ? "draft" : void 0;
81
+ if (status !== void 0 && batch) return yield* new VerifySelectionError({ reason: "status-and-batch" });
73
82
  if (batch) {
74
83
  const result = yield* runVerifyBatch({
75
84
  bundleRoot: resolved.bundleRoot,
@@ -111,7 +120,8 @@ const verifyCommand = Command.make("verify", {
111
120
  projectRoot: resolved.projectRoot,
112
121
  config: resolved.config,
113
122
  at,
114
- dryRun: input.dryRun
123
+ dryRun: input.dryRun,
124
+ ...status === void 0 ? {} : { status }
115
125
  });
116
126
  const displayPath = `${displayRoot(cwd, result.bundleRoot, path)}/${result.conceptPath}`;
117
127
  if (input.format === "json") {
@@ -122,6 +132,7 @@ const verifyCommand = Command.make("verify", {
122
132
  by: result.by,
123
133
  at: result.at,
124
134
  dryRun: result.dryRun,
135
+ status: result.status,
125
136
  ...Option.isSome(distribution) ? { distribution: distribution.value } : {}
126
137
  });
127
138
  yield* Console.log(JSON.stringify(Schema.encodeSync(VerifyEnvelope)(envelope)));
@@ -131,7 +142,9 @@ const verifyCommand = Command.make("verify", {
131
142
  at: result.at,
132
143
  priorAt: result.priorAt,
133
144
  dryRun: result.dryRun,
134
- fragment: result.fragment
145
+ fragment: result.fragment,
146
+ status: result.status,
147
+ statusFragment: result.statusFragment
135
148
  })) yield* Console.log(line);
136
149
  setExitCode(0);
137
150
  }).pipe(Effect.provide(Git.layer), provideConfig({
@@ -140,7 +153,7 @@ const verifyCommand = Command.make("verify", {
140
153
  }));
141
154
  if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION, Option.getOrUndefined(distribution))))));
142
155
  return yield* body;
143
- })).pipe(Command.withDescription("Record a human's attestation that a concept has been reviewed: append one verified entry, { by: human:<id>, at: <now> }, to its frontmatter. The actor is always your own git identity; there is no --by. Give a concept id, or --all/--type <Type> (repeatable) to attest a batch of unverified concepts at once; exactly one of the two selections is required. This is a human-run command: no agent, hook, or MCP tool ever invokes it."));
156
+ })).pipe(Command.withDescription("Record a human's attestation that a concept has been reviewed: append one verified entry, { by: human:<id>, at: <now> }, to its frontmatter. The actor is always your own git identity; there is no --by. Give a concept id, or --all/--type <Type> (repeatable) to attest a batch of unverified concepts at once; exactly one of the two selections is required. With a concept id, --stable or --draft also sets its status in the same write. This is a human-run command: no agent, hook, or MCP tool ever invokes it."));
144
157
 
145
158
  //#endregion
146
159
  export { verifyCommand };
package/errors.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { DocumentStdinIsTerminalError } from "./internal/stdin.js";
2
- import { ConfigMalformedError, ConfigPathNotFoundError, DocumentPathError, InitOverwriteError, SyncStagedLogError, VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError } from "@okfit/engine";
2
+ import { ConfigMalformedError, ConfigPathNotFoundError, DocumentPathError, InitOverwriteError, QueryConceptNotFoundError, QuerySelectionError, QueryUnknownVocabularyError, SyncStagedLogError, VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError } from "@okfit/engine";
3
3
  import { ConfigIssueRenderer } from "@effected/cli";
4
4
 
5
5
  //#region src/errors.ts
@@ -50,6 +50,9 @@ const relativeToCwd = (path, cwd) => {
50
50
  * contradictory, empty, or named an undeclared type.
51
51
  * 5c. `DocumentPathError` and `DocumentStdinIsTerminalError` (`--document`)
52
52
  * each render as one `error: <message>` line.
53
+ * 5d. `QueryUnknownVocabularyError`, `QueryConceptNotFoundError` and
54
+ * `QuerySelectionError` (`okfit query`) each render as one
55
+ * `error: <message>` line.
53
56
  * 6. Everything else — core's `BundleRootNotFoundError`/`BundleReadError`/
54
57
  * `BundleDepthExceededError`, config-file's other errors, `XdgEnvError`
55
58
  * (the K-13 `HOME`-unset case) — renders as the single line
@@ -72,6 +75,9 @@ const renderFailure = (error) => {
72
75
  if (error instanceof VerifyUnsupportedFrontmatterError) return [`error: ${error.message}`];
73
76
  if (error instanceof SyncStagedLogError) return [`error: ${error.message}`];
74
77
  if (error instanceof VerifySelectionError) return [`error: ${error.message}`];
78
+ if (error instanceof QueryUnknownVocabularyError) return [`error: ${error.message}`];
79
+ if (error instanceof QueryConceptNotFoundError) return [`error: ${error.message}`];
80
+ if (error instanceof QuerySelectionError) return [`error: ${error.message}`];
75
81
  if (error instanceof DocumentPathError) return [`error: ${error.message}`];
76
82
  if (error instanceof DocumentStdinIsTerminalError) return [`error: ${error.message}`];
77
83
  if (hasTag(error, "ConfigValidationError")) {
package/index.d.ts CHANGED
@@ -10,8 +10,9 @@ import { ContextEnvelope, RenderedDiagnostic } from "@okfit/engine";
10
10
  * `okfit` therefore prints the root help and exits `0` with no code in this
11
11
  * package at all.
12
12
  *
13
- * `validate`, `init`, `context`, `verify`, `sync`, `lint`, `graph`, and
14
- * `stale` are the whole command tree; nothing else is registered here.
13
+ * `validate`, `init`, `context`, `verify`, `sync`, `lint`, `graph`,
14
+ * `stale`, and `query` (with its `list`/`get`/`neighbors`) are the whole
15
+ * command tree; nothing else is registered here.
15
16
  * Each is appended in introduction order, never reordered in, so
16
17
  * `--help`'s subcommand list reads that way too (contract §4.2). The
17
18
  * top-level description is left unchanged: neither `context` nor `verify`
@@ -20,7 +21,7 @@ import { ContextEnvelope, RenderedDiagnostic } from "@okfit/engine";
20
21
  *
21
22
  * @public
22
23
  */
23
- export declare const rootCommand: Command.Command<"okfit", {} | {}, {}, import("@okfit/profiles").AgentActorUnconfiguredError | import("@okfit/core").BundleDepthExceededError | import("@okfit/core").BundleReadError | import("@okfit/core").BundleRootNotFoundError | import("@effected/config-file").ConfigCodecError | import("@effected/config-file").ConfigFileReadError | import("@effected/config-file").ConfigFileWriteError | import("@okfit/engine").ConfigMalformedError | import("@okfit/engine").ConfigPathNotFoundError | import("@effected/config-file").ConfigValidationError | import("@okfit/engine").DocumentPathError | DocumentStdinIsTerminalError | import("@effected/markdown").FrontmatterEncodeError | import("@effected/markdown").FrontmatterFormatMismatchError | import("@effected/markdown").FrontmatterValidationError | import("@effected/git").GitCommandError | import("@okfit/profiles").GitHistoryError | import("@okfit/profiles").HumanActorUnresolvedError | import("@okfit/engine").InitOverwriteError | import("@effected/markdown").MarkdownParseError | import("@effected/git").NotARepositoryError | import("effect/PlatformError").PlatformError | import("effect/Schema").SchemaError | import("@okfit/engine").SyncStagedLogError | import("@effected/git").UnknownRefError | import("@okfit/engine").VerifyConceptNotFoundError | import("@okfit/engine").VerifySelectionError | import("@okfit/engine").VerifyUnsupportedFrontmatterError | import("@effected/yaml").YamlParseError, import("@effected/xdg").AppDirs | import("effect/process/ChildProcessSpawner").ChildProcessSpawner | import("effect/Crypto").Crypto | import("effect/FileSystem").FileSystem | import("@okfit/engine").Now | import("effect/Path").Path | import("effect/Stdio").Stdio | import("@effected/xdg").Xdg>;
24
+ export declare const rootCommand: Command.Command<"okfit", {} | {}, {}, import("@okfit/profiles").AgentActorUnconfiguredError | import("@okfit/core").BundleDepthExceededError | import("@okfit/core").BundleReadError | import("@okfit/core").BundleRootNotFoundError | import("@effected/config-file").ConfigCodecError | import("@effected/config-file").ConfigFileReadError | import("@effected/config-file").ConfigFileWriteError | import("@okfit/engine").ConfigMalformedError | import("@okfit/engine").ConfigPathNotFoundError | import("@effected/config-file").ConfigValidationError | import("@okfit/engine").DocumentPathError | DocumentStdinIsTerminalError | import("@effected/markdown").FrontmatterEncodeError | import("@effected/markdown").FrontmatterFormatMismatchError | import("@effected/markdown").FrontmatterValidationError | import("@effected/git").GitCommandError | import("@okfit/profiles").GitHistoryError | import("@okfit/profiles").HumanActorUnresolvedError | import("@okfit/engine").InitOverwriteError | import("@effected/markdown").MarkdownParseError | import("@effected/git").NotARepositoryError | import("effect/PlatformError").PlatformError | import("@okfit/engine").QueryConceptNotFoundError | import("@okfit/engine").QuerySelectionError | import("@okfit/engine").QueryUnknownVocabularyError | import("effect/Schema").SchemaError | import("@okfit/engine").SyncStagedLogError | import("@effected/git").UnknownRefError | import("@okfit/engine").VerifyConceptNotFoundError | import("@okfit/engine").VerifySelectionError | import("@okfit/engine").VerifyUnsupportedFrontmatterError | import("@effected/yaml").YamlParseError, import("@effected/xdg").AppDirs | import("effect/process/ChildProcessSpawner").ChildProcessSpawner | import("effect/Crypto").Crypto | import("effect/FileSystem").FileSystem | import("@okfit/engine").Now | import("effect/Path").Path | import("effect/Stdio").Stdio | import("@effected/xdg").Xdg>;
24
25
  //#endregion
25
26
  //#region src/errors.d.ts
26
27
  /**
@@ -63,6 +64,9 @@ export declare const rootCommand: Command.Command<"okfit", {} | {}, {}, import("
63
64
  * contradictory, empty, or named an undeclared type.
64
65
  * 5c. `DocumentPathError` and `DocumentStdinIsTerminalError` (`--document`)
65
66
  * each render as one `error: <message>` line.
67
+ * 5d. `QueryUnknownVocabularyError`, `QueryConceptNotFoundError` and
68
+ * `QuerySelectionError` (`okfit query`) each render as one
69
+ * `error: <message>` line.
66
70
  * 6. Everything else — core's `BundleRootNotFoundError`/`BundleReadError`/
67
71
  * `BundleDepthExceededError`, config-file's other errors, `XdgEnvError`
68
72
  * (the K-13 `HOME`-unset case) — renders as the single line
@@ -156,6 +160,13 @@ interface VerifyLines {
156
160
  readonly dryRun: boolean;
157
161
  /** The exact bytes a real run would splice in; printed only when `dryRun`. */
158
162
  readonly fragment: string;
163
+ /** Issue #185: the status change requested, or `null` when no status flag was given. */
164
+ readonly status: {
165
+ readonly from: string | null;
166
+ readonly to: string;
167
+ } | null;
168
+ /** The exact status bytes a real run would splice in; printed only when `dryRun`. */
169
+ readonly statusFragment: string | null;
159
170
  }
160
171
  /**
161
172
  * V-11's human output: one line per fact. One `already verified` line per
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okfit/cli",
3
- "version": "0.6.11",
3
+ "version": "0.7.0",
4
4
  "private": false,
5
5
  "description": "The okfit command line: validate, lint, index, and inspect Open Knowledge Format (OKF) bundles.",
6
6
  "keywords": [
@@ -52,10 +52,10 @@
52
52
  "@effected/markdown": "^0.14.0",
53
53
  "@effected/schemastore": "^0.17.0",
54
54
  "@effected/toml": "^0.10.0",
55
- "@effected/walker": "^0.14.0",
55
+ "@effected/walker": "^0.14.1",
56
56
  "@effected/yaml": "^0.18.0",
57
57
  "@okfit/core": "0.8.3",
58
- "@okfit/engine": "0.9.3",
58
+ "@okfit/engine": "0.10.0",
59
59
  "@okfit/profiles": "0.8.2",
60
60
  "effect": "4.0.0-rc.118"
61
61
  },
@@ -0,0 +1,58 @@
1
+ import { Schema } from "effect";
2
+ import { Timestamp } from "@okfit/core";
3
+
4
+ //#region src/render/query.ts
5
+ /**
6
+ * One line per concept: `<id> <status> <type> <title>`. `items` arrive
7
+ * already sorted by id (`ConceptQuery.list`), so this never re-sorts.
8
+ *
9
+ * @public
10
+ */
11
+ const humanQueryList = (items) => items.map((item) => `${item.id} ${item.status} ${item.type} ${item.title}`);
12
+ /**
13
+ * `<N> concepts in <root>` (`1 concept` for one) — the one-line stderr summary.
14
+ *
15
+ * @public
16
+ */
17
+ const queryListSummary = (total, root) => `${total} ${total === 1 ? "concept" : "concepts"} in ${root}`;
18
+ /**
19
+ * The bare id, then `key: value` lines, one `verified:` line per attestation,
20
+ * then the outgoing links.
21
+ *
22
+ * @public
23
+ */
24
+ const humanQueryGet = (input) => {
25
+ const { summary } = input;
26
+ return [
27
+ summary.id,
28
+ `type: ${summary.type}`,
29
+ `title: ${summary.title}`,
30
+ `status: ${summary.status}`,
31
+ `tags: ${summary.tags.length === 0 ? "(none)" : summary.tags.join(", ")}`,
32
+ `path: ${summary.path}`,
33
+ ...input.verified.map((entry) => `verified: ${entry.by} at ${Schema.encodeSync(Timestamp)(entry.at)}`),
34
+ "links:",
35
+ ...input.links.length === 0 ? [" (none)"] : input.links.map((link) => {
36
+ const detail = [
37
+ link.kind,
38
+ link.source,
39
+ ...link.field === void 0 ? [] : [link.field]
40
+ ].join(", ");
41
+ return ` -> ${link.to} (${detail})`;
42
+ })
43
+ ];
44
+ };
45
+ /**
46
+ * `outgoing:` then `incoming:`, each a list of `->`/`<-` lines or `(none)`.
47
+ *
48
+ * @public
49
+ */
50
+ const humanQueryNeighbors = (input) => [
51
+ "outgoing:",
52
+ ...input.outgoing.length === 0 ? [" (none)"] : input.outgoing.map((n) => ` -> ${n.id} (${n.kind})`),
53
+ "incoming:",
54
+ ...input.incoming.length === 0 ? [" (none)"] : input.incoming.map((n) => ` <- ${n.id} (${n.kind})`)
55
+ ];
56
+
57
+ //#endregion
58
+ export { humanQueryGet, humanQueryList, humanQueryNeighbors, queryListSummary };
package/render/verify.js CHANGED
@@ -19,11 +19,15 @@ const indentFragment = (fragment) => {
19
19
  *
20
20
  * @public
21
21
  */
22
- const humanVerify = (input) => [
23
- ...input.priorAt.map((at) => `already verified by ${input.by} at ${at}; appending`),
24
- input.dryRun ? `would verify ${input.id} by ${input.by} at ${input.at} (dry run, nothing written)` : `verified ${input.id} by ${input.by} at ${input.at}`,
25
- ...input.dryRun ? ["would write:", ...indentFragment(input.fragment)] : []
26
- ];
22
+ const humanVerify = (input) => {
23
+ const statusSuffix = input.status === null ? "" : input.status.from === input.status.to ? `; status already ${input.status.to}` : `; status ${input.status.from ?? "(absent)"} -> ${input.status.to}`;
24
+ return [
25
+ ...input.priorAt.map((at) => `already verified by ${input.by} at ${at}; appending`),
26
+ input.dryRun ? `would verify ${input.id} by ${input.by} at ${input.at}${statusSuffix} (dry run, nothing written)` : `verified ${input.id} by ${input.by} at ${input.at}${statusSuffix}`,
27
+ ...input.dryRun ? ["would write:", ...indentFragment(input.fragment)] : [],
28
+ ...input.dryRun && input.statusFragment !== null ? ["would set status:", ...indentFragment(input.statusFragment)] : []
29
+ ];
30
+ };
27
31
  /**
28
32
  * V-11-at-batch-scale (#138): one line per skipped candidate, then one line
29
33
  * (or, under `--dry-run`, three) per verified concept, then a trailing tally.
@@ -31,7 +35,7 @@ const humanVerify = (input) => [
31
35
  * @public
32
36
  */
33
37
  const humanVerifyBatch = (input) => [
34
- ...input.skipped.map((entry) => entry.reason === "draft" ? `skipped ${entry.id}: draft` : `skipped ${entry.id}: already verified by ${input.by}`),
38
+ ...input.skipped.map((entry) => entry.reason === "already-verified" ? `skipped ${entry.id}: already verified by ${input.by}` : `skipped ${entry.id}: ${entry.reason}`),
35
39
  ...input.verified.flatMap((entry) => input.dryRun ? [
36
40
  `would verify ${entry.id} by ${input.by} at ${input.at}`,
37
41
  "would write:",
package/version.js CHANGED
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * @public
13
13
  */
14
- const CLI_VERSION = "0.6.11";
14
+ const CLI_VERSION = "0.7.0";
15
15
 
16
16
  //#endregion
17
17
  export { CLI_VERSION };