@okfit/cli 0.5.3 → 0.6.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
@@ -14,6 +14,13 @@ okfit context [path] [--config <file>] [--format human|json] [--help]
14
14
  okfit verify <id> [path] [--config <file>] [--at <iso>] [--dry-run] [--format human|json] [--help]
15
15
  ```
16
16
 
17
+ `okfit --version` prints `okfit <cli> (engine <engine>, okf <okf>,
18
+ config-schema <schema>)` — for example `okfit 0.5.4 (engine 0.6.0, okf 0.2,
19
+ config-schema 1.0)`, or `okfit 0.5.4 via @okfit/plugin 0.3.7 (engine 0.6.0,
20
+ okf 0.2, config-schema 1.0)` when installed through the meta-package. The
21
+ engine, OKF, and config-schema versions decide what a report says and what
22
+ a config may contain; the CLI version is packaging.
23
+
17
24
  `[path]` is the **project root** on every subcommand — the directory discovery
18
25
  starts from, and, for `init`, where `.config/okfit.toml` is written.
19
26
  It is never the bundle root; the bundle root is `<project root>/<bundle.path>`
@@ -277,8 +284,10 @@ nothing else (no summary line, no warnings — those still go to stderr):
277
284
  ```json
278
285
  {
279
286
  "schema": 1,
280
- "okfit_version": "0.1.0",
287
+ "okfit_version": "0.5.4",
288
+ "engine_version": "0.6.0",
281
289
  "producer": "okfit",
290
+ "distribution": null,
282
291
  "okf_version": "0.2",
283
292
  "root": "/abs/path/to/my-repo/okf",
284
293
  "profile": "software-project",
@@ -311,15 +320,21 @@ nothing else (no summary line, no warnings — those still go to stderr):
311
320
  }
312
321
  ```
313
322
 
314
- `profile` is `null` when the config sets `bundle.profile = "none"` or names a
315
- profile `okfit` does not recognise. `range` is zero-based, exactly as
323
+ `engine_version` and `okf_version` are the pair to compare across reports:
324
+ the MCP server's `validate_bundle` tool returns this same envelope with the
325
+ same two values over one bundle, and differs only in `okfit_version` (its own
326
+ version) and `producer` (`@okfit/mcp`). `distribution` names the
327
+ meta-package the bin was installed through (`{ "name": "@okfit/plugin",
328
+ "version": "0.3.7" }`) and is `null` for a direct install. `profile` is
329
+ `null` when the config sets `bundle.profile = "none"` or names a profile
330
+ `okfit` does not recognise. `range` is zero-based, exactly as
316
331
  `@okfit/core` computed it, and omitted for a range-less diagnostic.
317
332
 
318
333
  An infrastructure failure under `--format json` prints a different, smaller
319
334
  envelope to stdout and exits `3`:
320
335
 
321
336
  ```json
322
- { "schema": 1, "okfit_version": "0.1.0", "exit_code": 3, "error": { "tag": "ConfigPathNotFoundError", "message": "config path not found: /abs/ci-config.toml" } }
337
+ { "schema": 1, "okfit_version": "0.5.4", "engine_version": "0.6.0", "distribution": null, "exit_code": 3, "error": { "tag": "ConfigPathNotFoundError", "message": "config path not found: /abs/ci-config.toml" } }
323
338
  ```
324
339
 
325
340
  `init` has no `--format`; it is human output only.
@@ -330,6 +345,7 @@ one above:
330
345
  ```json
331
346
  {
332
347
  "schema": 1,
348
+ "config_schema_version": "1.0",
333
349
  "project_root": "/abs/path/to/my-repo",
334
350
  "bundle_root": "/abs/path/to/my-repo/okf",
335
351
  "config_path": null,
@@ -1,3 +1,4 @@
1
+ import { Distribution } from "../internal/distribution.js";
1
2
  import { setExitCode } from "../internal/exit.js";
2
3
  import { humanContext } from "../render/context.js";
3
4
  import { CLI_VERSION } from "../version.js";
@@ -35,6 +36,7 @@ const contextCommand = Command.make("context", {
35
36
  }, (input) => Effect.gen(function* () {
36
37
  const cwd = process.cwd();
37
38
  const discoveryCwd = Option.getOrElse(input.path, () => cwd);
39
+ const distribution = yield* Distribution;
38
40
  const body = Effect.gen(function* () {
39
41
  const { projectRoot, bundleRoot, config: merged, profile, profileName, discovered } = yield* resolveProjectConfig({
40
42
  pathArg: input.path,
@@ -67,7 +69,7 @@ const contextCommand = Command.make("context", {
67
69
  explicitConfigPath: input.config,
68
70
  discoveryCwd
69
71
  }));
70
- if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION)))));
72
+ if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION, Option.getOrUndefined(distribution))))));
71
73
  return yield* body;
72
74
  })).pipe(Command.withDescription("Print the resolved project root, bundle root, config path, profile, and type/tag vocabulary without loading the bundle."));
73
75
 
package/commands/graph.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { Distribution } from "../internal/distribution.js";
1
2
  import { setExitCode } from "../internal/exit.js";
2
3
  import { CLI_VERSION } from "../version.js";
3
4
  import { Argument, Command, Flag } from "effect/unstable/cli";
