@okfit/engine 0.11.1 → 0.13.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/edits/FrontmatterEdits.js +86 -3
- package/errors.js +13 -1
- package/index.d.ts +104 -1
- package/index.js +4 -3
- package/package.json +14 -14
- package/render/sync.js +28 -5
- package/sync/publication.js +98 -0
- package/validate/resources.js +3 -3
- package/validate/run.js +11 -2
- package/validate/surfaces.js +98 -0
- package/verify/locate.js +21 -4
- package/verify/run.js +2 -2
- package/verify/splice.js +15 -4
- package/version.js +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { documentNewline, locate, locateTopLevelScalar, stripBom } from "../verify/locate.js";
|
|
2
|
-
import { splice, spliceTopLevelScalar } from "../verify/splice.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { orderVerifyEdits, splice, spliceTopLevelScalar } from "../verify/splice.js";
|
|
3
|
+
import { DiagnosticRange } from "@okfit/core";
|
|
4
|
+
import { Effect, Result, Runtime, Schema } from "effect";
|
|
5
|
+
import { MarkdownDocument, MarkdownEdit } from "@effected/markdown";
|
|
5
6
|
|
|
6
7
|
//#region src/edits/FrontmatterEdits.ts
|
|
7
8
|
/**
|
|
@@ -28,6 +29,8 @@ const shift = (edit, by) => by === 0 ? edit : MarkdownEdit.make({
|
|
|
28
29
|
length: edit.length,
|
|
29
30
|
content: edit.content
|
|
30
31
|
});
|
|
32
|
+
/** The value of a located scalar's raw source text, quotes removed. */
|
|
33
|
+
const unquote = (raw, quote) => quote === "plain" ? raw.trim() : raw.slice(1, -1);
|
|
31
34
|
/**
|
|
32
35
|
* A public facade over the `verify/locate.ts` and `verify/splice.ts`
|
|
33
36
|
* machinery: byte-range `MarkdownEdit`s for a concept's top-level `status`
|
|
@@ -70,6 +73,86 @@ var FrontmatterEdits = class {
|
|
|
70
73
|
const edit = splice(target, entry, documentNewline(text));
|
|
71
74
|
return [shift(edit, bom.length)];
|
|
72
75
|
});
|
|
76
|
+
/**
|
|
77
|
+
* Edits that append one verified entry AND set top-level `status`, the combined write
|
|
78
|
+
* `okfit verify --stable` performs. A `status` scalar already equal to `status` makes no
|
|
79
|
+
* status edit, and equal-offset inserts are merged with `status:` first. Unlike the CLI,
|
|
80
|
+
* which reads the parsed status, this always locates `status`, so an unsupported shape
|
|
81
|
+
* on it fails even when its value already matches.
|
|
82
|
+
*/
|
|
83
|
+
static verifiedWithStatus = (source, entry, status) => Effect.gen(function* () {
|
|
84
|
+
const { text, bom } = stripBom(source);
|
|
85
|
+
const located = yield* locate(text);
|
|
86
|
+
if (located._tag === "unsupported") return yield* new UnsupportedFrontmatterError({
|
|
87
|
+
key: "verified",
|
|
88
|
+
shape: located.shape
|
|
89
|
+
});
|
|
90
|
+
const newline = documentNewline(text);
|
|
91
|
+
const verifiedEdit = splice(located, entry, newline);
|
|
92
|
+
let statusEdit;
|
|
93
|
+
const target = yield* locateTopLevelScalar(text, "status");
|
|
94
|
+
if (target._tag === "unsupported") return yield* new UnsupportedFrontmatterError({
|
|
95
|
+
key: "status",
|
|
96
|
+
shape: target.shape
|
|
97
|
+
});
|
|
98
|
+
if (target._tag !== "replaceScalar" || unquote(text.slice(target.start, target.end), target.quote) !== status) statusEdit = spliceTopLevelScalar(target, "status", status, newline);
|
|
99
|
+
return orderVerifyEdits(verifiedEdit, statusEdit).map((edit) => shift(edit, bom.length));
|
|
100
|
+
});
|
|
101
|
+
/**
|
|
102
|
+
* Edits that stamp `renders[i].body_sha256` with `digests[i]`: an existing value is replaced in
|
|
103
|
+
* place, a missing key is inserted on the line after `renders[i].path` at that key's column.
|
|
104
|
+
* Block-style `renders` only; flow style fails with `UnsupportedFrontmatterError`.
|
|
105
|
+
*/
|
|
106
|
+
static rendersDigests = (source, digests) => Effect.gen(function* () {
|
|
107
|
+
const { text, bom } = stripBom(source);
|
|
108
|
+
const unsupported = (shape) => new UnsupportedFrontmatterError({
|
|
109
|
+
key: "renders",
|
|
110
|
+
shape
|
|
111
|
+
});
|
|
112
|
+
const parsed = MarkdownDocument.parseResult(text, { frontmatter: true });
|
|
113
|
+
if (Result.isFailure(parsed)) return yield* unsupported("unparseable");
|
|
114
|
+
const document = parsed.success;
|
|
115
|
+
const block = DiagnosticRange.forFrontmatterPath(document, []);
|
|
116
|
+
const rendersSpan = DiagnosticRange.forFrontmatterPath(document, ["renders"]);
|
|
117
|
+
if (block === void 0 || rendersSpan === void 0 || rendersSpan.offset === block.offset) return yield* unsupported("absent");
|
|
118
|
+
if (text.charAt(rendersSpan.offset) === "[") return yield* unsupported("flow-sequence");
|
|
119
|
+
const newline = documentNewline(text);
|
|
120
|
+
const edits = [];
|
|
121
|
+
for (const [index, digest] of digests.entries()) {
|
|
122
|
+
const digestSpan = DiagnosticRange.forFrontmatterPath(document, [
|
|
123
|
+
"renders",
|
|
124
|
+
index,
|
|
125
|
+
"body_sha256"
|
|
126
|
+
]);
|
|
127
|
+
if (digestSpan !== void 0 && digestSpan.offset !== block.offset) {
|
|
128
|
+
const content = digestSpan.length === 0 ? ` ${digest}` : digest;
|
|
129
|
+
edits.push(shift(MarkdownEdit.make({
|
|
130
|
+
offset: digestSpan.offset,
|
|
131
|
+
length: digestSpan.length,
|
|
132
|
+
content
|
|
133
|
+
}), bom.length));
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const pathSpan = DiagnosticRange.forFrontmatterPath(document, [
|
|
137
|
+
"renders",
|
|
138
|
+
index,
|
|
139
|
+
"path"
|
|
140
|
+
]);
|
|
141
|
+
if (pathSpan === void 0 || pathSpan.offset === block.offset) return yield* unsupported("missing-path");
|
|
142
|
+
const lineStart = text.lastIndexOf("\n", pathSpan.offset - 1) + 1;
|
|
143
|
+
const prefix = /^[ ]*(?:-[ ]+)*(?:"path"|'path'|path)[ ]*:[ ]*$/u.exec(text.slice(lineStart, pathSpan.offset));
|
|
144
|
+
if (prefix === null) return yield* unsupported("flow-mapping");
|
|
145
|
+
const column = /^[ ]*(?:-[ ]+)*/u.exec(prefix[0])?.[0].length ?? 0;
|
|
146
|
+
const newlineAt = text.indexOf("\n", pathSpan.offset + pathSpan.length);
|
|
147
|
+
const lineEnd = newlineAt === -1 ? text.length : newlineAt > 0 && text.charAt(newlineAt - 1) === "\r" ? newlineAt - 1 : newlineAt;
|
|
148
|
+
edits.push(shift(MarkdownEdit.make({
|
|
149
|
+
offset: lineEnd,
|
|
150
|
+
length: 0,
|
|
151
|
+
content: `${newline}${" ".repeat(column)}body_sha256: ${digest}`
|
|
152
|
+
}), bom.length));
|
|
153
|
+
}
|
|
154
|
+
return edits;
|
|
155
|
+
});
|
|
73
156
|
};
|
|
74
157
|
|
|
75
158
|
//#endregion
|
package/errors.js
CHANGED
|
@@ -121,6 +121,18 @@ var SyncStagedLogError = class extends Schema.TaggedError()("SyncStagedLogError"
|
|
|
121
121
|
}
|
|
122
122
|
};
|
|
123
123
|
/**
|
|
124
|
+
* `okfit sync --publication` runs nothing else, so combining it with a mode
|
|
125
|
+
* or selection flag (`--only`, `--staged`, `--since`) is a usage error. Exit 64.
|
|
126
|
+
*
|
|
127
|
+
* @public
|
|
128
|
+
*/
|
|
129
|
+
var SyncPublicationConflictError = class extends Schema.TaggedError()("SyncPublicationConflictError", { flags: Schema.Array(Schema.String) }) {
|
|
130
|
+
[Runtime.errorExitCode] = 64;
|
|
131
|
+
get message() {
|
|
132
|
+
return `--publication restamps one Publication and runs nothing else; it cannot combine with ${this.flags.join(", ")}`;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
124
136
|
* Issue #138: `okfit verify`'s selection was contradictory or empty. Exit 64: a usage error.
|
|
125
137
|
*
|
|
126
138
|
* @public
|
|
@@ -222,4 +234,4 @@ var QuerySelectionError = class extends Schema.TaggedError()("QuerySelectionErro
|
|
|
222
234
|
};
|
|
223
235
|
|
|
224
236
|
//#endregion
|
|
225
|
-
export { ConfigMalformedError, ConfigPathNotFoundError, DocumentPathError, InitOverwriteError, QueryConceptNotFoundError, QuerySelectionError, QueryUnknownVocabularyError, SyncStagedLogError, VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError };
|
|
237
|
+
export { ConfigMalformedError, ConfigPathNotFoundError, DocumentPathError, InitOverwriteError, QueryConceptNotFoundError, QuerySelectionError, QueryUnknownVocabularyError, SyncPublicationConflictError, SyncStagedLogError, VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError };
|
package/index.d.ts
CHANGED
|
@@ -173,6 +173,19 @@ export declare class SyncStagedLogError extends SyncStagedLogError_base {
|
|
|
173
173
|
readonly [Runtime.errorExitCode] = 64;
|
|
174
174
|
get message(): string;
|
|
175
175
|
}
|
|
176
|
+
declare const SyncPublicationConflictError_base: Schema.Class<SyncPublicationConflictError, Schema.TaggedStruct<"SyncPublicationConflictError", {
|
|
177
|
+
readonly flags: Schema.$Array<Schema.String>;
|
|
178
|
+
}>, import("effect/Cause").YieldableError>;
|
|
179
|
+
/**
|
|
180
|
+
* `okfit sync --publication` runs nothing else, so combining it with a mode
|
|
181
|
+
* or selection flag (`--only`, `--staged`, `--since`) is a usage error. Exit 64.
|
|
182
|
+
*
|
|
183
|
+
* @public
|
|
184
|
+
*/
|
|
185
|
+
export declare class SyncPublicationConflictError extends SyncPublicationConflictError_base {
|
|
186
|
+
readonly [Runtime.errorExitCode] = 64;
|
|
187
|
+
get message(): string;
|
|
188
|
+
}
|
|
176
189
|
declare const VerifySelectionError_base: Schema.Class<VerifySelectionError, Schema.TaggedStruct<"VerifySelectionError", {
|
|
177
190
|
readonly reason: Schema.Literals<readonly ["no-selection", "id-and-batch", "unknown-type", "status-conflict", "status-and-batch"]>;
|
|
178
191
|
readonly detail: Schema.optionalKey<Schema.String>;
|
|
@@ -466,6 +479,23 @@ export declare class FrontmatterEdits {
|
|
|
466
479
|
readonly by: string;
|
|
467
480
|
readonly at: string;
|
|
468
481
|
}) => Effect.Effect<ReadonlyArray<MarkdownEdit>, YamlParseError | UnsupportedFrontmatterError>;
|
|
482
|
+
/**
|
|
483
|
+
* Edits that append one verified entry AND set top-level `status`, the combined write
|
|
484
|
+
* `okfit verify --stable` performs. A `status` scalar already equal to `status` makes no
|
|
485
|
+
* status edit, and equal-offset inserts are merged with `status:` first. Unlike the CLI,
|
|
486
|
+
* which reads the parsed status, this always locates `status`, so an unsupported shape
|
|
487
|
+
* on it fails even when its value already matches.
|
|
488
|
+
*/
|
|
489
|
+
static readonly verifiedWithStatus: (source: string, entry: {
|
|
490
|
+
readonly by: string;
|
|
491
|
+
readonly at: string;
|
|
492
|
+
}, status: Status) => Effect.Effect<ReadonlyArray<MarkdownEdit>, YamlParseError | UnsupportedFrontmatterError>;
|
|
493
|
+
/**
|
|
494
|
+
* Edits that stamp `renders[i].body_sha256` with `digests[i]`: an existing value is replaced in
|
|
495
|
+
* place, a missing key is inserted on the line after `renders[i].path` at that key's column.
|
|
496
|
+
* Block-style `renders` only; flow style fails with `UnsupportedFrontmatterError`.
|
|
497
|
+
*/
|
|
498
|
+
static readonly rendersDigests: (source: string, digests: ReadonlyArray<string>) => Effect.Effect<ReadonlyArray<MarkdownEdit>, YamlParseError | UnsupportedFrontmatterError>;
|
|
469
499
|
}
|
|
470
500
|
//#endregion
|
|
471
501
|
//#region src/external/ExternalReferences.d.ts
|
|
@@ -1480,6 +1510,22 @@ export declare const SyncModeEnvelope: Schema.Struct<{
|
|
|
1480
1510
|
}>;
|
|
1481
1511
|
/** @public */
|
|
1482
1512
|
export type SyncModeEnvelope = typeof SyncModeEnvelope.Type;
|
|
1513
|
+
/**
|
|
1514
|
+
* One `okfit sync --publication` result: the Publication id, whether its
|
|
1515
|
+
* file changed, and the digest stamped for each rendered source.
|
|
1516
|
+
*
|
|
1517
|
+
* @public
|
|
1518
|
+
*/
|
|
1519
|
+
export declare const PublicationEnvelope: Schema.Struct<{
|
|
1520
|
+
readonly id: Schema.String;
|
|
1521
|
+
readonly written: Schema.Boolean;
|
|
1522
|
+
readonly digests: Schema.$Array<Schema.Struct<{
|
|
1523
|
+
readonly path: Schema.String;
|
|
1524
|
+
readonly body_sha256: Schema.String;
|
|
1525
|
+
}>>;
|
|
1526
|
+
}>;
|
|
1527
|
+
/** @public */
|
|
1528
|
+
export type PublicationEnvelope = typeof PublicationEnvelope.Type;
|
|
1483
1529
|
/**
|
|
1484
1530
|
* Contract §14 note 5: `schema: 1` is INFERRED, not in the design's own
|
|
1485
1531
|
* §2 JSON example — every other envelope in this codebase
|
|
@@ -1529,6 +1575,15 @@ export declare const SyncEnvelope: Schema.Struct<{
|
|
|
1529
1575
|
readonly reason: Schema.Literals<readonly ["untracked", "dirty", "unborn", "generated-missing", "generated-unsupported", "log-unparseable"]>;
|
|
1530
1576
|
}>>;
|
|
1531
1577
|
}>;
|
|
1578
|
+
/** Present only for `okfit sync --publication`; then the three modes are all `selected: false`. */
|
|
1579
|
+
readonly publication: Schema.optionalKey<Schema.Struct<{
|
|
1580
|
+
readonly id: Schema.String;
|
|
1581
|
+
readonly written: Schema.Boolean;
|
|
1582
|
+
readonly digests: Schema.$Array<Schema.Struct<{
|
|
1583
|
+
readonly path: Schema.String;
|
|
1584
|
+
readonly body_sha256: Schema.String;
|
|
1585
|
+
}>>;
|
|
1586
|
+
}>>;
|
|
1532
1587
|
}>;
|
|
1533
1588
|
/** @public */
|
|
1534
1589
|
export type SyncEnvelope = typeof SyncEnvelope.Type;
|
|
@@ -1543,7 +1598,8 @@ export declare const syncEnvelope: (input: {
|
|
|
1543
1598
|
readonly okfitVersion: string;
|
|
1544
1599
|
readonly root: string;
|
|
1545
1600
|
readonly dryRun: boolean;
|
|
1546
|
-
readonly result
|
|
1601
|
+
readonly result?: SyncResult;
|
|
1602
|
+
readonly publication?: PublicationEnvelope;
|
|
1547
1603
|
readonly distribution?: Distribution;
|
|
1548
1604
|
}) => SyncEnvelope;
|
|
1549
1605
|
//#endregion
|
|
@@ -1853,6 +1909,49 @@ interface StaleRunResult {
|
|
|
1853
1909
|
*/
|
|
1854
1910
|
export declare const runStale: (options: StaleRunOptions) => Effect.Effect<StaleRunResult, BundleLoadError, FileSystem.FileSystem | Path.Path>;
|
|
1855
1911
|
//#endregion
|
|
1912
|
+
//#region src/sync/publication.d.ts
|
|
1913
|
+
declare const PublicationNotFoundError_base: Schema.Class<PublicationNotFoundError, Schema.TaggedStruct<"PublicationNotFoundError", {
|
|
1914
|
+
readonly id: Schema.String;
|
|
1915
|
+
readonly publication: Schema.optionalKey<Schema.String>;
|
|
1916
|
+
}>, import("effect/Cause").YieldableError>;
|
|
1917
|
+
/**
|
|
1918
|
+
* No concept answers to `id` -- or one of a Publication's `renders` paths
|
|
1919
|
+
* resolves to no concept, in which case `id` names that missing source.
|
|
1920
|
+
*
|
|
1921
|
+
* @public
|
|
1922
|
+
*/
|
|
1923
|
+
export declare class PublicationNotFoundError extends PublicationNotFoundError_base {
|
|
1924
|
+
readonly [Runtime.errorExitCode] = 64;
|
|
1925
|
+
get message(): string;
|
|
1926
|
+
}
|
|
1927
|
+
declare const NotAPublicationError_base: Schema.Class<NotAPublicationError, Schema.TaggedStruct<"NotAPublicationError", {
|
|
1928
|
+
readonly id: Schema.String;
|
|
1929
|
+
readonly type: Schema.String;
|
|
1930
|
+
}>, import("effect/Cause").YieldableError>;
|
|
1931
|
+
/**
|
|
1932
|
+
* The concept at `id` exists but is not a Publication (`type` is what it is).
|
|
1933
|
+
*
|
|
1934
|
+
* @public
|
|
1935
|
+
*/
|
|
1936
|
+
export declare class NotAPublicationError extends NotAPublicationError_base {
|
|
1937
|
+
readonly [Runtime.errorExitCode] = 64;
|
|
1938
|
+
get message(): string;
|
|
1939
|
+
}
|
|
1940
|
+
/**
|
|
1941
|
+
* Restamps one Publication's `renders[].body_sha256` digests from the current
|
|
1942
|
+
* body of each rendered source concept, writing the Publication file
|
|
1943
|
+
* atomically unless `dryRun`. `written` is whether the text changed (so a
|
|
1944
|
+
* `dryRun` that would change the file reports `true` without writing).
|
|
1945
|
+
* A malformed `renders` fails `UnsupportedFrontmatterError`.
|
|
1946
|
+
*
|
|
1947
|
+
* @public
|
|
1948
|
+
*/
|
|
1949
|
+
export declare const stampPublication: (options: {
|
|
1950
|
+
readonly bundleRoot: string;
|
|
1951
|
+
readonly id: string;
|
|
1952
|
+
readonly dryRun: boolean;
|
|
1953
|
+
}) => Effect.Effect<PublicationEnvelope, PublicationNotFoundError | NotAPublicationError | BundleLoadError | YamlParseError | UnsupportedFrontmatterError | PlatformError.PlatformError, FileSystem.FileSystem | Path.Path | Crypto.Crypto>;
|
|
1954
|
+
//#endregion
|
|
1856
1955
|
//#region src/validate/run.d.ts
|
|
1857
1956
|
declare const Now_base: Context.ServiceClass<Now, "@okfit/engine/Now", DateTime.Utc>;
|
|
1858
1957
|
/**
|
|
@@ -1920,6 +2019,10 @@ interface RunResult {
|
|
|
1920
2019
|
* the same `report.lint` array every other core lint diagnostic already
|
|
1921
2020
|
* does.
|
|
1922
2021
|
*
|
|
2022
|
+
* It then appends `lintResources`, `Publications.lint` (`publication-drift`
|
|
2023
|
+
* and `publication-orphan`, only under the `software-project` profile) and `lintSurfaces` (`surface-unmatched`), each
|
|
2024
|
+
* of which gates itself on its own severity.
|
|
2025
|
+
*
|
|
1923
2026
|
* K-52 holds structurally: `Bundle.load`'s failure short-circuits the
|
|
1924
2027
|
* generator, so `profile.check` never runs on a bundle that did not load.
|
|
1925
2028
|
*
|
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveBundleRoot, resolveProjectRoot } from "./config/anchor.js";
|
|
2
|
-
import { ConfigMalformedError, ConfigPathNotFoundError, DocumentPathError, InitOverwriteError, QueryConceptNotFoundError, QuerySelectionError, QueryUnknownVocabularyError, SyncStagedLogError, VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError } from "./errors.js";
|
|
2
|
+
import { ConfigMalformedError, ConfigPathNotFoundError, DocumentPathError, InitOverwriteError, QueryConceptNotFoundError, QuerySelectionError, QueryUnknownVocabularyError, SyncPublicationConflictError, SyncStagedLogError, VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError } from "./errors.js";
|
|
3
3
|
import { buildConfigLayer, provideConfig } from "./config/layer.js";
|
|
4
4
|
import { DEFAULT_PROFILE_NAME, resolveProjectConfig } from "./config/resolve.js";
|
|
5
5
|
import { runContext } from "./context/run.js";
|
|
@@ -21,14 +21,15 @@ import { JsonDiagnostic, JsonEnvelope, JsonErrorEnvelope, JsonSummary, json, jso
|
|
|
21
21
|
import { ConceptSummary, QueryGetEnvelope, QueryLink, QueryListEnvelope, QueryNeighbor, QueryNeighborsEnvelope, queryGetEnvelope, queryListEnvelope, queryNeighborsEnvelope, toConceptSummary } from "./render/query.js";
|
|
22
22
|
import { StaleEnvelope, StaleItem, StaleSummary, staleEnvelope } from "./render/stale.js";
|
|
23
23
|
import { SkipReason, runSync } from "./sync/run.js";
|
|
24
|
-
import { SyncEnvelope, SyncModeEnvelope, syncEnvelope } from "./render/sync.js";
|
|
24
|
+
import { PublicationEnvelope, SyncEnvelope, SyncModeEnvelope, syncEnvelope } from "./render/sync.js";
|
|
25
25
|
import { VerifyBatchEnvelope, VerifyEnvelope, verifyBatchEnvelope, verifyEnvelope } from "./render/verify.js";
|
|
26
26
|
import { Now, run } from "./validate/run.js";
|
|
27
27
|
import { conceptFor } from "./session/concept.js";
|
|
28
28
|
import { withFallbackRange } from "./session/range.js";
|
|
29
29
|
import { BundleSession } from "./session/BundleSession.js";
|
|
30
30
|
import { runStale } from "./stale/run.js";
|
|
31
|
+
import { NotAPublicationError, PublicationNotFoundError, stampPublication } from "./sync/publication.js";
|
|
31
32
|
import { resolveBatchTypes, selectAttestable, selectPickerCandidates } from "./verify/select.js";
|
|
32
33
|
import { loadPickerCandidates, runVerify, runVerifyBatch, runVerifyIds } from "./verify/run.js";
|
|
33
34
|
|
|
34
|
-
export { BundleSession, CONFIG_RELATIVE_PATH, ConceptQuery, ConceptSummary, ConfigMalformedError, ConfigPathNotFoundError, ContextEnvelope, ContextField, ContextFieldValue, ContextTag, ContextType, DEFAULT_PROFILE_NAME, DistributionField, DocumentPathError, ENGINE_VERSION, ExternalReferences, FrontmatterEdits, GraphEdgeEnvelope, GraphEnvelope, GraphNodeEnvelope, GraphSummary, InitOverwriteError, JsonDiagnostic, JsonEnvelope, JsonErrorEnvelope, JsonSummary, Now, OkfitPlatform, OverlayDocuments, QueryConceptNotFoundError, QueryGetEnvelope, QueryLink, QueryListEnvelope, QueryNeighbor, QueryNeighborsEnvelope, QuerySelectionError, QueryUnknownVocabularyError, ReferenceCheck, ReferenceState, SkipReason, StaleEnvelope, StaleItem, StaleSummary, SyncEnvelope, SyncModeEnvelope, SyncStagedLogError, UnsupportedFrontmatterError, VerifyBatchEnvelope, VerifyConceptNotFoundError, VerifyEnvelope, VerifySelectionError, VerifyUnsupportedFrontmatterError, buildConfigLayer, collect, conceptFor, configValue, contextEnvelope, files, forDiagnostics, graphEnvelope, json, jsonError, layerOverlayFileSystem, loadPickerCandidates, makeOverlayFileSystem, provideConfig, provideDocuments, queryGetEnvelope, queryListEnvelope, queryNeighborsEnvelope, resolveBatchTypes, resolveBundleRoot, resolveDocumentPath, resolveProjectConfig, resolveProjectRoot, run, runContext, runGraph, runStale, runSync, runVerify, runVerifyBatch, runVerifyIds, selectAttestable, selectPickerCandidates, sort, staleEnvelope, syncEnvelope, tally, targetPaths, toConceptSummary, verifyBatchEnvelope, verifyEnvelope, withFallbackRange };
|
|
35
|
+
export { BundleSession, CONFIG_RELATIVE_PATH, ConceptQuery, ConceptSummary, ConfigMalformedError, ConfigPathNotFoundError, ContextEnvelope, ContextField, ContextFieldValue, ContextTag, ContextType, DEFAULT_PROFILE_NAME, DistributionField, DocumentPathError, ENGINE_VERSION, ExternalReferences, FrontmatterEdits, GraphEdgeEnvelope, GraphEnvelope, GraphNodeEnvelope, GraphSummary, InitOverwriteError, JsonDiagnostic, JsonEnvelope, JsonErrorEnvelope, JsonSummary, NotAPublicationError, Now, OkfitPlatform, OverlayDocuments, PublicationEnvelope, PublicationNotFoundError, QueryConceptNotFoundError, QueryGetEnvelope, QueryLink, QueryListEnvelope, QueryNeighbor, QueryNeighborsEnvelope, QuerySelectionError, QueryUnknownVocabularyError, ReferenceCheck, ReferenceState, SkipReason, StaleEnvelope, StaleItem, StaleSummary, SyncEnvelope, SyncModeEnvelope, SyncPublicationConflictError, SyncStagedLogError, UnsupportedFrontmatterError, VerifyBatchEnvelope, VerifyConceptNotFoundError, VerifyEnvelope, VerifySelectionError, VerifyUnsupportedFrontmatterError, buildConfigLayer, collect, conceptFor, configValue, contextEnvelope, files, forDiagnostics, graphEnvelope, json, jsonError, layerOverlayFileSystem, loadPickerCandidates, makeOverlayFileSystem, provideConfig, provideDocuments, queryGetEnvelope, queryListEnvelope, queryNeighborsEnvelope, resolveBatchTypes, resolveBundleRoot, resolveDocumentPath, resolveProjectConfig, resolveProjectRoot, run, runContext, runGraph, runStale, runSync, runVerify, runVerifyBatch, runVerifyIds, selectAttestable, selectPickerCandidates, sort, staleEnvelope, stampPublication, syncEnvelope, tally, targetPaths, toConceptSummary, verifyBatchEnvelope, verifyEnvelope, withFallbackRange };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@okfit/engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The okfit engine: platform layer, config discovery, and the validate, verify, sync, init and context programs shared by the okfit CLI and MCP server.",
|
|
6
6
|
"keywords": [
|
|
@@ -34,23 +34,23 @@
|
|
|
34
34
|
"./package.json": "./package.json"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@effect/platform-node": "^4.0.
|
|
38
|
-
"@effected/app": "^0.
|
|
39
|
-
"@effected/config-file": "^0.14.
|
|
40
|
-
"@effected/engine": "^0.
|
|
37
|
+
"@effect/platform-node": "^4.0.1",
|
|
38
|
+
"@effected/app": "^0.21.2",
|
|
39
|
+
"@effected/config-file": "^0.14.2",
|
|
40
|
+
"@effected/engine": "^0.4.0",
|
|
41
41
|
"@effected/git": "^0.20.0",
|
|
42
42
|
"@effected/glob": "^0.10.0",
|
|
43
|
-
"@effected/jsonc": "^0.15.
|
|
44
|
-
"@effected/markdown": "^0.15.
|
|
45
|
-
"@effected/schemastore": "^0.
|
|
46
|
-
"@effected/store": "^0.
|
|
47
|
-
"@effected/toml": "^0.11.
|
|
43
|
+
"@effected/jsonc": "^0.15.1",
|
|
44
|
+
"@effected/markdown": "^0.15.1",
|
|
45
|
+
"@effected/schemastore": "^0.21.1",
|
|
46
|
+
"@effected/store": "^0.13.1",
|
|
47
|
+
"@effected/toml": "^0.11.1",
|
|
48
48
|
"@effected/walker": "^0.15.0",
|
|
49
49
|
"@effected/xdg": "^0.9.0",
|
|
50
|
-
"@effected/yaml": "^0.19.
|
|
51
|
-
"@okfit/core": "0.
|
|
52
|
-
"@okfit/profiles": "0.
|
|
53
|
-
"effect": "^4.0.
|
|
50
|
+
"@effected/yaml": "^0.19.1",
|
|
51
|
+
"@okfit/core": "0.9.1",
|
|
52
|
+
"@okfit/profiles": "0.9.0",
|
|
53
|
+
"effect": "^4.0.1"
|
|
54
54
|
},
|
|
55
55
|
"engines": {
|
|
56
56
|
"node": ">=24.11.0"
|
package/render/sync.js
CHANGED
|
@@ -15,6 +15,20 @@ const SyncModeEnvelope = Schema.Struct({
|
|
|
15
15
|
}))
|
|
16
16
|
});
|
|
17
17
|
/**
|
|
18
|
+
* One `okfit sync --publication` result: the Publication id, whether its
|
|
19
|
+
* file changed, and the digest stamped for each rendered source.
|
|
20
|
+
*
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
const PublicationEnvelope = Schema.Struct({
|
|
24
|
+
id: Schema.String,
|
|
25
|
+
written: Schema.Boolean,
|
|
26
|
+
digests: Schema.Array(Schema.Struct({
|
|
27
|
+
path: Schema.String,
|
|
28
|
+
body_sha256: Schema.String
|
|
29
|
+
}))
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
18
32
|
* Contract §14 note 5: `schema: 1` is INFERRED, not in the design's own
|
|
19
33
|
* §2 JSON example — every other envelope in this codebase
|
|
20
34
|
* (`JsonEnvelope`, `VerifyEnvelope`, `ContextEnvelope`) opens with it, so
|
|
@@ -35,8 +49,16 @@ const SyncEnvelope = Schema.Struct({
|
|
|
35
49
|
exit_code: Schema.Literal(0),
|
|
36
50
|
generated: SyncModeEnvelope,
|
|
37
51
|
index: SyncModeEnvelope,
|
|
38
|
-
log: SyncModeEnvelope
|
|
52
|
+
log: SyncModeEnvelope,
|
|
53
|
+
/** Present only for `okfit sync --publication`; then the three modes are all `selected: false`. */
|
|
54
|
+
publication: Schema.optionalKey(PublicationEnvelope)
|
|
39
55
|
});
|
|
56
|
+
const NOT_SELECTED = {
|
|
57
|
+
selected: false,
|
|
58
|
+
written: [],
|
|
59
|
+
unchanged: [],
|
|
60
|
+
skipped: []
|
|
61
|
+
};
|
|
40
62
|
const toModeEnvelope = (mode) => ({
|
|
41
63
|
selected: mode.selected,
|
|
42
64
|
written: [...mode.written],
|
|
@@ -61,10 +83,11 @@ const syncEnvelope = (input) => ({
|
|
|
61
83
|
root: input.root,
|
|
62
84
|
dry_run: input.dryRun,
|
|
63
85
|
exit_code: 0,
|
|
64
|
-
generated: toModeEnvelope(input.result.generated),
|
|
65
|
-
index: toModeEnvelope(input.result.index),
|
|
66
|
-
log: toModeEnvelope(input.result.log)
|
|
86
|
+
generated: input.result === void 0 ? NOT_SELECTED : toModeEnvelope(input.result.generated),
|
|
87
|
+
index: input.result === void 0 ? NOT_SELECTED : toModeEnvelope(input.result.index),
|
|
88
|
+
log: input.result === void 0 ? NOT_SELECTED : toModeEnvelope(input.result.log),
|
|
89
|
+
...input.publication === void 0 ? {} : { publication: input.publication }
|
|
67
90
|
});
|
|
68
91
|
|
|
69
92
|
//#endregion
|
|
70
|
-
export { SyncEnvelope, SyncModeEnvelope, syncEnvelope };
|
|
93
|
+
export { PublicationEnvelope, SyncEnvelope, SyncModeEnvelope, syncEnvelope };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { FrontmatterEdits, UnsupportedFrontmatterError } from "../edits/FrontmatterEdits.js";
|
|
2
|
+
import { writeAtomic } from "./write.js";
|
|
3
|
+
import { Bundle, ConceptId } from "@okfit/core";
|
|
4
|
+
import { Effect, Option, Path, Runtime, Schema } from "effect";
|
|
5
|
+
import { Derivation, PUBLICATION_TYPE, Publications } from "@okfit/profiles";
|
|
6
|
+
import { MarkdownEdit } from "@effected/markdown";
|
|
7
|
+
|
|
8
|
+
//#region src/sync/publication.ts
|
|
9
|
+
/**
|
|
10
|
+
* No concept answers to `id` -- or one of a Publication's `renders` paths
|
|
11
|
+
* resolves to no concept, in which case `id` names that missing source.
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
var PublicationNotFoundError = class extends Schema.TaggedError()("PublicationNotFoundError", {
|
|
16
|
+
id: Schema.String,
|
|
17
|
+
publication: Schema.optionalKey(Schema.String)
|
|
18
|
+
}) {
|
|
19
|
+
[Runtime.errorExitCode] = 64;
|
|
20
|
+
get message() {
|
|
21
|
+
return this.publication === void 0 ? `no concept "${this.id}" in the bundle` : `a renders entry of "${this.publication}" points at "${this.id}", which is not a concept in the bundle; fix that renders entry`;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The concept at `id` exists but is not a Publication (`type` is what it is).
|
|
26
|
+
*
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
var NotAPublicationError = class extends Schema.TaggedError()("NotAPublicationError", {
|
|
30
|
+
id: Schema.String,
|
|
31
|
+
type: Schema.String
|
|
32
|
+
}) {
|
|
33
|
+
[Runtime.errorExitCode] = 64;
|
|
34
|
+
get message() {
|
|
35
|
+
return `"${this.id}" is a ${this.type}, not a ${PUBLICATION_TYPE}`;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Restamps one Publication's `renders[].body_sha256` digests from the current
|
|
40
|
+
* body of each rendered source concept, writing the Publication file
|
|
41
|
+
* atomically unless `dryRun`. `written` is whether the text changed (so a
|
|
42
|
+
* `dryRun` that would change the file reports `true` without writing).
|
|
43
|
+
* A malformed `renders` fails `UnsupportedFrontmatterError`.
|
|
44
|
+
*
|
|
45
|
+
* @public
|
|
46
|
+
*/
|
|
47
|
+
const stampPublication = (options) => Effect.gen(function* () {
|
|
48
|
+
const bundle = yield* Bundle.load({ root: options.bundleRoot });
|
|
49
|
+
const normalized = ConceptId.normalize(options.id);
|
|
50
|
+
const id = Option.getOrElse(normalized, () => options.id);
|
|
51
|
+
const concept = Option.isSome(normalized) ? bundle.concepts.get(normalized.value) : void 0;
|
|
52
|
+
if (concept === void 0) return yield* new PublicationNotFoundError({ id });
|
|
53
|
+
if (concept.frontmatter.type !== PUBLICATION_TYPE) return yield* new NotAPublicationError({
|
|
54
|
+
id,
|
|
55
|
+
type: concept.frontmatter.type
|
|
56
|
+
});
|
|
57
|
+
const renders = Publications.rendersOf(concept);
|
|
58
|
+
if (renders === void 0) return yield* new UnsupportedFrontmatterError({
|
|
59
|
+
key: "renders",
|
|
60
|
+
shape: "malformed"
|
|
61
|
+
});
|
|
62
|
+
const digests = [];
|
|
63
|
+
for (const entry of renders) {
|
|
64
|
+
const sourceId = Publications.resolveRef(concept.path, entry.path);
|
|
65
|
+
const source = Option.match(ConceptId.normalize(sourceId), {
|
|
66
|
+
onNone: () => void 0,
|
|
67
|
+
onSome: (key) => bundle.concepts.get(key)
|
|
68
|
+
});
|
|
69
|
+
if (source === void 0) return yield* new PublicationNotFoundError({
|
|
70
|
+
id: sourceId === "" ? entry.path : sourceId,
|
|
71
|
+
publication: id
|
|
72
|
+
});
|
|
73
|
+
digests.push({
|
|
74
|
+
path: entry.path,
|
|
75
|
+
body_sha256: yield* Derivation.bodyDigest(source.document.source)
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const original = concept.document.source;
|
|
79
|
+
const edits = yield* FrontmatterEdits.rendersDigests(original, digests.map((d) => d.body_sha256));
|
|
80
|
+
const next = MarkdownEdit.applyAll(original, edits);
|
|
81
|
+
if (next === original) return {
|
|
82
|
+
id,
|
|
83
|
+
written: false,
|
|
84
|
+
digests
|
|
85
|
+
};
|
|
86
|
+
if (!options.dryRun) {
|
|
87
|
+
const path = yield* Path.Path;
|
|
88
|
+
yield* writeAtomic(path.join(bundle.root, concept.path), next);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
id,
|
|
92
|
+
written: true,
|
|
93
|
+
digests
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
//#endregion
|
|
98
|
+
export { NotAPublicationError, PublicationNotFoundError, stampPublication };
|
package/validate/resources.js
CHANGED
|
@@ -4,8 +4,8 @@ import { Effect, FileSystem, Path } from "effect";
|
|
|
4
4
|
//#region src/validate/resources.ts
|
|
5
5
|
/** A `resource`/`sources[].resource` value that names a URL, not a bundle-relative path (D-24). */
|
|
6
6
|
const SCHEME_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
|
7
|
-
/** Glob metacharacters that mark a value as a
|
|
8
|
-
const GLOB_RE = /[*?[{]/;
|
|
7
|
+
/** Glob metacharacters, extglob openers included, that mark a value as a pattern rather than a literal path. */
|
|
8
|
+
const GLOB_RE = /[*?[{]|[@+!]\(/;
|
|
9
9
|
/**
|
|
10
10
|
* True when `value` is a descriptor (a URL, a scope description, or a glob
|
|
11
11
|
* pattern) rather than a literal bundle-relative path — D-24 leaves
|
|
@@ -86,4 +86,4 @@ const lintResources = (bundle, config) => Effect.gen(function* () {
|
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
//#endregion
|
|
89
|
-
export { lintResources };
|
|
89
|
+
export { GLOB_RE, lintResources };
|
package/validate/run.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { lintResources } from "./resources.js";
|
|
2
|
+
import { lintSurfaces } from "./surfaces.js";
|
|
2
3
|
import { Bundle, OkfitConfig, Validate } from "@okfit/core";
|
|
3
4
|
import { Context, Effect, Option } from "effect";
|
|
4
|
-
import { Provenance } from "@okfit/profiles";
|
|
5
|
+
import { Provenance, Publications } from "@okfit/profiles";
|
|
5
6
|
|
|
6
7
|
//#region src/validate/run.ts
|
|
7
8
|
/**
|
|
@@ -37,6 +38,10 @@ var Now = class extends Context.Service()("@okfit/engine/Now") {};
|
|
|
37
38
|
* the same `report.lint` array every other core lint diagnostic already
|
|
38
39
|
* does.
|
|
39
40
|
*
|
|
41
|
+
* It then appends `lintResources`, `Publications.lint` (`publication-drift`
|
|
42
|
+
* and `publication-orphan`, only under the `software-project` profile) and `lintSurfaces` (`surface-unmatched`), each
|
|
43
|
+
* of which gates itself on its own severity.
|
|
44
|
+
*
|
|
40
45
|
* K-52 holds structurally: `Bundle.load`'s failure short-circuits the
|
|
41
46
|
* generator, so `profile.check` never runs on a bundle that did not load.
|
|
42
47
|
*
|
|
@@ -55,6 +60,8 @@ const run = (options) => Effect.gen(function* () {
|
|
|
55
60
|
});
|
|
56
61
|
const provenance = OkfitConfig.severityFor(options.config, "generated-at-drift") === "off" ? [] : yield* Provenance.lint(bundle, options.config, { skipGitTier: options.skipProvenance === true });
|
|
57
62
|
const resources = yield* lintResources(bundle, options.config);
|
|
63
|
+
const publications = Option.exists(options.profile, (profile) => profile.name === "software-project") ? yield* Publications.lint(bundle, options.config) : [];
|
|
64
|
+
const surfaces = yield* lintSurfaces(bundle, options.config);
|
|
58
65
|
return {
|
|
59
66
|
bundle,
|
|
60
67
|
report: {
|
|
@@ -62,7 +69,9 @@ const run = (options) => Effect.gen(function* () {
|
|
|
62
69
|
lint: [
|
|
63
70
|
...report.lint,
|
|
64
71
|
...provenance,
|
|
65
|
-
...resources
|
|
72
|
+
...resources,
|
|
73
|
+
...publications,
|
|
74
|
+
...surfaces
|
|
66
75
|
]
|
|
67
76
|
},
|
|
68
77
|
profileDiagnostics
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { GLOB_RE } from "./resources.js";
|
|
2
|
+
import { Diagnostic, DiagnosticRange, OkfitConfig } from "@okfit/core";
|
|
3
|
+
import { Effect, FileSystem, Path, Result } from "effect";
|
|
4
|
+
import { SURFACE_TYPE } from "@okfit/profiles";
|
|
5
|
+
import { GlobPattern } from "@effected/glob";
|
|
6
|
+
|
|
7
|
+
//#region src/validate/surfaces.ts
|
|
8
|
+
/**
|
|
9
|
+
* Lists `root` breadth-first, only as deep as `segments` (the glob's remaining
|
|
10
|
+
* segments) can match, or without a bound when the pattern contains `**`.
|
|
11
|
+
* `node_modules` and dot-entries are skipped unless a segment names them, and
|
|
12
|
+
* symlinked directories are listed but never descended into. Entry
|
|
13
|
+
* paths are `/`-joined and relative to `root`. An unreadable directory
|
|
14
|
+
* contributes nothing.
|
|
15
|
+
*/
|
|
16
|
+
const walk = (fs, path, root, segments) => Effect.gen(function* () {
|
|
17
|
+
const maxDepth = segments.some((segment) => segment.includes("**")) ? Number.POSITIVE_INFINITY : segments.length;
|
|
18
|
+
const namesHidden = segments.some((segment) => /(^|[{,(|[])\./.test(segment));
|
|
19
|
+
const namesModules = segments.some((segment) => segment.includes("node_modules"));
|
|
20
|
+
const skipped = (name) => name === "node_modules" && !namesModules || name.startsWith(".") && !namesHidden;
|
|
21
|
+
const out = [];
|
|
22
|
+
let level = [""];
|
|
23
|
+
for (let depth = 1; depth <= maxDepth && level.length > 0; depth++) {
|
|
24
|
+
const next = [];
|
|
25
|
+
for (const dir of level) {
|
|
26
|
+
const names = yield* fs.readDirectory(dir === "" ? root : path.join(root, dir)).pipe(Effect.orElseSucceed(() => []));
|
|
27
|
+
for (const name of names) {
|
|
28
|
+
if (skipped(name)) continue;
|
|
29
|
+
const relative = dir === "" ? name : `${dir}/${name}`;
|
|
30
|
+
out.push(relative);
|
|
31
|
+
if (depth < maxDepth) {
|
|
32
|
+
const full = path.join(root, relative);
|
|
33
|
+
const isDirectory = yield* fs.stat(full).pipe(Effect.map((info) => info.type === "Directory"), Effect.orElseSucceed(() => false));
|
|
34
|
+
const isSymlink = isDirectory ? yield* fs.readLink(full).pipe(Effect.as(true), Effect.orElseSucceed(() => false)) : false;
|
|
35
|
+
if (isDirectory && !isSymlink) next.push(relative);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
level = next;
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* A Surface's `resource` glob matches nothing on disk. Only concepts of type
|
|
45
|
+
* `Surface` whose `resource` contains a glob metacharacter are checked; a
|
|
46
|
+
* literal `resource` is `lintResources`'s concern (`source-resource-missing`).
|
|
47
|
+
* The pattern is split, as written, at its first metacharacter segment into a
|
|
48
|
+
* static `prefix` (resolved against the concept's directory) and a `rest`; the
|
|
49
|
+
* `prefix` directory is walked only as deep as `rest` can match (unbounded with
|
|
50
|
+
* `**`, skipping `node_modules` and dot-directories `rest` does not name) and
|
|
51
|
+
* `rest` is matched against the entries. A missing or unreadable `prefix` (for
|
|
52
|
+
* example a file), an empty match, or a `rest` that fails to compile all count
|
|
53
|
+
* as unmatched, and report a warning (a brand-new monorepo legitimately has no
|
|
54
|
+
* packages yet), ranged at the `["resource"]` frontmatter value. Total over
|
|
55
|
+
* severity (mirrors `lintResources`): returns `[]`, touching neither
|
|
56
|
+
* `FileSystem` nor `Path`, the moment
|
|
57
|
+
* `OkfitConfig.severityFor(config, "surface-unmatched")` is `"off"`.
|
|
58
|
+
*
|
|
59
|
+
* @public
|
|
60
|
+
*/
|
|
61
|
+
const lintSurfaces = (bundle, config) => Effect.gen(function* () {
|
|
62
|
+
const severity = OkfitConfig.severityFor(config, "surface-unmatched");
|
|
63
|
+
if (severity === "off") return [];
|
|
64
|
+
const fs = yield* FileSystem.FileSystem;
|
|
65
|
+
const path = yield* Path.Path;
|
|
66
|
+
const diagnostics = [];
|
|
67
|
+
for (const [, concept] of bundle.concepts) {
|
|
68
|
+
if (concept.frontmatter.type !== SURFACE_TYPE) continue;
|
|
69
|
+
const resource = concept.frontmatter.resource;
|
|
70
|
+
if (resource === void 0 || !GLOB_RE.test(resource)) continue;
|
|
71
|
+
const written = resource.split("/");
|
|
72
|
+
const firstGlob = written.findIndex((segment) => GLOB_RE.test(segment));
|
|
73
|
+
if (firstGlob < 0) continue;
|
|
74
|
+
const prefix = path.resolve(bundle.root, path.dirname(concept.path), ...written.slice(0, firstGlob));
|
|
75
|
+
const restSegments = written.slice(firstGlob);
|
|
76
|
+
const rest = restSegments.join("/");
|
|
77
|
+
let problem;
|
|
78
|
+
if (!(yield* fs.exists(prefix).pipe(Effect.orElseSucceed(() => false)))) problem = "";
|
|
79
|
+
else {
|
|
80
|
+
const compiled = GlobPattern.compileResult(rest);
|
|
81
|
+
if (Result.isFailure(compiled)) problem = `: ${compiled.failure.message}`;
|
|
82
|
+
else if (!(yield* walk(fs, path, prefix, restSegments)).some((entry) => compiled.success.matches(entry))) problem = "";
|
|
83
|
+
}
|
|
84
|
+
if (problem === void 0) continue;
|
|
85
|
+
const range = DiagnosticRange.forFrontmatterPath(concept.document, ["resource"]);
|
|
86
|
+
diagnostics.push(Diagnostic.make({
|
|
87
|
+
file: concept.path,
|
|
88
|
+
code: "surface-unmatched",
|
|
89
|
+
severity,
|
|
90
|
+
message: `Surface resource "${resource}" matches nothing under ${path.relative(bundle.root, prefix) || "."}${problem}`,
|
|
91
|
+
...range === void 0 ? {} : { range }
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
return diagnostics;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
//#endregion
|
|
98
|
+
export { lintSurfaces };
|
package/verify/locate.js
CHANGED
|
@@ -336,10 +336,27 @@ const locateGeneratedBlock = Effect.fn("okfit/verify/locateGeneratedBlock")(func
|
|
|
336
336
|
const parsed = yield* parseTopLevelMapping(source);
|
|
337
337
|
if ("_tag" in parsed) return parsed;
|
|
338
338
|
const { value, valueStart, contents } = parsed;
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
339
|
+
const pair = contents.items.find((item) => item.key instanceof YamlScalar && item.key.value === "generated");
|
|
340
|
+
if (pair !== void 0) {
|
|
341
|
+
const node = pair.value;
|
|
342
|
+
if (!(node === null || node instanceof YamlScalar && node.style === "plain" && node.value === null && node.tag === void 0 && node.anchor === void 0) || contents.style === "flow") return {
|
|
343
|
+
_tag: "unsupported",
|
|
344
|
+
shape: "generated-present"
|
|
345
|
+
};
|
|
346
|
+
const lineStart = value.lastIndexOf("\n", pair.key.offset - 1) + 1;
|
|
347
|
+
const tail = node === null ? pair.key.offset + pair.key.length : node.offset + node.length;
|
|
348
|
+
const nl = value.indexOf("\n", tail);
|
|
349
|
+
const lineEnd = nl === -1 ? value.length : nl + 1;
|
|
350
|
+
if (lineStart >= lineEnd || value.slice(lineStart, pair.key.offset).trim() !== "") return {
|
|
351
|
+
_tag: "unsupported",
|
|
352
|
+
shape: "generated-present"
|
|
353
|
+
};
|
|
354
|
+
return {
|
|
355
|
+
_tag: "replace",
|
|
356
|
+
start: valueStart + lineStart,
|
|
357
|
+
end: valueStart + lineEnd
|
|
358
|
+
};
|
|
359
|
+
}
|
|
343
360
|
return {
|
|
344
361
|
_tag: "absent",
|
|
345
362
|
insertAt: valueStart + value.length
|
package/verify/run.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { VerifyConceptNotFoundError, VerifyUnsupportedFrontmatterError } from "../errors.js";
|
|
2
2
|
import { documentNewline, locate, locateTopLevelScalar, stripBom } from "./locate.js";
|
|
3
|
-
import {
|
|
3
|
+
import { orderVerifyEdits, splice, spliceTopLevelScalar } from "./splice.js";
|
|
4
4
|
import { resolveBatchTypes, selectAttestable, selectPickerCandidates } from "./select.js";
|
|
5
5
|
import { Bundle, ConceptId, Timestamp } from "@okfit/core";
|
|
6
6
|
import { Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
@@ -52,7 +52,7 @@ const prepareVerify = Effect.fn("okfit/verify/prepareVerify")(function* (bundle,
|
|
|
52
52
|
});
|
|
53
53
|
statusEdit = spliceTopLevelScalar(target, "status", status, newline);
|
|
54
54
|
}
|
|
55
|
-
const edits =
|
|
55
|
+
const edits = orderVerifyEdits(verifiedEdit, statusEdit);
|
|
56
56
|
return {
|
|
57
57
|
absolutePath,
|
|
58
58
|
finalText: bom + MarkdownEdit.applyAll(text, edits),
|
package/verify/splice.js
CHANGED
|
@@ -118,14 +118,15 @@ const spliceTopLevelScalar = (target, key, value, newline) => {
|
|
|
118
118
|
};
|
|
119
119
|
/**
|
|
120
120
|
* A whole `generated:` block mapping, appended as the LAST top-level key
|
|
121
|
-
* (issue #73)
|
|
121
|
+
* (issue #73), or written over a null-valued `generated:` line in place
|
|
122
|
+
* (issue #144) — block style, matching how this repository writes every
|
|
122
123
|
* `generated:` and how `splice`'s own `absent` case writes `verified:`.
|
|
123
124
|
*
|
|
124
125
|
* @internal
|
|
125
126
|
*/
|
|
126
127
|
const spliceGeneratedBlock = (target, fields, newline) => MarkdownEdit.make({
|
|
127
|
-
offset: target.insertAt,
|
|
128
|
-
length: 0,
|
|
128
|
+
offset: target._tag === "absent" ? target.insertAt : target.start,
|
|
129
|
+
length: target._tag === "absent" ? 0 : target.end - target.start,
|
|
129
130
|
content: `generated:${newline} by: ${fields.by}${newline} at: ${fields.at}${newline} body_sha256: ${fields.bodySha256}${newline}`
|
|
130
131
|
});
|
|
131
132
|
/**
|
|
@@ -152,6 +153,16 @@ const mergeSameOffset = (edits) => {
|
|
|
152
153
|
}
|
|
153
154
|
return out;
|
|
154
155
|
};
|
|
156
|
+
/**
|
|
157
|
+
* Fold a concept's `verified` edit and optional `status` edit into the ordered,
|
|
158
|
+
* merged edit list both `okfit verify --stable` and `FrontmatterEdits` apply:
|
|
159
|
+
* status first on equal offsets, so the merged insert emits `status:` before
|
|
160
|
+
* `verified:`. The tie-break is by object identity -- the two edits are
|
|
161
|
+
* otherwise indistinguishable.
|
|
162
|
+
*
|
|
163
|
+
* @internal
|
|
164
|
+
*/
|
|
165
|
+
const orderVerifyEdits = (verifiedEdit, statusEdit) => mergeSameOffset([verifiedEdit, ...statusEdit === void 0 ? [] : [statusEdit]].toSorted((a, b) => a.offset - b.offset || (a === statusEdit ? -1 : b === statusEdit ? 1 : 0)));
|
|
155
166
|
|
|
156
167
|
//#endregion
|
|
157
|
-
export { mergeSameOffset, splice, spliceGenerated, spliceGeneratedBlock, spliceGeneratedFields, spliceTopLevelScalar };
|
|
168
|
+
export { mergeSameOffset, orderVerifyEdits, splice, spliceGenerated, spliceGeneratedBlock, spliceGeneratedFields, spliceTopLevelScalar };
|