@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 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
- interface Distribution {
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.7.5",
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.0",
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.7.4",
50
- "@okfit/profiles": "0.7.5",
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": {
@@ -1,11 +1,8 @@
1
- import { Schema } from "effect";
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 = Schema.NullOr(Schema.Struct({
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 { ConceptId, DiagnosticRange } from "@okfit/core";
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.match(ConceptId.fromPath(diagnostic.file), {
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,
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.59.1"
8
+ "packageVersion": "7.59.2"
9
9
  }
10
10
  ]
11
11
  }
@@ -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(concept.frontmatter.resource);
29
- for (const source of concept.frontmatter.sources ?? []) values.push(source.resource);
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. Total over severity (mirrors
40
- * `Provenance.lint`'s S-28 posture): returns `[]`, touching neither
41
- * `FileSystem` nor `Path`, the moment
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
- * Split `source`'s frontmatter, parse its value, find the top-level
59
- * `verified` pair, and classify it (contract §3.1). Fails only when the
60
- * frontmatter YAML is fatally unparseable — impossible for a concept that
61
- * reached `bundle.concepts`, but typed rather than assumed.
62
- *
63
- * Offset translation is `valueStart = 3 + newline.length` — `"---".length`
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 locate = Effect.fn("okfit/verify/locate")(function* (source) {
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 block = FrontmatterSource.split(source).frontmatter;
162
- if (block === void 0) return {
163
- _tag: "unsupported",
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
- /** @internal */
258
- const locateGeneratedBlock = Effect.fn("okfit/verify/locateGeneratedBlock")(function* (source) {
259
- const block = FrontmatterSource.split(source).frontmatter;
260
- if (block === void 0) return {
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: "no-frontmatter"
283
+ shape: "flow-mapping"
263
284
  };
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 {
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: "not-a-mapping"
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 };
package/version.js CHANGED
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * @public
13
13
  */
14
- const ENGINE_VERSION = "0.7.5";
14
+ const ENGINE_VERSION = "0.9.0";
15
15
 
16
16
  //#endregion
17
17
  export { ENGINE_VERSION };