@@ -39,6 +40,7 @@ const graphCommand = Command.make("graph", {
39
40
  }, (input) => Effect.gen(function* () {
40
41
  const cwd = process.cwd();
41
42
  const discoveryCwd = Option.getOrElse(input.path, () => cwd);
43
+ const distribution = yield* Distribution;
42
44
  const body = Effect.gen(function* () {
43
45
  const { bundleRoot, config: merged, profile } = yield* resolveProjectConfig({
44
46
  pathArg: input.path,
@@ -57,7 +59,8 @@ const graphCommand = Command.make("graph", {
57
59
  onSome: (p) => p.name
58
60
  }),
59
61
  nodes: result.graph.nodes,
60
- edges: result.graph.edges
62
+ edges: result.graph.edges,
63
+ ...Option.isSome(distribution) ? { distribution: distribution.value } : {}
61
64
  });
62
65
  yield* Console.log(JSON.stringify(Schema.encodeSync(GraphEnvelope)(envelope)));
63
66
  } else if (input.format === "dot") yield* Console.log(result.graph.toGraphViz());
@@ -67,7 +70,7 @@ const graphCommand = Command.make("graph", {
67
70
  explicitConfigPath: input.config,
68
71
  discoveryCwd
69
72
  }));
70
- if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION)))));
73
+ if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION, Option.getOrUndefined(distribution))))));
71
74
  return yield* body;
72
75
  })).pipe(Command.withDescription("Render the bundle's link graph: frontmatter path fields and body links, as Mermaid, DOT, or JSON."));
73
76
 
package/commands/init.js CHANGED
@@ -2,9 +2,9 @@ import { setExitCode } from "../internal/exit.js";
2
2
  import { useColor } from "../internal/tty.js";
3
3
  import { displayRoot, human, summary } from "../render/human.js";
4
4
  import { Argument, Command, Flag } from "effect/unstable/cli";
5
- import { CONFIG_RELATIVE_PATH, InitOverwriteError, Now, SCHEMA_DIRECTIVE, collect, configValue, files, forDiagnostics, provideConfig, resolveBundleRoot, resolveProjectRoot, run, targetPaths } from "@okfit/engine";
5
+ import { CONFIG_RELATIVE_PATH, InitOverwriteError, Now, collect, configValue, files, forDiagnostics, provideConfig, resolveBundleRoot, resolveProjectRoot, run, targetPaths } from "@okfit/engine";
6
6
  import { Console, DateTime, Effect, FileSystem, Layer, Option, Path } from "effect";
7
- import { OKF_SPEC_VERSION, OkfitConfig, OkfitConfigFile } from "@okfit/core";
7
+ import { OKF_SPEC_VERSION, OkfitConfig, OkfitConfigFile, SCHEMA_DIRECTIVE } from "@okfit/core";
8
8
  import { Git } from "@effected/git";
9
9
  import { GitHistory, Profiles } from "@okfit/profiles";
10
10
 
package/commands/lint.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { Distribution } from "../internal/distribution.js";
1
2
  import { setExitCode } from "../internal/exit.js";
2
3
  import { CLI_VERSION } from "../version.js";
3
4
  import { useColor } from "../internal/tty.js";
@@ -42,6 +43,7 @@ const lintCommand = Command.make("lint", {
42
43
  const discoveryCwd = Option.getOrElse(input.path, () => cwd);
43
44
  const now = yield* Now;
44
45
  const path = yield* Path.Path;
46
+ const distribution = yield* Distribution;
45
47
  const body = Effect.gen(function* () {
46
48
  const { bundleRoot, config: merged, profile } = yield* resolveProjectConfig({
47
49
  pathArg: input.path,
@@ -69,7 +71,8 @@ const lintCommand = Command.make("lint", {
69
71
  }),
70
72
  exitCode: code,
71
73
  concepts: result.bundle.concepts.size,
72
- diagnostics
74
+ diagnostics,
75
+ ...Option.isSome(distribution) ? { distribution: distribution.value } : {}
73
76
  });
74
77
  yield* Console.log(JSON.stringify(Schema.encodeSync(JsonEnvelope)(envelope)));
75
78
  } else {
@@ -87,7 +90,7 @@ const lintCommand = Command.make("lint", {
87
90
  explicitConfigPath: input.config,
88
91
  discoveryCwd
89
92
  }));
90
- if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION)))));
93
+ if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION, Option.getOrUndefined(distribution))))));
91
94
  return yield* body;
92
95
  })).pipe(Command.withDescription("Load config and bundle, run lint checks (and the profile check), and render the diagnostics; never conformance."));
93
96
 
package/commands/stale.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { Distribution } from "../internal/distribution.js";
1
2
  import { setExitCode } from "../internal/exit.js";
2
3
  import { CLI_VERSION } from "../version.js";
3
4
  import { displayRoot } from "../render/human.js";
@@ -35,6 +36,7 @@ const staleCommand = Command.make("stale", {
35
36
  const discoveryCwd = Option.getOrElse(input.path, () => cwd);
36
37
  const now = yield* Now;
37
38
  const path = yield* Path.Path;
39
+ const distribution = yield* Distribution;
38
40
  const body = Effect.gen(function* () {
39
41
  const { bundleRoot, config: merged, profile } = yield* resolveProjectConfig({
40
42
  pathArg: input.path,
@@ -56,7 +58,8 @@ const staleCommand = Command.make("stale", {
56
58
  }),
57
59
  now,
58
60
  concepts: result.bundle.concepts.size,
59
- items: result.items
61
+ items: result.items,
62
+ ...Option.isSome(distribution) ? { distribution: distribution.value } : {}
60
63
  });
61
64
  if (input.format === "json") yield* Console.log(JSON.stringify(Schema.encodeSync(StaleEnvelope)(envelope)));
62
65
  else {
@@ -68,7 +71,7 @@ const staleCommand = Command.make("stale", {
68
71
  explicitConfigPath: input.config,
69
72
  discoveryCwd
70
73
  }));
71
- if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION)))));
74
+ if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION, Option.getOrUndefined(distribution))))));
72
75
  return yield* body;
