@okfit/engine 0.1.0 → 0.2.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/config/anchor.js CHANGED
@@ -15,7 +15,7 @@ const anchorForExplicit = (configPath, path) => {
15
15
  /**
16
16
  * K-12's project root, as amended by K-58 and C1-2, in order:
17
17
  *
18
- * 1. `pathArg`, if given. `Argument.path` has already resolved it absolute.
18
+ * 1. `pathArg`, if given. `Argument.Path` has already resolved it absolute.
19
19
  * 2. otherwise, if `--config` was given: `anchorForExplicit` applied to that
20
20
  * path.
21
21
  * 3. otherwise, if a config was discovered by the `"project"` resolver
package/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { BundleLoadError, ConceptId, Diagnostic, DiagnosticRange, DiagnosticSeverity, LoadedBundle, OkfitConfig, OkfitConfigFile, ValidationReport } from "@okfit/core";
2
- import { Context, DateTime, Effect, FileSystem, Layer, Option, Path, PlatformError, Runtime, Schema } from "effect";
2
+ import { Context, Crypto, DateTime, Effect, FileSystem, Layer, Option, Path, PlatformError, Runtime, Schema } from "effect";
3
3
  import { AppDirs, Xdg } from "@effected/xdg";
4
4
  import { ConfigReadError } from "@effected/config-file";
5
5
  import { GeneratedAtError, GitHistory, GitHistoryError, Layout, Profile, ProfileDiagnostic } from "@okfit/profiles";
@@ -32,7 +32,7 @@ interface DiscoveredConfig {
32
32
  /**
33
33
  * K-12's project root, as amended by K-58 and C1-2, in order:
34
34
  *
35
- * 1. `pathArg`, if given. `Argument.path` has already resolved it absolute.
35
+ * 1. `pathArg`, if given. `Argument.Path` has already resolved it absolute.
36
36
  * 2. otherwise, if `--config` was given: `anchorForExplicit` applied to that
37
37
  * path.
38
38
  * 3. otherwise, if a config was discovered by the `"project"` resolver
@@ -714,7 +714,7 @@ interface SyncResult {
714
714
  *
715
715
  * @public
716
716
  */
717
- export declare const runSync: (options: SyncOptions) => Effect.Effect<SyncResult, BundleLoadError | GeneratedAtError, Git | GitHistory | FileSystem.FileSystem | Path.Path>;
717
+ export declare const runSync: (options: SyncOptions) => Effect.Effect<SyncResult, BundleLoadError | GeneratedAtError, Git | GitHistory | FileSystem.FileSystem | Path.Path | Crypto.Crypto>;
718
718
  //#endregion
719
719
  //#region src/render/sync.d.ts
720
720
  /** @public */
@@ -860,13 +860,17 @@ interface RunOptions {
860
860
  /** `OKFIT_NOW` or `DateTime.now`, from `bin.ts` (K-47); enables the `stale` rule (D-34). */
861
861
  readonly now: DateTime.Utc;
862
862
  /**
863
- * S-31: skip `Provenance.lint`'s git tier for this one invocation,
864
- * without touching the project's `[lint]` table. `commands/validate.ts`
865
- * threads `--skip-provenance` here; the PostToolUse hook passes the flag
866
- * so an edit-time `validate` stays git-free even when the config leaves
867
- * `generated-at-drift` at its default severity. Defaults to `false`, and
868
- * is checked the same way `severity === "off"` already is — `run` skips
869
- * the walk when EITHER is true.
863
+ * S-31: skip `Provenance.lint`'s git TIER (tier 2, the git-derived date
864
+ * comparison for concepts with no recorded `generated.body_sha256`)
865
+ * for this one invocation, without touching the project's `[lint]`
866
+ * table. `commands/validate.ts` threads `--skip-provenance` here; the
867
+ * PostToolUse hook passes the flag so an edit-time `validate` stays
868
+ * git-free even when the config leaves `generated-at-drift` at its
869
+ * default severity. Tier 1 (the pure, in-memory body-digest comparison)
870
+ * still runs regardless — that is exactly the signal edit time wants,
871
+ * at the cost of one sha256 over text already in memory. Defaults to
872
+ * `false`; `run` still gates the whole lint call on `severity ===
873
+ * "off"`, but no longer on this flag.
870
874
  */
871
875
  readonly skipProvenance?: boolean;
872
876
  }
@@ -879,11 +883,15 @@ interface RunResult {
879
883
  }
880
884
  /**
881
885
  * Load, validate both tiers, run the profile check, then — UNLESS the
882
- * `generated-at-drift` lint is `off` OR `options.skipProvenance` is `true`
883
- * — run `Provenance.lint` and append its `Diagnostic`s to `report.lint`.
884
- * The "skip the git walk entirely" gate lives HERE, in `run`, not inside
885
- * `Provenance.lint` (S-8's own wording): a bundle configured `off`, or a
886
- * caller that passed `--skip-provenance`, never pays for a git spawn
886
+ * `generated-at-drift` lint is `off` — run `Provenance.lint` and append its
887
+ * `Diagnostic`s to `report.lint`. The "skip the whole lint" gate lives
888
+ * HERE, in `run`: a bundle configured `off` never calls `Provenance.lint`
889
+ * at all. `options.skipProvenance` is no longer that gate — it is threaded
890
+ * through as `Provenance.lint`'s `skipGitTier` option instead, so tier 1
891
+ * (the pure, in-memory `generated.body_sha256` comparison) still runs and
892
+ * still reports drift even when `skipProvenance` is `true`; only tier 2
893
+ * (the git-derived date comparison, for concepts with no recorded digest)
894
+ * is skipped, and with it the only git spawn `Provenance.lint` can make
887
895
  * (S-31). An `error` severity on the appended diagnostics yields exit `1`
888
896
  * through the EXISTING lint-tier rule in `render/exit.ts` — no renderer
889
897
  * branch, no new `DiagnosticSource`, since these diagnostics flow through
@@ -899,7 +907,7 @@ interface RunResult {
899
907
  *
900
908
  * @public
901
909
  */
902
- export declare const run: (options: RunOptions) => Effect.Effect<RunResult, BundleLoadError | GitHistoryError | GitCommandError | UnknownRefError | PlatformError.PlatformError, FileSystem.FileSystem | Path.Path | Git | GitHistory>;
910
+ export declare const run: (options: RunOptions) => Effect.Effect<RunResult, BundleLoadError | GitHistoryError | GitCommandError | UnknownRefError | PlatformError.PlatformError, FileSystem.FileSystem | Path.Path | Git | GitHistory | Crypto.Crypto>;
903
911
  //#endregion
904
912
  //#region src/verify/run.d.ts
905
913
  /** @internal */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okfit/engine",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
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": [
@@ -34,21 +34,21 @@
34
34
  "./package.json": "./package.json"
35
35
  },
