@okfit/engine 0.1.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,47 @@
1
+ import { Schema } from "effect";
2
+
3
+ //#region src/render/verify.ts
4
+ /**
5
+ * V-11's success envelope, schema 1, snake_case — the same convention as
6
+ * `render/json.ts`'s `JsonEnvelope`. Identical in shape for a dry run,
7
+ * which sets `dry_run: true` and still exits 0. There is no content tier,
8
+ * so `exit_code` is the literal `0`; a failure produces K-22's
9
+ * `JsonErrorEnvelope` instead, unchanged.
10
+ *
11
+ * @public
12
+ */
13
+ const VerifyEnvelope = Schema.Struct({
14
+ schema: Schema.Literal(1),
15
+ okfit_version: Schema.String,
16
+ id: Schema.String,
17
+ path: Schema.String,
18
+ verified: Schema.Struct({
19
+ by: Schema.String,
20
+ at: Schema.String
21
+ }),
22
+ dry_run: Schema.Boolean,
23
+ exit_code: Schema.Literal(0)
24
+ });
25
+ /**
26
+ * `path` is already the display form — `displayRoot(cwd, bundle.root, path)`
27
+ * joined to the bundle-relative `concept.path` — because `LoadedConcept.path`
28
+ * alone would print `decisions/cli-exit-codes.md`, not
29
+ * `okf/decisions/cli-exit-codes.md` (contract §12 note 5).
30
+ *
31
+ * @public
32
+ */
33
+ const verifyEnvelope = (input) => ({
34
+ schema: 1,
35
+ okfit_version: input.okfitVersion,
36
+ id: input.id,
37
+ path: input.path,
38
+ verified: {
39
+ by: input.by,
40
+ at: input.at
41
+ },
42
+ dry_run: input.dryRun,
43
+ exit_code: 0
44
+ });
45
+
46
+ //#endregion
47
+ export { VerifyEnvelope, verifyEnvelope };
@@ -0,0 +1,111 @@
1
+ import { detectNewline, locateGenerated, stripBom } from "../verify/locate.js";
2
+ import { spliceGenerated } from "../verify/splice.js";
3
+ import { writeAtomic } from "./write.js";
4
+ import { Timestamp } from "@okfit/core";
5
+ import { DateTime, Effect, FileSystem, Path, Schema } from "effect";
6
+ import { MarkdownEdit } from "@effected/markdown";
7
+
8
+ //#region src/sync/generated.ts
9
+ /**
10
+ * The five `SkipReason` members contract §6.2's algorithm can actually
11
+ * produce — a strict subset of contract §5's six-member `SkipReason`
12
+ * (`sync/run.ts`, Task B3; the sixth, `"log-unparseable"`, is log mode's
13
+ * alone). Declared locally because `sync/run.ts` does not exist when this
14
+ * task lands (fixed order A1 -> A2 -> B1 -> B2 -> B3); every member here is
15
+ * spelled identically to its `SkipReason` counterpart, so Task B3 assigns
16
+ * a `GeneratedSkipReason` value into a `SkipReason`-typed field with no
17
+ * cast.
18
+ *
19
+ * @internal
20
+ */
21
+ const GeneratedSkipReason = Schema.Literals([
22
+ "untracked",
23
+ "dirty",
24
+ "unborn",
25
+ "generated-missing",
26
+ "generated-unsupported"
27
+ ]);
28
+ const encodeAt = Schema.encodeSync(Timestamp);
29
+ /**
30
+ * Contract §6.2's twelve-step algorithm, run once per `bundle.concepts`
31
+ * entry (S-22: `index.md`/`log.md` are reserved files, never concepts,
32
+ * never seen here). `provenance` is the single `Derivation.generatedAt`
33
+ * walk `runSync` (Task B3) computes once and shares with log mode; this
34
+ * function never calls git itself and never substitutes `now` for an
35
+ * uncommitted body (design §3 rule 2). Every write is atomic (temp file
36
+ * plus rename in the same directory), matching `verify`'s own discipline
37
+ * (`verify/run.ts`'s `runVerify`).
38
+ *
39
+ * `locateGenerated`'s `YamlParseError` is folded into a defect here
40
+ * (`Effect.orDie`), not widened into this function's typed error channel:
41
+ * the frontmatter it re-parses already decoded once, successfully, for
42
+ * this concept to be a member of `bundle.concepts` at all -- the same
43
+ * reasoning `locate.ts`'s own `locate` documents for its identical
44
+ * failure mode. `runSync`'s stated error union (`BundleLoadError |
45
+ * GeneratedAtError`, contract §5) already includes `PlatformError.
46
+ * PlatformError` inside `GeneratedAtError` (contract §14 note 2), so this
47
+ * function's own `PlatformError.PlatformError` channel composes with no
48
+ * widening on either side.
49
+ *
50
+ * @internal
51
+ */
52
+ const syncGenerated = Effect.fn("okfit/sync/syncGenerated")(function* (bundle, provenance, dryRun) {
53
+ const fs = yield* FileSystem.FileSystem;
54
+ const path = yield* Path.Path;
55
+ const written = [];
56
+ const unchanged = [];
57
+ const skipped = [];
58
+ for (const [id, concept] of bundle.concepts) {
59
+ const derived = provenance.get(id);
60
+ if (derived === void 0) return yield* Effect.die(/* @__PURE__ */ new Error(`syncGenerated: no provenance computed for concept ${id}`));
61
+ if (derived._tag === "uncommitted") {
62
+ skipped.push({
63
+ id,
64
+ reason: derived.reason
65
+ });
66
+ continue;
67
+ }
68
+ const generated = concept.frontmatter.generated;
69
+ if (generated === void 0) {
70
+ skipped.push({
71
+ id,
72
+ reason: "generated-missing"
73
+ });
74
+ continue;
75
+ }
76
+ const recorded = generated.at;
77
+ if (recorded !== void 0 && DateTime.Equivalence(recorded, derived.at)) {
78
+ unchanged.push(id);
79
+ continue;
80
+ }
81
+ const absolutePath = path.join(bundle.root, concept.path);
82
+ const source = yield* fs.readFileString(absolutePath);
83
+ const { text, bom } = stripBom(source);
84
+ const newline = detectNewline(text);
85
+ const located = yield* locateGenerated(text).pipe(Effect.orDie);
86
+ if (located._tag === "unsupported") {
87
+ skipped.push({
88
+ id,
89
+ reason: "generated-unsupported"
90
+ });
91
+ continue;
92
+ }
93
+ const encodedAt = encodeAt(derived.at);
94
+ const edit = spliceGenerated(located, encodedAt, newline);
95
+ const finalText = bom + MarkdownEdit.applyAll(text, [edit]);
96
+ if (dryRun) {
97
+ written.push(id);
98
+ continue;
99
+ }
100
+ yield* writeAtomic(absolutePath, finalText);
101
+ written.push(id);
102
+ }
103
+ return {
104
+ written,
105
+ unchanged,
106
+ skipped
107
+ };
108
+ });
109
+
110
+ //#endregion
111
+ export { syncGenerated };
package/sync/index.js ADDED
@@ -0,0 +1,73 @@
1
+ import { writeAtomic } from "./write.js";
2
+ import { Derive, OKF_SPEC_VERSION } from "@okfit/core";
3
+ import { Effect, FileSystem, Path } from "effect";
4
+
5
+ //#region src/sync/index.ts
6
+ /**
7
+ * Immediate child directories of `dir`, unioned from `bundle.directories`
8
+ * and `bundle.indexes.keys()` -- the same union `Derive.synthesizeIndex`
9
+ * walks (S-29, amending S-20's wording): a directory holding only an
10
+ * `index.md` and no concepts is never a member of `bundle.directories`
11
+ * (`Bundle.load` records a directory there only when it holds a concept),
12
+ * but it is still a key of `bundle.indexes`, so omitting that map would
13
+ * silently drop it from the root's `# Subdirectories` section while every
14
+ * other directory's own `synthesizeIndex` call still lists it as a child.
15
+ */
16
+ const childDirectoriesOf = (bundle, dir) => {
17
+ const prefix = dir === "" ? "" : `${dir}/`;
18
+ const children = /* @__PURE__ */ new Set();
19
+ for (const candidate of [...bundle.directories, ...bundle.indexes.keys()]) {
20
+ if (candidate === "" || candidate === dir || !candidate.startsWith(prefix)) continue;
21
+ const head = candidate.slice(prefix.length).split("/")[0];
22
+ if (head !== void 0 && head !== "") children.add(head);
23
+ }
24
+ return [...children];
25
+ };
26
+ const writeOrCompare = Effect.fn("okfit/sync/index/writeOrCompare")(function* (targetPath, relativeId, rendered, dryRun, written, unchanged) {
27
+ if ((yield* (yield* FileSystem.FileSystem).readFileString(targetPath).pipe(Effect.map((text) => text), Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(void 0)))) === rendered) {
28
+ unchanged.push(relativeId);
29
+ return;
30
+ }
31
+ if (dryRun) {
32
+ written.push(relativeId);
33
+ return;
34
+ }
35
+ yield* writeAtomic(targetPath, rendered);
36
+ written.push(relativeId);
37
+ });
38
+ /**
39
+ * Contract §7. Directories: the bundle root plus every directory in
40
+ * `bundle.directories` that holds at least one concept -- the same set
41
+ * `Derive.synthesizeIndex` already walks (S-20). A directory with an
42
+ * `index.md` but no concepts is left untouched (design §4: "nothing to
43
+ * derive") because such a directory is never a member of
44
+ * `bundle.directories` in the first place (`Bundle.load` only records a
45
+ * directory there when it holds a concept -- `Bundle.ts:301-323`).
46
+ *
47
+ * @internal
48
+ */
49
+ const syncIndex = Effect.fn("okfit/sync/syncIndex")(function* (bundle, dryRun) {
50
+ const path = yield* Path.Path;
51
+ const written = [];
52
+ const unchanged = [];
53
+ const rootConcepts = [...bundle.concepts.values()].filter((concept) => !concept.path.includes("/"));
54
+ const rootRendered = Derive.renderIndex("", rootConcepts, {
55
+ okfVersion: OKF_SPEC_VERSION,
56
+ subdirectories: childDirectoriesOf(bundle, "")
57
+ });
58
+ yield* writeOrCompare(path.join(bundle.root, "index.md"), "index.md", rootRendered, dryRun, written, unchanged);
59
+ for (const dir of bundle.directories) {
60
+ if (dir === "") continue;
61
+ const rendered = Derive.synthesizeIndex(bundle, dir);
62
+ yield* writeOrCompare(path.join(bundle.root, dir, "index.md"), `${dir}/index.md`, rendered, dryRun, written, unchanged);
63
+ }
64
+ return {
65
+ selected: true,
66
+ written,
67
+ unchanged,
68
+ skipped: []
69
+ };
70
+ });
71
+
72
+ //#endregion
73
+ export { syncIndex };
package/sync/log.js ADDED
@@ -0,0 +1,127 @@
1
+ import { writeAtomic } from "./write.js";
2
+ import { Derive } from "@okfit/core";
3
+ import { DateTime, Effect, FileSystem, Path } from "effect";
4
+
5
+ //#region src/sync/log.ts
6
+ const byTitle = (a, b) => a.title < b.title ? -1 : a.title > b.title ? 1 : 0;
7
+ const renderItem = (item) => item.added ? `Added ${item.title}` : `Updated ${item.title}`;
8
+ /**
9
+ * S-10: takes STRUCTURED additions (`{date, title, added}`), not
10
+ * pre-rendered strings -- this is the corrected shape of CP §2's own
11
+ * proposal (contract §14 note 4): rendering "Added <title>"/
12
+ * "Updated <title>" happens INSIDE `mergeLog`, so "sort by title" (design
13
+ * §5 step 4) sorts the actual title field, never a substring of a
14
+ * pre-rendered line. Renders a NEW group's items via
15
+ * `Derive.renderLogEntry` (S-11's per-group renderer); an EXISTING
16
+ * group's text (heading and items) is re-emitted verbatim by slicing
17
+ * `existing.source` on the group's own `LogGroup.range` -- never
18
+ * re-serialised, so a hand-written item, comment, or unusual formatting
19
+ * inside an existing group survives byte-for-byte.
20
+ *
21
+ * @internal
22
+ */
23
+ const mergeLog = (existing, additions) => {
24
+ const title = existing?.doc.title ?? "Log";
25
+ const byDate = /* @__PURE__ */ new Map();
26
+ if (existing !== void 0) for (const group of existing.doc.groups) {
27
+ const verbatim = existing.source.slice(group.range.offset, group.range.offset + group.range.length);
28
+ byDate.set(group.date, {
29
+ verbatim,
30
+ pending: []
31
+ });
32
+ }
33
+ for (const addition of additions) {
34
+ const bucket = byDate.get(addition.date) ?? {
35
+ verbatim: void 0,
36
+ pending: []
37
+ };
38
+ bucket.pending.push({
39
+ title: addition.title,
40
+ added: addition.added
41
+ });
42
+ byDate.set(addition.date, bucket);
43
+ }
44
+ const dates = [...byDate.keys()].sort((a, b) => a < b ? 1 : a > b ? -1 : 0);
45
+ const pieces = dates.map((date, index) => {
46
+ const bucket = byDate.get(date);
47
+ let piece;
48
+ if (bucket === void 0) piece = "";
49
+ else if (bucket.verbatim === void 0) {
50
+ const items = [...bucket.pending].sort(byTitle).map(renderItem);
51
+ piece = Derive.renderLogEntry({
52
+ date,
53
+ items
54
+ });
55
+ } else if (bucket.pending.length === 0) piece = bucket.verbatim;
56
+ else {
57
+ const trailingNewlines = /\n+$/.exec(bucket.verbatim)?.[0] ?? "";
58
+ const base = bucket.verbatim.slice(0, bucket.verbatim.length - trailingNewlines.length);
59
+ const trailer = trailingNewlines.slice(1);
60
+ piece = `${base}\n${[...bucket.pending].sort(byTitle).map((item) => `* ${renderItem(item)}\n`).join("")}${trailer}`;
61
+ }
62
+ return !(index === dates.length - 1) && !piece.endsWith("\n\n") ? `${piece}\n` : piece;
63
+ });
64
+ return dates.length === 0 ? `# ${title}\n` : `# ${title}\n\n${pieces.join("")}`;
65
+ };
66
+ /**
67
+ * Contract §8.2. Only the root `log.md`; `bundle.concepts` is the only
68
+ * thing scanned -- `index.md`/`log.md` are reserved files, never concepts
69
+ * (S-22, contract §14 note 8 -- closes PB's Open question 1 outright: an
70
+ * `Updated index`/`Updated log` line can never occur).
71
+ *
72
+ * @internal
73
+ */
74
+ const syncLog = Effect.fn("okfit/sync/syncLog")(function* (bundle, provenance, dryRun) {
75
+ const fs = yield* FileSystem.FileSystem;
76
+ const logPath = (yield* Path.Path).join(bundle.root, "log.md");
77
+ const source = yield* fs.readFileString(logPath).pipe(Effect.map((text) => text), Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(void 0)));
78
+ const existingDoc = bundle.logs.get("");
79
+ if (source !== void 0 && existingDoc === void 0) return {
80
+ selected: true,
81
+ written: [],
82
+ unchanged: [],
83
+ skipped: [{
84
+ id: "log.md",
85
+ reason: "log-unparseable"
86
+ }]
87
+ };
88
+ const newestLogged = (existingDoc?.groups ?? []).map((entryGroup) => entryGroup.date).reduce((max, date) => max === void 0 || date > max ? date : max, void 0);
89
+ const additions = [];
90
+ for (const [id, concept] of bundle.concepts) {
91
+ const derived = provenance.get(id);
92
+ if (derived === void 0 || derived._tag !== "committed") continue;
93
+ const date = DateTime.formatIsoDate(derived.at);
94
+ if (newestLogged !== void 0 && date <= newestLogged) continue;
95
+ additions.push({
96
+ date,
97
+ title: Derive.title(concept),
98
+ added: derived.creating
99
+ });
100
+ }
101
+ const merged = mergeLog(source === void 0 || existingDoc === void 0 ? void 0 : {
102
+ doc: existingDoc,
103
+ source
104
+ }, additions);
105
+ if (merged === (source ?? "")) return {
106
+ selected: true,
107
+ written: [],
108
+ unchanged: ["log.md"],
109
+ skipped: []
110
+ };
111
+ if (dryRun) return {
112
+ selected: true,
113
+ written: ["log.md"],
114
+ unchanged: [],
115
+ skipped: []
116
+ };
117
+ yield* writeAtomic(logPath, merged);
118
+ return {
119
+ selected: true,
120
+ written: ["log.md"],
121
+ unchanged: [],
122
+ skipped: []
123
+ };
124
+ });
125
+
126
+ //#endregion
127
+ export { mergeLog, syncLog };
package/sync/run.js ADDED
@@ -0,0 +1,65 @@
1
+ import { syncGenerated } from "./generated.js";
2
+ import { syncIndex } from "./index.js";
3
+ import { syncLog } from "./log.js";
4
+ import { Bundle } from "@okfit/core";
5
+ import { Effect, Schema } from "effect";
6
+ import { Derivation } from "@okfit/profiles";
7
+
8
+ //#region src/sync/run.ts
9
+ /**
10
+ * S-14: a closed union, not free text. The human renderer maps each member
11
+ * to one fixed sentence (`render/sync.ts#humanSync`); the JSON envelope's
12
+ * `skipped[].reason` is this exact union.
13
+ *
14
+ * @public
15
+ */
16
+ const SkipReason = Schema.Literals([
17
+ "untracked",
18
+ "dirty",
19
+ "unborn",
20
+ "generated-missing",
21
+ "generated-unsupported",
22
+ "log-unparseable"
23
+ ]);
24
+ /** A selected-`false` placeholder for a mode `--only` excluded from (contract §5). */
25
+ const UNSELECTED = {
26
+ selected: false,
27
+ written: [],
28
+ unchanged: [],
29
+ skipped: []
30
+ };
31
+ /**
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).
38
+ *
39
+ * @public
40
+ */
41
+ const runSync = Effect.fn("okfit/sync/runSync")(function* (options) {
42
+ 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);
48
+ }
49
+ const generated = options.modes.has("generated") ? {
50
+ selected: true,
51
+ ...yield* syncGenerated(bundle, provenance, options.dryRun)
52
+ } : UNSELECTED;
53
+ 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;
55
+ return {
56
+ bundleRoot: options.bundleRoot,
57
+ dryRun: options.dryRun,
58
+ generated,
59
+ index,
60
+ log
61
+ };
62
+ });
63
+
64
+ //#endregion
65
+ export { SkipReason, runSync };
package/sync/write.js ADDED
@@ -0,0 +1,48 @@
1
+ import { Effect, FileSystem, Option, Path } from "effect";
2
+ import { randomUUID } from "node:crypto";
3
+
4
+ //#region src/sync/write.ts
5
+ /**
6
+ * S-33: the one atomic write every sync mode shares. Resolves `target`'s
7
+ * real path first when it already exists (so a symlinked
8
+ * `index.md`/`log.md`/concept file is replaced by writing through the
9
+ * link rather than over it -- `generated` mode's own discipline, formerly
10
+ * not shared with `index`/`log`) and falls back to `target` itself for a
11
+ * file `sync` is about to create for the first time (`index`/`log` modes
12
+ * both write files that may not exist yet; `generated` mode's targets
13
+ * always exist, since they come from an already-loaded concept). Writes
14
+ * `contents` to a uniquely-scoped temp file beside the resolved target (so
15
+ * two concurrent `sync` runs never collide on a fixed name), preserves an
16
+ * existing target's mode via `stat` + `chmod` (a brand-new file keeps
17
+ * whatever mode `writeFileString` gives it), and renames the temp file
18
+ * over the target. On any failure -- including a failed `rename` -- the
19
+ * temp file is removed (`Effect.onError`), so a partial write never
20
+ * leaves a stray `<file>.okfit-sync.<token>.tmp` behind (F-5/S-33).
21
+ *
22
+ * DISCREPANCY from S-33's literal "the temp name carries the pid": `src/`
23
+ * outside `bin.ts`/`commands/*`/`internal/exit.ts`/`internal/tty.ts` never
24
+ * reads the global `process` (K-39, enforced by `__test__/boundaries.test.ts`),
25
+ * and `sync/write.ts` is not on that allowlist. A `randomUUID()` token
26
+ * (`node:crypto`, not `process`) satisfies S-33's actual requirement --
27
+ * two concurrent `sync` runs never collide on a fixed temp name -- without
28
+ * widening the process-access boundary for one helper. Recorded here per
29
+ * the Global Constraints' "record the discrepancy" instruction.
30
+ *
31
+ * @internal
32
+ */
33
+ const writeAtomic = Effect.fn("okfit/sync/writeAtomic")(function* (target, contents) {
34
+ const fs = yield* FileSystem.FileSystem;
35
+ const path = yield* Path.Path;
36
+ const resolved = yield* fs.realPath(target).pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(target)));
37
+ const dir = path.dirname(resolved);
38
+ const base = path.basename(resolved);
39
+ const tempPath = path.join(dir, `${base}.okfit-sync.${randomUUID()}.tmp`);
40
+ const originalMode = yield* fs.stat(resolved).pipe(Effect.map((info) => Option.some(info.mode)), Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(Option.none())));
41
+ yield* fs.writeFileString(tempPath, contents).pipe(Effect.tap(() => Option.match(originalMode, {
42
+ onNone: () => Effect.void,
43
+ onSome: (mode) => fs.chmod(tempPath, mode)
44
+ })), Effect.tap(() => fs.rename(tempPath, resolved)), Effect.onError(() => fs.remove(tempPath, { force: true }).pipe(Effect.ignore)));
45
+ });
46
+
47
+ //#endregion
48
+ export { writeAtomic };
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.59.0"
9
+ }
10
+ ]
11
+ }
@@ -0,0 +1,63 @@
1
+ import { Bundle, OkfitConfig, Validate } from "@okfit/core";
2
+ import { Context, Effect, Option } from "effect";
3
+ import { Provenance } from "@okfit/profiles";
4
+
5
+ //#region src/validate/run.ts
6
+ /**
7
+ * K-47's ambient clock. `bin.ts` resolves `OKFIT_NOW` (or the wall clock)
8
+ * exactly once and provides it with `Effect.provideService(Now, value)`
9
+ * around the whole command tree; `commands/validate.ts` and
10
+ * `commands/init.ts` each read it with `const now = yield* Now;` before
11
+ * building {@link RunOptions} or a scaffold's `today`. Not re-exported from
12
+ * `index.ts`: like `internal/exit.ts` and `internal/tty.ts`, it means
13
+ * nothing outside a spawned process (K-49). `Context.Tag` does not exist on
14
+ * the Effect v4 line; the v4 shape is
15
+ * `Context.Service<Self, Shape>()(id)`, the same form core and profiles use
16
+ * for their own services (`PROFILES/GitHistory.ts:140`) — source wins over
17
+ * the contract's literal snippet here (K-62).
18
+ *
19
+ * @internal
20
+ */
21
+ var Now = class extends Context.Service()("@okfit/engine/Now") {};
22
+ /**
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
29
+ * (S-31). An `error` severity on the appended diagnostics yields exit `1`
30
+ * through the EXISTING lint-tier rule in `render/exit.ts` — no renderer
31
+ * branch, no new `DiagnosticSource`, since these diagnostics flow through
32
+ * the same `report.lint` array every other core lint diagnostic already
33
+ * does.
34
+ *
35
+ * K-52 holds structurally: `Bundle.load`'s failure short-circuits the
36
+ * generator, so `profile.check` never runs on a bundle that did not load.
37
+ *
38
+ * This is a BREAKING signature change: `@okfit/mcp`'s `validateBundle.ts`
39
+ * calls this function directly and must be updated to match (Task C1);
40
+ * `commands/validate.ts` is the other caller.
41
+ *
42
+ * @public
43
+ */
44
+ const run = (options) => Effect.gen(function* () {
45
+ const bundle = yield* Bundle.load({ root: options.root });
46
+ const report = Validate.all(bundle, options.config, { now: options.now });
47
+ const profileDiagnostics = Option.match(options.profile, {
48
+ onNone: () => [],
49
+ onSome: (profile) => profile.check(bundle)
50
+ });
51
+ const provenance = OkfitConfig.severityFor(options.config, "generated-at-drift") === "off" || options.skipProvenance === true ? [] : yield* Provenance.lint(bundle, options.config);
52
+ return {
53
+ bundle,
54
+ report: {
55
+ ...report,
56
+ lint: [...report.lint, ...provenance]
57
+ },
58
+ profileDiagnostics
59
+ };
60
+ });
61
+
62
+ //#endregion
63
+ export { Now, run };