@okfit/cli 0.2.0 → 0.3.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/init/scaffold.js DELETED
@@ -1,174 +0,0 @@
1
- import { Effect, Option, Schema } from "effect";
2
- import { Concept, ConceptId, Derive, LoadedConcept, OKF_SPEC_VERSION } from "@okfit/core";
3
- import { MarkdownDocument, MarkdownFrontmatter, MarkdownParseOptions, YamlFrontmatter } from "@effected/markdown";
4
-
5
- //#region src/init/scaffold.ts
6
- /** `.config/okfit.toml`, relative to the project root (C-10). @public */
7
- const CONFIG_RELATIVE_PATH = ".config/okfit.toml";
8
- /**
9
- * C-1's three project-level names, in precedence order. `init` refuses when
10
- * ANY of them already exists in the target directory (C-9), so a fresh
11
- * `okfit init` beside a hand-written `.okfit.toml` cannot silently write a
12
- * second config the discovery order then shadows.
13
- *
14
- * Scope is `projectRoot` only, never the upward chain: an ancestor's config
15
- * is a legitimate discovery hit, not a collision.
16
- *
17
- * @public
18
- */
19
- const PROJECT_CONFIG_NAMES = [
20
- ".okfit.toml",
21
- "okfit.toml",
22
- ".config/okfit.toml"
23
- ];
24
- /** C-22's directive, plus the blank line Tombi requires. @public */
25
- const SCHEMA_DIRECTIVE = "#:schema https://raw.githubusercontent.com/spencerbeggs/okfit/main/schemas/config/okfit-1.0.0.json\n\n";
26
- /**
27
- * K-23: the THIN override only — never the merged config. `bundle.path` is
28
- * the bundle root relative to the project root; `extensions: {}` satisfies
29
- * `OkfitConfig`'s one non-optional field (`CORE/OkfitConfig.ts:164`).
30
- * Everything else resolves from `DEFAULTS` and the profile at load time.
31
- * `actors.agent` is deliberately absent (K-24, P-17).
32
- *
33
- * @public
34
- */
35
- const configValue = (options) => ({
36
- bundle: {
37
- path: options.bundlePath,
38
- profile: options.profileName
39
- },
40
- extensions: {}
41
- });
42
- /**
43
- * Every path `init` will create, absolute, in write order, for the K-28
44
- * pre-flight: the three project-level config names, the bundle root's
45
- * `index.md`/`log.md`/`project.md`, then one `index.md` per layout directory
46
- * in `layout`'s own declared order — never re-sorted here. `Derive.renderIndex`
47
- * sorts its own `Subdirectories` section independently (C13), so this
48
- * function's order has no bearing on the rendered index text.
49
- *
50
- * @public
51
- */
52
- const targetPaths = (options) => [
53
- ...PROJECT_CONFIG_NAMES.map((name) => `${options.projectRoot}/${name}`),
54
- `${options.bundleRoot}/${options.layout.root.index}`,
55
- `${options.bundleRoot}/${options.layout.root.log}`,
56
- `${options.bundleRoot}/${options.layout.root.project}`,
57
- ...options.layout.directories.map((directory) => `${options.bundleRoot}/${directory.directory}/${options.layout.root.index}`)
58
- ];
59
- /** Frontmatter capture on — the write seam's own precondition (`MD/index.d.ts:1907-1920`; `CORE/internal/reserved.ts:15` precedent). */
60
- const PARSE_OPTIONS = MarkdownParseOptions.make({ frontmatter: true });
61
- /**
62
- * The four keys `project.md` actually carries (contract section 3.4's
63
- * worked transcript) — deliberately NOT the full `Concept` class. `Concept`'s
64
- * `extensions` and `raw` fields are required, not `optionalKey`
65
- * (`CORE/Concept.ts:39-40`), so encoding a `Concept.make` value through
66
- * `Concept` itself would serialize `extensions: {}` and `raw: {}` into the
67
- * frontmatter block too (verified: `Schema.encodeSync(Concept)` against a
68
- * `Concept.make` value with empty `extensions`/`raw` includes both keys,
69
- * this session). `Bundle.load` re-decodes a file written with this narrower
70
- * schema exactly as it would one written with the full class —
71
- * `internal/conceptDecode.ts` derives `extensions`/`raw` from whatever keys
72
- * the frontmatter block actually carries, never from what wrote it (verified
73
- * with a `Bundle.load` round trip, this session) — so the narrower schema
74
- * costs nothing at read time and matches the transcript byte-for-byte.
75
- */
76
- const ProjectFrontmatter = Schema.Struct({
77
- type: Schema.Literal("Project"),
78
- title: Schema.String,
79
- description: Schema.String,
80
- status: Schema.Literals([
81
- "draft",
82
- "stable",
83
- "deprecated"
84
- ])
85
- });
86
- const PROJECT_DESCRIPTION = "What this project is, its boundaries, and its non-goals.";
87
- const capitalize = (name) => `${name.charAt(0).toUpperCase()}${name.slice(1)}`;
88
- const projectBody = (title) => [
89
- `# ${title}`,
90
- "",
91
- "## Purpose",
92
- "",
93
- "Describe what this project is for in one paragraph.",
94
- "",
95
- "## Boundaries",
96
- "",
97
- "Describe what this project owns and what it deliberately leaves to others.",
98
- "",
99
- "## Non-goals",
100
- "",
101
- "List what this project will not do, so a reader never infers it from silence.",
102
- ""
103
- ].join("\n");
104
- /**
105
- * The three root markdown files and the per-directory indexes (K-25 to
106
- * K-27, K-59). `project.md`'s frontmatter is written with
107
- * `MarkdownFrontmatter.setToString` — the INSERT path, since the body below
108
- * carries no frontmatter block of its own (`MD/index.d.ts:1959`,
109
- * `FrontmatterWriteError`'s own doc comment: "a document with no frontmatter
110
- * capture is the insert path, not an error"). The root `index.md` is
111
- * `Derive.renderIndex` over a `LoadedConcept` this function synthesizes from
112
- * the just-rendered `project.md` text, re-parsed with `MarkdownDocument.parse`
113
- * (K-59; `renderIndex` reads only `frontmatter.type`/`title`/`description`
114
- * and `path`, never `document`, `CORE/Derive.ts:75-101`, so the synthesized
115
- * value is faithful). Each per-directory `index.md` is the literal
116
- * `# <Directory>` — NOT `Derive.renderIndex`, which returns the empty string
117
- * for an empty concept list with no options (contract Judge notes item 2).
118
- *
119
- * @public
120
- */
121
- const files = (options) => Effect.gen(function* () {
122
- const { bundleRoot, layout, profileName, projectTitle, today } = options;
123
- const frontmatterData = {
124
- type: "Project",
125
- title: projectTitle,
126
- description: PROJECT_DESCRIPTION,
127
- status: "draft"
128
- };
129
- const draftDocument = yield* MarkdownDocument.parse(projectBody(projectTitle), PARSE_OPTIONS);
130
- const projectText = yield* MarkdownFrontmatter.setToString(ProjectFrontmatter, YamlFrontmatter)(draftDocument, frontmatterData);
131
- const projectDocument = yield* MarkdownDocument.parse(projectText, PARSE_OPTIONS);
132
- const projectConcept = LoadedConcept.make({
133
- id: Option.getOrThrow(ConceptId.fromPath("project.md")),
134
- path: "project.md",
135
- frontmatter: Concept.make({
136
- ...frontmatterData,
137
- extensions: {},
138
- raw: frontmatterData
139
- }),
140
- document: projectDocument,
141
- computationBody: Option.none()
142
- });
143
- const subdirectories = layout.directories.map((directory) => directory.directory);
144
- const rootIndexText = Derive.renderIndex("", [projectConcept], {
145
- okfVersion: OKF_SPEC_VERSION,
146
- subdirectories
147
- });
148
- const logText = Derive.renderLogEntry({
149
- date: today,
150
- items: [`Initialized the bundle with the ${profileName} profile`]
151
- });
152
- const directoryFiles = layout.directories.map((directory) => ({
153
- path: `${bundleRoot}/${directory.directory}/${layout.root.index}`,
154
- contents: `# ${capitalize(directory.directory)}\n`
155
- }));
156
- return [
157
- {
158
- path: `${bundleRoot}/${layout.root.index}`,
159
- contents: rootIndexText
160
- },
161
- {
162
- path: `${bundleRoot}/${layout.root.log}`,
163
- contents: logText
164
- },
165
- {
166
- path: `${bundleRoot}/${layout.root.project}`,
167
- contents: projectText
168
- },
169
- ...directoryFiles
170
- ];
171
- });
172
-
173
- //#endregion
174
- export { CONFIG_RELATIVE_PATH, PROJECT_CONFIG_NAMES, SCHEMA_DIRECTIVE, configValue, files, targetPaths };
package/package.js DELETED
@@ -1,5 +0,0 @@
1
- //#region package.json
2
- var version = "0.2.0";
3
-
4
- //#endregion
5
- export { version };
package/render/exit.js DELETED
@@ -1,53 +0,0 @@
1
- //#region src/render/exit.ts
2
- /**
3
- * One pass over the collected diagnostics. `core.conformance` entries are
4
- * always severity `error` (D-33); the counts are still taken from `severity`
5
- * so a future non-error conformance code cannot silently change the exit
6
- * code.
7
- *
8
- * @public
9
- */
10
- const tally = (diagnostics) => {
11
- let conformanceErrors = 0;
12
- let lintErrors = 0;
13
- let lintWarnings = 0;
14
- let lintInfo = 0;
15
- let profileErrors = 0;
16
- for (const d of diagnostics) {
17
- if (d.source === "core.conformance") {
18
- if (d.severity === "error") conformanceErrors++;
19
- continue;
20
- }
21
- if (d.source === "core.lint") {
22
- if (d.severity === "error") lintErrors++;
23
- else if (d.severity === "warning") lintWarnings++;
24
- else lintInfo++;
25
- continue;
26
- }
27
- if (d.severity === "error") profileErrors++;
28
- }
29
- return {
30
- conformanceErrors,
31
- lintErrors,
32
- lintWarnings,
33
- lintInfo,
34
- profileErrors
35
- };
36
- };
37
- /**
38
- * K-7/K-8's two lowest tiers: `2` when any conformance error is present, else
39
- * `1` when any lint OR profile error is present, else `0`. Warnings and info
40
- * never move it. `130`, `64` and `3` are set elsewhere — by the runtime's
41
- * interrupt branch, by the `ShowHelp` remap in `bin.ts`, and by the typed
42
- * errors' own `[Runtime.errorExitCode]` — so this function returns only
43
- * `0 | 1 | 2`.
44
- *
45
- * @public
46
- */
47
- const forDiagnostics = (diagnostics) => {
48
- const t = tally(diagnostics);
49
- return t.conformanceErrors > 0 ? 2 : t.lintErrors + t.profileErrors > 0 ? 1 : 0;
50
- };
51
-
52
- //#endregion
53
- export { forDiagnostics, tally };
package/render/json.js DELETED
@@ -1,126 +0,0 @@
1
- import { tally } from "./exit.js";
2
- import { sort } from "./sort.js";
3
- import { Schema } from "effect";
4
- import { DiagnosticRange, DiagnosticSeverity } from "@okfit/core";
5
-
6
- //#region src/render/json.ts
7
- /** One entry of the `diagnostics` array (K-21). @public */
8
- const JsonDiagnostic = Schema.Struct({
9
- source: Schema.Literals([
10
- "core.conformance",
11
- "core.lint",
12
- "profile"
13
- ]),
14
- file: Schema.String,
15
- code: Schema.String,
16
- severity: DiagnosticSeverity,
17
- message: Schema.String,
18
- range: Schema.optionalKey(DiagnosticRange)
19
- });
20
- /** @public */
21
- const JsonSummary = Schema.Struct({
22
- conformance_errors: Schema.Number,
23
- lint_errors: Schema.Number,
24
- lint_warnings: Schema.Number,
25
- lint_info: Schema.Number,
26
- profile_errors: Schema.Number,
27
- concepts: Schema.Number
28
- });
29
- /**
30
- * K-21's success envelope, snake_case. `exit_code` is `0 | 1 | 2` only: an
31
- * infrastructure failure never produces this envelope, it produces the
32
- * `JsonErrorEnvelope` (K-22).
33
- *
34
- * @public
35
- */
36
- const JsonEnvelope = Schema.Struct({
37
- schema: Schema.Literal(1),
38
- okfit_version: Schema.String,
39
- okf_version: Schema.String,
40
- root: Schema.String,
41
- profile: Schema.NullOr(Schema.String),
42
- exit_code: Schema.Literals([
43
- 0,
44
- 1,
45
- 2
46
- ]),
47
- summary: JsonSummary,
48
- diagnostics: Schema.Array(JsonDiagnostic)
49
- });
50
- /** K-22's envelope: the ONLY thing stdout carries under `--format json` on an exit-3 failure. @public */
51
- const JsonErrorEnvelope = Schema.Struct({
52
- schema: Schema.Literal(1),
53
- okfit_version: Schema.String,
54
- exit_code: Schema.Literal(3),
55
- error: Schema.Struct({
56
- tag: Schema.String,
57
- message: Schema.String
58
- })
59
- });
60
- const toJsonDiagnostic = (d) => ({
61
- source: d.source,
62
- file: d.file,
63
- code: d.code,
64
- severity: d.severity,
65
- message: d.message,
66
- ...d.range === void 0 ? {} : { range: d.range }
67
- });
68
- /**
69
- * Build the success envelope. `diagnostics` is sorted here with the same
70
- * `sort` the human renderer uses (K-21's "diagnostics are in the K-17
71
- * order"); `range` passes through as core computed it, zero-based.
72
- *
73
- * The value returned is in `Type` form (its `range`s are `DiagnosticRange`
74
- * instances). What stdout carries is `Schema.encodeSync(JsonEnvelope)` of it,
75
- * `JSON.stringify`-ed — one document, no trailing text.
76
- *
77
- * @public
78
- */
79
- const json = (input) => {
80
- const t = tally(input.diagnostics);
81
- return {
82
- schema: 1,
83
- okfit_version: input.okfitVersion,
84
- okf_version: input.okfVersion,
85
- root: input.root,
86
- profile: input.profile,
87
- exit_code: input.exitCode,
88
- summary: {
89
- conformance_errors: t.conformanceErrors,
90
- lint_errors: t.lintErrors,
91
- lint_warnings: t.lintWarnings,
92
- lint_info: t.lintInfo,
93
- profile_errors: t.profileErrors,
94
- concepts: input.concepts
95
- },
96
- diagnostics: sort(input.diagnostics).map(toJsonDiagnostic)
97
- };
98
- };
99
- /** The error's `_tag` when it has one, else its constructor name; `"UnknownError"` for a non-object value. */
100
- const tagOf = (error) => {
101
- if (typeof error !== "object" || error === null) return "UnknownError";
102
- const tag = error._tag;
103
- if (typeof tag === "string") return tag;
104
- return error.constructor?.name ?? "UnknownError";
105
- };
106
- /** The error's `message` when it has one as a string, else `String(error)`. */
107
- const messageOf = (error) => {
108
- if (typeof error === "object" && error !== null && "message" in error) {
109
- const message = error.message;
110
- if (typeof message === "string") return message;
111
- }
112
- return String(error);
113
- };
114
- /** K-22. `tag` is the error's `_tag` when it has one, else its constructor name. @public */
115
- const jsonError = (error, okfitVersion) => ({
116
- schema: 1,
117
- okfit_version: okfitVersion,
118
- exit_code: 3,
119
- error: {
120
- tag: tagOf(error),
121
- message: messageOf(error)
122
- }
123
- });
124
-
125
- //#endregion
126
- export { JsonDiagnostic, JsonEnvelope, JsonErrorEnvelope, JsonSummary, json, jsonError };
package/render/sort.js DELETED
@@ -1,46 +0,0 @@
1
- //#region src/render/sort.ts
2
- const toRendered = (source) => (entry) => ({
3
- source,
4
- file: entry.file,
5
- code: entry.code,
6
- severity: entry.severity,
7
- message: entry.message,
8
- ...entry.range === void 0 ? {} : { range: entry.range }
9
- });
10
- /**
11
- * Tag core's two arrays and profiles' one, in producer order. Not named
12
- * `merge`: `OkfitConfig.merge` already owns that word in this codebase.
13
- *
14
- * @public
15
- */
16
- const collect = (conformance, lint, profile) => [
17
- ...conformance.map(toRendered("core.conformance")),
18
- ...lint.map(toRendered("core.lint")),
19
- ...profile.map(toRendered("profile"))
20
- ];
21
- /** Range-less entries sort before ranged ones within the same file; ranged entries by `offset`. */
22
- const compareRange = (a, b) => {
23
- if (a.range === void 0 && b.range === void 0) return 0;
24
- if (a.range === void 0) return -1;
25
- if (b.range === void 0) return 1;
26
- return a.range.offset - b.range.offset;
27
- };
28
- /**
29
- * K-17: `file` ascending (so `""` — the bundle-level finding — leads), then
30
- * range-less before ranged within a file, then `range.offset` ascending, then
31
- * `code`. Plain code-unit comparison, never locale-dependent (the same rule
32
- * profiles' own `check` sorts by, `PROFILES/SoftwareProject.ts:119`). Stable:
33
- * implemented with `toSorted`, which is specified as stable.
34
- *
35
- * @public
36
- */
37
- const sort = (diagnostics) => diagnostics.toSorted((a, b) => {
38
- if (a.file !== b.file) return a.file < b.file ? -1 : 1;
39
- const byRange = compareRange(a, b);
40
- if (byRange !== 0) return byRange;
41
- if (a.code !== b.code) return a.code < b.code ? -1 : 1;
42
- return 0;
43
- });
44
-
45
- //#endregion
46
- export { collect, sort };
package/sync/generated.js DELETED
@@ -1,111 +0,0 @@
1
- import { detectNewline, locateGenerated, stripBom } from "../verify/locate.js";
2
- import { spliceGenerated } from "../verify/splice.js";
3
- import { writeAtomic } from "./write.js";
4
- import { DateTime, Effect, FileSystem, Path, Schema } from "effect";
5
- import { Timestamp } from "@okfit/core";
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 DELETED
@@ -1,73 +0,0 @@
1
- import { writeAtomic } from "./write.js";
2
- import { Effect, FileSystem, Path } from "effect";
3
- import { Derive, OKF_SPEC_VERSION } from "@okfit/core";
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 };