@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/README.md +10 -7
- package/bin/okfit.js +2 -51
- package/commands/context.js +3 -6
- package/commands/init.js +3 -9
- package/commands/sync.js +3 -6
- package/commands/validate.js +3 -8
- package/commands/verify.js +3 -7
- package/errors.js +2 -105
- package/index.d.ts +25 -769
- package/index.js +4 -13
- package/main.d.ts +15 -0
- package/main.js +44 -0
- package/package.json +9 -6
- package/render/context.js +1 -80
- package/render/human.js +1 -1
- package/render/sync.js +1 -61
- package/render/verify.js +1 -44
- package/version.js +6 -6
- package/config/anchor.js +0 -57
- package/config/layer.js +0 -129
- package/config/resolve.js +0 -67
- package/context/run.js +0 -35
- package/init/scaffold.js +0 -174
- package/package.js +0 -5
- package/render/exit.js +0 -53
- package/render/json.js +0 -126
- package/render/sort.js +0 -46
- package/sync/generated.js +0 -111
- package/sync/index.js +0 -73
- package/sync/log.js +0 -127
- package/sync/run.js +0 -65
- package/sync/write.js +0 -48
- package/validate/run.js +0 -63
- package/verify/locate.js +0 -249
- package/verify/run.js +0 -106
- package/verify/splice.js +0 -75
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
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 };
|