@okfit/engine 0.6.0 → 0.7.1

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/verify/run.js CHANGED
@@ -1,4 +1,4 @@
1
- import { VerifyConceptNotFoundError, VerifyUnsupportedFrontmatterError } from "../errors.js";
1
+ import { VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError } from "../errors.js";
2
2
  import { documentNewline, locate, stripBom } from "./locate.js";
3
3
  import { splice } from "./splice.js";
4
4
  import { Bundle, ConceptId, Timestamp } from "@okfit/core";
@@ -23,6 +23,39 @@ const UNDECODABLE_CODES = /* @__PURE__ */ new Set([
23
23
  "frontmatter-missing",
24
24
  "type-missing"
25
25
  ]);
26
+ /** 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 fs = yield* FileSystem.FileSystem;
29
+ const absolutePath = (yield* Path.Path).join(bundle.root, concept.path);
30
+ const source = yield* fs.readFileString(absolutePath);
31
+ const { text, bom } = stripBom(source);
32
+ const located = yield* locate(text);
33
+ if (located._tag === "unsupported") return yield* new VerifyUnsupportedFrontmatterError({
34
+ id: concept.id,
35
+ shape: located.shape
36
+ });
37
+ const priorAt = (concept.frontmatter.verified ?? []).filter((entry) => entry.by === actor).map((entry) => Schema.encodeSync(Timestamp)(entry.at));
38
+ const edit = splice(located, {
39
+ by: actor,
40
+ at
41
+ }, documentNewline(text));
42
+ return {
43
+ absolutePath,
44
+ finalText: bom + MarkdownEdit.applyAll(text, [edit]),
45
+ fragment: edit.content,
46
+ priorAt
47
+ };
48
+ });
49
+ /** The I4/V-16 atomic write: real path, temp beside it, mode preserved, rename. */
50
+ const writeVerified = Effect.fn("okfit/verify/writeVerified")(function* (absolutePath, finalText) {
51
+ const fs = yield* FileSystem.FileSystem;
52
+ const target = yield* fs.realPath(absolutePath);
53
+ const tempPath = `${target}.okfit-verify.tmp`;
54
+ const original = yield* fs.stat(target);
55
+ yield* fs.writeFileString(tempPath, finalText);
56
+ yield* fs.chmod(tempPath, original.mode);
57
+ yield* fs.rename(tempPath, target).pipe(Effect.onError(() => fs.remove(tempPath, { force: true }).pipe(Effect.ignore)));
58
+ });
26
59
  /**
27
60
  * Contract §2.3 steps 6–15. Loads the bundle, resolves the id, resolves the
28
61
  * human actor from git, splices exactly one entry into the concept's
@@ -34,8 +67,6 @@ const UNDECODABLE_CODES = /* @__PURE__ */ new Set([
34
67
  * @internal
35
68
  */
36
69
  const runVerify = Effect.fn("okfit/verify/runVerify")(function* (options) {
37
- const fs = yield* FileSystem.FileSystem;
38
- const path = yield* Path.Path;
39
70
  const bundle = yield* Bundle.load({ root: options.bundleRoot });
40
71
  const normalized = ConceptId.normalize(options.id);
41
72
  if (Option.isNone(normalized)) return yield* new VerifyConceptNotFoundError({
@@ -67,40 +98,85 @@ const runVerify = Effect.fn("okfit/verify/runVerify")(function* (options) {
67
98
  cwd: options.projectRoot,
68
99
  config: options.config
69
100
  });
70
- const absolutePath = path.join(bundle.root, concept.path);
71
- const source = yield* fs.readFileString(absolutePath);
72
- const { text, bom } = stripBom(source);
73
- const located = yield* locate(text);
74
- if (located._tag === "unsupported") return yield* new VerifyUnsupportedFrontmatterError({
101
+ const at = Schema.encodeSync(Timestamp)(options.at);
102
+ const prepared = yield* prepareVerify(bundle, concept, actor, at);
103
+ if (!options.dryRun) yield* writeVerified(prepared.absolutePath, prepared.finalText);
104
+ return {
75
105
  id,
76
- shape: located.shape
106
+ bundleRoot: bundle.root,
107
+ conceptPath: concept.path,
108
+ by: actor,
109
+ at,
110
+ priorAt: prepared.priorAt,
111
+ dryRun: options.dryRun,
112
+ fragment: prepared.fragment
113
+ };
114
+ });
115
+ /**
116
+ * Issue #138. Attests every concept selected by `options.types` (or, when
117
+ * 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
119
+ * resolved actor. Every candidate is located FIRST -- an unsupported
120
+ * `verified` shape on any one of them fails the whole batch with nothing
121
+ * written, mirroring `runVerify`'s own fail-closed rule at batch scale.
122
+ *
123
+ * @public
124
+ */
125
+ const runVerifyBatch = Effect.fn("okfit/verify/runVerifyBatch")(function* (options) {
126
+ 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] : []));
136
+ const actor = yield* Derivation.generatedBy({
137
+ writer: "human",
138
+ cwd: options.projectRoot,
139
+ config: options.config
77
140
  });
78
141
  const at = Schema.encodeSync(Timestamp)(options.at);
79
- const priorAt = (concept.frontmatter.verified ?? []).filter((entry) => entry.by === actor).map((entry) => Schema.encodeSync(Timestamp)(entry.at));
80
- const edit = splice(located, {
81
- by: actor,
82
- at
83
- }, documentNewline(text));
84
- const finalText = bom + MarkdownEdit.applyAll(text, [edit]);
85
- if (!options.dryRun) {
86
- const target = yield* fs.realPath(absolutePath);
87
- const tempPath = `${target}.okfit-verify.tmp`;
88
- const original = yield* fs.stat(target);
89
- yield* fs.writeFileString(tempPath, finalText);
90
- yield* fs.chmod(tempPath, original.mode);
91
- yield* fs.rename(tempPath, target).pipe(Effect.onError(() => fs.remove(tempPath, { force: true }).pipe(Effect.ignore)));
142
+ const skipped = [];
143
+ 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
+ });
92
165
  }
166
+ if (!options.dryRun) for (const entry of prepared) yield* writeVerified(entry.absolutePath, entry.finalText);
93
167
  return {
94
- id,
95
168
  bundleRoot: bundle.root,
96
- conceptPath: concept.path,
97
169
  by: actor,
98
170
  at,
99
- priorAt,
100
171
  dryRun: options.dryRun,
101
- fragment: edit.content
172
+ verified: prepared.map(({ id, conceptPath, fragment }) => ({
173
+ id,
174
+ conceptPath,
175
+ fragment
176
+ })),
177
+ skipped
102
178
  };
103
179
  });
104
180
 
105
181
  //#endregion
106
- export { runVerify };
182
+ export { runVerify, runVerifyBatch };
package/verify/splice.js CHANGED
@@ -92,6 +92,18 @@ const spliceGeneratedFields = (atTarget, bodySha256Target, encodedAt, bodySha256
92
92
  })];
