@okfit/engine 0.9.4 → 0.10.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/errors.js CHANGED
@@ -60,7 +60,8 @@ var VerifyConceptNotFoundError = class extends Schema.TaggedError()("VerifyConce
60
60
  }
61
61
  };
62
62
  /**
63
- * V-14: fail closed on a `verified` shape the classifier does not name.
63
+ * V-14: fail closed on a `verified` (or, under `--stable`/`--draft`, `status`)
64
+ * shape the classifier does not name; `key` says which, omitted meaning `verified`.
64
65
  * `shape` is one of `"alias"`, `"merge-key"`, `"scalar"`, `"empty"` (and,
65
66
  * defensively, `"no-frontmatter"` or `"not-a-mapping"`, neither reachable
66
67
  * for a concept that reached `bundle.concepts`). The file is never opened
@@ -70,11 +71,12 @@ var VerifyConceptNotFoundError = class extends Schema.TaggedError()("VerifyConce
70
71
  */
71
72
  var VerifyUnsupportedFrontmatterError = class extends Schema.TaggedError()("VerifyUnsupportedFrontmatterError", {
72
73
  id: Schema.String,
73
- shape: Schema.String
74
+ shape: Schema.String,
75
+ key: Schema.optionalKey(Schema.Literals(["verified", "status"]))
74
76
  }) {
75
77
  [Runtime.errorExitCode] = 3;
76
78
  get message() {
77
- return `"${this.id}"'s verified value is a shape okfit verify cannot edit (${this.shape}); edit it by hand`;
79
+ return `"${this.id}"'s ${this.key ?? "verified"} value is a shape okfit verify cannot edit (${this.shape}); edit it by hand`;
78
80
  }
79
81
  };
80
82
  /** The error's `message` when it has one as a string, else `String(error)`. */
@@ -127,7 +129,9 @@ var VerifySelectionError = class extends Schema.TaggedError()("VerifySelectionEr
127
129
  reason: Schema.Literals([
128
130
  "no-selection",
129
131
  "id-and-batch",
130
- "unknown-type"
132
+ "unknown-type",
133
+ "status-conflict",
134
+ "status-and-batch"
131
135
  ]),
132
136
  detail: Schema.optionalKey(Schema.String)
133
137
  }) {
@@ -135,6 +139,8 @@ var VerifySelectionError = class extends Schema.TaggedError()("VerifySelectionEr
135
139
  get message() {
136
140
  if (this.reason === "no-selection") return "verify needs a concept id, --all, or --type <Type>";
137
141
  if (this.reason === "id-and-batch") return "verify takes either a concept id or --all/--type, not both";
142
+ if (this.reason === "status-conflict") return "verify takes --stable or --draft, not both";
143
+ if (this.reason === "status-and-batch") return "--stable and --draft need a concept id; batch mode never changes status";
138
144
  return `type "${this.detail ?? ""}" is not declared in the config`;
139
145
  }
140
146
  };
@@ -170,6 +176,50 @@ var DocumentPathError = class extends Schema.TaggedError()("DocumentPathError",
170
176
  return `document path "${this.path}" ${DOCUMENT_PATH_REASON_TEXT[this.reason]}`;
171
177
  }
172
178
  };
179
+ /**
180
+ * `okfit query list` named a type or tag the config does not declare. `valid`
181
+ * is the sorted declared names. Exit 64: a usage error.
182
+ *
183
+ * @public
184
+ */
185
+ var QueryUnknownVocabularyError = class extends Schema.TaggedError()("QueryUnknownVocabularyError", {
186
+ kind: Schema.Literals(["type", "tag"]),
187
+ requested: Schema.String,
188
+ valid: Schema.Array(Schema.String)
189
+ }) {
190
+ [Runtime.errorExitCode] = 64;
191
+ get message() {
192
+ return `${this.kind} "${this.requested}" is not declared in the config; declared: ${this.valid.join(", ")}`;
193
+ }
194
+ };
195
+ /**
196
+ * A query's concept id named no concept: empty after normalisation
197
+ * (`empty-id`) or absent from the bundle (`not-a-concept`). Exit 3.
198
+ *
199
+ * @public
200
+ */
201
+ var QueryConceptNotFoundError = class extends Schema.TaggedError()("QueryConceptNotFoundError", {
202
+ id: Schema.String,
203
+ reason: Schema.Literals(["empty-id", "not-a-concept"])
204
+ }) {
205
+ [Runtime.errorExitCode] = 3;
206
+ get message() {
207
+ if (this.reason === "empty-id") return "a concept id must not be empty";
208
+ return `no concept "${this.id}" in this bundle`;
209
+ }
210
+ };
211
+ /**
212
+ * `okfit query list` was given contradictory selection flags. Exit 64: a
213
+ * usage error.
214
+ *
215
+ * @public
216
+ */
217
+ var QuerySelectionError = class extends Schema.TaggedError()("QuerySelectionError", { reason: Schema.Literals(["verified-conflict"]) }) {
218
+ [Runtime.errorExitCode] = 64;
219
+ get message() {
220
+ return "query list takes --verified or --unverified, not both";
221
+ }
222
+ };
173
223
 
174
224
  //#endregion
175
- export { ConfigMalformedError, ConfigPathNotFoundError, DocumentPathError, InitOverwriteError, SyncStagedLogError, VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError };
225
+ export { ConfigMalformedError, ConfigPathNotFoundError, DocumentPathError, InitOverwriteError, QueryConceptNotFoundError, QuerySelectionError, QueryUnknownVocabularyError, SyncStagedLogError, VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError };
package/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Actor, BundleLoadError, ConceptId, Diagnostic, DiagnosticRange, DiagnosticSeverity, GraphLink, GraphNode, LinkGraph, LoadedBundle, LoadedConcept, OkfitConfig, OkfitConfigFile, StaleConcept, Status, ValidationReport } from "@okfit/core";
1
+ import { Actor, BundleLoadError, ConceptId, Diagnostic, DiagnosticRange, DiagnosticSeverity, GraphLink, GraphNode, GraphNodeKind, LinkGraph, LoadedBundle, LoadedConcept, OkfitConfig, OkfitConfigFile, StaleConcept, Status, ValidationReport } from "@okfit/core";
2
2
  import { Context, Crypto, DateTime, Effect, FileSystem, Layer, Option, Path, PlatformError, Result, Runtime, Schema } from "effect";
3
3
  import { AppDirs, Xdg } from "@effected/xdg";
4
4
  import { ConfigReadError } from "@effected/config-file";
@@ -126,9 +126,11 @@ export declare class VerifyConceptNotFoundError extends VerifyConceptNotFoundErr
126
126
  declare const VerifyUnsupportedFrontmatterError_base: Schema.Class<VerifyUnsupportedFrontmatterError, Schema.TaggedStruct<"VerifyUnsupportedFrontmatterError", {
127
127
  readonly id: Schema.String;
128
128
  readonly shape: Schema.String;
129
+ readonly key: Schema.optionalKey<Schema.Literals<readonly ["verified", "status"]>>;
129
130
  }>, import("effect/Cause").YieldableError>;
