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