@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/render/json.js CHANGED
@@ -1,7 +1,9 @@
1
+ import { DistributionField } from "./distribution.js";
1
2
  import { tally } from "./exit.js";
3
+ import { ENGINE_VERSION } from "../version.js";
2
4
  import { sort } from "./sort.js";
3
5
  import { DiagnosticRange, DiagnosticSeverity } from "@okfit/core";
4
- import { Schema } from "effect";
6
+ import { Runtime, Schema } from "effect";
5
7
 
6
8
  //#region src/render/json.ts
7
9
  /** One entry of the `diagnostics` array (K-21). @public */
@@ -29,18 +31,23 @@ const JsonSummary = Schema.Struct({
29
31
  /**
30
32
  * K-21's success envelope, snake_case. `exit_code` is `0 | 1 | 2` only: an
31
33
  * infrastructure failure never produces this envelope, it produces the
32
- * `JsonErrorEnvelope` (K-22). `okfit_version` names the version of the
33
- * package that produced the report (J-2), so the CLI and the MCP server
34
- * legitimately report different numbers over one bundle; `producer` names
35
- * that package (`okfit` or `@okfit/mcp`) so the difference reads as two
36
- * producers, not as drift (okfit #75).
34
+ * `JsonErrorEnvelope` (K-22). `okfit_version` and `producer` identify the
35
+ * PACKAGE that produced the report (J-2), so the CLI and the MCP server
36
+ * legitimately report different numbers over one bundle without that
37
+ * reading as drift (okfit #75); `engine_version` and `okf_version` are the
38
+ * pair that determines the report's content and the pair a reader should
39
+ * actually compare across two producers. `distribution` names the
40
+ * meta-package the bins were installed through (`@okfit/plugin`), or `null`
41
+ * for a direct install of `@okfit/cli`/`@okfit/mcp` (okfit #137).
37
42
  *
38
43
  * @public
39
44
  */
40
45
  const JsonEnvelope = Schema.Struct({
41
46
  schema: Schema.Literal(1),
42
47
  okfit_version: Schema.String,
48
+ engine_version: Schema.String,
43
49
  producer: Schema.String,
50
+ distribution: DistributionField,
44
51
  okf_version: Schema.String,
45
52
  root: Schema.String,
46
53
  profile: Schema.NullOr(Schema.String),
@@ -52,11 +59,20 @@ const JsonEnvelope = Schema.Struct({
52
59
  summary: JsonSummary,
53
60
  diagnostics: Schema.Array(JsonDiagnostic)
54
61
  });
55
- /** K-22's envelope: the ONLY thing stdout carries under `--format json` on an exit-3 failure. @public */
62
+ /**
63
+ * K-22's envelope: the ONLY thing stdout carries under `--format json` on an
64
+ * infrastructure failure. `exit_code` is `3` for most errors, or `64` for the
65
+ * usage-tier errors (`SyncStagedLogError`, `VerifySelectionError`) that carry
66
+ * their own `[Runtime.errorExitCode]`.
67
+ *
68
+ * @public
69
+ */
56
70
  const JsonErrorEnvelope = Schema.Struct({
57
71
  schema: Schema.Literal(1),
58
72
  okfit_version: Schema.String,
59
- exit_code: Schema.Literal(3),
73
+ engine_version: Schema.String,
74
+ distribution: DistributionField,
75
+ exit_code: Schema.Literals([3, 64]),
60
76
  error: Schema.Struct({
61
77
  tag: Schema.String,
62
78
  message: Schema.String
@@ -86,7 +102,9 @@ const json = (input) => {
86
102
  return {
87
103
  schema: 1,
88
104
  okfit_version: input.okfitVersion,
105
+ engine_version: ENGINE_VERSION,
89
106
  producer: input.producer,
107
+ distribution: input.distribution ?? null,
90
108
  okf_version: input.okfVersion,
91
109
  root: input.root,
92
110
  profile: input.profile,
@@ -117,11 +135,25 @@ const messageOf = (error) => {
117
135
  }
118
136
  return String(error);
119
137
  };
138
+ /**
139
+ * The `[Runtime.errorExitCode]` marker's value when it is present and is one
140
+ * of the two exit codes the JSON error envelope can carry; `3` otherwise
141
+ * (the envelope's own default, distinct from `Runtime.getErrorExitCode`'s
142
+ * process-level default of `1`).
143
+ */
144
+ const exitCodeOf = (error) => {
145
+ if (typeof error === "object" && error !== null && Runtime.errorExitCode in error) {
146
+ if (error[Runtime.errorExitCode] === 64) return 64;
147
+ }
148
+ return 3;
149
+ };
120
150
  /** K-22. `tag` is the error's `_tag` when it has one, else its constructor name. @public */
121
- const jsonError = (error, okfitVersion) => ({
151
+ const jsonError = (error, okfitVersion, distribution) => ({
122
152
  schema: 1,
123
153
  okfit_version: okfitVersion,
124
- exit_code: 3,
154
+ engine_version: ENGINE_VERSION,
155
+ distribution: distribution ?? null,
156
+ exit_code: exitCodeOf(error),
125
157
  error: {
126
158
  tag: tagOf(error),
127
159
  message: messageOf(error)
package/render/stale.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { DistributionField } from "./distribution.js";
2
+ import { ENGINE_VERSION } from "../version.js";
1
3
  import { DateTime, Schema } from "effect";
2
4
 
3
5
  //#region src/render/stale.ts
@@ -24,7 +26,9 @@ const StaleSummary = Schema.Struct({
24
26
  const StaleEnvelope = Schema.Struct({
25
27
  schema: Schema.Literal(1),
26
28
  okfit_version: Schema.String,
29
+ engine_version: Schema.String,
27
30
  producer: Schema.String,
31
+ distribution: DistributionField,
28
32
  okf_version: Schema.String,
29
33
  root: Schema.String,
30
34
  profile: Schema.NullOr(Schema.String),
@@ -42,7 +46,9 @@ const StaleEnvelope = Schema.Struct({
42
46
  const staleEnvelope = (input) => ({
43
47
  schema: 1,
44
48
  okfit_version: input.okfitVersion,
49
+ engine_version: ENGINE_VERSION,
45
50
  producer: input.producer,
51
+ distribution: input.distribution ?? null,
46
52
  okf_version: input.okfVersion,
47
53
  root: input.root,
48
54
  profile: input.profile,
package/render/sync.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { DistributionField } from "./distribution.js";
2
+ import { ENGINE_VERSION } from "../version.js";
1
3
  import { SkipReason } from "../sync/run.js";
2
4
  import { Schema } from "effect";
3
5
 
@@ -26,6 +28,8 @@ const SyncModeEnvelope = Schema.Struct({
26
28
  const SyncEnvelope = Schema.Struct({
27
29
  schema: Schema.Literal(1),
28
30
  okfit_version: Schema.String,
31
+ engine_version: Schema.String,
32
+ distribution: DistributionField,
29
33
  root: Schema.String,
30
34
  dry_run: Schema.Boolean,
31
35
  exit_code: Schema.Literal(0),
@@ -52,6 +56,8 @@ const toModeEnvelope = (mode) => ({
52
56
  const syncEnvelope = (input) => ({
53
57
  schema: 1,
54
58
  okfit_version: input.okfitVersion,
59
+ engine_version: ENGINE_VERSION,
60
+ distribution: input.distribution ?? null,
55
61
  root: input.root,
56
62
  dry_run: input.dryRun,
57
63
  exit_code: 0,
package/render/verify.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { DistributionField } from "./distribution.js";
2
+ import { ENGINE_VERSION } from "../version.js";
1
3
  import { Schema } from "effect";
2
4
 
3
5
  //#region src/render/verify.ts
@@ -13,6 +15,8 @@ import { Schema } from "effect";
13
15
  const VerifyEnvelope = Schema.Struct({
14
16
  schema: Schema.Literal(1),
15
17
  okfit_version: Schema.String,
18
+ engine_version: Schema.String,
19
+ distribution: DistributionField,
16
20
  id: Schema.String,
17
21
  path: Schema.String,
18
22
  verified: Schema.Struct({
@@ -33,6 +37,8 @@ const VerifyEnvelope = Schema.Struct({
33
37
  const verifyEnvelope = (input) => ({
34
38
  schema: 1,
35
39
  okfit_version: input.okfitVersion,
40
+ engine_version: ENGINE_VERSION,
41
+ distribution: input.distribution ?? null,
36
42
  id: input.id,
37
43
  path: input.path,
38
44
  verified: {
@@ -42,6 +48,50 @@ const verifyEnvelope = (input) => ({
42
48
  dry_run: input.dryRun,
43
49
  exit_code: 0
44
50
  });
51
+ /**
52
+ * Issue #138's batch success envelope, schema 1, the same snake_case
53
+ * convention as `VerifyEnvelope`. `concepts` carries every concept
54
+ * `okfit verify --all`/`--type` attested (or would attest under a dry run),
55
+ * in write order; `skipped` carries every candidate it declined, with why.
56
+ *
57
+ * @public
58
+ */
59
+ const VerifyBatchEnvelope = Schema.Struct({
60
+ schema: Schema.Literal(1),
61
+ okfit_version: Schema.String,
62
+ engine_version: Schema.String,
63
+ distribution: DistributionField,
64
+ verified_by: Schema.String,
65
+ verified_at: Schema.String,
66
+ concepts: Schema.Array(Schema.Struct({
67
+ id: Schema.String,
68
+ path: Schema.String
69
+ })),
70
+ skipped: Schema.Array(Schema.Struct({
71
+ id: Schema.String,
72
+ reason: Schema.Literals(["draft", "already-verified"])
73
+ })),
74
+ dry_run: Schema.Boolean,
75
+ exit_code: Schema.Literal(0)
76
+ });
77
+ /**
78
+ * `concepts[].path` is already the display form, the same convention
79
+ * {@link verifyEnvelope} uses for its own `path`.
80
+ *
81
+ * @public
82
+ */
83
+ const verifyBatchEnvelope = (input) => ({
84
+ schema: 1,
85
+ okfit_version: input.okfitVersion,
86
+ engine_version: ENGINE_VERSION,
87
+ distribution: input.distribution ?? null,
88
+ verified_by: input.by,
89
+ verified_at: input.at,
90
+ concepts: input.concepts,
91
+ skipped: input.skipped,
92
+ dry_run: input.dryRun,
93
+ exit_code: 0
94
+ });
45
95
 
46
96
  //#endregion
47
- export { VerifyEnvelope, verifyEnvelope };
97
+ export { VerifyBatchEnvelope, VerifyEnvelope, verifyBatchEnvelope, verifyEnvelope };
package/sync/generated.js CHANGED
@@ -1,5 +1,5 @@
1
- import { detectNewline, locateGenerated, locateGeneratedBodySha256, stripBom } from "../verify/locate.js";
2
- import { spliceGeneratedFields } from "../verify/splice.js";
1
+ import { detectNewline, locateGenerated, locateGeneratedBlock, locateGeneratedBodySha256, stripBom } from "../verify/locate.js";
2
+ import { spliceGeneratedBlock, spliceGeneratedFields } from "../verify/splice.js";
3
3
  import { writeAtomic } from "./write.js";
4
4
  import { Timestamp } from "@okfit/core";
5
5
  import { Effect, FileSystem, Path, Schema } from "effect";
@@ -28,14 +28,21 @@ const GeneratedSkipReason = Schema.Literals([
28
28
  ]);
29
29
  const encodeAt = Schema.encodeSync(Timestamp);
30
30
  /**
31
- * Contract §6.2's twelve-step algorithm, run once per `bundle.concepts`
32
- * entry (S-22: `index.md`/`log.md` are reserved files, never concepts,
33
- * never seen here). `provenance` is the single `Derivation.generatedAt`
34
- * walk `runSync` (Task B3) computes once and shares with log mode; this
35
- * function never calls git itself and never substitutes `now` for an
36
- * uncommitted body (design §3 rule 2). Every write is atomic (temp file
37
- * plus rename in the same directory), matching `verify`'s own discipline
38
- * (`verify/run.ts`'s `runVerify`).
31
+ * Contract §6.2's algorithm, run once per `bundle.concepts` entry (S-22:
32
+ * `index.md`/`log.md` are reserved files, never concepts, never seen here).
33
+ * `options.provenance` is the single `Derivation.generatedAt` walk `runSync`
34
+ * (Task B3) computes once and shares with log mode; this function never
35
+ * calls git itself and never substitutes `now` for an uncommitted body
36
+ * (design §3 rule 2). Every write is atomic (temp file plus rename in the
37
+ * same directory), matching `verify`'s own discipline (`verify/run.ts`'s
38
+ * `runVerify`).
39
+ *
40
+ * Ordering (later tasks rely on it): (1) scope filter -> (2) digest match
41
+ * against `generated.body_sha256` => `unchanged`, BEFORE any provenance
42
+ * lookup -> (3) provenance absent => `Effect.die` (caller contract) -> (4)
43
+ * `uncommitted` => skipped with its reason -> (5) no `generated` block:
44
+ * `agent` unset => skipped `generated-missing`, else create -> (6) existing
45
+ * block: splice `at` + `body_sha256` as today.
39
46
  *
40
47
  * `locateGenerated`'s `YamlParseError` is folded into a defect here
41
48
  * (`Effect.orDie`), not widened into this function's typed error channel:
@@ -50,13 +57,21 @@ const encodeAt = Schema.encodeSync(Timestamp);
50
57
  *
51
58
  * @internal
52
59
  */
53
- const syncGenerated = Effect.fn("okfit/sync/syncGenerated")(function* (bundle, provenance, dryRun) {
60
+ const syncGenerated = Effect.fn("okfit/sync/syncGenerated")(function* (bundle, options) {
54
61
  const fs = yield* FileSystem.FileSystem;
55
62
  const path = yield* Path.Path;
63
+ const { provenance, dryRun, agent, scope } = options;
56
64
  const written = [];
57
65
  const unchanged = [];
58
66
  const skipped = [];
59
67
  for (const [id, concept] of bundle.concepts) {
68
+ if (scope !== void 0 && !scope.has(id)) continue;
69
+ const generated = concept.frontmatter.generated;
70
+ const currentDigest = yield* Derivation.bodyDigest(concept.document.source);
71
+ if (generated?.body_sha256 !== void 0 && generated.body_sha256 === currentDigest) {
72
+ unchanged.push(id);
73
+ continue;
74
+ }
60
75
  const derived = provenance.get(id);
61
76
  if (derived === void 0) return yield* Effect.die(/* @__PURE__ */ new Error(`syncGenerated: no provenance computed for concept ${id}`));
62
77
  if (derived._tag === "uncommitted") {
@@ -66,34 +81,46 @@ const syncGenerated = Effect.fn("okfit/sync/syncGenerated")(function* (bundle, p
66
81
  });
67
82
  continue;
68
83
  }
69
- const generated = concept.frontmatter.generated;
70
- if (generated === void 0) {
84
+ const creating = generated === void 0 ? agent : void 0;
85
+ if (generated === void 0 && creating === void 0) {
71
86
  skipped.push({
72
87
  id,
73
88
  reason: "generated-missing"
74
89
  });
75
90
  continue;
76
91
  }
77
- const currentDigest = yield* Derivation.bodyDigest(concept.document.source);
78
- if (generated.body_sha256 !== void 0 && generated.body_sha256 === currentDigest) {
79
- unchanged.push(id);
80
- continue;
81
- }
82
92
  const absolutePath = path.join(bundle.root, concept.path);
83
93
  const source = yield* fs.readFileString(absolutePath);
84
94
  const { text, bom } = stripBom(source);
85
95
  const newline = detectNewline(text);
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") {
89
- skipped.push({
90
- id,
91
- reason: "generated-unsupported"
92
- });
93
- continue;
94
- }
95
96
  const encodedAt = encodeAt(derived.at);
96
- const edits = spliceGeneratedFields(atLocated, bodySha256Located, encodedAt, currentDigest, newline);
97
+ let edits;
98
+ if (creating !== void 0) {
99
+ const located = yield* locateGeneratedBlock(text).pipe(Effect.orDie);
100
+ if (located._tag === "unsupported") {
101
+ skipped.push({
102
+ id,
103
+ reason: "generated-unsupported"
104
+ });
105
+ continue;
106
+ }
107
+ edits = [spliceGeneratedBlock(located, {
108
+ by: creating,
109
+ at: encodedAt,
110
+ bodySha256: currentDigest
111
+ }, newline)];
112
+ } else {
113
+ const atLocated = yield* locateGenerated(text).pipe(Effect.orDie);
114
+ const bodySha256Located = yield* locateGeneratedBodySha256(text).pipe(Effect.orDie);
115
+ if (atLocated._tag === "unsupported" || bodySha256Located._tag === "unsupported") {
116
+ skipped.push({
117
+ id,
118
+ reason: "generated-unsupported"
119
+ });
120
+ continue;
121
+ }
122
+ edits = spliceGeneratedFields(atLocated, bodySha256Located, encodedAt, currentDigest, newline);
123
+ }
97
124
  const finalText = bom + MarkdownEdit.applyAll(text, edits);
98
125
  if (dryRun) {
99
126
  written.push(id);
package/sync/log.js CHANGED
@@ -63,15 +63,35 @@ const mergeLog = (existing, additions) => {
63
63
  });
64
64
  return dates.length === 0 ? `# ${title}\n` : `# ${title}\n\n${pieces.join("")}`;
65
65
  };
66
+ /** @internal */
67
+ const logWindow = (doc, since) => {
68
+ const groups = doc?.groups ?? [];
69
+ const newest = groups.map((entryGroup) => entryGroup.date).reduce((max, date) => max === void 0 || date > max ? date : max, void 0);
70
+ const named = /* @__PURE__ */ new Map();
71
+ for (const entryGroup of groups) named.set(entryGroup.date, new Set(entryGroup.items.map((item) => item.text)));
72
+ return {
73
+ since: since ?? newest,
74
+ named
75
+ };
76
+ };
77
+ /** @internal */
78
+ const wantsLogEntry = (window, date, title) => {
79
+ if (window.since !== void 0 && date < window.since) return false;
80
+ const items = window.named.get(date);
81
+ return items === void 0 || !(items.has(`Added ${title}`) || items.has(`Updated ${title}`));
82
+ };
66
83
  /**
67
84
  * Contract §8.2. Only the root `log.md`; `bundle.concepts` is the only
68
85
  * thing scanned -- `index.md`/`log.md` are reserved files, never concepts
69
86
  * (S-22, contract §14 note 8 -- closes PB's Open question 1 outright: an
70
- * `Updated index`/`Updated log` line can never occur).
87
+ * `Updated index`/`Updated log` line can never occur). Issue #18: the
88
+ * selection window defaults to the newest logged date (inclusive), so a
89
+ * concept committed the same day as an existing group is appended into it
90
+ * rather than dropped; an explicit `since` replaces that default floor.
71
91
  *
72
92
  * @internal
73
93
  */
74
- const syncLog = Effect.fn("okfit/sync/syncLog")(function* (bundle, provenance, dryRun) {
94
+ const syncLog = Effect.fn("okfit/sync/syncLog")(function* (bundle, provenance, dryRun, since) {
75
95
  const fs = yield* FileSystem.FileSystem;
76
96
  const logPath = (yield* Path.Path).join(bundle.root, "log.md");
77
97
  const source = yield* fs.readFileString(logPath).pipe(Effect.map((text) => text), Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(void 0)));
@@ -85,16 +105,17 @@ const syncLog = Effect.fn("okfit/sync/syncLog")(function* (bundle, provenance, d
85
105
  reason: "log-unparseable"
86
106
  }]
87
107
  };
88
- const newestLogged = (existingDoc?.groups ?? []).map((entryGroup) => entryGroup.date).reduce((max, date) => max === void 0 || date > max ? date : max, void 0);
108
+ const window = logWindow(existingDoc, since);
89
109
  const additions = [];
90
110
  for (const [id, concept] of bundle.concepts) {
91
111
  const derived = provenance.get(id);
92
112
  if (derived === void 0 || derived._tag !== "committed") continue;
93
113
  const date = DateTime.formatIsoDate(derived.at);
94
- if (newestLogged !== void 0 && date <= newestLogged) continue;
114
+ const title = Derive.title(concept);
115
+ if (!wantsLogEntry(window, date, title)) continue;
95
116
  additions.push({
96
117
  date,
97
- title: Derive.title(concept),
118
+ title,
98
119
  added: derived.creating
99
120
  });
100
121
  }
@@ -124,4 +145,4 @@ const syncLog = Effect.fn("okfit/sync/syncLog")(function* (bundle, provenance, d
124
145
  });
125
146
 
126
147
  //#endregion
127
- export { mergeLog, syncLog };
148
+ export { logWindow, mergeLog, syncLog, wantsLogEntry };
package/sync/run.js CHANGED
@@ -1,9 +1,11 @@
1
+ import { SyncStagedLogError } from "../errors.js";
1
2
  import { syncGenerated } from "./generated.js";
2
3
  import { syncIndex } from "./index.js";
3
- import { syncLog } from "./log.js";
4
- import { Bundle } from "@okfit/core";
5
- import { Effect, Schema } from "effect";
4
+ import { logWindow, syncLog, wantsLogEntry } from "./log.js";
5
+ import { Bundle, Derive } from "@okfit/core";
6
+ import { DateTime, Effect, FileSystem, Path, Schema } from "effect";
6
7
  import { Derivation } from "@okfit/profiles";
8
+ import { Git } from "@effected/git";
7
9
 
8
10
  //#region src/sync/run.ts
9
11
  /**
@@ -29,29 +31,75 @@ const UNSELECTED = {
29
31
  skipped: []
30
32
  };
31
33
  /**
32
- * Contract §5's six-step algorithm: one `Bundle.load`, one
33
- * `Derivation.generatedAt` per concept shared by the generated and log
34
- * modes (skipped entirely when neither is selected), then the three modes
35
- * in a FIXED order — generated, index, log — regardless of `--only`'s own
36
- * occurrence order, so a generated write never invalidates an index or log
37
- * rendered in the same run (design §2).
34
+ * Contract §5's six-step algorithm: one `Bundle.load`, then a lazy
35
+ * `Derivation.generatedAt` walk shared by the generated and log modes
36
+ * (issue #21) — computed only for a concept that still needs one, not for
37
+ * every concept — then the three modes in a FIXED order — generated,
38
+ * index, log — regardless of `--only`'s own occurrence order, so a
39
+ * generated write never invalidates an index or log rendered in the same
40
+ * run (design §2).
38
41
  *
39
42
  * @public
40
43
  */
41
44
  const runSync = Effect.fn("okfit/sync/runSync")(function* (options) {
42
45
  const bundle = yield* Bundle.load({ root: options.bundleRoot });
43
- const needsGit = options.modes.has("generated") || options.modes.has("log");
44
- const provenance = /* @__PURE__ */ new Map();
45
- if (needsGit) for (const [id, concept] of bundle.concepts) {
46
- const derived = yield* Derivation.generatedAt({ file: `${bundle.root}/${concept.path}` });
47
- provenance.set(id, derived);
46
+ if (options.staged !== void 0 && options.modes.has("log")) return yield* new SyncStagedLogError({});
47
+ const walked = /* @__PURE__ */ new Map();
48
+ let staged;
49
+ let scope;
50
+ let repoRoot;
51
+ let realPaths;
52
+ if (options.staged !== void 0) {
53
+ const git = yield* Git;
54
+ const fs = yield* FileSystem.FileSystem;
55
+ const path = yield* Path.Path;
56
+ repoRoot = yield* fs.realPath(yield* git.repoRoot(bundle.root));
57
+ const stagedPaths = new Set(yield* git.stagedChanges(repoRoot));
58
+ const selected = /* @__PURE__ */ new Set();
59
+ const provenanceStaged = /* @__PURE__ */ new Map();
60
+ const resolvedPaths = /* @__PURE__ */ new Map();
61
+ for (const [id, concept] of bundle.concepts) {
62
+ const real = yield* fs.realPath(path.join(bundle.root, concept.path));
63
+ const rel = path.relative(repoRoot, real).split(path.sep).join("/");
64
+ if (!stagedPaths.has(rel)) continue;
65
+ selected.add(id);
66
+ provenanceStaged.set(id, {
67
+ _tag: "committed",
68
+ at: options.staged.at
69
+ });
70
+ resolvedPaths.set(id, real);
71
+ }
72
+ staged = provenanceStaged;
73
+ scope = selected;
74
+ realPaths = resolvedPaths;
75
+ } else if (options.modes.has("generated") || options.modes.has("log")) {
76
+ const window = logWindow(bundle.logs.get(""), options.logSince);
77
+ for (const [id, concept] of bundle.concepts) {
78
+ const generated = concept.frontmatter.generated;
79
+ const digest = yield* Derivation.bodyDigest(concept.document.source);
80
+ const stampedAt = generated?.body_sha256 !== void 0 && generated.body_sha256 === digest ? generated.at : void 0;
81
+ if (!(stampedAt === void 0 || options.modes.has("log") && wantsLogEntry(window, DateTime.formatIsoDate(stampedAt), Derive.title(concept)))) continue;
82
+ const derived = yield* Derivation.generatedAt({ file: `${bundle.root}/${concept.path}` });
83
+ walked.set(id, derived);
84
+ }
48
85
  }
49
86
  const generated = options.modes.has("generated") ? {
50
87
  selected: true,
51
- ...yield* syncGenerated(bundle, provenance, options.dryRun)
88
+ ...yield* syncGenerated(bundle, {
89
+ provenance: staged ?? walked,
90
+ dryRun: options.dryRun,
91
+ ...options.config.actors?.agent === void 0 ? {} : { agent: options.config.actors.agent },
92
+ ...scope === void 0 ? {} : { scope }
93
+ })
52
94
  } : UNSELECTED;
53
95
  const index = options.modes.has("index") ? yield* syncIndex(bundle, options.dryRun) : UNSELECTED;
54
- const log = options.modes.has("log") ? yield* syncLog(bundle, provenance, options.dryRun) : UNSELECTED;
96
+ const log = options.modes.has("log") ? yield* syncLog(bundle, walked, options.dryRun, options.logSince) : UNSELECTED;
97
+ if (options.staged !== void 0 && !options.dryRun && repoRoot !== void 0) {
98
+ const path = yield* Path.Path;
99
+ const writtenIds = new Set(generated.written);
100
+ const files = [...realPaths === void 0 ? [] : Array.from(realPaths.entries()).filter(([id]) => writtenIds.has(id)).map(([, real]) => real), ...index.written.map((relative) => path.join(bundle.root, relative))];
101
+ if (files.length > 0) yield* (yield* Git).add(repoRoot, files);
102
+ }
55
103
  return {
56
104
  bundleRoot: options.bundleRoot,
57
105
  dryRun: options.dryRun,
package/verify/locate.js CHANGED
@@ -254,6 +254,29 @@ const locateGenerated = (source) => locateGeneratedField(source, "at");
254
254
  * @internal
255
255
  */
256
256
  const locateGeneratedBodySha256 = (source) => locateGeneratedField(source, "body_sha256");
257
+ /** @internal */
258
+ const locateGeneratedBlock = Effect.fn("okfit/verify/locateGeneratedBlock")(function* (source) {
259
+ const block = FrontmatterSource.split(source).frontmatter;
260
+ if (block === void 0) return {
261
+ _tag: "unsupported",
262
+ shape: "no-frontmatter"
263
+ };
264
+ const value = block.value;
265
+ const valueStart = 3 + (block.newline ?? "\n").length;
266
+ const contents = (yield* YamlDocument.parse(value)).contents;
267
+ if (!(contents instanceof YamlMap)) return {
268
+ _tag: "unsupported",
269
+ shape: "not-a-mapping"
270
+ };
271
+ if (contents.items.find((item) => item.key instanceof YamlScalar && item.key.value === "generated") !== void 0) return {
272
+ _tag: "unsupported",
273
+ shape: "generated-present"
274
+ };
275
+ return {
276
+ _tag: "absent",
277
+ insertAt: valueStart + value.length
278
+ };
279
+ });
257
280
 
258
281
  //#endregion
259
- export { detectNewline, documentNewline, indentAt, locate, locateGenerated, locateGeneratedBodySha256, stripBom };
282
+ export { detectNewline, documentNewline, indentAt, locate, locateGenerated, locateGeneratedBlock, locateGeneratedBodySha256, stripBom };