130
131
  /**
131
- * V-14: fail closed on a `verified` shape the classifier does not name.
132
+ * V-14: fail closed on a `verified` (or, under `--stable`/`--draft`, `status`)
133
+ * shape the classifier does not name; `key` says which, omitted meaning `verified`.
132
134
  * `shape` is one of `"alias"`, `"merge-key"`, `"scalar"`, `"empty"` (and,
133
135
  * defensively, `"no-frontmatter"` or `"not-a-mapping"`, neither reachable
134
136
  * for a concept that reached `bundle.concepts`). The file is never opened
@@ -172,7 +174,7 @@ export declare class SyncStagedLogError extends SyncStagedLogError_base {
172
174
  get message(): string;
173
175
  }
174
176
  declare const VerifySelectionError_base: Schema.Class<VerifySelectionError, Schema.TaggedStruct<"VerifySelectionError", {
175
- readonly reason: Schema.Literals<readonly ["no-selection", "id-and-batch", "unknown-type"]>;
177
+ readonly reason: Schema.Literals<readonly ["no-selection", "id-and-batch", "unknown-type", "status-conflict", "status-and-batch"]>;
176
178
  readonly detail: Schema.optionalKey<Schema.String>;
177
179
  }>, import("effect/Cause").YieldableError>;
178
180
  /**
@@ -202,6 +204,48 @@ export declare class DocumentPathError extends DocumentPathError_base {
202
204
  readonly [Runtime.errorExitCode] = 64;
203
205
  get message(): string;
204
206
  }
207
+ declare const QueryUnknownVocabularyError_base: Schema.Class<QueryUnknownVocabularyError, Schema.TaggedStruct<"QueryUnknownVocabularyError", {
208
+ readonly kind: Schema.Literals<readonly ["type", "tag"]>;
209
+ readonly requested: Schema.String;
210
+ readonly valid: Schema.$Array<Schema.String>;
211
+ }>, import("effect/Cause").YieldableError>;
212
+ /**
213
+ * `okfit query list` named a type or tag the config does not declare. `valid`
214
+ * is the sorted declared names. Exit 64: a usage error.
215
+ *
216
+ * @public
217
+ */
218
+ export declare class QueryUnknownVocabularyError extends QueryUnknownVocabularyError_base {
219
+ readonly [Runtime.errorExitCode] = 64;
220
+ get message(): string;
221
+ }
222
+ declare const QueryConceptNotFoundError_base: Schema.Class<QueryConceptNotFoundError, Schema.TaggedStruct<"QueryConceptNotFoundError", {
223
+ readonly id: Schema.String;
224
+ readonly reason: Schema.Literals<readonly ["empty-id", "not-a-concept"]>;
225
+ }>, import("effect/Cause").YieldableError>;
226
+ /**
227
+ * A query's concept id named no concept: empty after normalisation
228
+ * (`empty-id`) or absent from the bundle (`not-a-concept`). Exit 3.
229
+ *
230
+ * @public
231
+ */
232
+ export declare class QueryConceptNotFoundError extends QueryConceptNotFoundError_base {
233
+ readonly [Runtime.errorExitCode] = 3;
234
+ get message(): string;
235
+ }
236
+ declare const QuerySelectionError_base: Schema.Class<QuerySelectionError, Schema.TaggedStruct<"QuerySelectionError", {
237
+ readonly reason: Schema.Literals<readonly ["verified-conflict"]>;
238
+ }>, import("effect/Cause").YieldableError>;
239
+ /**
240
+ * `okfit query list` was given contradictory selection flags. Exit 64: a
241
+ * usage error.
242
+ *
243
+ * @public
244
+ */
245
+ export declare class QuerySelectionError extends QuerySelectionError_base {
246
+ readonly [Runtime.errorExitCode] = 64;
247
+ get message(): string;
248
+ }
205
249
  //#endregion
206
250
  //#region src/config/layer.d.ts
207
251
  /**
@@ -652,6 +696,58 @@ export declare const layerOverlayFileSystem: Layer.Layer<FileSystem.FileSystem,
652
696
  */
653
697
  export declare const OkfitPlatform: Layer.Layer<AppDirs | Xdg | NodeServices.NodeServices, import("@effected/xdg").XdgEnvError, never>;
654
698
  //#endregion
699
+ //#region src/query/ConceptQuery.d.ts
700
+ /**
701
+ * Filter for {@link ConceptQuery.list}. Every field is optional; an absent
702
+ * field does not constrain.
703
+ *
704
+ * @public
705
+ */
706
+ interface ConceptFilter {
707
+ /** OR across types. */
708
+ readonly types?: ReadonlyArray<string>;
709
+ /** AND: every tag must be present. */
710
+ readonly tags?: ReadonlyArray<string>;
711
+ /** OR, compared against `Derive.status` (an absent status reads as stable). */
712
+ readonly statuses?: ReadonlyArray<Status>;
713
+ /** `true`: at least one verified entry; `false`: none. */
714
+ readonly verified?: boolean;
715
+ }
716
+ /** One outgoing link of a concept. @public */
717
+ interface ConceptLink {
718
+ readonly to: string;
719
+ readonly kind: GraphNodeKind;
720
+ readonly source: "body" | "frontmatter";
721
+ readonly field?: string;
722
+ }
723
+ /** One graph neighbour; `concept` is `null` for a non-concept node (J-4). @public */
724
+ interface ConceptNeighbor {
725
+ readonly id: string;
726
+ readonly kind: GraphNodeKind;
727
+ readonly concept: LoadedConcept | null;
728
+ }
729
+ /**
730
+ * Read-only queries over a loaded bundle, shared by `okfit query` and the MCP
731
+ * tools.
732
+ *
733
+ * @public
734
+ */
735
+ export declare const ConceptQuery: {
736
+ /** Validates types then tags against config (first unknown fails), then filters and sorts by id. */
737
+ readonly list: (bundle: LoadedBundle, config: OkfitConfig, filter: ConceptFilter) => Effect.Effect<ReadonlyArray<LoadedConcept>, QueryUnknownVocabularyError>;
738
+ /** One concept by tolerant id, with its outgoing links. */
739
+ readonly get: (bundle: LoadedBundle, id: string) => Effect.Effect<{
740
+ readonly concept: LoadedConcept;
741
+ readonly links: ReadonlyArray<ConceptLink>;
742
+ }, QueryConceptNotFoundError>;
743
+ /** Outgoing and incoming graph neighbours of one concept. */
744
+ readonly neighbors: (bundle: LoadedBundle, id: string) => Effect.Effect<{
745
+ readonly id: string;
746
+ readonly outgoing: ReadonlyArray<ConceptNeighbor>;
747
+ readonly incoming: ReadonlyArray<ConceptNeighbor>;
748
+ }, QueryConceptNotFoundError>;
749
+ };
750
+ //#endregion
655
751
  //#region src/render/context.d.ts
656
752
  /** One enumerated value of a `fields[]` entry. @public */