36
36
  "dependencies": {
37
- "@effect/platform-node": "4.0.0-rc.112",
38
- "@effected/app": "^0.15.0",
39
- "@effected/config-file": "^0.7.0",
40
- "@effected/git": "^0.12.0",
41
- "@effected/glob": "^0.5.0",
42
- "@effected/jsonc": "^0.9.0",
43
- "@effected/markdown": "^0.9.1",
44
- "@effected/store": "^0.7.0",
45
- "@effected/toml": "^0.6.0",
46
- "@effected/walker": "^0.7.0",
47
- "@effected/xdg": "^0.4.1",
48
- "@effected/yaml": "^0.14.0",
49
- "@okfit/core": "0.2.0",
50
- "@okfit/profiles": "0.2.0",
51
- "effect": "4.0.0-rc.112"
37
+ "@effect/platform-node": "4.0.0-rc.115",
38
+ "@effected/app": "^0.16.0",
39
+ "@effected/config-file": "^0.8.0",
40
+ "@effected/git": "^0.15.0",
41
+ "@effected/glob": "^0.6.0",
42
+ "@effected/jsonc": "^0.11.0",
43
+ "@effected/markdown": "^0.10.0",
44
+ "@effected/store": "^0.8.0",
45
+ "@effected/toml": "^0.7.0",
46
+ "@effected/walker": "^0.8.0",
47
+ "@effected/xdg": "^0.5.0",
48
+ "@effected/yaml": "^0.15.0",
49
+ "@okfit/core": "0.3.1",
50
+ "@okfit/profiles": "0.3.1",
51
+ "effect": "4.0.0-rc.115"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=24.11.0"
package/sync/generated.js CHANGED
@@ -1,8 +1,9 @@
1
- import { detectNewline, locateGenerated, stripBom } from "../verify/locate.js";
2
- import { spliceGenerated } from "../verify/splice.js";
1
+ import { detectNewline, locateGenerated, locateGeneratedBodySha256, stripBom } from "../verify/locate.js";
2
+ import { spliceGeneratedFields } from "../verify/splice.js";
3
3
  import { writeAtomic } from "./write.js";
4
4
  import { Timestamp } from "@okfit/core";
5
- import { DateTime, Effect, FileSystem, Path, Schema } from "effect";
5
+ import { Effect, FileSystem, Path, Schema } from "effect";
6
+ import { Derivation } from "@okfit/profiles";
6
7
  import { MarkdownEdit } from "@effected/markdown";
7
8
 
8
9
  //#region src/sync/generated.ts
@@ -73,8 +74,8 @@ const syncGenerated = Effect.fn("okfit/sync/syncGenerated")(function* (bundle, p
73
74
  });
74
75
  continue;
75
76
  }
