@okfit/engine 0.7.5 → 0.9.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 +2 -0
- package/edits/FrontmatterEdits.js +76 -0
- package/index.d.ts +74 -9
- package/index.js +3 -1
- package/package.json +5 -4
- package/render/distribution.js +3 -6
- package/session/concept.js +34 -0
- package/session/range.js +3 -5
- package/tsdoc-metadata.json +1 -1
- package/validate/resources.js +25 -9
- package/verify/locate.js +101 -33
- package/verify/splice.js +25 -1
- package/version.js +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ The shared engine behind [okfit](https://github.com/spencerbeggs/okfit): the pla
|
|
|
4
4
|
|
|
5
5
|
You probably want `@okfit/plugin` (both bins), `@okfit/cli` (the `okfit` bin) or `@okfit/mcp` (the `okfit-mcp` bin) instead. This package is what they are built from.
|
|
6
6
|
|
|
7
|
+
`FrontmatterEdits` is a public facade over the `verify` splice machinery: it produces `MarkdownEdit`s for a concept's top-level `status` scalar and its `verified` list, with whole-file offsets into the source as passed (BOM included), so a consumer such as `@okfit/lsp` can apply them directly or map them to editor ranges without adjustment.
|
|
8
|
+
|
|
7
9
|
## License
|
|
8
10
|
|
|
9
11
|
[MIT](LICENSE)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { documentNewline, locate, locateTopLevelScalar, stripBom } from "../verify/locate.js";
|
|
2
|
+
import { splice, spliceTopLevelScalar } from "../verify/splice.js";
|
|
3
|
+
import { Effect, Runtime, Schema } from "effect";
|
|
4
|
+
import { MarkdownEdit } from "@effected/markdown";
|
|
5
|
+
|
|
6
|
+
//#region src/edits/FrontmatterEdits.ts
|
|
7
|
+
/**
|
|
8
|
+
* A top-level frontmatter key `FrontmatterEdits` was asked to edit turned
|
|
9
|
+
* out to be a shape it cannot splice: `shape` is whichever locator string
|
|
10
|
+
* `verify/locate.ts` produced for that key (e.g. `"flow-mapping"`,
|
|
11
|
+
* `"alias"`, `"status-block-scalar"`). The file is never touched when this
|
|
12
|
+
* is raised -- callers should surface it and let a human edit by hand.
|
|
13
|
+
*
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
var UnsupportedFrontmatterError = class extends Schema.TaggedError()("UnsupportedFrontmatterError", {
|
|
17
|
+
key: Schema.String,
|
|
18
|
+
shape: Schema.String
|
|
19
|
+
}) {
|
|
20
|
+
[Runtime.errorExitCode] = 3;
|
|
21
|
+
get message() {
|
|
22
|
+
return `"${this.key}"'s frontmatter value is a shape FrontmatterEdits cannot edit (${this.shape}); edit it by hand`;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/** `edit` shifted forward by `by` bytes, or `edit` itself when `by` is zero. */
|
|
26
|
+
const shift = (edit, by) => by === 0 ? edit : MarkdownEdit.make({
|
|
27
|
+
offset: edit.offset + by,
|
|
28
|
+
length: edit.length,
|
|
29
|
+
content: edit.content
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* A public facade over the `verify/locate.ts` and `verify/splice.ts`
|
|
33
|
+
* machinery: byte-range `MarkdownEdit`s for a concept's top-level `status`
|
|
34
|
+
* scalar and its `verified` list. `verified` builds the exact same edit
|
|
35
|
+
* `okfit verify`'s `prepareVerify` builds -- same `locate`, same `splice`,
|
|
36
|
+
* same `documentNewline` newline choice -- so its output matches
|
|
37
|
+
* `okfit verify`'s splice byte for byte; `status` reuses the identical
|
|
38
|
+
* newline rule for consistency, over `locateTopLevelScalar` and
|
|
39
|
+
* `spliceTopLevelScalar` instead.
|
|
40
|
+
*
|
|
41
|
+
* Every offset returned is a WHOLE-FILE offset into `source` AS PASSED --
|
|
42
|
+
* BOM included when present -- so a caller can apply the edits with
|
|
43
|
+
* `MarkdownEdit.applyAll(source, edits)` directly, or map them to editor
|
|
44
|
+
* ranges without adjustment. Internally the BOM is stripped for parsing and
|
|
45
|
+
* its length added back to every offset before returning.
|
|
46
|
+
*
|
|
47
|
+
* @public
|
|
48
|
+
*/
|
|
49
|
+
var FrontmatterEdits = class {
|
|
50
|
+
constructor() {}
|
|
51
|
+
/** Edits that set top-level `status` to `status`, replacing an existing scalar or inserting after `title:` (else `type:`). */
|
|
52
|
+
static status = (source, status) => Effect.gen(function* () {
|
|
53
|
+
const { text, bom } = stripBom(source);
|
|
54
|
+
const target = yield* locateTopLevelScalar(text, "status");
|
|
55
|
+
if (target._tag === "unsupported") return yield* new UnsupportedFrontmatterError({
|
|
56
|
+
key: "status",
|
|
57
|
+
shape: target.shape
|
|
58
|
+
});
|
|
59
|
+
const edit = spliceTopLevelScalar(target, "status", status, documentNewline(text));
|
|
60
|
+
return [shift(edit, bom.length)];
|
|
61
|
+
});
|
|
62
|
+
/** Edits that append one verified entry, the same splice `okfit verify` performs. */
|
|
63
|
+
static verified = (source, entry) => Effect.gen(function* () {
|
|
64
|
+
const { text, bom } = stripBom(source);
|
|
65
|
+
const target = yield* locate(text);
|
|
66
|
+
if (target._tag === "unsupported") return yield* new UnsupportedFrontmatterError({
|
|
67
|
+
key: "verified",
|
|
68
|
+
shape: target.shape
|
|
69
|
+
});
|
|
70
|
+
const edit = splice(target, entry, documentNewline(text));
|
|
71
|
+
return [shift(edit, bom.length)];
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
//#endregion
|
|
76
|
+
export { FrontmatterEdits, UnsupportedFrontmatterError };
|
package/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { Actor, BundleLoadError, ConceptId, Diagnostic, DiagnosticRange, DiagnosticSeverity, GraphLink, GraphNode, LinkGraph, LoadedBundle, OkfitConfig, OkfitConfigFile, StaleConcept, ValidationReport } from "@okfit/core";
|
|
1
|
+
import { Actor, BundleLoadError, ConceptId, Diagnostic, DiagnosticRange, DiagnosticSeverity, GraphLink, GraphNode, LinkGraph, LoadedBundle, LoadedConcept, OkfitConfig, OkfitConfigFile, StaleConcept, Status, ValidationReport } from "@okfit/core";
|
|
2
2
|
import { Context, Crypto, DateTime, Effect, FileSystem, Layer, Option, Path, PlatformError, Result, Runtime, Schema } from "effect";
|
|
3
3
|
import { AppDirs, Xdg } from "@effected/xdg";
|
|
4
4
|
import { ConfigReadError } from "@effected/config-file";
|
|
5
5
|
import { GeneratedAtError, GitHistory, GitHistoryError, Layout, Profile, ProfileDiagnostic } from "@okfit/profiles";
|
|
6
|
-
import { FrontmatterWriteError, MarkdownParseError } from "@effected/markdown";
|
|
6
|
+
import { FrontmatterWriteError, MarkdownEdit, MarkdownParseError } from "@effected/markdown";
|
|
7
|
+
import { YamlParseError } from "@effected/yaml";
|
|
7
8
|
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
9
|
+
import { Distribution as Distribution$1 } from "@effected/engine";
|
|
8
10
|
import { Git, GitCommandError, UnknownRefError } from "@effected/git";
|
|
9
11
|
//#region src/config/anchor.d.ts
|
|
10
12
|
/**
|
|
@@ -375,6 +377,53 @@ interface ContextResult {
|
|
|
375
377
|
*/
|
|
376
378
|
export declare const runContext: (options: ContextRunOptions) => Effect.Effect<ContextResult, never, FileSystem.FileSystem | Path.Path>;
|
|
377
379
|
//#endregion
|
|
380
|
+
//#region src/edits/FrontmatterEdits.d.ts
|
|
381
|
+
declare const UnsupportedFrontmatterError_base: Schema.Class<UnsupportedFrontmatterError, Schema.TaggedStruct<"UnsupportedFrontmatterError", {
|
|
382
|
+
readonly key: Schema.String;
|
|
383
|
+
readonly shape: Schema.String;
|
|
384
|
+
}>, import("effect/Cause").YieldableError>;
|
|
385
|
+
/**
|
|
386
|
+
* A top-level frontmatter key `FrontmatterEdits` was asked to edit turned
|
|
387
|
+
* out to be a shape it cannot splice: `shape` is whichever locator string
|
|
388
|
+
* `verify/locate.ts` produced for that key (e.g. `"flow-mapping"`,
|
|
389
|
+
* `"alias"`, `"status-block-scalar"`). The file is never touched when this
|
|
390
|
+
* is raised -- callers should surface it and let a human edit by hand.
|
|
391
|
+
*
|
|
392
|
+
* @public
|
|
393
|
+
*/
|
|
394
|
+
export declare class UnsupportedFrontmatterError extends UnsupportedFrontmatterError_base {
|
|
395
|
+
readonly [Runtime.errorExitCode] = 3;
|
|
396
|
+
get message(): string;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* A public facade over the `verify/locate.ts` and `verify/splice.ts`
|
|
400
|
+
* machinery: byte-range `MarkdownEdit`s for a concept's top-level `status`
|
|
401
|
+
* scalar and its `verified` list. `verified` builds the exact same edit
|
|
402
|
+
* `okfit verify`'s `prepareVerify` builds -- same `locate`, same `splice`,
|
|
403
|
+
* same `documentNewline` newline choice -- so its output matches
|
|
404
|
+
* `okfit verify`'s splice byte for byte; `status` reuses the identical
|
|
405
|
+
* newline rule for consistency, over `locateTopLevelScalar` and
|
|
406
|
+
* `spliceTopLevelScalar` instead.
|
|
407
|
+
*
|
|
408
|
+
* Every offset returned is a WHOLE-FILE offset into `source` AS PASSED --
|
|
409
|
+
* BOM included when present -- so a caller can apply the edits with
|
|
410
|
+
* `MarkdownEdit.applyAll(source, edits)` directly, or map them to editor
|
|
411
|
+
* ranges without adjustment. Internally the BOM is stripped for parsing and
|
|
412
|
+
* its length added back to every offset before returning.
|
|
413
|
+
*
|
|
414
|
+
* @public
|
|
415
|
+
*/
|
|
416
|
+
export declare class FrontmatterEdits {
|
|
417
|
+
private constructor();
|
|
418
|
+
/** Edits that set top-level `status` to `status`, replacing an existing scalar or inserting after `title:` (else `type:`). */
|
|
419
|
+
static readonly status: (source: string, status: Status) => Effect.Effect<ReadonlyArray<MarkdownEdit>, YamlParseError | UnsupportedFrontmatterError>;
|
|
420
|
+
/** Edits that append one verified entry, the same splice `okfit verify` performs. */
|
|
421
|
+
static readonly verified: (source: string, entry: {
|
|
422
|
+
readonly by: string;
|
|
423
|
+
readonly at: string;
|
|
424
|
+
}) => Effect.Effect<ReadonlyArray<MarkdownEdit>, YamlParseError | UnsupportedFrontmatterError>;
|
|
425
|
+
}
|
|
426
|
+
//#endregion
|
|
378
427
|
//#region src/external/ExternalReferences.d.ts
|
|
379
428
|
/**
|
|
380
429
|
* Reachability of an external reference: `unknown` means never checked.
|
|
@@ -756,16 +805,18 @@ export declare const contextEnvelope: (input: {
|
|
|
756
805
|
* reports; `engine_version` and `okf_version` are the pair a reader
|
|
757
806
|
* compares.
|
|
758
807
|
*
|
|
808
|
+
* Re-exported from `@effected/engine`'s own `Distribution` -- the shape
|
|
809
|
+
* both `@okfit/cli`'s `CurrentDistribution` (K-137/carrier-version-threading)
|
|
810
|
+
* and every JSON envelope in this package agree on, instead of two
|
|
811
|
+
* independently hand-rolled `{ name, version }` structs drifting apart.
|
|
812
|
+
*
|
|
759
813
|
* @public
|
|
760
814
|
*/
|
|
761
|
-
|
|
762
|
-
readonly name: string;
|
|
763
|
-
readonly version: string;
|
|
764
|
-
}
|
|
815
|
+
type Distribution = Distribution$1;
|
|
765
816
|
/** Every envelope's `distribution` field: a `Distribution` or `null`. @public */
|
|
766
|
-
export declare const DistributionField: Schema.NullOr<Schema.Struct<{
|
|
767
|
-
readonly name: Schema.String;
|
|
768
|
-
readonly version: Schema.String;
|
|
817
|
+
export declare const DistributionField: import("effect/Schema").NullOr<import("effect/Schema").Struct<{
|
|
818
|
+
readonly name: import("effect/Schema").String;
|
|
819
|
+
readonly version: import("effect/Schema").String;
|
|
769
820
|
}>>;
|
|
770
821
|
/** @public */
|
|
771
822
|
export type DistributionField = typeof DistributionField.Type;
|
|
@@ -1428,6 +1479,20 @@ export declare class BundleSession extends BundleSession_base {
|
|
|
1428
1479
|
static readonly make: (options: BundleSessionOptions) => Effect.Effect<BundleSessionShape, never, BundleSessionServices>;
|
|
1429
1480
|
}
|
|
1430
1481
|
//#endregion
|
|
1482
|
+
//#region src/session/concept.d.ts
|
|
1483
|
+
/**
|
|
1484
|
+
* The loaded concept at `absolutePath`, if any. `absolutePath` must already sit under
|
|
1485
|
+
* `bundle.root` (a real resolved path, never re-resolved here: `@okfit/core`'s own
|
|
1486
|
+
* `internal/posixPath.ts` avoids `path.resolve`/`path.relative` on purpose, because both read
|
|
1487
|
+
* `process.cwd()`, and this package holds the same line). Strips `bundle.root` as a plain
|
|
1488
|
+
* string prefix, then applies the same `ConceptId.fromPath` normalisation `withFallbackRange`
|
|
1489
|
+
* uses: `None` for a path outside the bundle, a reserved file (`index.md`, `log.md`), a
|
|
1490
|
+
* non-markdown file, or a markdown file that never decoded into a concept.
|
|
1491
|
+
*
|
|
1492
|
+
* @public
|
|
1493
|
+
*/
|
|
1494
|
+
export declare const conceptFor: (bundle: LoadedBundle, absolutePath: string) => Option.Option<LoadedConcept>;
|
|
1495
|
+
//#endregion
|
|
1431
1496
|
//#region src/session/range.d.ts
|
|
1432
1497
|
/**
|
|
1433
1498
|
* Give a range-less diagnostic somewhere to point (spec 4.3): the concept's
|
package/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { ConfigMalformedError, ConfigPathNotFoundError, DocumentPathError, InitO
|
|
|
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";
|
|
6
|
+
import { FrontmatterEdits, UnsupportedFrontmatterError } from "./edits/FrontmatterEdits.js";
|
|
6
7
|
import { ExternalReferences, ReferenceCheck, ReferenceState } from "./external/ExternalReferences.js";
|
|
7
8
|
import { runGraph } from "./graph/run.js";
|
|
8
9
|
import { CONFIG_RELATIVE_PATH, configValue, files, targetPaths } from "./init/scaffold.js";
|
|
@@ -21,9 +22,10 @@ import { SkipReason, runSync } from "./sync/run.js";
|
|
|
21
22
|
import { SyncEnvelope, SyncModeEnvelope, syncEnvelope } from "./render/sync.js";
|
|
22
23
|
import { VerifyBatchEnvelope, VerifyEnvelope, verifyBatchEnvelope, verifyEnvelope } from "./render/verify.js";
|
|
23
24
|
import { Now, run } from "./validate/run.js";
|
|
25
|
+
import { conceptFor } from "./session/concept.js";
|
|
24
26
|
import { withFallbackRange } from "./session/range.js";
|
|
25
27
|
import { BundleSession } from "./session/BundleSession.js";
|
|
26
28
|
import { runStale } from "./stale/run.js";
|
|
27
29
|
import { runVerify, runVerifyBatch } from "./verify/run.js";
|
|
28
30
|
|
|
29
|
-
export { BundleSession, CONFIG_RELATIVE_PATH, ConfigMalformedError, ConfigPathNotFoundError, ContextEnvelope, ContextField, ContextFieldValue, ContextTag, ContextType, DEFAULT_PROFILE_NAME, DistributionField, DocumentPathError, ENGINE_VERSION, ExternalReferences, GraphEdgeEnvelope, GraphEnvelope, GraphNodeEnvelope, GraphSummary, InitOverwriteError, JsonDiagnostic, JsonEnvelope, JsonErrorEnvelope, JsonSummary, Now, OkfitPlatform, OverlayDocuments, ReferenceCheck, ReferenceState, SkipReason, StaleEnvelope, StaleItem, StaleSummary, SyncEnvelope, SyncModeEnvelope, SyncStagedLogError, VerifyBatchEnvelope, VerifyConceptNotFoundError, VerifyEnvelope, VerifySelectionError, VerifyUnsupportedFrontmatterError, buildConfigLayer, collect, configValue, contextEnvelope, files, forDiagnostics, graphEnvelope, json, jsonError, layerOverlayFileSystem, makeOverlayFileSystem, provideConfig, provideDocuments, resolveBundleRoot, resolveDocumentPath, resolveProjectConfig, resolveProjectRoot, run, runContext, runGraph, runStale, runSync, runVerify, runVerifyBatch, sort, staleEnvelope, syncEnvelope, tally, targetPaths, verifyBatchEnvelope, verifyEnvelope, withFallbackRange };
|
|
31
|
+
export { BundleSession, CONFIG_RELATIVE_PATH, 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, 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, makeOverlayFileSystem, provideConfig, provideDocuments, resolveBundleRoot, resolveDocumentPath, resolveProjectConfig, resolveProjectRoot, run, runContext, runGraph, runStale, runSync, runVerify, runVerifyBatch, sort, staleEnvelope, syncEnvelope, tally, targetPaths, verifyBatchEnvelope, verifyEnvelope, withFallbackRange };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@okfit/engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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": [
|
|
@@ -37,17 +37,18 @@
|
|
|
37
37
|
"@effect/platform-node": "4.0.0-rc.117",
|
|
38
38
|
"@effected/app": "^0.18.0",
|
|
39
39
|
"@effected/config-file": "^0.12.0",
|
|
40
|
+
"@effected/engine": "^0.1.0",
|
|
40
41
|
"@effected/git": "^0.17.0",
|
|
41
42
|
"@effected/glob": "^0.8.0",
|
|
42
43
|
"@effected/jsonc": "^0.13.0",
|
|
43
|
-
"@effected/markdown": "^0.12.
|
|
44
|
+
"@effected/markdown": "^0.12.1",
|
|
44
45
|
"@effected/store": "^0.10.0",
|
|
45
46
|
"@effected/toml": "^0.9.0",
|
|
46
47
|
"@effected/walker": "^0.12.0",
|
|
47
48
|
"@effected/xdg": "^0.7.0",
|
|
48
49
|
"@effected/yaml": "^0.17.0",
|
|
49
|
-
"@okfit/core": "0.
|
|
50
|
-
"@okfit/profiles": "0.
|
|
50
|
+
"@okfit/core": "0.8.0",
|
|
51
|
+
"@okfit/profiles": "0.8.0",
|
|
51
52
|
"effect": "4.0.0-rc.117"
|
|
52
53
|
},
|
|
53
54
|
"engines": {
|
package/render/distribution.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DistributionField } from "@effected/engine";
|
|
2
2
|
|
|
3
3
|
//#region src/render/distribution.ts
|
|
4
4
|
/** Every envelope's `distribution` field: a `Distribution` or `null`. @public */
|
|
5
|
-
const DistributionField =
|
|
6
|
-
name: Schema.String,
|
|
7
|
-
version: Schema.String
|
|
8
|
-
}));
|
|
5
|
+
const DistributionField$1 = DistributionField;
|
|
9
6
|
|
|
10
7
|
//#endregion
|
|
11
|
-
export { DistributionField };
|
|
8
|
+
export { DistributionField$1 as DistributionField };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ConceptId } from "@okfit/core";
|
|
2
|
+
import { Option } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/session/concept.ts
|
|
5
|
+
/** Backslashes to forward slashes; the rest of this module treats every path as already resolved. */
|
|
6
|
+
const posixOf = (value) => value.split("\\").join("/");
|
|
7
|
+
/** Drops every trailing `/` without a regex (a `/\/+$/` pattern is quadratic on a long run of slashes). */
|
|
8
|
+
const stripTrailingSlashes = (path) => {
|
|
9
|
+
let end = path.length;
|
|
10
|
+
while (end > 0 && path.charCodeAt(end - 1) === 47) end -= 1;
|
|
11
|
+
return path.slice(0, end);
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The loaded concept at `absolutePath`, if any. `absolutePath` must already sit under
|
|
15
|
+
* `bundle.root` (a real resolved path, never re-resolved here: `@okfit/core`'s own
|
|
16
|
+
* `internal/posixPath.ts` avoids `path.resolve`/`path.relative` on purpose, because both read
|
|
17
|
+
* `process.cwd()`, and this package holds the same line). Strips `bundle.root` as a plain
|
|
18
|
+
* string prefix, then applies the same `ConceptId.fromPath` normalisation `withFallbackRange`
|
|
19
|
+
* uses: `None` for a path outside the bundle, a reserved file (`index.md`, `log.md`), a
|
|
20
|
+
* non-markdown file, or a markdown file that never decoded into a concept.
|
|
21
|
+
*
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
const conceptFor = (bundle, absolutePath) => {
|
|
25
|
+
const root = stripTrailingSlashes(posixOf(bundle.root));
|
|
26
|
+
const target = posixOf(absolutePath);
|
|
27
|
+
const prefix = `${root}/`;
|
|
28
|
+
if (!target.startsWith(prefix)) return Option.none();
|
|
29
|
+
const relative = target.slice(prefix.length);
|
|
30
|
+
return Option.flatMap(ConceptId.fromPath(relative), (id) => Option.fromUndefinedOr(bundle.concepts.get(id)));
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
export { conceptFor };
|
package/session/range.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { conceptFor } from "./concept.js";
|
|
2
|
+
import { DiagnosticRange } from "@okfit/core";
|
|
2
3
|
import { Option } from "effect";
|
|
3
4
|
|
|
4
5
|
//#region src/session/range.ts
|
|
@@ -20,10 +21,7 @@ const FILE_START = DiagnosticRange.make({
|
|
|
20
21
|
*/
|
|
21
22
|
const withFallbackRange = (bundle, diagnostic) => {
|
|
22
23
|
if (diagnostic.range !== void 0 || diagnostic.file === "") return diagnostic;
|
|
23
|
-
const concept = Option.
|
|
24
|
-
onNone: () => void 0,
|
|
25
|
-
onSome: (id) => bundle.concepts.get(id)
|
|
26
|
-
});
|
|
24
|
+
const concept = Option.getOrUndefined(conceptFor(bundle, `${bundle.root}/${diagnostic.file}`));
|
|
27
25
|
const node = concept?.document.frontmatter;
|
|
28
26
|
if (concept === void 0 || node === void 0) return {
|
|
29
27
|
...diagnostic,
|
package/tsdoc-metadata.json
CHANGED
package/validate/resources.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Diagnostic, OkfitConfig } from "@okfit/core";
|
|
1
|
+
import { Diagnostic, DiagnosticRange, OkfitConfig } from "@okfit/core";
|
|
2
2
|
import { Effect, FileSystem, Path } from "effect";
|
|
3
3
|
|
|
4
4
|
//#region src/validate/resources.ts
|
|
@@ -22,11 +22,23 @@ const stripSuffix = (value) => {
|
|
|
22
22
|
const at = value.search(/[?#]/);
|
|
23
23
|
return at === -1 ? value : value.slice(0, at);
|
|
24
24
|
};
|
|
25
|
-
/** Every `resource` value on a concept: the top-level field, then each source's. */
|
|
25
|
+
/** Every `resource` value on a concept, each with its own frontmatter path: the top-level field, then each source's. */
|
|
26
26
|
const resourceValuesOf = (concept) => {
|
|
27
27
|
const values = [];
|
|
28
|
-
if (concept.frontmatter.resource !== void 0) values.push(
|
|
29
|
-
|
|
28
|
+
if (concept.frontmatter.resource !== void 0) values.push({
|
|
29
|
+
value: concept.frontmatter.resource,
|
|
30
|
+
path: ["resource"]
|
|
31
|
+
});
|
|
32
|
+
concept.frontmatter.sources?.forEach((source, index) => {
|
|
33
|
+
values.push({
|
|
34
|
+
value: source.resource,
|
|
35
|
+
path: [
|
|
36
|
+
"sources",
|
|
37
|
+
index,
|
|
38
|
+
"resource"
|
|
39
|
+
]
|
|
40
|
+
});
|
|
41
|
+
});
|
|
30
42
|
return values;
|
|
31
43
|
};
|
|
32
44
|
/**
|
|
@@ -36,9 +48,11 @@ const resourceValuesOf = (concept) => {
|
|
|
36
48
|
* pattern) or a literal path resolved in D-23 order: relative to the
|
|
37
49
|
* concept's own directory, then (for a bare path) relative to the bundle
|
|
38
50
|
* root, or with a leading `/` relative to the bundle root only; only the
|
|
39
|
-
* latter is checked against the filesystem.
|
|
40
|
-
*
|
|
41
|
-
* `
|
|
51
|
+
* latter is checked against the filesystem. Ranges at the offending value
|
|
52
|
+
* (phase 4 decision 2, via `DiagnosticRange.forFrontmatterPath`) using the
|
|
53
|
+
* `["resource"]` or `["sources", index, "resource"]` path the value came
|
|
54
|
+
* from. Total over severity (mirrors `Provenance.lint`'s S-28 posture):
|
|
55
|
+
* returns `[]`, touching neither `FileSystem` nor `Path`, the moment
|
|
42
56
|
* `OkfitConfig.severityFor(config, "source-resource-missing")` is `"off"`.
|
|
43
57
|
*
|
|
44
58
|
* @public
|
|
@@ -49,7 +63,7 @@ const lintResources = (bundle, config) => Effect.gen(function* () {
|
|
|
49
63
|
const fs = yield* FileSystem.FileSystem;
|
|
50
64
|
const path = yield* Path.Path;
|
|
51
65
|
const diagnostics = [];
|
|
52
|
-
for (const [, concept] of bundle.concepts) for (const value of resourceValuesOf(concept)) {
|
|
66
|
+
for (const [, concept] of bundle.concepts) for (const { value, path: fieldPath } of resourceValuesOf(concept)) {
|
|
53
67
|
if (isDescriptor(value)) continue;
|
|
54
68
|
const stripped = stripSuffix(value);
|
|
55
69
|
const candidates = stripped.startsWith("/") ? [path.join(bundle.root, stripped.slice(1))] : [path.resolve(bundle.root, path.dirname(concept.path), stripped), ...stripped.startsWith("./") || stripped.startsWith("../") ? [] : [path.resolve(bundle.root, stripped)]];
|
|
@@ -59,11 +73,13 @@ const lintResources = (bundle, config) => Effect.gen(function* () {
|
|
|
59
73
|
break;
|
|
60
74
|
}
|
|
61
75
|
if (exists) continue;
|
|
76
|
+
const range = DiagnosticRange.forFrontmatterPath(concept.document, fieldPath);
|
|
62
77
|
diagnostics.push(Diagnostic.make({
|
|
63
78
|
file: concept.path,
|
|
64
79
|
code: "source-resource-missing",
|
|
65
80
|
severity,
|
|
66
|
-
message: `Resource "${value}" does not exist relative to ${concept.path}
|
|
81
|
+
message: `Resource "${value}" does not exist relative to ${concept.path}`,
|
|
82
|
+
...range === void 0 ? {} : { range }
|
|
67
83
|
}));
|
|
68
84
|
}
|
|
69
85
|
return diagnostics;
|
package/verify/locate.js
CHANGED
|
@@ -55,19 +55,16 @@ const hasMergeKey = (node) => {
|
|
|
55
55
|
return false;
|
|
56
56
|
};
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
59
|
-
* `
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* plus the opening fence's own terminator, taken from `split`'s fidelity
|
|
65
|
-
* field rather than re-derived with `indexOf` (contract §12 note 9). Every
|
|
66
|
-
* whole-file offset below is `valueStart + <yaml offset>`.
|
|
58
|
+
* The shared prologue every locator in this file starts with: split
|
|
59
|
+
* `source`'s frontmatter, parse its value, and confirm the parsed document
|
|
60
|
+
* is a (non-flow) top-level mapping. Returns the same `"no-frontmatter"` /
|
|
61
|
+
* `"not-a-mapping"` `unsupported` shapes each locator returned inline
|
|
62
|
+
* before this helper existed -- callers still name those shapes themselves,
|
|
63
|
+
* this only removes the duplicated parsing steps that produced them.
|
|
67
64
|
*
|
|
68
65
|
* @internal
|
|
69
66
|
*/
|
|
70
|
-
const
|
|
67
|
+
const parseTopLevelMapping = Effect.fn("okfit/verify/parseTopLevelMapping")(function* (source) {
|
|
71
68
|
const block = FrontmatterSource.split(source).frontmatter;
|
|
72
69
|
if (block === void 0) return {
|
|
73
70
|
_tag: "unsupported",
|
|
@@ -80,6 +77,29 @@ const locate = Effect.fn("okfit/verify/locate")(function* (source) {
|
|
|
80
77
|
_tag: "unsupported",
|
|
81
78
|
shape: "not-a-mapping"
|
|
82
79
|
};
|
|
80
|
+
return {
|
|
81
|
+
value,
|
|
82
|
+
valueStart,
|
|
83
|
+
contents
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
/**
|
|
87
|
+
* Split `source`'s frontmatter, parse its value, find the top-level
|
|
88
|
+
* `verified` pair, and classify it (contract §3.1). Fails only when the
|
|
89
|
+
* frontmatter YAML is fatally unparseable — impossible for a concept that
|
|
90
|
+
* reached `bundle.concepts`, but typed rather than assumed.
|
|
91
|
+
*
|
|
92
|
+
* Offset translation is `valueStart = 3 + newline.length` — `"---".length`
|
|
93
|
+
* plus the opening fence's own terminator, taken from `split`'s fidelity
|
|
94
|
+
* field rather than re-derived with `indexOf` (contract §12 note 9). Every
|
|
95
|
+
* whole-file offset below is `valueStart + <yaml offset>`.
|
|
96
|
+
*
|
|
97
|
+
* @internal
|
|
98
|
+
*/
|
|
99
|
+
const locate = Effect.fn("okfit/verify/locate")(function* (source) {
|
|
100
|
+
const parsed = yield* parseTopLevelMapping(source);
|
|
101
|
+
if ("_tag" in parsed) return parsed;
|
|
102
|
+
const { value, valueStart, contents } = parsed;
|
|
83
103
|
const pair = contents.items.find((item) => item.key instanceof YamlScalar && item.key.value === "verified");
|
|
84
104
|
if (pair === void 0) return {
|
|
85
105
|
_tag: "absent",
|
|
@@ -158,18 +178,9 @@ const locate = Effect.fn("okfit/verify/locate")(function* (source) {
|
|
|
158
178
|
* @internal
|
|
159
179
|
*/
|
|
160
180
|
const locateGeneratedField = Effect.fn("okfit/verify/locateGeneratedField")(function* (source, field) {
|
|
161
|
-
const
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
shape: "no-frontmatter"
|
|
165
|
-
};
|
|
166
|
-
const value = block.value;
|
|
167
|
-
const valueStart = 3 + (block.newline ?? "\n").length;
|
|
168
|
-
const contents = (yield* YamlDocument.parse(value)).contents;
|
|
169
|
-
if (!(contents instanceof YamlMap)) return {
|
|
170
|
-
_tag: "unsupported",
|
|
171
|
-
shape: "not-a-mapping"
|
|
172
|
-
};
|
|
181
|
+
const parsed = yield* parseTopLevelMapping(source);
|
|
182
|
+
if ("_tag" in parsed) return parsed;
|
|
183
|
+
const { value, valueStart, contents } = parsed;
|
|
173
184
|
const pair = contents.items.find((item) => item.key instanceof YamlScalar && item.key.value === "generated");
|
|
174
185
|
if (pair === void 0) return {
|
|
175
186
|
_tag: "unsupported",
|
|
@@ -254,20 +265,77 @@ const locateGenerated = (source) => locateGeneratedField(source, "at");
|
|
|
254
265
|
* @internal
|
|
255
266
|
*/
|
|
256
267
|
const locateGeneratedBodySha256 = (source) => locateGeneratedField(source, "body_sha256");
|
|
257
|
-
/**
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
268
|
+
/**
|
|
269
|
+
* Locate a top-level scalar key (`key`) inside a concept's frontmatter,
|
|
270
|
+
* mirroring `locateGeneratedField`'s parsing prologue. Unlike `generated`'s
|
|
271
|
+
* fields, this key is a direct top-level sibling, not nested inside a block
|
|
272
|
+
* mapping, so there is no intermediate `generated:` lookup and no indent to
|
|
273
|
+
* track for the insert case -- `title:`/`type:` anchor the insert instead.
|
|
274
|
+
*
|
|
275
|
+
* @internal
|
|
276
|
+
*/
|
|
277
|
+
const locateTopLevelScalar = Effect.fn("okfit/verify/locateTopLevelScalar")(function* (source, key) {
|
|
278
|
+
const parsed = yield* parseTopLevelMapping(source);
|
|
279
|
+
if ("_tag" in parsed) return parsed;
|
|
280
|
+
const { value, valueStart, contents } = parsed;
|
|
281
|
+
if (contents.style === "flow") return {
|
|
261
282
|
_tag: "unsupported",
|
|
262
|
-
shape: "
|
|
283
|
+
shape: "flow-mapping"
|
|
263
284
|
};
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
285
|
+
const pair = contents.items.find((item) => item.key instanceof YamlScalar && item.key.value === key);
|
|
286
|
+
if (pair !== void 0) {
|
|
287
|
+
const node = pair.value;
|
|
288
|
+
if (node === null) return {
|
|
289
|
+
_tag: "unsupported",
|
|
290
|
+
shape: `${key}-empty`
|
|
291
|
+
};
|
|
292
|
+
if (node instanceof YamlAlias) return {
|
|
293
|
+
_tag: "unsupported",
|
|
294
|
+
shape: "alias"
|
|
295
|
+
};
|
|
296
|
+
if (!(node instanceof YamlScalar)) return {
|
|
297
|
+
_tag: "unsupported",
|
|
298
|
+
shape: `${key}-not-scalar`
|
|
299
|
+
};
|
|
300
|
+
if (node.style === "block-literal" || node.style === "block-folded") return {
|
|
301
|
+
_tag: "unsupported",
|
|
302
|
+
shape: `${key}-block-scalar`
|
|
303
|
+
};
|
|
304
|
+
return {
|
|
305
|
+
_tag: "replaceScalar",
|
|
306
|
+
start: valueStart + node.offset,
|
|
307
|
+
end: valueStart + node.offset + node.length,
|
|
308
|
+
quote: node.style
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const findKey = (name) => contents.items.find((item) => item.key instanceof YamlScalar && item.key.value === name);
|
|
312
|
+
const titlePair = findKey("title");
|
|
313
|
+
const anchor = titlePair !== void 0 ? {
|
|
314
|
+
pair: titlePair,
|
|
315
|
+
afterKey: "title"
|
|
316
|
+
} : void 0;
|
|
317
|
+
const typePair = anchor === void 0 ? findKey("type") : void 0;
|
|
318
|
+
const resolved = anchor ?? (typePair !== void 0 ? {
|
|
319
|
+
pair: typePair,
|
|
320
|
+
afterKey: "type"
|
|
321
|
+
} : void 0);
|
|
322
|
+
if (resolved === void 0) return {
|
|
268
323
|
_tag: "unsupported",
|
|
269
|
-
shape: "
|
|
324
|
+
shape: "no-anchor-key"
|
|
325
|
+
};
|
|
326
|
+
const end = resolved.pair.value !== null ? resolved.pair.value.offset + resolved.pair.value.length : resolved.pair.key.offset + resolved.pair.key.length;
|
|
327
|
+
const nl = value.indexOf("\n", end);
|
|
328
|
+
return {
|
|
329
|
+
_tag: "insertAfterKey",
|
|
330
|
+
insertAt: valueStart + (value[end - 1] === "\n" ? end : nl === -1 ? value.length : nl + 1),
|
|
331
|
+
afterKey: resolved.afterKey
|
|
270
332
|
};
|
|
333
|
+
});
|
|
334
|
+
/** @internal */
|
|
335
|
+
const locateGeneratedBlock = Effect.fn("okfit/verify/locateGeneratedBlock")(function* (source) {
|
|
336
|
+
const parsed = yield* parseTopLevelMapping(source);
|
|
337
|
+
if ("_tag" in parsed) return parsed;
|
|
338
|
+
const { value, valueStart, contents } = parsed;
|
|
271
339
|
if (contents.items.find((item) => item.key instanceof YamlScalar && item.key.value === "generated") !== void 0) return {
|
|
272
340
|
_tag: "unsupported",
|
|
273
341
|
shape: "generated-present"
|
|
@@ -279,4 +347,4 @@ const locateGeneratedBlock = Effect.fn("okfit/verify/locateGeneratedBlock")(func
|
|
|
279
347
|
});
|
|
280
348
|
|
|
281
349
|
//#endregion
|
|
282
|
-
export { detectNewline, documentNewline, indentAt, locate, locateGenerated, locateGeneratedBlock, locateGeneratedBodySha256, stripBom };
|
|
350
|
+
export { detectNewline, documentNewline, indentAt, locate, locateGenerated, locateGeneratedBlock, locateGeneratedBodySha256, locateTopLevelScalar, stripBom };
|
package/verify/splice.js
CHANGED
|
@@ -93,6 +93,30 @@ const spliceGeneratedFields = (atTarget, bodySha256Target, encodedAt, bodySha256
|
|
|
93
93
|
return [spliceGenerated(atTarget, encodedAt, newline, "at"), spliceGenerated(bodySha256Target, bodySha256, newline, "body_sha256")];
|
|
94
94
|
};
|
|
95
95
|
/**
|
|
96
|
+
* Build the one edit for `target` (a top-level scalar key such as
|
|
97
|
+
* `status`). Never calls a YAML stringifier: the only syntax emitted is
|
|
98
|
+
* `<key>: `, the preserved quote character (if any), and `newline`.
|
|
99
|
+
*
|
|
100
|
+
* @internal
|
|
101
|
+
*/
|
|
102
|
+
const spliceTopLevelScalar = (target, key, value, newline) => {
|
|
103
|
+
switch (target._tag) {
|
|
104
|
+
case "insertAfterKey": return MarkdownEdit.make({
|
|
105
|
+
offset: target.insertAt,
|
|
106
|
+
length: 0,
|
|
107
|
+
content: `${key}: ${value}${newline}`
|
|
108
|
+
});
|
|
109
|
+
case "replaceScalar": {
|
|
110
|
+
const quoted = target.quote === "single-quoted" ? `'${value}'` : target.quote === "double-quoted" ? `"${value}"` : value;
|
|
111
|
+
return MarkdownEdit.make({
|
|
112
|
+
offset: target.start,
|
|
113
|
+
length: target.end - target.start,
|
|
114
|
+
content: quoted
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
96
120
|
* A whole `generated:` block mapping, appended as the LAST top-level key
|
|
97
121
|
* (issue #73) — block style, matching how this repository writes every
|
|
98
122
|
* `generated:` and how `splice`'s own `absent` case writes `verified:`.
|
|
@@ -106,4 +130,4 @@ const spliceGeneratedBlock = (target, fields, newline) => MarkdownEdit.make({
|
|
|
106
130
|
});
|
|
107
131
|
|
|
108
132
|
//#endregion
|
|
109
|
-
export { splice, spliceGenerated, spliceGeneratedBlock, spliceGeneratedFields };
|
|
133
|
+
export { splice, spliceGenerated, spliceGeneratedBlock, spliceGeneratedFields, spliceTopLevelScalar };
|