657
753
  export declare const ContextFieldValue: Schema.Struct<{
@@ -1088,6 +1184,159 @@ export declare const json: (input: {
1088
1184
  /** K-22. `tag` is the error's `_tag` when it has one, else its constructor name. @public */
1089
1185
  export declare const jsonError: (error: unknown, okfitVersion: string, distribution?: Distribution) => JsonErrorEnvelope;
1090
1186
  //#endregion
1187
+ //#region src/render/query.d.ts
1188
+ /** The seven-field shape every list-like query result carries (N-16). @public */
1189
+ export declare const ConceptSummary: Schema.Struct<{
1190
+ readonly id: Schema.String;
1191
+ readonly type: Schema.String;
1192
+ readonly title: Schema.String;
1193
+ readonly description: Schema.NullOr<Schema.String>;
1194
+ readonly status: Schema.Literals<readonly ["draft", "stable", "deprecated"]>;
1195
+ readonly tags: Schema.$Array<Schema.String>;
1196
+ readonly path: Schema.String;
1197
+ }>;
1198
+ /** @public */
1199
+ export type ConceptSummary = typeof ConceptSummary.Type;
1200
+ /** Project one loaded concept into its summary. @public */
1201
+ export declare const toConceptSummary: (concept: LoadedConcept) => ConceptSummary;
1202
+ /** `okfit query list --format json`'s envelope, schema 1, snake_case. @public */
1203
+ export declare const QueryListEnvelope: Schema.Struct<{
1204
+ readonly schema: Schema.Literal<1>;
1205
+ readonly okfit_version: Schema.String;
1206
+ readonly engine_version: Schema.String;
1207
+ readonly distribution: Schema.NullOr<Schema.Struct<{
1208
+ readonly name: Schema.String;
1209
+ readonly version: Schema.String;
1210
+ }>>;
1211
+ readonly total: Schema.Int;
1212
+ readonly items: Schema.$Array<Schema.Struct<{
1213
+ readonly id: Schema.String;
1214
+ readonly type: Schema.String;
1215
+ readonly title: Schema.String;
1216
+ readonly description: Schema.NullOr<Schema.String>;
1217
+ readonly status: Schema.Literals<readonly ["draft", "stable", "deprecated"]>;
1218
+ readonly tags: Schema.$Array<Schema.String>;
1219
+ readonly path: Schema.String;
1220
+ }>>;
1221
+ }>;
1222
+ /** @public */
1223
+ export type QueryListEnvelope = typeof QueryListEnvelope.Type;
1224
+ /** One outgoing link of a concept, as `okfit query get` reports it. @public */
1225
+ export declare const QueryLink: Schema.Struct<{
1226
+ readonly to: Schema.String;
1227
+ readonly kind: Schema.Literals<readonly ["concept", "file", "missing"]>;
1228
+ readonly source: Schema.Literals<readonly ["body", "frontmatter"]>;
1229
+ readonly field: Schema.optionalKey<Schema.String>;
1230
+ }>;
1231
+ /** @public */
1232
+ export type QueryLink = typeof QueryLink.Type;
1233
+ /** `okfit query get --format json`'s envelope, schema 1, snake_case. @public */
1234
+ export declare const QueryGetEnvelope: Schema.Struct<{
1235
+ readonly schema: Schema.Literal<1>;
1236
+ readonly okfit_version: Schema.String;
1237
+ readonly engine_version: Schema.String;
1238
+ readonly distribution: Schema.NullOr<Schema.Struct<{
1239
+ readonly name: Schema.String;
1240
+ readonly version: Schema.String;
1241
+ }>>;
1242
+ readonly concept: Schema.Struct<{
1243
+ readonly id: Schema.String;
1244
+ readonly type: Schema.String;
1245
+ readonly title: Schema.String;
1246
+ readonly description: Schema.NullOr<Schema.String>;
1247
+ readonly status: Schema.Literals<readonly ["draft", "stable", "deprecated"]>;
1248
+ readonly tags: Schema.$Array<Schema.String>;
1249
+ readonly path: Schema.String;
1250
+ readonly frontmatter: Schema.$Record<Schema.String, Schema.Unknown>;
1251
+ readonly links: Schema.$Array<Schema.Struct<{
1252
+ readonly to: Schema.String;
1253
+ readonly kind: Schema.Literals<readonly ["concept", "file", "missing"]>;
1254
+ readonly source: Schema.Literals<readonly ["body", "frontmatter"]>;
1255
+ readonly field: Schema.optionalKey<Schema.String>;
1256
+ }>>;
1257
+ }>;
1258
+ }>;
1259
+ /** @public */
1260
+ export type QueryGetEnvelope = typeof QueryGetEnvelope.Type;
1261
+ /** One neighbour of a concept; `summary` is `null` for a non-concept node. @public */
1262
+ export declare const QueryNeighbor: Schema.Struct<{
1263
+ readonly id: Schema.String;
1264
+ readonly kind: Schema.Literals<readonly ["concept", "file", "missing"]>;
1265
+ readonly summary: Schema.NullOr<Schema.Struct<{
1266
+ readonly id: Schema.String;
1267
+ readonly type: Schema.String;
1268
+ readonly title: Schema.String;
1269
+ readonly description: Schema.NullOr<Schema.String>;
1270
+ readonly status: Schema.Literals<readonly ["draft", "stable", "deprecated"]>;
1271
+ readonly tags: Schema.$Array<Schema.String>;
1272
+ readonly path: Schema.String;
1273
+ }>>;
1274
+ }>;
1275
+ /** @public */
1276
+ export type QueryNeighbor = typeof QueryNeighbor.Type;
1277
+ /** `okfit query neighbors --format json`'s envelope, schema 1, snake_case. @public */
1278
+ export declare const QueryNeighborsEnvelope: Schema.Struct<{
1279
+ readonly schema: Schema.Literal<1>;
1280
+ readonly okfit_version: Schema.String;
1281
+ readonly engine_version: Schema.String;
1282
+ readonly distribution: Schema.NullOr<Schema.Struct<{
1283
+ readonly name: Schema.String;
1284
+ readonly version: Schema.String;
1285
+ }>>;
1286
+ readonly id: Schema.String;
1287
+ readonly outgoing: Schema.$Array<Schema.Struct<{
1288
+ readonly id: Schema.String;
1289
+ readonly kind: Schema.Literals<readonly ["concept", "file", "missing"]>;
1290
+ readonly summary: Schema.NullOr<Schema.Struct<{
1291
+ readonly id: Schema.String;
1292
+ readonly type: Schema.String;
1293
+ readonly title: Schema.String;
1294
+ readonly description: Schema.NullOr<Schema.String>;
1295
+ readonly status: Schema.Literals<readonly ["draft", "stable", "deprecated"]>;
1296
+ readonly tags: Schema.$Array<Schema.String>;
1297
+ readonly path: Schema.String;
1298
+ }>>;
1299
+ }>>;
1300
+ readonly incoming: Schema.$Array<Schema.Struct<{
1301
+ readonly id: Schema.String;
1302
+ readonly kind: Schema.Literals<readonly ["concept", "file", "missing"]>;
1303
+ readonly summary: Schema.NullOr<Schema.Struct<{
1304
+ readonly id: Schema.String;
1305
+ readonly type: Schema.String;
1306
+ readonly title: Schema.String;
1307
+ readonly description: Schema.NullOr<Schema.String>;
1308
+ readonly status: Schema.Literals<readonly ["draft", "stable", "deprecated"]>;
1309
+ readonly tags: Schema.$Array<Schema.String>;
1310
+ readonly path: Schema.String;
1311
+ }>>;
1312
+ }>>;
1313
+ }>;
1314
+ /** @public */
1315
+ export type QueryNeighborsEnvelope = typeof QueryNeighborsEnvelope.Type;
1316
+ /** Build the list envelope; `items` is the already-paged, already-sorted summaries. @public */
1317
+ export declare const queryListEnvelope: (input: {
1318
+ readonly okfitVersion: string;
1319
+ readonly total: number;
1320
+ readonly items: ReadonlyArray<ConceptSummary>;
1321
+ readonly distribution?: Distribution;
1322
+ }) => QueryListEnvelope;
1323
+ /** Build the get envelope from a summary, the raw frontmatter and the outgoing links. @public */
1324
+ export declare const queryGetEnvelope: (input: {
1325
+ readonly okfitVersion: string;
1326
+ readonly concept: ConceptSummary;
1327
+ readonly frontmatter: Readonly<Record<string, unknown>>;
1328
+ readonly links: ReadonlyArray<QueryLink>;
1329
+ readonly distribution?: Distribution;
1330
+ }) => QueryGetEnvelope;
1331
+ /** Build the neighbors envelope. @public */
1332
+ export declare const queryNeighborsEnvelope: (input: {
1333
+ readonly okfitVersion: string;
1334
+ readonly id: string;
1335
+ readonly outgoing: ReadonlyArray<QueryNeighbor>;
1336
+ readonly incoming: ReadonlyArray<QueryNeighbor>;
1337
+ readonly distribution?: Distribution;
1338
+ }) => QueryNeighborsEnvelope;
1339
+ //#endregion
1091
1340
  //#region src/render/stale.d.ts
1092
1341
  /** One entry of the `items` array. @public */
1093
1342
  export declare const StaleItem: Schema.Struct<{
@@ -1298,6 +1547,47 @@ export declare const syncEnvelope: (input: {
1298
1547
  readonly distribution?: Distribution;
1299
1548
  }) => SyncEnvelope;
1300
1549
  //#endregion
1550
+ //#region src/verify/select.d.ts
1551
+ /**
1552
+ * Why a candidate concept was skipped by batch selection: `"draft"` and
1553
+ * `"deprecated"` mirror `status`, `"already-verified"` means the actor already
1554
+ * carries a `verified` entry.
1555
+ *
1556
+ * @public
1557
+ */
1558
+ type VerifyBatchSkipReason = "draft" | "deprecated" | "already-verified";
1559
+ /**
1560
+ * The outcome of {@link selectAttestable}.
1561
+ *
1562
+ * @public
1563
+ */
1564
+ interface AttestableSelection {
1565
+ /** In bundle iteration order. */
1566
+ readonly candidates: ReadonlyArray<LoadedConcept>;
1567
+ readonly skipped: ReadonlyArray<{
1568
+ readonly id: string;
1569
+ readonly reason: VerifyBatchSkipReason;
1570
+ }>;
1571
+ }
1572
+ /**
1573
+ * Issue #138/#143: which concepts of `types` the actor may attest in one
1574
+ * batch. A draft is unsettled and a deprecated concept is retired (#143), so
1575
+ * both are skipped before the already-verified check.
1576
+ *
1577
+ * @public
1578
+ */
1579
+ export declare const selectAttestable: (bundle: LoadedBundle, types: ReadonlySet<string>, actor: string) => AttestableSelection;
1580
+ /**
1581
+ * The batch type-resolution rule shared by `runVerifyBatch` and any future
1582
+ * interactive picker: explicit `types` must each be declared in
1583
+ * `config.types` (else `VerifySelectionError` with reason `unknown-type`);
1584
+ * an empty `types` selects every declared type whose `require_verified` is
1585
+ * `true`.
1586
+ *
1587
+ * @public
1588
+ */
1589
+ export declare const resolveBatchTypes: (config: OkfitConfig, types: ReadonlyArray<string>) => Effect.Effect<ReadonlySet<string>, VerifySelectionError>;
1590
+ //#endregion
1301
1591
  //#region src/render/verify.d.ts
1302
1592
  /**
1303
1593
  * V-11's success envelope, schema 1, snake_case — the same convention as
@@ -1322,6 +1612,11 @@ export declare const VerifyEnvelope: Schema.Struct<{
1322
1612
  readonly by: Schema.String;
1323
1613
  readonly at: Schema.String;
1324
1614
  }>;
1615
+ /** Issue #185: the status change `--stable`/`--draft` made; `null` when neither flag was given. */
1616
+ readonly status: Schema.NullOr<Schema.Struct<{
1617
+ readonly from: Schema.NullOr<Schema.Literals<readonly ["draft", "stable", "deprecated"]>>;
1618
+ readonly to: Schema.Literals<readonly ["stable", "draft"]>;
1619
+ }>>;
1325
1620
  readonly dry_run: Schema.Boolean;
1326
1621
  readonly exit_code: Schema.Literal<0>;
1327
1622
  }>;
@@ -1342,6 +1637,10 @@ export declare const verifyEnvelope: (input: {
1342
1637
  readonly by: string;
1343
1638
  readonly at: string;
1344
1639
  readonly dryRun: boolean;
1640
+ readonly status: {
1641
+ readonly from: Status | null;
1642
+ readonly to: "stable" | "draft";
1643
+ } | null;
1345
1644
  readonly distribution?: Distribution;
1346
1645
  }) => VerifyEnvelope;
1347
1646
  /**
@@ -1368,7 +1667,7 @@ export declare const VerifyBatchEnvelope: Schema.Struct<{
1368
1667
  }>>;
1369
1668
  readonly skipped: Schema.$Array<Schema.Struct<{
1370
1669
  readonly id: Schema.String;
1371
- readonly reason: Schema.Literals<readonly ["draft", "already-verified"]>;
1670
+ readonly reason: Schema.Literals<readonly ["draft", "deprecated", "already-verified"]>;
1372
1671
  }>>;
1373
1672
  readonly dry_run: Schema.Boolean;
1374
1673
  readonly exit_code: Schema.Literal<0>;
@@ -1391,7 +1690,7 @@ export declare const verifyBatchEnvelope: (input: {
1391
1690
  }>;
1392
1691
  readonly skipped: ReadonlyArray<{
1393
1692
  readonly id: string;
1394
- readonly reason: "draft" | "already-verified";
1693
+ readonly reason: VerifyBatchSkipReason;
1395
1694
  }>;
1396
1695
  readonly dryRun: boolean;
1397
1696
  readonly distribution?: Distribution;
@@ -1621,6 +1920,8 @@ interface VerifyOptions {
1621
1920
  /** `--at` when given, else the `Now` service (K-47/K-49). */
1622
1921
  readonly at: DateTime.Utc;
1623
1922
  readonly dryRun: boolean;
1923
+ /** Issue #185: also set the concept's `status` in the same write. */
1924
+ readonly status?: "stable" | "draft";
1624
1925
  }
1625
1926
  /** @internal */
1626
1927
  interface VerifyResult {
@@ -1638,6 +1939,13 @@ interface VerifyResult {
1638
1939
  readonly dryRun: boolean;
1639
1940
  /** The exact bytes `splice`'s edit would insert, written or not. */
1640
1941
  readonly fragment: string;
1942
+ /** `null` when no status flag was given; `from` is the RAW frontmatter status. */
1943
+ readonly status: {
1944
+ readonly from: Status | null;
1945
+ readonly to: "stable" | "draft";
1946
+ } | null;
1947
+ /** The exact status bytes spliced, or `null` when no status edit was made. */
1948
+ readonly statusFragment: string | null;
1641
1949
  }
1642
1950
  /**
1643
1951
  * Issue #138: options for {@link runVerifyBatch}. `types` restricts the
@@ -1655,14 +1963,6 @@ interface VerifyBatchOptions {
1655
1963
  /** Restrict to these types; empty = every type with `require_verified = true`. */
1656
1964
  readonly types: ReadonlyArray<string>;
1657
1965
  }
1658
- /**
1659
- * Why {@link runVerifyBatch} skipped a candidate concept: `"draft"` for
1660
- * `status === "draft"`, `"already-verified"` when the resolved actor already
1661
- * carries a `verified` entry.
1662
- *
1663
- * @public
1664
- */
1665
- type VerifyBatchSkipReason = "draft" | "already-verified";
1666
1966
  /**
1667
1967
  * The result of {@link runVerifyBatch}.
1668
1968
  *
@@ -1689,8 +1989,9 @@ interface VerifyBatchResult {
1689
1989
  * human actor from git, splices exactly one entry into the concept's
1690
1990
  * frontmatter and writes it back atomically.
1691
1991
  *
1692
- * Nothing here reads `status`, `stale_after`, or a type's
1693
- * `require_verified`: `verify` is mechanical, not config-aware (V-4, V-5).
1992
+ * Nothing here reads `stale_after` or a type's `require_verified`; `status`
1993
+ * is read only to skip a no-op edit under `options.status` (#185). `verify`
1994
+ * is mechanical, not config-aware (V-4, V-5).
1694
1995
  *
1695
1996
  * @internal
1696
1997
  */
@@ -1703,11 +2004,16 @@ export declare const runVerify: (options: VerifyOptions) => Effect.Effect<{
1703
2004
  priorAt: string[];
1704
2005
  dryRun: boolean;
1705
2006
  fragment: string;
2007
+ status: {
2008
+ from: "deprecated" | "draft" | "stable" | null;
2009
+ to: "draft" | "stable";
2010
+ } | null;
2011
+ statusFragment: string | null;
1706
2012
  }, import("effect/PlatformError").PlatformError | VerifyConceptNotFoundError | VerifyUnsupportedFrontmatterError | import("@effected/yaml").YamlParseError | import("@okfit/core").BundleLoadError | import("@okfit/profiles").GeneratedByError, FileSystem.FileSystem | import("@effected/git").Git | Path.Path>;
1707
2013
  /**
1708
2014
  * Issue #138. Attests every concept selected by `options.types` (or, when
1709
2015
  * empty, every type whose declaration sets `require_verified = true`) that
1710
- * is not a draft and does not already carry a `verified` entry by the
2016
+ * is not a draft, is not deprecated (#143) and does not already carry a `verified` entry by the
1711
2017
  * resolved actor. Every candidate is located FIRST -- an unsupported
1712
2018
  * `verified` shape on any one of them fails the whole batch with nothing
1713
2019
  * written, mirroring `runVerify`'s own fail-closed rule at batch scale.
@@ -1724,7 +2030,7 @@ export declare const runVerifyBatch: (options: VerifyBatchOptions) => Effect.Eff
1724
2030
  conceptPath: string;
1725
2031
  fragment: string;
1726
2032
  }[];
1727
- skipped: {
2033
+ skipped: readonly {
1728
2034
  readonly id: string;
1729
2035
  readonly reason: VerifyBatchSkipReason;
1730
2036
  }[];
@@ -1745,5 +2051,5 @@ export declare const runVerifyBatch: (options: VerifyBatchOptions) => Effect.Eff
1745
2051
  */
1746
2052
  export declare const ENGINE_VERSION: string;
1747
2053
  //#endregion
1748
- export type { BundleSessionOptions, BundleSessionServices, BundleSessionShape, ContextResult, ContextRunOptions, DiagnosticSource, DiscoveredConfig, Distribution, DocumentInput, ExternalReferencesShape, GraphRunOptions, GraphRunResult, OverlayDocument, OverlayDocumentsShape, RenderedDiagnostic, ResolveProjectConfigInput, ResolvedProjectConfig, RevalidateError, RevalidateOptions, RevalidateResult, RevalidateTier, RunOptions, RunResult, ScaffoldFile, ScaffoldOptions, StaleRunOptions, StaleRunResult, SyncMode, SyncModeResult, SyncOptions, SyncResult, Tally, VerifyBatchOptions, VerifyBatchResult, VerifyBatchSkipReason, VerifyOptions, VerifyResult, WatchedFilesOutcome };
2054
+ export type { AttestableSelection, BundleSessionOptions, BundleSessionServices, BundleSessionShape, ConceptFilter, ConceptLink, ConceptNeighbor, ContextResult, ContextRunOptions, DiagnosticSource, DiscoveredConfig, Distribution, DocumentInput, ExternalReferencesShape, GraphRunOptions, GraphRunResult, OverlayDocument, OverlayDocumentsShape, RenderedDiagnostic, ResolveProjectConfigInput, ResolvedProjectConfig, RevalidateError, RevalidateOptions, RevalidateResult, RevalidateTier, RunOptions, RunResult, ScaffoldFile, ScaffoldOptions, StaleRunOptions, StaleRunResult, SyncMode, SyncModeResult, SyncOptions, SyncResult, Tally, VerifyBatchOptions, VerifyBatchResult, VerifyBatchSkipReason, VerifyOptions, VerifyResult, WatchedFilesOutcome };
1749
2055
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { resolveBundleRoot, resolveProjectRoot } from "./config/anchor.js";
2
- import { ConfigMalformedError, ConfigPathNotFoundError, DocumentPathError, InitOverwriteError, SyncStagedLogError, VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError } from "./errors.js";
2
+ import { ConfigMalformedError, ConfigPathNotFoundError, DocumentPathError, InitOverwriteError, QueryConceptNotFoundError, QuerySelectionError, QueryUnknownVocabularyError, SyncStagedLogError, VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError } from "./errors.js";
3
3
  import { buildConfigLayer, provideConfig } from "./config/layer.js";
4
4
  import { DEFAULT_PROFILE_NAME, resolveProjectConfig } from "./config/resolve.js";
5
5
  import { runContext } from "./context/run.js";
@@ -10,6 +10,7 @@ import { CONFIG_RELATIVE_PATH, configValue, files, targetPaths } from "./init/sc
10
10
  import { OverlayDocuments, layerOverlayFileSystem, makeOverlayFileSystem } from "./overlay/layer.js";
11
11
  import { provideDocuments, resolveDocumentPath } from "./overlay/documents.js";
12
12
  import { OkfitPlatform } from "./platform.js";
13
+ import { ConceptQuery } from "./query/ConceptQuery.js";
13
14
  import { ContextEnvelope, ContextField, ContextFieldValue, ContextTag, ContextType, contextEnvelope } from "./render/context.js";
14
15
  import { DistributionField } from "./render/distribution.js";
15
16
  import { forDiagnostics, tally } from "./render/exit.js";
@@ -17,6 +18,7 @@ import { ENGINE_VERSION } from "./version.js";
17
18
  import { GraphEdgeEnvelope, GraphEnvelope, GraphNodeEnvelope, GraphSummary, graphEnvelope } from "./render/graph.js";
18
19
  import { collect, sort } from "./render/sort.js";
19
20
  import { JsonDiagnostic, JsonEnvelope, JsonErrorEnvelope, JsonSummary, json, jsonError } from "./render/json.js";
21
+ import { ConceptSummary, QueryGetEnvelope, QueryLink, QueryListEnvelope, QueryNeighbor, QueryNeighborsEnvelope, queryGetEnvelope, queryListEnvelope, queryNeighborsEnvelope, toConceptSummary } from "./render/query.js";
20
22
  import { StaleEnvelope, StaleItem, StaleSummary, staleEnvelope } from "./render/stale.js";
21
23
  import { SkipReason, runSync } from "./sync/run.js";
22
24
  import { SyncEnvelope, SyncModeEnvelope, syncEnvelope } from "./render/sync.js";
@@ -26,6 +28,7 @@ import { conceptFor } from "./session/concept.js";
26
28
  import { withFallbackRange } from "./session/range.js";
27
29
  import { BundleSession } from "./session/BundleSession.js";
28
30
  import { runStale } from "./stale/run.js";
31
+ import { resolveBatchTypes, selectAttestable } from "./verify/select.js";
29
32
  import { runVerify, runVerifyBatch } from "./verify/run.js";
30
33
 
31
- export { BundleSession, CONFIG_RELATIVE_PATH, ConfigMalformedError, ConfigPathNotFoundError, ContextEnvelope, ContextField, ContextFieldValue, ContextTag, ContextType, DEFAULT_PROFILE_NAME, DistributionField, DocumentPathError, ENGINE_VERSION, ExternalReferences, FrontmatterEdits, GraphEdgeEnvelope, GraphEnvelope, GraphNodeEnvelope, GraphSummary, InitOverwriteError, JsonDiagnostic, JsonEnvelope, JsonErrorEnvelope, JsonSummary, Now, OkfitPlatform, OverlayDocuments, ReferenceCheck, ReferenceState, SkipReason, StaleEnvelope, StaleItem, StaleSummary, SyncEnvelope, SyncModeEnvelope, SyncStagedLogError, UnsupportedFrontmatterError, VerifyBatchEnvelope, VerifyConceptNotFoundError, VerifyEnvelope, VerifySelectionError, VerifyUnsupportedFrontmatterError, buildConfigLayer, collect, conceptFor, configValue, contextEnvelope, files, forDiagnostics, graphEnvelope, json, jsonError, layerOverlayFileSystem, makeOverlayFileSystem, provideConfig, provideDocuments, resolveBundleRoot, resolveDocumentPath, resolveProjectConfig, resolveProjectRoot, run, runContext, runGraph, runStale, runSync, runVerify, runVerifyBatch, sort, staleEnvelope, syncEnvelope, tally, targetPaths, verifyBatchEnvelope, verifyEnvelope, withFallbackRange };
34
+ export { BundleSession, CONFIG_RELATIVE_PATH, ConceptQuery, ConceptSummary, ConfigMalformedError, ConfigPathNotFoundError, ContextEnvelope, ContextField, ContextFieldValue, ContextTag, ContextType, DEFAULT_PROFILE_NAME, DistributionField, DocumentPathError, ENGINE_VERSION, ExternalReferences, FrontmatterEdits, GraphEdgeEnvelope, GraphEnvelope, GraphNodeEnvelope, GraphSummary, InitOverwriteError, JsonDiagnostic, JsonEnvelope, JsonErrorEnvelope, JsonSummary, Now, OkfitPlatform, OverlayDocuments, QueryConceptNotFoundError, QueryGetEnvelope, QueryLink, QueryListEnvelope, QueryNeighbor, QueryNeighborsEnvelope, QuerySelectionError, QueryUnknownVocabularyError, ReferenceCheck, ReferenceState, SkipReason, StaleEnvelope, StaleItem, StaleSummary, SyncEnvelope, SyncModeEnvelope, SyncStagedLogError, UnsupportedFrontmatterError, VerifyBatchEnvelope, VerifyConceptNotFoundError, VerifyEnvelope, VerifySelectionError, VerifyUnsupportedFrontmatterError, buildConfigLayer, collect, conceptFor, configValue, contextEnvelope, files, forDiagnostics, graphEnvelope, json, jsonError, layerOverlayFileSystem, makeOverlayFileSystem, provideConfig, provideDocuments, queryGetEnvelope, queryListEnvelope, queryNeighborsEnvelope, resolveBatchTypes, resolveBundleRoot, resolveDocumentPath, resolveProjectConfig, resolveProjectRoot, run, runContext, runGraph, runStale, runSync, runVerify, runVerifyBatch, selectAttestable, sort, staleEnvelope, syncEnvelope, tally, targetPaths, toConceptSummary, verifyBatchEnvelope, verifyEnvelope, withFallbackRange };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okfit/engine",
3
- "version": "0.9.4",
3
+ "version": "0.10.0",
4
4
  "private": false,
5
5
  "description": "The okfit engine: platform layer, config discovery, and the validate, verify, sync, init and context programs shared by the okfit CLI and MCP server.",
6
6
  "keywords": [
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@effect/platform-node": "4.0.0-rc.118",
38
- "@effected/app": "^0.19.0",
38
+ "@effected/app": "^0.19.1",
39
39
  "@effected/config-file": "^0.13.1",
40
40
  "@effected/engine": "^0.2.0",
41
41
  "@effected/git": "^0.19.0",
@@ -45,7 +45,7 @@
45
45
  "@effected/schemastore": "^0.17.0",
46
46
  "@effected/store": "^0.11.0",
47
47
  "@effected/toml": "^0.10.0",
48
- "@effected/walker": "^0.14.0",
48
+ "@effected/walker": "^0.14.1",
49
49
  "@effected/xdg": "^0.8.2",
50
50
  "@effected/yaml": "^0.18.0",
51
51
  "@okfit/core": "0.8.3",
@@ -0,0 +1,102 @@
1
+ import { QueryConceptNotFoundError, QueryUnknownVocabularyError } from "../errors.js";
2
+ import { ConceptId, Derive, Graph } from "@okfit/core";
3
+ import { Effect, Option } from "effect";
4
+
5
+ //#region src/query/ConceptQuery.ts
6
+ const byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
7
+ const resolve = (bundle, rawId) => Effect.gen(function* () {
8
+ const normalized = ConceptId.normalize(rawId);
9
+ if (Option.isNone(normalized)) return yield* Effect.fail(new QueryConceptNotFoundError({
10
+ id: rawId,
11
+ reason: "empty-id"
12
+ }));
13
+ const id = normalized.value;
14
+ const concept = bundle.concepts.get(id);
15
+ if (concept === void 0) return yield* Effect.fail(new QueryConceptNotFoundError({
16
+ id: rawId,
17
+ reason: "not-a-concept"
18
+ }));
19
+ return {
20
+ id,
21
+ concept
22
+ };
23
+ });
24
+ /**
25
+ * Read-only queries over a loaded bundle, shared by `okfit query` and the MCP
26
+ * tools.
27
+ *
28
+ * @public
29
+ */
30
+ const ConceptQuery = {
31
+ /** Validates types then tags against config (first unknown fails), then filters and sorts by id. */
32
+ list: (bundle, config, filter) => Effect.gen(function* () {
33
+ const declaredTypes = Object.keys(config.types ?? {}).toSorted();
34
+ const declaredTags = Object.keys(config.tags ?? {}).toSorted();
35
+ const types = filter.types ?? [];
36
+ for (const type of types) if (!declaredTypes.includes(type)) return yield* Effect.fail(new QueryUnknownVocabularyError({
37
+ kind: "type",
38
+ requested: type,
39
+ valid: declaredTypes
40
+ }));
41
+ const requestedTags = filter.tags ?? [];
42
+ for (const tag of requestedTags) if (!declaredTags.includes(tag)) return yield* Effect.fail(new QueryUnknownVocabularyError({
43
+ kind: "tag",
44
+ requested: tag,
45
+ valid: declaredTags
46
+ }));
47
+ const statuses = filter.statuses ?? [];
48
+ return [...bundle.concepts.values()].filter((concept) => {
49
+ if (types.length > 0 && !types.includes(concept.frontmatter.type)) return false;
50
+ const tags = concept.frontmatter.tags ?? [];
51
+ if (!requestedTags.every((tag) => tags.includes(tag))) return false;
52
+ if (statuses.length > 0 && !statuses.includes(Derive.status(concept.frontmatter))) return false;
53
+ if (filter.verified !== void 0) {
54
+ if ((concept.frontmatter.verified ?? []).length > 0 !== filter.verified) return false;
55
+ }
56
+ return true;
57
+ }).toSorted(byId);
58
+ }),
59
+ /** One concept by tolerant id, with its outgoing links. */
60
+ get: (bundle, id) => Effect.gen(function* () {
61
+ const resolved = yield* resolve(bundle, id);
62
+ const graph = Graph.fromBundle(bundle);
63
+ const links = graph.edges.filter((edge) => edge.from === resolved.id).map((edge) => ({
64
+ to: edge.to,
65
+ kind: Option.match(graph.node(edge.to), {
66
+ onNone: () => "missing",
67
+ onSome: (node) => node.kind
68
+ }),
69
+ source: edge.data.source,
70
+ ...edge.data.field === void 0 ? {} : { field: edge.data.field }
71
+ }));
72
+ return {
73
+ concept: resolved.concept,
74
+ links
75
+ };
76
+ }),
77
+ /** Outgoing and incoming graph neighbours of one concept. */
78
+ neighbors: (bundle, id) => Effect.gen(function* () {
79
+ const resolved = yield* resolve(bundle, id);
80
+ const graph = Graph.fromBundle(bundle);
81
+ const project = (node) => {
82
+ if (node.kind !== "concept") return {
83
+ id: node.id,
84
+ kind: node.kind,
85
+ concept: null
86
+ };
87
+ return {
88
+ id: node.id,
89
+ kind: node.kind,
90
+ concept: bundle.concepts.get(node.id) ?? null
91
+ };
92
+ };
93
+ return {
94
+ id: resolved.id,
95
+ outgoing: graph.successors(resolved.id).map(project),
96
+ incoming: graph.predecessors(resolved.id).map(project)
97
+ };
98
+ })
99
+ };
100
+
101
+ //#endregion
102
+ export { ConceptQuery };
@@ -0,0 +1,104 @@
1
+ import { DistributionField } from "./distribution.js";
2
+ import { ENGINE_VERSION } from "../version.js";
3
+ import { Derive, GraphNodeKind, Status } from "@okfit/core";
4
+ import { Schema } from "effect";
5
+
6
+ //#region src/render/query.ts
7
+ /** The seven-field shape every list-like query result carries (N-16). @public */
8
+ const ConceptSummary = Schema.Struct({
9
+ id: Schema.String,
10
+ type: Schema.String,
11
+ title: Schema.String,
12
+ description: Schema.NullOr(Schema.String),
13
+ status: Status,
14
+ tags: Schema.Array(Schema.String),
15
+ path: Schema.String
16
+ });
17
+ /** Project one loaded concept into its summary. @public */
18
+ const toConceptSummary = (concept) => ({
19
+ id: concept.id,
20
+ type: concept.frontmatter.type,
21
+ title: Derive.title(concept),
22
+ description: concept.frontmatter.description ?? null,
23
+ status: Derive.status(concept.frontmatter),
24
+ tags: [...concept.frontmatter.tags ?? []],
25
+ path: concept.path
26
+ });
27
+ /** `okfit query list --format json`'s envelope, schema 1, snake_case. @public */
28
+ const QueryListEnvelope = Schema.Struct({
29
+ schema: Schema.Literal(1),
30
+ okfit_version: Schema.String,
31
+ engine_version: Schema.String,
32
+ distribution: DistributionField,
33
+ total: Schema.Int,
34
+ items: Schema.Array(ConceptSummary)
35
+ });
36
+ /** One outgoing link of a concept, as `okfit query get` reports it. @public */
37
+ const QueryLink = Schema.Struct({
38
+ to: Schema.String,
39
+ kind: GraphNodeKind,
40
+ source: Schema.Literals(["body", "frontmatter"]),
41
+ field: Schema.optionalKey(Schema.String)
42
+ });
43
+ /** `okfit query get --format json`'s envelope, schema 1, snake_case. @public */
44
+ const QueryGetEnvelope = Schema.Struct({
45
+ schema: Schema.Literal(1),
46
+ okfit_version: Schema.String,
47
+ engine_version: Schema.String,
48
+ distribution: DistributionField,
49
+ concept: Schema.Struct({
50
+ ...ConceptSummary.fields,
51
+ frontmatter: Schema.Record(Schema.String, Schema.Unknown),
52
+ links: Schema.Array(QueryLink)
53
+ })
54
+ });
55
+ /** One neighbour of a concept; `summary` is `null` for a non-concept node. @public */
56
+ const QueryNeighbor = Schema.Struct({
57
+ id: Schema.String,
58
+ kind: GraphNodeKind,
59
+ summary: Schema.NullOr(ConceptSummary)
60
+ });
61
+ /** `okfit query neighbors --format json`'s envelope, schema 1, snake_case. @public */
62
+ const QueryNeighborsEnvelope = Schema.Struct({
63
+ schema: Schema.Literal(1),
64
+ okfit_version: Schema.String,
65
+ engine_version: Schema.String,
66
+ distribution: DistributionField,
67
+ id: Schema.String,
68
+ outgoing: Schema.Array(QueryNeighbor),
69
+ incoming: Schema.Array(QueryNeighbor)
70
+ });
71
+ /** Build the list envelope; `items` is the already-paged, already-sorted summaries. @public */
72
+ const queryListEnvelope = (input) => ({
73
+ schema: 1,
74
+ okfit_version: input.okfitVersion,
75
+ engine_version: ENGINE_VERSION,
76
+ distribution: input.distribution ?? null,
77
+ total: input.total,
78
+ items: input.items
79
+ });
80
+ /** Build the get envelope from a summary, the raw frontmatter and the outgoing links. @public */
81
+ const queryGetEnvelope = (input) => ({
82
+ schema: 1,
83
+ okfit_version: input.okfitVersion,
84
+ engine_version: ENGINE_VERSION,
85
+ distribution: input.distribution ?? null,
86
+ concept: {
87
+ ...input.concept,
88
+ frontmatter: input.frontmatter,
89
+ links: input.links
90
+ }
91
+ });
92
+ /** Build the neighbors envelope. @public */
93
+ const queryNeighborsEnvelope = (input) => ({
94
+ schema: 1,
95
+ okfit_version: input.okfitVersion,
96
+ engine_version: ENGINE_VERSION,
97
+ distribution: input.distribution ?? null,
98
+ id: input.id,
99
+ outgoing: input.outgoing,
100
+ incoming: input.incoming
101
+ });
102
+
103
+ //#endregion
104
+ export { ConceptSummary, QueryGetEnvelope, QueryLink, QueryListEnvelope, QueryNeighbor, QueryNeighborsEnvelope, queryGetEnvelope, queryListEnvelope, queryNeighborsEnvelope, toConceptSummary };
package/render/verify.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { DistributionField } from "./distribution.js";
2
2
  import { ENGINE_VERSION } from "../version.js";
3
+ import { Status } from "@okfit/core";
3
4
  import { Schema } from "effect";
4
5
 
5
6
  //#region src/render/verify.ts
@@ -23,6 +24,11 @@ const VerifyEnvelope = Schema.Struct({
23
24
  by: Schema.String,
24
25
  at: Schema.String
25
26
  }),
27
+ /** Issue #185: the status change `--stable`/`--draft` made; `null` when neither flag was given. */
28
+ status: Schema.NullOr(Schema.Struct({
29
+ from: Schema.NullOr(Status),
30
+ to: Schema.Literals(["stable", "draft"])
31
+ })),
26
32
  dry_run: Schema.Boolean,
27
33
  exit_code: Schema.Literal(0)
28
34
  });
@@ -45,6 +51,7 @@ const verifyEnvelope = (input) => ({
45
51
  by: input.by,
46
52
  at: input.at
47
53
  },
54
+ status: input.status,
48
55
  dry_run: input.dryRun,
49
56
  exit_code: 0
50
57
  });