93
93
  return [spliceGenerated(atTarget, encodedAt, newline, "at"), spliceGenerated(bodySha256Target, bodySha256, newline, "body_sha256")];
94
94
  };
95
+ /**
96
+ * A whole `generated:` block mapping, appended as the LAST top-level key
97
+ * (issue #73) — block style, matching how this repository writes every
98
+ * `generated:` and how `splice`'s own `absent` case writes `verified:`.
99
+ *
100
+ * @internal
101
+ */
102
+ const spliceGeneratedBlock = (target, fields, newline) => MarkdownEdit.make({
103
+ offset: target.insertAt,
104
+ length: 0,
105
+ content: `generated:${newline} by: ${fields.by}${newline} at: ${fields.at}${newline} body_sha256: ${fields.bodySha256}${newline}`
106
+ });
95
107
 
96
108
  //#endregion
97
- export { splice, spliceGenerated, spliceGeneratedFields };
109
+ export { splice, spliceGenerated, spliceGeneratedBlock, spliceGeneratedFields };
package/version.js ADDED
@@ -0,0 +1,17 @@
1
+ //#region src/version.ts
2
+ /**
3
+ * The version this engine reports as `engine_version` in every JSON
4
+ * envelope. `@savvy-web/bundler` replaces `process.env.__PACKAGE_VERSION__`
5
+ * with this package's own version at build time (K-32), so a release can
6
+ * never desync from the printed version. `"0.0.0"` is the unbuilt-source
7
+ * fallback and reads as dev mode. This is the ONE `process` read this
8
+ * package's own `boundaries.test.ts` allowlists — see that file and this
9
+ * package's `CLAUDE.md` for why it is safe here even though every other
10
+ * `process` read is forbidden.
11
+ *
12
+ * @public
13
+ */
14
+ const ENGINE_VERSION = "0.7.1";
15
+
16
+ //#endregion
17
+ export { ENGINE_VERSION };