73
76
  })).pipe(Command.withDescription("List every concept whose stale_after instant has passed as of now, with how many whole days past it."));
74
77
 
package/commands/sync.js CHANGED
@@ -1,10 +1,11 @@
1
+ import { Distribution } from "../internal/distribution.js";
1
2
  import { setExitCode } from "../internal/exit.js";
2
3
  import { CLI_VERSION } from "../version.js";
3
4
  import { displayRoot } from "../render/human.js";
4
5
  import { humanSync } from "../render/sync.js";
5
6
  import { Argument, Command, Flag } from "effect/unstable/cli";
6
- import { SyncEnvelope, jsonError, provideConfig, resolveProjectConfig, runSync, syncEnvelope } from "@okfit/engine";
7
- import { Console, Effect, Layer, Option, Path, Schema } from "effect";
7
+ import { Now, SyncEnvelope, jsonError, provideConfig, resolveProjectConfig, runSync, syncEnvelope } from "@okfit/engine";
8
+ import { Console, DateTime, Effect, Layer, Option, Path, Schema } from "effect";
8
9
  import { Git } from "@effected/git";
9
10
  import { GitHistory } from "@okfit/profiles";
10
11
 
@@ -38,23 +39,30 @@ const onlyFlag = Flag.Literals("only", [
38
39
  ]).pipe(Flag.atLeast(0), Flag.withDescription("restrict the run to these modes (repeatable: --only generated --only index); default: all three"));
39
40
  const dryRunFlag = Flag.Boolean("dry-run").pipe(Flag.withDefault(false), Flag.withDescription("compute every result and write nothing"));
40
41
  const formatFlag = Flag.Literals("format", ["human", "json"]).pipe(Flag.withDefault("human"), Flag.withDescription("output format: human (default) or json"));
42
+ /** #18: log mode's inclusive floor; a malformed value fails at parse time (`ShowHelp` -> exit `64`). */
43
+ const ISO_DATE = Schema.String.pipe(Schema.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}$/, { message: "Expected a YYYY-MM-DD date" })));
44
+ const sinceFlag = Flag.String("since").pipe(Flag.withSchema(ISO_DATE), Flag.optional, Flag.withDescription("log mode's inclusive floor (YYYY-MM-DD): consider every committed concept dated on or after it; default: the newest date already in log.md"));
45
+ const stagedFlag = Flag.Boolean("staged").pipe(Flag.withDefault(false), Flag.withDescription("pre-commit mode: stamp only the concepts in the git index, with now as generated.at, and re-add what was written; log mode is excluded (default modes: generated, index)"));
41
46
  /**
42
47
  * `okfit sync [path] [--config <file>] [--only <mode>]... [--dry-run]
43
- * [--format human|json]`.
48
+ * [--format human|json] [--since <YYYY-MM-DD>] [--staged]`.
44
49
  *
45
50
  * Handler order fixed by contract §4.3. Steps 1–3 are `context`'s/
46
51
  * `validate`'s handler in substance — stat `--config` (K-1) via
47
52
  * `provideConfig`, resolve the project and bundle roots through
48
53
  * `resolveProjectConfig` — then it diverges: build the `--only` mode
49
- * set (default: all three, order irrelevant — `runSync`'s own fixed
50
- * order wins, not `--only`'s occurrence order), run `runSync` with BOTH
51
- * `Git.layer` and `GitHistory.layer` provided (S-16, mirroring
52
- * `verify.ts:134`'s `Git.layer`-alone provision one layer up: here two
53
- * layers are needed because `GitHistory.layer` does not re-expose `Git`
54
- * even though it is built on it), render, and always exit `0`. There is
55
- * no content tier: every typed failure is exit `3` through `bin.ts`'s
56
- * existing `reportFailures`; an unknown `--only` token never reaches
57
- * this handler at all — it fails at parse time, exit `64`.
54
+ * set (default: all three modes, or `generated`+`index` under `--staged`;
55
+ * order irrelevant either way — `runSync`'s own fixed order wins, not
56
+ * `--only`'s occurrence order), run `runSync` with BOTH `Git.layer` and
57
+ * `GitHistory.layer` provided (S-16, mirroring `verify.ts:134`'s
58
+ * `Git.layer`-alone provision one layer up: here two layers are needed
59
+ * because `GitHistory.layer` does not re-expose `Git` even though it is
60
+ * built on it), render, and always exit `0`. There is no content tier:
61
+ * every typed failure is exit `3` through `bin.ts`'s existing
62
+ * `reportFailures`, EXCEPT `SyncStagedLogError` (`--staged` combined with
63
+ * `--only log`), which carries its own `[Runtime.errorExitCode] = 64`;
64
+ * an unknown `--only` token never reaches this handler at all — it fails
65
+ * at parse time, exit `64`.
58
66
  *
59
67
  * @public
60
68
  */
