@okfit/cli 0.2.0 → 0.4.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.
package/sync/log.js DELETED
@@ -1,127 +0,0 @@
1
- import { writeAtomic } from "./write.js";
2
- import { DateTime, Effect, FileSystem, Path } from "effect";
3
- import { Derive } from "@okfit/core";
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 DELETED
@@ -1,65 +0,0 @@
1
- import { syncGenerated } from "./generated.js";
2
- import { syncIndex } from "./index.js";
3
- import { syncLog } from "./log.js";
4
- import { Effect, Schema } from "effect";
5
- import { Bundle } from "@okfit/core";
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 DELETED
@@ -1,48 +0,0 @@
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 };
package/validate/run.js DELETED
@@ -1,63 +0,0 @@
1
- import { Context, Effect, Option } from "effect";
2
- import { Bundle, OkfitConfig, Validate } from "@okfit/core";
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/cli/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 };
package/verify/locate.js DELETED
@@ -1,249 +0,0 @@
1
- import { Effect } from "effect";
2
- import { FrontmatterSource } from "@effected/markdown";
3
- import { YamlAlias, YamlDocument, YamlMap, YamlScalar, YamlSeq } from "@effected/yaml";
4
-
5
- //#region src/verify/locate.ts
6
- /**
7
- * Strip a leading BOM before anything else; the caller re-prepends `bom`
8
- * verbatim on write (V-15). `FrontmatterSource.split`'s own remarks require
9
- * this: "a leading BOM means offset 0 is not a fence — strip one before
10
- * calling" (`FrontmatterSource.ts:123`).
11
- *
12
- * @internal
13
- */
14
- const stripBom = (source) => source.charCodeAt(0) === 65279 ? {
15
- text: source.slice(1),
16
- bom: ""
17
- } : {
18
- text: source,
19
- bom: ""
20
- };
21
- /** CRLF when the document's FIRST newline is CRLF, else LF (V-15). @internal */
22
- const detectNewline = (source) => {
23
- const index = source.indexOf("\n");
24
- return index > 0 && source.charCodeAt(index - 1) === 13 ? "\r\n" : "\n";
25
- };
26
- /**
27
- * The terminator the spliced fragment must use: `split`'s own fidelity
28
- * field for the opening fence's terminator when it is LF or CRLF, else
29
- * {@link detectNewline}. `FrontmatterNewline` also admits a lone `"\r"`,
30
- * which no fragment may emit, so that case falls through to the scan.
31
- *
32
- * @internal
33
- */
34
- const documentNewline = (text) => {
35
- const newline = FrontmatterSource.split(text).frontmatter?.newline;
36
- if (newline === "\r\n") return "\r\n";
37
- if (newline === "\n") return "\n";
38
- return detectNewline(text);
39
- };
40
- /** The run of spaces ending at `offset`, or `""` when anything else precedes it on the line. @internal */
41
- const indentAt = (value, offset) => {
42
- const lineStart = value.lastIndexOf("\n", offset - 1) + 1;
43
- const prefix = value.slice(lineStart, offset);
44
- return /^ *$/.test(prefix) ? prefix : "";
45
- };
46
- /**
47
- * True when `node`, or any mapping directly inside a sequence `node`,
48
- * carries a `<<` key. That is every level a legal `Verification.List` can
49
- * reach; anything deeper already fails to decode as a `Verification`
50
- * (contract §11 item 3).
51
- */
52
- const hasMergeKey = (node) => {
53
- if (node instanceof YamlMap) return node.items.some((pair) => pair.key instanceof YamlScalar && pair.key.value === "<<");
54
- if (node instanceof YamlSeq) return node.items.some((item) => item instanceof YamlMap && hasMergeKey(item));
55
- return false;
56
- };
57
- /**
58
- * Split `source`'s frontmatter, parse its value, find the top-level
59
- * `verified` pair, and classify it (contract §3.1). Fails only when the
60
- * frontmatter YAML is fatally unparseable — impossible for a concept that
61
- * reached `bundle.concepts`, but typed rather than assumed.
62
- *
63
- * Offset translation is `valueStart = 3 + newline.length` — `"---".length`
64
- * plus the opening fence's own terminator, taken from `split`'s fidelity
65
- * field rather than re-derived with `indexOf` (contract §12 note 9). Every
66
- * whole-file offset below is `valueStart + <yaml offset>`.
67
- *
68
- * @internal
69
- */
70
- const locate = Effect.fn("okfit/verify/locate")(function* (source) {
71
- const block = FrontmatterSource.split(source).frontmatter;
72
- if (block === void 0) return {
73
- _tag: "unsupported",
74
- shape: "no-frontmatter"
75
- };
76
- const value = block.value;
77
- const valueStart = 3 + (block.newline ?? "\n").length;
78
- const contents = (yield* YamlDocument.parse(value)).contents;
79
- if (!(contents instanceof YamlMap)) return {
80
- _tag: "unsupported",
81
- shape: "not-a-mapping"
82
- };
83
- const pair = contents.items.find((item) => item.key instanceof YamlScalar && item.key.value === "verified");
84
- if (pair === void 0) return {
85
- _tag: "absent",
86
- insertAt: valueStart + value.length
87
- };
88
- const key = pair.key;
89
- const node = pair.value;
90
- if (node === null) return {
91
- _tag: "unsupported",
92
- shape: "empty"
93
- };
94
- if (node instanceof YamlAlias) return {
95
- _tag: "unsupported",
96
- shape: "alias"
97
- };
98
- if (hasMergeKey(node)) return {
99
- _tag: "unsupported",
100
- shape: "merge-key"
101
- };
102
- if (node instanceof YamlScalar) return {
103
- _tag: "unsupported",
104
- shape: "scalar"
105
- };
106
- if (node instanceof YamlSeq) {
107
- if (node.style === "flow") return {
108
- _tag: "flowSeq",
109
- insertAt: valueStart + node.offset + node.length - 1,
110
- empty: node.items.length === 0
111
- };
112
- const last = node.items[node.items.length - 1];
113
- if (last === void 0) return {
114
- _tag: "unsupported",
115
- shape: "empty"
116
- };
117
- const end = last.offset + last.length;
118
- return {
119
- _tag: "blockSeq",
120
- insertAt: valueStart + end,
121
- afterNewline: value[end - 1] === "\n",
122
- indent: indentAt(value, node.offset),
123
- lastItemStyle: last instanceof YamlMap && last.style === "flow" ? "flow" : "block"
124
- };
125
- }
126
- if (node instanceof YamlMap) {
127
- const start = valueStart + key.offset + key.length + 1;
128
- const rawEnd = node.offset + node.length;
129
- const raw = value.slice(node.offset, rawEnd);
130
- const original = raw.replace(/(?:\r\n|\n|\r)$/, "");
131
- return {
132
- _tag: "bareMapping",
133
- style: node.style === "flow" ? "flow" : "block",
134
- start,
135
- end: valueStart + rawEnd,
136
- indent: `${indentAt(value, key.offset)} `,
137
- original,
138
- endsWithNewline: original.length !== raw.length
139
- };
140
- }
141
- return {
142
- _tag: "unsupported",
143
- shape: "not-a-mapping"
144
- };
145
- });
146
- /**
147
- * Locate the `at` key inside a concept's top-level `generated` BLOCK
148
- * mapping. Callers MUST have already confirmed `generated` exists on the
149
- * decoded concept (`generated-missing`, S-3/design §3 rule 5, is the
150
- * caller's check against `concept.frontmatter.generated`, never this
151
- * function's) — matches how `locate` already re-parses independently of
152
- * whatever `Bundle.load` retained, for `verified`. The insert style is
153
- * one new newline-terminated line after the last key's line of the
154
- * mapping, at that mapping's indent, using the document newline (S-2,
155
- * confirmed as already correct in FW's own proposal — contract §14 note
156
- * 1 — with no correction needed against this file's `locate` precedent).
157
- *
158
- * @internal
159
- */
160
- const locateGenerated = Effect.fn("okfit/verify/locateGenerated")(function* (source) {
161
- const block = FrontmatterSource.split(source).frontmatter;
162
- if (block === void 0) return {
163
- _tag: "unsupported",
164
- shape: "no-frontmatter"
165
- };
166
- const value = block.value;
167
- const valueStart = 3 + (block.newline ?? "\n").length;
168
- const contents = (yield* YamlDocument.parse(value)).contents;
169
- if (!(contents instanceof YamlMap)) return {
170
- _tag: "unsupported",
171
- shape: "not-a-mapping"
172
- };
173
- const pair = contents.items.find((item) => item.key instanceof YamlScalar && item.key.value === "generated");
174
- if (pair === void 0) return {
175
- _tag: "unsupported",
176
- shape: "no-generated-key"
177
- };
178
- const node = pair.value;
179
- if (node === null) return {
180
- _tag: "unsupported",
181
- shape: "empty"
182
- };
183
- if (node instanceof YamlAlias) return {
184
- _tag: "unsupported",
185
- shape: "alias"
186
- };
187
- if (hasMergeKey(node)) return {
188
- _tag: "unsupported",
189
- shape: "merge-key"
190
- };
191
- if (node instanceof YamlScalar) return {
192
- _tag: "unsupported",
193
- shape: "scalar"
194
- };
195
- if (node instanceof YamlSeq) return {
196
- _tag: "unsupported",
197
- shape: "sequence"
198
- };
199
- if (!(node instanceof YamlMap)) return {
200
- _tag: "unsupported",
201
- shape: "not-a-mapping"
202
- };
203
- if (node.style === "flow") return {
204
- _tag: "unsupported",
205
- shape: "flow-mapping"
206
- };
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) {
210
- const last = node.items[node.items.length - 1];
211
- if (last === void 0) return {
212
- _tag: "unsupported",
213
- shape: "empty"
214
- };
215
- const lastEnd = last.value !== null ? last.value.offset + last.value.length : last.key.offset + last.key.length;
216
- const nl = value.indexOf("\n", lastEnd);
217
- return {
218
- _tag: "insertAfterLastKey",
219
- insertAt: valueStart + (nl === -1 ? value.length : nl + 1),
220
- indent
221
- };
222
- }
223
- const atValue = atPair.value;
224
- if (atValue === null) return {
225
- _tag: "unsupported",
226
- shape: "at-empty"
227
- };
228
- if (atValue instanceof YamlAlias) return {
229
- _tag: "unsupported",
230
- shape: "alias"
231
- };
232
- if (!(atValue instanceof YamlScalar)) return {
233
- _tag: "unsupported",
234
- shape: "at-not-scalar"
235
- };
236
- if (atValue.style === "block-literal" || atValue.style === "block-folded") return {
237
- _tag: "unsupported",
238
- shape: "at-block-scalar"
239
- };
240
- return {
241
- _tag: "replaceScalar",
242
- start: valueStart + atValue.offset,
243
- end: valueStart + atValue.offset + atValue.length,
244
- quote: atValue.style
245
- };
246
- });
247
-
248
- //#endregion
249
- export { detectNewline, documentNewline, indentAt, locate, locateGenerated, stripBom };
package/verify/run.js DELETED
@@ -1,106 +0,0 @@
1
- import { VerifyConceptNotFoundError, VerifyUnsupportedFrontmatterError } from "../errors.js";
2
- import { documentNewline, locate, stripBom } from "./locate.js";
3
- import { splice } from "./splice.js";
4
- import { Effect, FileSystem, Option, Path, Schema } from "effect";
5
- import { Bundle, ConceptId, Timestamp } from "@okfit/core";
6
- import { Derivation } from "@okfit/profiles";
7
- import { MarkdownEdit } from "@effected/markdown";
8
-
9
- //#region src/verify/run.ts
10
- /**
11
- * The only four diagnostic codes that can co-occur with a MISSING
12
- * `concepts` entry (V-9, contract §12 note 10): `Bundle.ts:143/164/175/186`
13
- * emit the three frontmatter codes and return without a concept, and
14
- * `internal/frontmatter.ts:73` emits `type-missing` for `decodeConcept`'s
15
- * only failure branch. Every other code — `family-invalid`,
16
- * `computation-runtime-missing`, `legacy-timestamp` — is pushed as an issue
17
- * while `decodeConcept` still returns a concept, so the concept IS in the
18
- * map (`internal/conceptDecode.ts:84-134`).
19
- */
20
- const UNDECODABLE_CODES = /* @__PURE__ */ new Set([
21
- "frontmatter-unparseable",
22
- "frontmatter-unclosed",
23
- "frontmatter-missing",
24
- "type-missing"
25
- ]);
26
- /**
27
- * Contract §2.3 steps 6–15. Loads the bundle, resolves the id, resolves the
28
- * human actor from git, splices exactly one entry into the concept's
29
- * frontmatter and writes it back atomically.
30
- *
31
- * Nothing here reads `status`, `stale_after`, or a type's
32
- * `require_verified`: `verify` is mechanical, not config-aware (V-4, V-5).
33
- *
34
- * @internal
35
- */
36
- const runVerify = Effect.fn("okfit/verify/runVerify")(function* (options) {
37
- const fs = yield* FileSystem.FileSystem;
38
- const path = yield* Path.Path;
39
- const bundle = yield* Bundle.load({ root: options.bundleRoot });
40
- const normalized = ConceptId.normalize(options.id);
41
- if (Option.isNone(normalized)) return yield* new VerifyConceptNotFoundError({
42
- id: options.id,
43
- root: bundle.root,
44
- reason: "not-a-concept"
45
- });
46
- const id = normalized.value;
47
- const conceptFile = ConceptId.toPath(id);
48
- if (ConceptId.isReservedFile(conceptFile)) return yield* new VerifyConceptNotFoundError({
49
- id,
50
- root: bundle.root,
51
- reason: "reserved"
52
- });
53
- const concept = bundle.concepts.get(id);
54
- if (concept === void 0) {
55
- const diagnostic = bundle.diagnostics.find((entry) => entry.file === conceptFile && UNDECODABLE_CODES.has(entry.code));
56
- return yield* new VerifyConceptNotFoundError({
57
- id,
58
- root: bundle.root,
59
- ...diagnostic === void 0 ? { reason: "not-a-concept" } : {
60
- reason: "undecodable",
61
- diagnosticCode: diagnostic.code
62
- }
63
- });
64
- }
65
- const actor = yield* Derivation.generatedBy({
66
- writer: "human",
67
- cwd: options.projectRoot,
68
- config: options.config
69
- });
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({
75
- id,
76
- shape: located.shape
77
- });
78
- 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)));
92
- }
93
- return {
94
- id,
95
- bundleRoot: bundle.root,
96
- conceptPath: concept.path,
97
- by: actor,
98
- at,
99
- priorAt,
100
- dryRun: options.dryRun,
101
- fragment: edit.content
102
- };
103
- });
104
-
105
- //#endregion
106
- export { runVerify };