@@ -69,7 +76,11 @@ const VerifyBatchEnvelope = Schema.Struct({
69
76
  })),
70
77
  skipped: Schema.Array(Schema.Struct({
71
78
  id: Schema.String,
72
- reason: Schema.Literals(["draft", "already-verified"])
79
+ reason: Schema.Literals([
80
+ "draft",
81
+ "deprecated",
82
+ "already-verified"
83
+ ])
73
84
  })),
74
85
  dry_run: Schema.Boolean,
75
86
  exit_code: Schema.Literal(0)
package/verify/run.js CHANGED
@@ -1,6 +1,7 @@
1
- import { VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError } from "../errors.js";
2
- import { documentNewline, locate, stripBom } from "./locate.js";
3
- import { splice } from "./splice.js";
1
+ import { VerifyConceptNotFoundError, VerifyUnsupportedFrontmatterError } from "../errors.js";
2
+ import { documentNewline, locate, locateTopLevelScalar, stripBom } from "./locate.js";
3
+ import { mergeSameOffset, splice, spliceTopLevelScalar } from "./splice.js";
4
+ import { resolveBatchTypes, selectAttestable } from "./select.js";
4
5
  import { Bundle, ConceptId, Timestamp } from "@okfit/core";
5
6
  import { Effect, FileSystem, Option, Path, Schema } from "effect";