@@ -63,10 +71,13 @@ const syncCommand = Command.make("sync", {
63
71
  config: configFlag,
64
72
  only: onlyFlag,
65
73
  dryRun: dryRunFlag,
66
- format: formatFlag
74
+ format: formatFlag,
75
+ since: sinceFlag,
76
+ staged: stagedFlag
67
77
  }, (input) => Effect.gen(function* () {
68
78
  const cwd = process.cwd();
69
79
  const discoveryCwd = Option.getOrElse(input.path, () => cwd);
80
+ const distribution = yield* Distribution;
70
81
  const body = Effect.gen(function* () {
71
82
  const path = yield* Path.Path;
72
83
  const resolved = yield* resolveProjectConfig({
@@ -74,16 +85,19 @@ const syncCommand = Command.make("sync", {
74
85
  explicitConfigPath: input.config,
75
86
  cwd
76
87
  });
77
- const modes = input.only.length === 0 ? /* @__PURE__ */ new Set([
88
+ const modes = input.only.length > 0 ? new Set(input.only) : input.staged ? /* @__PURE__ */ new Set(["generated", "index"]) : /* @__PURE__ */ new Set([
78
89
  "generated",
79
90
  "index",
80
91
  "log"
81
- ]) : new Set(input.only);
92
+ ]);
93
+ const staged = input.staged ? { at: DateTime.startOf(yield* Now, "second") } : void 0;
82
94
  const result = yield* runSync({
83
95
  bundleRoot: resolved.bundleRoot,
84
96
  config: resolved.config,
85
97
  modes,
86
- dryRun: input.dryRun
98
+ dryRun: input.dryRun,
99
+ ...Option.isSome(input.since) ? { logSince: input.since.value } : {},
100
+ ...staged === void 0 ? {} : { staged }
87
101
  }).pipe(Effect.provide(Layer.mergeAll(Git.layer, GitHistory.layer)));
88
102
  const displayPath = displayRoot(cwd, result.bundleRoot, path);
89
103
  if (input.format === "json") {
@@ -91,7 +105,8 @@ const syncCommand = Command.make("sync", {
91
105
  okfitVersion: CLI_VERSION,
92
106
  root: displayPath,
93
107
  dryRun: result.dryRun,
94
- result
108
+ result,
109
+ ...Option.isSome(distribution) ? { distribution: distribution.value } : {}
95
110
  });
96
111
  yield* Console.log(JSON.stringify(Schema.encodeSync(SyncEnvelope)(envelope)));
97
112
  } else for (const line of humanSync(result)) yield* Console.log(line);
@@ -100,7 +115,7 @@ const syncCommand = Command.make("sync", {
100
115
  explicitConfigPath: input.config,
101
116
  discoveryCwd
102
117
  }));
103
- if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION)))));
118
+ if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION, Option.getOrUndefined(distribution))))));
104
119
  return yield* body;
105
120
  })).pipe(Command.withDescription("Regenerate the derived-content families that no other command produces: generated.at, index.md, and log.md, all from git history and the bundle's own concepts. Mechanical and agent-runnable; never an attestation and never touches verified."));
106
121
 
@@ -1,3 +1,4 @@
1
+ import { Distribution } from "../internal/distribution.js";
1
2
  import { setExitCode } from "../internal/exit.js";
2
3
  import { CLI_VERSION } from "../version.js";
3
4
  import { useColor } from "../internal/tty.js";
@@ -41,6 +42,7 @@ const validateCommand = Command.make("validate", {
41
42
  const discoveryCwd = Option.getOrElse(input.path, () => cwd);
42
43
  const now = yield* Now;
43
44
  const path = yield* Path.Path;
45
+ const distribution = yield* Distribution;
44
46
  const body = Effect.gen(function* () {
45
47
  const { bundleRoot, config: merged, profile } = yield* resolveProjectConfig({
46
48
  pathArg: input.path,
@@ -68,7 +70,8 @@ const validateCommand = Command.make("validate", {
68
70
  }),
69
71
  exitCode: code,
70
72
  concepts: result.bundle.concepts.size,
71
- diagnostics
73
+ diagnostics,
74
+ ...Option.isSome(distribution) ? { distribution: distribution.value } : {}
72
75
  });
73
76
  yield* Console.log(JSON.stringify(Schema.encodeSync(JsonEnvelope)(envelope)));
74
77
  } else {
@@ -86,7 +89,7 @@ const validateCommand = Command.make("validate", {
86
89
  explicitConfigPath: input.config,
87
90
  discoveryCwd
88
91
  }));
89
- if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION)))));
92
+ if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION, Option.getOrUndefined(distribution))))));
90
93
  return yield* body;
91
94
  })).pipe(Command.withDescription("Load config and bundle, run conformance and lint checks, and render the diagnostics."));
92
95
 
@@ -1,16 +1,25 @@
1
+ import { Distribution } from "../internal/distribution.js";
1
2
  import { setExitCode } from "../internal/exit.js";
2
3
  import { CLI_VERSION } from "../version.js";
3
4
  import { displayRoot } from "../render/human.js";
4
- import { humanVerify } from "../render/verify.js";
5
+ import { humanVerify, humanVerifyBatch } from "../render/verify.js";
5
6
  import { Argument, Command, Flag } from "effect/unstable/cli";
6
- import { Now, VerifyEnvelope, jsonError, provideConfig, resolveProjectConfig, runVerify, verifyEnvelope } from "@okfit/engine";
7
+ import { Now, VerifyBatchEnvelope, VerifyEnvelope, VerifySelectionError, jsonError, provideConfig, resolveProjectConfig, runVerify, runVerifyBatch, verifyBatchEnvelope, verifyEnvelope } from "@okfit/engine";
7
8
  import { Console, DateTime, Effect, Option, Path, Schema } from "effect";
8
9
  import { Timestamp } from "@okfit/core";
9
10
  import { Git } from "@effected/git";
10
11
 
11
12
  //#region src/commands/verify.ts
