@okfit/engine 0.9.4 → 0.11.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.
@@ -0,0 +1,86 @@
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
+ const requireVerifiedTypes = (config) => new Set(Object.entries(config.types ?? {}).flatMap(([name, spec]) => spec.require_verified === true ? [name] : []));
40
+ /**
41
+ * Every concept whose type sets `require_verified` that `actor` has not
42
+ * attested: drafts included, deprecated concepts and the actor's own
43
+ * attestations excluded. Sorted by type, then id; grouping is the caller's.
44
+ *
45
+ * @public
46
+ */
47
+ const selectPickerCandidates = (bundle, config, actor) => {
48
+ const types = requireVerifiedTypes(config);
49
+ const rows = [];
50
+ for (const [id, concept] of bundle.concepts) {
51
+ const { type, status, title, description } = concept.frontmatter;
52
+ if (!types.has(type) || status === "deprecated") continue;
53
+ const verified = concept.frontmatter.verified ?? [];
54
+ if (verified.some((entry) => entry.by === actor)) continue;
55
+ rows.push({
56
+ id,
57
+ type,
58
+ status: status === "draft" ? "draft" : "stable",
59
+ title: title ?? null,
60
+ description: description ?? null,
61
+ otherAttestations: verified.length
62
+ });
63
+ }
64
+ return rows.toSorted((a, b) => a.type < b.type ? -1 : a.type > b.type ? 1 : a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
65
+ };
66
+ /**
67
+ * The batch type-resolution rule shared by `runVerifyBatch` and any future
68
+ * interactive picker: explicit `types` must each be declared in
69
+ * `config.types` (else `VerifySelectionError` with reason `unknown-type`);
70
+ * an empty `types` selects every declared type whose `require_verified` is
71
+ * `true`.
72
+ *
73
+ * @public
74
+ */
75
+ const resolveBatchTypes = (config, types) => {
76
+ const declared = config.types ?? {};
77
+ if (types.length === 0) return Effect.succeed(requireVerifiedTypes(config));
78
+ for (const type of types) if (!Object.hasOwn(declared, type)) return Effect.fail(new VerifySelectionError({
79
+ reason: "unknown-type",
80
+ detail: type
81
+ }));
82
+ return Effect.succeed(new Set(types));
83
+ };
84
+
85
+ //#endregion
86
+ export { resolveBatchTypes, selectAttestable, selectPickerCandidates };
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.11.0";
15
15
 
16
16
  //#endregion
17
17
  export { ENGINE_VERSION };