@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 +52 -2
- package/commands/query.js +174 -0
- package/commands/root.js +6 -3
- package/commands/verify.js +18 -5
- package/errors.js +7 -1
- package/index.d.ts +14 -3
- package/package.json +3 -3
- package/render/query.js +58 -0
- package/render/verify.js +10 -6
- package/version.js +1 -1
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`,
|
|
21
|
-
* `stale
|
|
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
|
package/commands/verify.js
CHANGED
|
@@ -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>]... [--
|
|
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`,
|
|
14
|
-
* `stale
|
|
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.
|
|
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.
|
|
55
|
+
"@effected/walker": "^0.14.1",
|
|
56
56
|
"@effected/yaml": "^0.18.0",
|
|
57
57
|
"@okfit/core": "0.8.3",
|
|
58
|
-
"@okfit/engine": "0.
|
|
58
|
+
"@okfit/engine": "0.10.0",
|
|
59
59
|
"@okfit/profiles": "0.8.2",
|
|
60
60
|
"effect": "4.0.0-rc.118"
|
|
61
61
|
},
|
package/render/query.js
ADDED
|
@@ -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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 === "
|
|
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:",
|