12
- /** V-6: tolerant id, normalised through `ConceptId.normalize`; never `Argument.Path`. */
13
- const idArg = Argument.String("id").pipe(Argument.withDescription("concept id to verify, with or without a leading slash or trailing .md"));
13
+ /**
14
+ * V-6: tolerant id, normalised through `ConceptId.normalize`; never
15
+ * `Argument.Path`. Issue #138 makes it optional: `--all`/`--type` select a
16
+ * batch instead of a single concept.
17
+ */
18
+ const idArg = Argument.String("id").pipe(Argument.optional, Argument.withDescription("concept id to verify, with or without a leading slash or trailing .md; omit with --all or --type"));
19
+ /** Issue #138: attest every unverified concept whose type sets `require_verified`. */
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
+ /** Issue #138: narrow (or, without --all, define) the batch to these types. */
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"));
14
23
  /** K-2: `[path]` is the PROJECT root, byte-identical to validate/init/context's. */
15
24
  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"));
16
25
  /** K-1: no `mustExist`; the handler stats it via `provideConfig`. */
@@ -21,8 +30,8 @@ const atFlag = Flag.String("at").pipe(Flag.optional, Flag.withDescription("ISO 8
21
30
  const dryRunFlag = Flag.Boolean("dry-run").pipe(Flag.withDefault(false), Flag.withDescription("print the exact fragment a real run would splice in; write nothing"));
22
31
  const formatFlag = Flag.Literals("format", ["human", "json"]).pipe(Flag.withDefault("human"), Flag.withDescription("output format: human (default) or json"));
23
32
  /**
24
- * `okfit verify <id> [path] [--config <file>] [--at <iso>] [--dry-run]
25
- * [--format human|json]`.
33
+ * `okfit verify [<id>] [path] [--all] [--type <Type>]... [--config <file>]
34
+ * [--at <iso>] [--dry-run] [--format human|json]`.
26
35
  *
27
36
  * Handler order fixed by contract §2.3. Steps 1–3 are `context`'s handler
28
37
  * in substance — stat `--config` (K-1) via `provideConfig`, discover,
@@ -40,22 +49,64 @@ const verifyCommand = Command.make("verify", {
40
49
  id: idArg,
41
50
  path: pathArg,
42
51
  config: configFlag,
52
+ all: allFlag,
53
+ type: typeFlag,
43
54
  at: atFlag,
44
55
  dryRun: dryRunFlag,
45
56
  format: formatFlag
46
57
  }, (input) => Effect.gen(function* () {
47
58
  const cwd = process.cwd();
48
- const discoveryCwd = Option.getOrElse(input.path, () => cwd);
59
+ const batch = input.all || input.type.length > 0;
60
+ const effectivePath = batch && Option.isSome(input.id) && Option.isNone(input.path) ? input.id : input.path;
61
+ const discoveryCwd = Option.getOrElse(effectivePath, () => cwd);
62
+ const distribution = yield* Distribution;
49
63
  const body = Effect.gen(function* () {
50
64
  const path = yield* Path.Path;
51
65
  const resolved = yield* resolveProjectConfig({
52
- pathArg: input.path,
66
+ pathArg: effectivePath,
53
67
  explicitConfigPath: input.config,
54
68
  cwd
55
69
  });
56
70
  const at = Option.isNone(input.at) ? DateTime.startOf(yield* Now, "second") : yield* Schema.decodeUnknownEffect(Timestamp)(input.at.value);
71
+ if (Option.isSome(input.id) && Option.isSome(input.path) && batch) return yield* new VerifySelectionError({ reason: "id-and-batch" });
72
+ if (Option.isNone(input.id) && !batch) return yield* new VerifySelectionError({ reason: "no-selection" });
73
+ if (batch) {
74
+ const result = yield* runVerifyBatch({
75
+ bundleRoot: resolved.bundleRoot,
76
+ projectRoot: resolved.projectRoot,
77
+ config: resolved.config,
78
+ at,
79
+ dryRun: input.dryRun,
80
+ types: input.type
81
+ });
82
+ const root = displayRoot(cwd, result.bundleRoot, path);
83
+ if (input.format === "json") {
84
+ const envelope = verifyBatchEnvelope({
85
+ okfitVersion: CLI_VERSION,
86
+ by: result.by,
87
+ at: result.at,
88
+ concepts: result.verified.map((entry) => ({
89
+ id: entry.id,
90
+ path: `${root}/${entry.conceptPath}`
91
+ })),
92
+ skipped: result.skipped,
93
+ dryRun: result.dryRun,
94
+ ...Option.isSome(distribution) ? { distribution: distribution.value } : {}
95
+ });
96
+ yield* Console.log(JSON.stringify(Schema.encodeSync(VerifyBatchEnvelope)(envelope)));
97
+ } else for (const line of humanVerifyBatch({
98
+ by: result.by,
99
+ at: result.at,
100
+ dryRun: result.dryRun,
101
+ verified: result.verified,
102
+ skipped: result.skipped
103
+ })) yield* Console.log(line);
104
+ setExitCode(0);
105
+ return;
106
+ }
107
+ if (Option.isNone(input.id)) return yield* new VerifySelectionError({ reason: "no-selection" });
57
108
  const result = yield* runVerify({
58
- id: input.id,
109
+ id: input.id.value,
59
110
  bundleRoot: resolved.bundleRoot,
60
111
  projectRoot: resolved.projectRoot,
61
112
  config: resolved.config,
@@ -70,7 +121,8 @@ const verifyCommand = Command.make("verify", {
70
121
  path: displayPath,
71
122
  by: result.by,
72
123
  at: result.at,
73
- dryRun: result.dryRun
124
+ dryRun: result.dryRun,
125
+ ...Option.isSome(distribution) ? { distribution: distribution.value } : {}
74
126
  });
75
127
  yield* Console.log(JSON.stringify(Schema.encodeSync(VerifyEnvelope)(envelope)));
76
128
  } else for (const line of humanVerify({
@@ -86,9 +138,9 @@ const verifyCommand = Command.make("verify", {
86
138
  explicitConfigPath: input.config,
87
139
  discoveryCwd
88
140
  }));
89
- if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION)))));
141
+ if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION, Option.getOrUndefined(distribution))))));
90
142
  return yield* body;
91
- })).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. This is a human-run command: no agent, hook, or MCP tool ever invokes it."));
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."));
92
144
 
93
145
  //#endregion
94
146
  export { verifyCommand };
package/errors.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ConfigMalformedError, ConfigPathNotFoundError, InitOverwriteError, VerifyConceptNotFoundError, VerifyUnsupportedFrontmatterError } from "@okfit/engine";
1
+ import { ConfigMalformedError, ConfigPathNotFoundError, InitOverwriteError, SyncStagedLogError, VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError } from "@okfit/engine";
2
2
  import { ConfigIssueRenderer } from "@effected/cli";
3
3
 
4
4
  //#region src/errors.ts
@@ -39,6 +39,12 @@ const relativeToCwd = (path, cwd) => {
39
39
  * each render as their own `error: <message>` line; both messages
40
40
  * already name the concept id and what to do about it, and neither
41
41
  * carries a filesystem path needing K-51 relativisation.
42
+ * 5a. `SyncStagedLogError` (issue #140) renders the same way, one
43
+ * `error: <message>` line naming why `--staged` and `--only log` cannot
44
+ * combine.
45
+ * 5b. `VerifySelectionError` (issue #138) renders the same way, one
46
+ * `error: <message>` line naming why `okfit verify`'s selection was
47
+ * contradictory, empty, or named an undeclared type.
42
48
  * 6. Everything else — core's `BundleRootNotFoundError`/`BundleReadError`/
43
49
  * `BundleDepthExceededError`, config-file's other errors, `XdgEnvError`
44
50
  * (the K-13 `HOME`-unset case) — renders as the single line
@@ -59,6 +65,8 @@ const renderFailure = (error) => {
59
65
  ];
60
66
  if (error instanceof VerifyConceptNotFoundError) return [`error: ${error.message}`];
61
67
  if (error instanceof VerifyUnsupportedFrontmatterError) return [`error: ${error.message}`];
68
+ if (error instanceof SyncStagedLogError) return [`error: ${error.message}`];
69
+ if (error instanceof VerifySelectionError) return [`error: ${error.message}`];
62
70
  if (hasTag(error, "ConfigValidationError")) {
63
71
  const validationError = error;
64
72
  return [`error: ${String(validationError)}`, ...ConfigIssueRenderer.render(validationError).map((line) => ` ${line}`)];
package/index.d.ts CHANGED
@@ -20,7 +20,7 @@ import "effect";
20
20
  *
21
21
  * @public
22
22
  */
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("@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("@effected/git").UnknownRefError | import("@okfit/engine").VerifyConceptNotFoundError | import("@okfit/engine").VerifyUnsupportedFrontmatterError | import("@effected/yaml").YamlParseError, import("@effected/xdg").AppDirs | import("effect/unstable/process/ChildProcessSpawner").ChildProcessSpawner | import("effect/Crypto").Crypto | import("effect/FileSystem").FileSystem | import("@okfit/engine").Now | import("effect/Path").Path | import("@effected/xdg").Xdg>;
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("@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/unstable/process/ChildProcessSpawner").ChildProcessSpawner | import("effect/Crypto").Crypto | import("effect/FileSystem").FileSystem | import("@okfit/engine").Now | import("effect/Path").Path | import("@effected/xdg").Xdg>;
24
24
  //#endregion
25
25
  //#region src/errors.d.ts
26
26
  /**
@@ -53,6 +53,12 @@ export declare const rootCommand: Command.Command<"okfit", {} | {}, {}, import("
53
53
  * each render as their own `error: <message>` line; both messages
54
54
  * already name the concept id and what to do about it, and neither
55
55
  * carries a filesystem path needing K-51 relativisation.
56
+ * 5a. `SyncStagedLogError` (issue #140) renders the same way, one
57
+ * `error: <message>` line naming why `--staged` and `--only log` cannot
58
+ * combine.
59
+ * 5b. `VerifySelectionError` (issue #138) renders the same way, one
60
+ * `error: <message>` line naming why `okfit verify`'s selection was
61
+ * contradictory, empty, or named an undeclared type.
56
62
  * 6. Everything else — core's `BundleRootNotFoundError`/`BundleReadError`/
57
63
  * `BundleDepthExceededError`, config-file's other errors, `XdgEnvError`
58
64
  * (the K-13 `HOME`-unset case) — renders as the single line
@@ -147,11 +153,14 @@ export declare const humanVerify: (input: VerifyLines) => ReadonlyArray<string>;
147
153
  //#endregion
148
154
  //#region src/version.d.ts
149
155
  /**
150
- * The version string reported by `okfit --version`. `@savvy-web/bundler`
151
- * replaces `process.env.__PACKAGE_VERSION__` with this package's own
152
- * version at build time (K-32), so a release can never desync from the
153
- * printed version. `"0.0.0"` is the unbuilt-source fallback and reads as
154
- * dev mode.
156
+ * The version string reported by `okfit --version`, alongside `@okfit/engine`'s
157
+ * `ENGINE_VERSION` and `@okfit/core`'s `OKF_SPEC_VERSION` (and, when `main()`
158
+ * was given a distribution, `via <name> <version>`) --
159
+ * `internal/versionFormatter.ts` assembles the full line (okfit #137).
160
+ * `@savvy-web/bundler` replaces `process.env.__PACKAGE_VERSION__` with this
161
+ * package's own version at build time (K-32), so a release can never desync
162
+ * from the printed version. `"0.0.0"` is the unbuilt-source fallback and
163
+ * reads as dev mode.
155
164
  *
156
165
  * @public
157
166
  */
@@ -0,0 +1,21 @@
1
+ import { Context, Option } from "effect";
2
+
3
+ //#region src/internal/distribution.ts
4
+ /**
5
+ * The meta-package this run's bins were installed through (currently only
6
+ * `@okfit/plugin`), threaded from `main(options)` down to every command
7
+ * that builds a JSON envelope or the custom `--version` formatter (okfit
8
+ * #137). Defaults to `Option.none()` for a direct install of `@okfit/cli`;
9
+ * `main()` provides `Option.some(options.distribution)` exactly once, at
10
+ * the top of the command tree, when the caller supplied one.
11
+ *
12
+ * A `Context.Reference`, not a `Context.Service` (unlike `Now`): it carries
13
+ * its own default, so a command can read it with no explicit provision at
14
+ * all when no distribution was given.
15
+ *
16
+ * @internal
17
+ */
18
+ const Distribution = Context.Reference("@okfit/cli/Distribution", { defaultValue: () => Option.none() });
19
+
20
+ //#endregion
21
+ export { Distribution };
@@ -0,0 +1,32 @@
1
+ import { useColor } from "./tty.js";
2
+ import { CliOutput } from "effect/unstable/cli";
3
+ import { ENGINE_VERSION } from "@okfit/engine";
4
+ import { CONFIG_SCHEMA_VERSION, OKF_SPEC_VERSION } from "@okfit/core";
5
+
6
+ //#region src/internal/versionFormatter.ts
7
+ /**
8
+ * `okfit --version`'s full text (okfit #137):
9
+ * `<name> <version>[ via <distName> <distVersion>] (engine <ENGINE_VERSION>, okf <OKF_SPEC_VERSION>, config-schema <CONFIG_SCHEMA_VERSION>)`.
10
+ * `GlobalFlag.Version`'s built-in `run` calls `formatter.formatVersion(command.name,
11
+ * version)` (`command.name` is always `"okfit"`, `version` is `CLI_VERSION` --
12
+ * `unstable/cli/GlobalFlag.ts:180-186`), so those two arguments alone are
13
+ * exactly `okfit <CLI_VERSION>`; this appends the engine, okf, and
14
+ * distribution parts.
15
+ *
16
+ * Only `formatVersion` is overridden -- every other `Formatter` method
17
+ * (help, error rendering) is `CliOutput.defaultFormatter`'s own, built with
18
+ * the SAME colour decision (`internal/tty.ts#useColor`, "exactly the
19
+ * framework's own colour rule") the rest of this package already uses, so
20
+ * help/error output is byte-identical to the built-in formatter.
21
+ *
22
+ * @internal
23
+ */
24
+ const versionFormatter = (distribution) => ({
25
+ ...CliOutput.defaultFormatter({ colors: useColor() }),
26
+ formatVersion: (name, version) => {
27
+ return `${name} ${version}${distribution === void 0 ? "" : ` via ${distribution.name} ${distribution.version}`} (engine ${ENGINE_VERSION}, okf ${OKF_SPEC_VERSION}, config-schema ${CONFIG_SCHEMA_VERSION})`;
28
+ }
29
+ });
30
+
31
+ //#endregion
32
+ export { versionFormatter };
package/main.d.ts CHANGED
@@ -1,15 +1,22 @@
1
+ import { Distribution } from "@okfit/engine";
1
2
  //#region src/main.d.ts
2
3
  /**
3
- * The assembled okfit CLI program.
4
+ * Options `@okfit/plugin`'s bin shims (and only they, today) pass to
5
+ * {@link main}. `distribution` names the meta-package the `okfit` bin was
6
+ * installed through; omitted (or `undefined`) for a direct install of
7
+ * `@okfit/cli` (okfit #137).
4
8
  *
5
- * @packageDocumentation
9
+ * @public
6
10
  */
11
+ export interface MainOptions {
12
+ readonly distribution?: Distribution;
13
+ }
7
14
  /**
8
15
  * Run the okfit CLI. Owns the process: installs the runtime teardown and
9
16
  * sets the exit code. `NodeRuntime.runMain` does not return a promise.
10
17
  *
11
18
  * @public
12
19
  */
13
- export declare const main: () => void;
20
+ export declare const main: (options?: MainOptions) => void;
14
21
  //#endregion
15
22
  //# sourceMappingURL=main.d.ts.map
package/main.js CHANGED
@@ -1,7 +1,9 @@
1
+ import { Distribution } from "./internal/distribution.js";
1
2
  import { CLI_VERSION } from "./version.js";
2
3
  import { rootCommand } from "./commands/root.js";
3
4
  import { renderFailure } from "./errors.js";
4
- import { Command } from "effect/unstable/cli";
5
+ import { versionFormatter } from "./internal/versionFormatter.js";
6
+ import { CliOutput, Command } from "effect/unstable/cli";
5
7
  import { Now, OkfitPlatform } from "@okfit/engine";
6
8
  import { DateTime, Effect, Option } from "effect";
7
9
  import { CliLogger, CliRuntime } from "@effected/cli";
@@ -29,11 +31,12 @@ const nowEffect = Option.fromNullishOr(process.env.OKFIT_NOW).pipe(Option.flatMa
29
31
  *
30
32
  * @public
31
33
  */
32
- const main = () => {
34
+ const main = (options = {}) => {
35
+ const distribution = Option.fromNullishOr(options.distribution);
33
36
  const program = Effect.gen(function* () {
34
37
  const now = yield* nowEffect;
35
- return yield* Command.run(rootCommand, { version: CLI_VERSION }).pipe(Effect.provideService(Now, now), Effect.catchTag("ShowHelp", (help) => Effect.fail(CliRuntime.reported(help, help.errors.length > 0 ? 64 : 0))));
36
- }).pipe(Effect.provide(OkfitPlatform), CliRuntime.reportFailures({
38
+ return yield* Command.run(rootCommand, { version: CLI_VERSION }).pipe(Effect.provideService(Now, now), Effect.provideService(Distribution, distribution), Effect.catchTag("ShowHelp", (help) => Effect.fail(CliRuntime.reported(help, help.errors.length > 0 ? 64 : 0))));
39
+ }).pipe(Effect.provide(CliOutput.layer(versionFormatter(options.distribution))), Effect.provide(OkfitPlatform), CliRuntime.reportFailures({
37
40
  exitCode: 3,
38
41
  render: renderFailure
39
42
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okfit/cli",
3
- "version": "0.5.3",
3
+ "version": "0.6.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,9 +52,9 @@
52
52
  "@effected/toml": "^0.7.0",
53
53
  "@effected/walker": "^0.9.0",
54
54
  "@effected/yaml": "^0.15.1",
55
- "@okfit/core": "0.5.0",
56
- "@okfit/engine": "0.5.0",
57
- "@okfit/profiles": "0.7.0",
55
+ "@okfit/core": "0.7.0",
56
+ "@okfit/engine": "0.7.0",
57
+ "@okfit/profiles": "0.7.2",
58
58
  "effect": "4.0.0-rc.115"
59
59
  },
60
60
  "engines": {
package/render/sync.js CHANGED
@@ -4,7 +4,7 @@ const REASON_SENTENCE = {
4
4
  untracked: "not tracked by git",
5
5
  dirty: "has uncommitted changes",
6
6
  unborn: "the repository has no commits yet",
7
- "generated-missing": "has no generated block",
7
+ "generated-missing": "has no generated block and actors.agent is not configured; set generated.by by hand or configure actors.agent",
8
8
  "generated-unsupported": "generated.at is a shape sync cannot edit; edit it by hand",
9
9
  "log-unparseable": "log.md could not be parsed; see okfit validate"
10
10
  };
package/render/verify.js CHANGED
@@ -24,6 +24,21 @@ const humanVerify = (input) => [
24
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
25
  ...input.dryRun ? ["would write:", ...indentFragment(input.fragment)] : []
26
26
  ];
27
+ /**
28
+ * V-11-at-batch-scale (#138): one line per skipped candidate, then one line
29
+ * (or, under `--dry-run`, three) per verified concept, then a trailing tally.
30
+ *
31
+ * @public
32
+ */
33
+ const humanVerifyBatch = (input) => [
34
+ ...input.skipped.map((entry) => entry.reason === "draft" ? `skipped ${entry.id}: draft` : `skipped ${entry.id}: already verified by ${input.by}`),
35
+ ...input.verified.flatMap((entry) => input.dryRun ? [
36
+ `would verify ${entry.id} by ${input.by} at ${input.at}`,
37
+ "would write:",
38
+ ...indentFragment(entry.fragment)
39
+ ] : [`verified ${entry.id} by ${input.by} at ${input.at}`]),
40
+ input.dryRun ? `would verify ${input.verified.length}, skipped ${input.skipped.length} (dry run, nothing written)` : `verified ${input.verified.length}, skipped ${input.skipped.length}`
41
+ ];
27
42
 
28
43
  //#endregion
29
- export { humanVerify };
44
+ export { humanVerify, humanVerifyBatch };
package/version.js CHANGED
@@ -1,14 +1,17 @@
1
1
  //#region src/version.ts
2
2
  /**
3
- * The version string reported by `okfit --version`. `@savvy-web/bundler`
4
- * replaces `process.env.__PACKAGE_VERSION__` with this package's own
5
- * version at build time (K-32), so a release can never desync from the
6
- * printed version. `"0.0.0"` is the unbuilt-source fallback and reads as
7
- * dev mode.
3
+ * The version string reported by `okfit --version`, alongside `@okfit/engine`'s
4
+ * `ENGINE_VERSION` and `@okfit/core`'s `OKF_SPEC_VERSION` (and, when `main()`
5
+ * was given a distribution, `via <name> <version>`) --
6
+ * `internal/versionFormatter.ts` assembles the full line (okfit #137).
7
+ * `@savvy-web/bundler` replaces `process.env.__PACKAGE_VERSION__` with this
8
+ * package's own version at build time (K-32), so a release can never desync
9
+ * from the printed version. `"0.0.0"` is the unbuilt-source fallback and
10
+ * reads as dev mode.
8
11
  *
9
12
  * @public
10
13
  */
11
- const CLI_VERSION = "0.5.3";
14
+ const CLI_VERSION = "0.6.0";
12
15
 
13
16
  //#endregion
14
17
  export { CLI_VERSION };