6
7
  import { Derivation } from "@okfit/profiles";
@@ -24,25 +25,39 @@ const UNDECODABLE_CODES = /* @__PURE__ */ new Set([
24
25
  "type-missing"
25
26
  ]);
26
27
  /** Steps 9-14 of runVerify: read, locate (fail closed), splice; no write. */
27
- const prepareVerify = Effect.fn("okfit/verify/prepareVerify")(function* (bundle, concept, actor, at) {
28
+ const prepareVerify = Effect.fn("okfit/verify/prepareVerify")(function* (bundle, concept, actor, at, status) {
28
29
  const fs = yield* FileSystem.FileSystem;
29
30
  const absolutePath = (yield* Path.Path).join(bundle.root, concept.path);
30
- const source = yield* fs.readFileString(absolutePath);
31
+ const source = new TextDecoder("utf-8", { ignoreBOM: true }).decode(yield* fs.readFile(absolutePath));
31
32
  const { text, bom } = stripBom(source);
32
33
  const located = yield* locate(text);
33
34
  if (located._tag === "unsupported") return yield* new VerifyUnsupportedFrontmatterError({
34
35
  id: concept.id,
35
- shape: located.shape
36
+ shape: located.shape,
37
+ key: "verified"
36
38
  });
37
39
  const priorAt = (concept.frontmatter.verified ?? []).filter((entry) => entry.by === actor).map((entry) => Schema.encodeSync(Timestamp)(entry.at));
38
- const edit = splice(located, {
40
+ const newline = documentNewline(text);
41
+ const verifiedEdit = splice(located, {
39
42
  by: actor,
40
43
  at
41
- }, documentNewline(text));
44
+ }, newline);
45
+ let statusEdit;
46
+ if (status !== void 0 && concept.frontmatter.status !== status) {
47
+ const target = yield* locateTopLevelScalar(text, "status");
48
+ if (target._tag === "unsupported") return yield* new VerifyUnsupportedFrontmatterError({
49
+ id: concept.id,
50
+ shape: target.shape,
51
+ key: "status"
52
+ });
53
+ statusEdit = spliceTopLevelScalar(target, "status", status, newline);
54
+ }
55
+ const edits = mergeSameOffset([verifiedEdit, ...statusEdit === void 0 ? [] : [statusEdit]].toSorted((a, b) => a.offset - b.offset || (a === statusEdit ? -1 : b === statusEdit ? 1 : 0)));
42
56
  return {
43
57
  absolutePath,
44
- finalText: bom + MarkdownEdit.applyAll(text, [edit]),
45
- fragment: edit.content,
58
+ finalText: bom + MarkdownEdit.applyAll(text, edits),
59
+ fragment: verifiedEdit.content,
60
+ statusFragment: statusEdit?.content ?? null,
46
61
  priorAt
47
62
  };
48
63
  });
@@ -61,8 +76,9 @@ const writeVerified = Effect.fn("okfit/verify/writeVerified")(function* (absolut
61
76
  * human actor from git, splices exactly one entry into the concept's
62
77
  * frontmatter and writes it back atomically.
63
78
  *
64
- * Nothing here reads `status`, `stale_after`, or a type's
65
- * `require_verified`: `verify` is mechanical, not config-aware (V-4, V-5).
79
+ * Nothing here reads `stale_after` or a type's `require_verified`; `status`
80
+ * is read only to skip a no-op edit under `options.status` (#185). `verify`
81
+ * is mechanical, not config-aware (V-4, V-5).
66
82
  *
67
83
  * @internal
68
84
  */
@@ -99,7 +115,7 @@ const runVerify = Effect.fn("okfit/verify/runVerify")(function* (options) {
99
115
  config: options.config
100
116
  });
101
117
  const at = Schema.encodeSync(Timestamp)(options.at);
102
- const prepared = yield* prepareVerify(bundle, concept, actor, at);
118
+ const prepared = yield* prepareVerify(bundle, concept, actor, at, options.status);
103
119
  if (!options.dryRun) yield* writeVerified(prepared.absolutePath, prepared.finalText);
104
120
  return {
105
121
  id,
@@ -109,13 +125,18 @@ const runVerify = Effect.fn("okfit/verify/runVerify")(function* (options) {
109
125
  at,
110
126
  priorAt: prepared.priorAt,
111
127
  dryRun: options.dryRun,
112
- fragment: prepared.fragment
128
+ fragment: prepared.fragment,
129
+ status: options.status === void 0 ? null : {
130
+ from: concept.frontmatter.status ?? null,
131
+ to: options.status
132
+ },
133
+ statusFragment: prepared.statusFragment
113
134
  };
114
135
  });
115
136
  /**
116
137
  * Issue #138. Attests every concept selected by `options.types` (or, when
117
138
  * empty, every type whose declaration sets `require_verified = true`) that
118
- * is not a draft and does not already carry a `verified` entry by the
139
+ * is not a draft, is not deprecated (#143) and does not already carry a `verified` entry by the
119
140
  * resolved actor. Every candidate is located FIRST -- an unsupported
120
141
  * `verified` shape on any one of them fails the whole batch with nothing
121
142
  * written, mirroring `runVerify`'s own fail-closed rule at batch scale.
@@ -124,45 +145,20 @@ const runVerify = Effect.fn("okfit/verify/runVerify")(function* (options) {
124
145
  */
125
146
  const runVerifyBatch = Effect.fn("okfit/verify/runVerifyBatch")(function* (options) {
126
147
  const bundle = yield* Bundle.load({ root: options.bundleRoot });
127
- const declared = options.config.types ?? {};
128
- let types;
129
- if (options.types.length > 0) {
130
- for (const type of options.types) if (!Object.hasOwn(declared, type)) return yield* new VerifySelectionError({
131
- reason: "unknown-type",
132
- detail: type
133
- });
134
- types = new Set(options.types);
135
- } else types = new Set(Object.entries(declared).flatMap(([name, spec]) => spec.require_verified === true ? [name] : []));
148
+ const types = yield* resolveBatchTypes(options.config, options.types);
136
149
  const actor = yield* Derivation.generatedBy({
137
150
  writer: "human",
138
151
  cwd: options.projectRoot,
139
152
  config: options.config
140
153
  });
141
154
  const at = Schema.encodeSync(Timestamp)(options.at);
142
- const skipped = [];
155
+ const selection = selectAttestable(bundle, types, actor);
143
156
  const prepared = [];
144
- for (const [id, concept] of bundle.concepts) {
145
- if (!types.has(concept.frontmatter.type)) continue;
146
- if (concept.frontmatter.status === "draft") {
147
- skipped.push({
148
- id,
149
- reason: "draft"
150
- });
151
- continue;
152
- }
153
- if ((concept.frontmatter.verified ?? []).some((entry) => entry.by === actor)) {
154
- skipped.push({
155
- id,
156
- reason: "already-verified"
157
- });
158
- continue;
159
- }
160
- prepared.push({
161
- id,
162
- conceptPath: concept.path,
163
- ...yield* prepareVerify(bundle, concept, actor, at)
164
- });
165
- }
157
+ for (const concept of selection.candidates) prepared.push({
158
+ id: concept.id,
159
+ conceptPath: concept.path,
160
+ ...yield* prepareVerify(bundle, concept, actor, at, void 0)
161
+ });
166
162
  if (!options.dryRun) for (const entry of prepared) yield* writeVerified(entry.absolutePath, entry.finalText);
167
163
  return {
168
164
  bundleRoot: bundle.root,
@@ -174,7 +170,7 @@ const runVerifyBatch = Effect.fn("okfit/verify/runVerifyBatch")(function* (optio
174
170
  conceptPath,
175
171
  fragment
176
172
  })),
177
- skipped
173
+ skipped: selection.skipped
178
174
  };
179
175
  });
180
176
 
@@ -0,0 +1,59 @@
1
+ import { VerifySelectionError } from "../errors.js";
2
+ import { Effect } from "effect";
3
+
4
+ //#region src/verify/select.ts
5
+ /**
6
+ * Issue #138/#143: which concepts of `types` the actor may attest in one
7
+ * batch. A draft is unsettled and a deprecated concept is retired (#143), so
8
+ * both are skipped before the already-verified check.
9
+ *
10
+ * @public
11
+ */
12
+ const selectAttestable = (bundle, types, actor) => {
13
+ const candidates = [];
14
+ const skipped = [];
15
+ for (const [id, concept] of bundle.concepts) {
16
+ if (!types.has(concept.frontmatter.type)) continue;
17
+ const status = concept.frontmatter.status;
18
+ if (status === "draft" || status === "deprecated") {
19
+ skipped.push({
20
+ id,
21
+ reason: status
22
+ });
23
+ continue;
24
+ }
25
+ if ((concept.frontmatter.verified ?? []).some((entry) => entry.by === actor)) {
26
+ skipped.push({
27
+ id,
28
+ reason: "already-verified"
29
+ });
30
+ continue;
31
+ }
32
+ candidates.push(concept);
33
+ }
34
+ return {
35
+ candidates,
36
+ skipped
37
+ };
38
+ };
39
+ /**
40
+ * The batch type-resolution rule shared by `runVerifyBatch` and any future
41
+ * interactive picker: explicit `types` must each be declared in
42
+ * `config.types` (else `VerifySelectionError` with reason `unknown-type`);
43
+ * an empty `types` selects every declared type whose `require_verified` is
44
+ * `true`.
45
+ *
46
+ * @public
47
+ */
48
+ const resolveBatchTypes = (config, types) => {
49
+ const declared = config.types ?? {};
50
+ if (types.length === 0) return Effect.succeed(new Set(Object.entries(declared).flatMap(([name, spec]) => spec.require_verified === true ? [name] : [])));
51
+ for (const type of types) if (!Object.hasOwn(declared, type)) return Effect.fail(new VerifySelectionError({
52
+ reason: "unknown-type",
53
+ detail: type
54
+ }));
55
+ return Effect.succeed(new Set(types));
56
+ };
57
+
58
+ //#endregion
59
+ export { resolveBatchTypes, selectAttestable };
package/verify/splice.js CHANGED
@@ -128,6 +128,30 @@ const spliceGeneratedBlock = (target, fields, newline) => MarkdownEdit.make({
128
128
  length: 0,
129
129
  content: `generated:${newline} by: ${fields.by}${newline} at: ${fields.at}${newline} body_sha256: ${fields.bodySha256}${newline}`
130
130
  });
131
+ /**
132
+ * Issue #185: `okfit verify --stable` splices `status` and `verified` in one
133
+ * write. When `status` is absent it is inserted after `title:` (else `type:`),
134
+ * and when `verified` is absent it is appended as the last key, so both land
135
+ * on the same offset when that anchor is the last key. `MarkdownEdit.applyAll`
136
+ * rejects two zero-length inserts at one offset, the same case
137
+ * `spliceGeneratedFields` merges, so this folds them into one edit, input
138
+ * order preserved.
139
+ *
140
+ * @internal
141
+ */
142
+ const mergeSameOffset = (edits) => {
143
+ const out = [];
144
+ for (const edit of edits) {
145
+ const previous = out.at(-1);
146
+ if (previous !== void 0 && previous.length === 0 && edit.length === 0 && previous.offset === edit.offset) out[out.length - 1] = MarkdownEdit.make({
147
+ offset: edit.offset,
148
+ length: 0,
149
+ content: previous.content + edit.content
150
+ });
151
+ else out.push(edit);
152
+ }
153
+ return out;
154
+ };
131
155
 
132
156
  //#endregion
133
- export { splice, spliceGenerated, spliceGeneratedBlock, spliceGeneratedFields, spliceTopLevelScalar };
157
+ export { mergeSameOffset, splice, spliceGenerated, spliceGeneratedBlock, spliceGeneratedFields, spliceTopLevelScalar };
package/version.js CHANGED
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * @public
13
13
  */
14
- const ENGINE_VERSION = "0.9.4";
14
+ const ENGINE_VERSION = "0.10.0";
15
15
 
16
16
  //#endregion
17
17
  export { ENGINE_VERSION };