76
- const recorded = generated.at;
77
- if (recorded !== void 0 && DateTime.Equivalence(recorded, derived.at)) {
77
+ const currentDigest = yield* Derivation.bodyDigest(concept.document.source);
78
+ if (generated.body_sha256 !== void 0 && generated.body_sha256 === currentDigest) {
78
79
  unchanged.push(id);
79
80
  continue;
80
81
  }
@@ -82,8 +83,9 @@ const syncGenerated = Effect.fn("okfit/sync/syncGenerated")(function* (bundle, p
82
83
  const source = yield* fs.readFileString(absolutePath);
83
84
  const { text, bom } = stripBom(source);
84
85
  const newline = detectNewline(text);
85
- const located = yield* locateGenerated(text).pipe(Effect.orDie);
86
- if (located._tag === "unsupported") {
86
+ const atLocated = yield* locateGenerated(text).pipe(Effect.orDie);
87
+ const bodySha256Located = yield* locateGeneratedBodySha256(text).pipe(Effect.orDie);
88
+ if (atLocated._tag === "unsupported" || bodySha256Located._tag === "unsupported") {
87
89
  skipped.push({
88
90
  id,
89
91
  reason: "generated-unsupported"
@@ -91,8 +93,8 @@ const syncGenerated = Effect.fn("okfit/sync/syncGenerated")(function* (bundle, p
91
93
  continue;
92
94
  }
93
95
  const encodedAt = encodeAt(derived.at);
94
- const edit = spliceGenerated(located, encodedAt, newline);
95
- const finalText = bom + MarkdownEdit.applyAll(text, [edit]);
96
+ const edits = spliceGeneratedFields(atLocated, bodySha256Located, encodedAt, currentDigest, newline);
97
+ const finalText = bom + MarkdownEdit.applyAll(text, edits);
96
98
  if (dryRun) {
97
99
  written.push(id);
98
100
  continue;
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.59.0"
8
+ "packageVersion": "7.59.1"
9
9
  }
10
10
  ]
11
11
  }
package/validate/run.js CHANGED
@@ -21,11 +21,15 @@ import { Provenance } from "@okfit/profiles";
21
21
  var Now = class extends Context.Service()("@okfit/engine/Now") {};
22
22
  /**
23
23
  * Load, validate both tiers, run the profile check, then — UNLESS the
24
- * `generated-at-drift` lint is `off` OR `options.skipProvenance` is `true`
25
- * — run `Provenance.lint` and append its `Diagnostic`s to `report.lint`.
26
- * The "skip the git walk entirely" gate lives HERE, in `run`, not inside
27
- * `Provenance.lint` (S-8's own wording): a bundle configured `off`, or a
28
- * caller that passed `--skip-provenance`, never pays for a git spawn
24
+ * `generated-at-drift` lint is `off` — run `Provenance.lint` and append its
25
+ * `Diagnostic`s to `report.lint`. The "skip the whole lint" gate lives
26
+ * HERE, in `run`: a bundle configured `off` never calls `Provenance.lint`
27
+ * at all. `options.skipProvenance` is no longer that gate — it is threaded
28
+ * through as `Provenance.lint`'s `skipGitTier` option instead, so tier 1
29
+ * (the pure, in-memory `generated.body_sha256` comparison) still runs and
30
+ * still reports drift even when `skipProvenance` is `true`; only tier 2
31
+ * (the git-derived date comparison, for concepts with no recorded digest)
32
+ * is skipped, and with it the only git spawn `Provenance.lint` can make
29
33
  * (S-31). An `error` severity on the appended diagnostics yields exit `1`
30
34
  * through the EXISTING lint-tier rule in `render/exit.ts` — no renderer
31
35
  * branch, no new `DiagnosticSource`, since these diagnostics flow through
@@ -48,7 +52,7 @@ const run = (options) => Effect.gen(function* () {
48
52
  onNone: () => [],
49
53
  onSome: (profile) => profile.check(bundle)
50
54
  });
51
- const provenance = OkfitConfig.severityFor(options.config, "generated-at-drift") === "off" || options.skipProvenance === true ? [] : yield* Provenance.lint(bundle, options.config);
55
+ const provenance = OkfitConfig.severityFor(options.config, "generated-at-drift") === "off" ? [] : yield* Provenance.lint(bundle, options.config, { skipGitTier: options.skipProvenance === true });
52
56
  return {
53
57
  bundle,
54
58
  report: {
package/verify/locate.js CHANGED
@@ -157,7 +157,7 @@ const locate = Effect.fn("okfit/verify/locate")(function* (source) {
157
157
  *
158
158
  * @internal
159
159
  */
160
- const locateGenerated = Effect.fn("okfit/verify/locateGenerated")(function* (source) {
160
+ const locateGeneratedField = Effect.fn("okfit/verify/locateGeneratedField")(function* (source, field) {
161
161
  const block = FrontmatterSource.split(source).frontmatter;
162
162
  if (block === void 0) return {
163
163
  _tag: "unsupported",
@@ -205,8 +205,8 @@ const locateGenerated = Effect.fn("okfit/verify/locateGenerated")(function* (sou
205
205
  shape: "flow-mapping"
206
206
  };
207
207
  const indent = indentAt(value, node.offset);
208
- const atPair = node.items.find((item) => item.key instanceof YamlScalar && item.key.value === "at");
209
- if (atPair === void 0) {
208
+ const fieldPair = node.items.find((item) => item.key instanceof YamlScalar && item.key.value === field);
209
+ if (fieldPair === void 0) {
210
210
  const last = node.items[node.items.length - 1];
211
211
  if (last === void 0) return {
212
212
  _tag: "unsupported",
@@ -220,30 +220,40 @@ const locateGenerated = Effect.fn("okfit/verify/locateGenerated")(function* (sou
220
220
  indent
221
221
  };
222
222
  }
223
- const atValue = atPair.value;
224
- if (atValue === null) return {
223
+ const fieldValue = fieldPair.value;
224
+ if (fieldValue === null) return {
225
225
  _tag: "unsupported",
226
- shape: "at-empty"
226
+ shape: `${field}-empty`
227
227
  };
228
- if (atValue instanceof YamlAlias) return {
228
+ if (fieldValue instanceof YamlAlias) return {
229
229
  _tag: "unsupported",
230
230
  shape: "alias"
231
231
  };
232
- if (!(atValue instanceof YamlScalar)) return {
232
+ if (!(fieldValue instanceof YamlScalar)) return {
233
233
  _tag: "unsupported",
234
- shape: "at-not-scalar"
234
+ shape: `${field}-not-scalar`
235
235
  };
236
- if (atValue.style === "block-literal" || atValue.style === "block-folded") return {
236
+ if (fieldValue.style === "block-literal" || fieldValue.style === "block-folded") return {
237
237
  _tag: "unsupported",
238
- shape: "at-block-scalar"
238
+ shape: `${field}-block-scalar`
239
239
  };
240
240
  return {
241
241
  _tag: "replaceScalar",
242
- start: valueStart + atValue.offset,
243
- end: valueStart + atValue.offset + atValue.length,
244
- quote: atValue.style
242
+ start: valueStart + fieldValue.offset,
243
+ end: valueStart + fieldValue.offset + fieldValue.length,
244
+ quote: fieldValue.style
245
245
  };
246
246
  });
247
+ const locateGenerated = (source) => locateGeneratedField(source, "at");
248
+ /**
249
+ * As {@link locateGenerated}, but for the top-level `generated.body_sha256`
250
+ * key (issue #19). Shares every branch with `locateGenerated` -- the two
251
+ * fields are siblings of the same `generated:` block mapping -- parametrized
252
+ * only by which key name is sought.
253
+ *
254
+ * @internal
255
+ */
256
+ const locateGeneratedBodySha256 = (source) => locateGeneratedField(source, "body_sha256");
247
257
 
248
258
  //#endregion
249
- export { detectNewline, documentNewline, indentAt, locate, locateGenerated, stripBom };
259
+ export { detectNewline, documentNewline, indentAt, locate, locateGenerated, locateGeneratedBodySha256, stripBom };
package/verify/splice.js CHANGED
@@ -45,23 +45,24 @@ const splice = (target, entry, newline) => {
45
45
  };
46
46
  /**
47
47
  * Build the one edit for `target` (contract §6.1). Never calls a YAML
48
- * stringifier: the only syntax emitted is the indent, `at: `, the
49
- * preserved quote character (if any), and `newline`. `encodedAt` is
50
- * already `Schema.encodeSync(Timestamp)`'d by the caller (S-1: splice,
51
- * never `YamlFormat.modify` — it drops quote style and re-normalises line
52
- * endings, V-12/V-15 violations `sync/generated.ts` cannot afford).
48
+ * stringifier: the only syntax emitted is the indent, `<field>: `, the
49
+ * preserved quote character (if any), and `newline`. `value` is already
50
+ * encoded by the caller -- `Schema.encodeSync(Timestamp)`'d for `at`, the raw
51
+ * lowercase hex digest for `body_sha256` (S-1: splice, never `YamlFormat.
52
+ * modify` — it drops quote style and re-normalises line endings, V-12/V-15
53
+ * violations `sync/generated.ts` cannot afford).
53
54
  *
54
55
  * @internal
55
56
  */
56
- const spliceGenerated = (target, encodedAt, newline) => {
57
+ const spliceGenerated = (target, value, newline, field = "at") => {
57
58
  switch (target._tag) {
58
59
  case "insertAfterLastKey": return MarkdownEdit.make({
59
60
  offset: target.insertAt,
60
61
  length: 0,
61
- content: `${target.indent}at: ${encodedAt}${newline}`
62
+ content: `${target.indent}${field}: ${value}${newline}`
62
63
  });
63
64
  case "replaceScalar": {
64
- const quoted = target.quote === "single-quoted" ? `'${encodedAt}'` : target.quote === "double-quoted" ? `"${encodedAt}"` : encodedAt;
65
+ const quoted = target.quote === "single-quoted" ? `'${value}'` : target.quote === "double-quoted" ? `"${value}"` : value;
65
66
  return MarkdownEdit.make({
66
67
  offset: target.start,
67
68
  length: target.end - target.start,
@@ -70,6 +71,27 @@ const spliceGenerated = (target, encodedAt, newline) => {
70
71
  }
71
72
  }
72
73
  };
74
+ /**
75
+ * Build the edit(s) that write BOTH `generated.at` and `generated.
76
+ * body_sha256` together (issue #19: `sync/generated.ts` never writes one
77
+ * without the other). Each field's own `spliceGenerated` edit is used when
78
+ * the two land at different offsets; when both are absent they resolve to
79
+ * the identical `insertAfterLastKey` anchor (the mapping's last existing
80
+ * key), and inserting two zero-length edits at the same offset is exactly
81
+ * the "overlapping edits" case `MarkdownEdit.applyAll` treats as a
82
+ * programmer error -- so that one case is merged into a single edit whose
83
+ * content carries both lines, `at` first.
84
+ *
85
+ * @internal
86
+ */
87
+ const spliceGeneratedFields = (atTarget, bodySha256Target, encodedAt, bodySha256, newline) => {
88
+ if (atTarget._tag === "insertAfterLastKey" && bodySha256Target._tag === "insertAfterLastKey" && atTarget.insertAt === bodySha256Target.insertAt) return [MarkdownEdit.make({
89
+ offset: atTarget.insertAt,
90
+ length: 0,
91
+ content: `${atTarget.indent}at: ${encodedAt}${newline}${bodySha256Target.indent}body_sha256: ${bodySha256}${newline}`
92
+ })];
93
+ return [spliceGenerated(atTarget, encodedAt, newline, "at"), spliceGenerated(bodySha256Target, bodySha256, newline, "body_sha256")];
94
+ };
73
95
 
74
96
  //#endregion
75
- export { splice, spliceGenerated };
97
+ export { splice, spliceGenerated, spliceGeneratedFields };