@okfit/engine 0.7.4 → 0.8.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/errors.js CHANGED
@@ -138,6 +138,38 @@ var VerifySelectionError = class extends Schema.TaggedError()("VerifySelectionEr
138
138
  return `type "${this.detail ?? ""}" is not declared in the config`;
139
139
  }
140
140
  };
141
+ const DOCUMENT_PATH_REASON_TEXT = {
142
+ "not-relative": "must be a bundle-relative posix path",
143
+ "escapes-bundle": "resolves outside the bundle root",
144
+ "not-markdown": "must name a .md file",
145
+ duplicate: "was given more than once",
146
+ "no-directory": "names a directory that does not exist in the bundle"
147
+ };
148
+ /**
149
+ * An unsaved-document input named a path the overlay cannot honestly shadow:
150
+ * absolute or backslashed (`not-relative`), outside the bundle
151
+ * (`escapes-bundle`), not a markdown file (`not-markdown`), the same file as
152
+ * an earlier input after normalization (`duplicate`), or under a directory that
153
+ * does not exist on disk, so the bundle walk would never reach it
154
+ * (`no-directory`). Exit 64: a usage error.
155
+ *
156
+ * @public
157
+ */
158
+ var DocumentPathError = class extends Schema.TaggedError()("DocumentPathError", {
159
+ path: Schema.String,
160
+ reason: Schema.Literals([
161
+ "not-relative",
162
+ "escapes-bundle",
163
+ "not-markdown",
164
+ "duplicate",
165
+ "no-directory"
166
+ ])
167
+ }) {
168
+ [Runtime.errorExitCode] = 64;
169
+ get message() {
170
+ return `document path "${this.path}" ${DOCUMENT_PATH_REASON_TEXT[this.reason]}`;
171
+ }
172
+ };
141
173
 
142
174
  //#endregion
143
- export { ConfigMalformedError, ConfigPathNotFoundError, InitOverwriteError, SyncStagedLogError, VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError };
175
+ export { ConfigMalformedError, ConfigPathNotFoundError, DocumentPathError, InitOverwriteError, SyncStagedLogError, VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError };
@@ -0,0 +1,41 @@
1
+ import { Context, Effect, Layer, Option, Schema } from "effect";
2
+
3
+ //#region src/external/ExternalReferences.ts
4
+ /**
5
+ * Reachability of an external reference: `unknown` means never checked.
6
+ *
7
+ * @public
8
+ */
9
+ const ReferenceState = Schema.Literals([
10
+ "ok",
11
+ "unreachable",
12
+ "unknown"
13
+ ]);
14
+ /**
15
+ * One reachability answer. `checkedAt` is `None` when nothing was checked: the
16
+ * engine never reads a Clock to invent a timestamp.
17
+ *
18
+ * @public
19
+ */
20
+ const ReferenceCheck = Schema.Struct({
21
+ url: Schema.String,
22
+ state: ReferenceState,
23
+ checkedAt: Schema.Option(Schema.DateTimeUtc)
24
+ });
25
+ /**
26
+ * External-reference reachability (spec 4.4). Phase 2 ships only `layerNoop`;
27
+ * phase 7 adds an HTTP layer over a store-backed cache.
28
+ *
29
+ * @public
30
+ */
31
+ var ExternalReferences = class ExternalReferences extends Context.Service()("@okfit/engine/ExternalReferences") {
32
+ /** Answers `unknown`, never checked, for every url. */
33
+ static layerNoop = Layer.succeed(ExternalReferences, { check: (url) => Effect.succeed(ReferenceCheck.make({
34
+ url,
35
+ state: "unknown",
36
+ checkedAt: Option.none()
37
+ })) });
38
+ };
39
+
40
+ //#endregion
41
+ export { ExternalReferences, ReferenceCheck, ReferenceState };
package/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Actor, BundleLoadError, ConceptId, Diagnostic, DiagnosticRange, DiagnosticSeverity, GraphLink, GraphNode, LinkGraph, LoadedBundle, OkfitConfig, OkfitConfigFile, StaleConcept, ValidationReport } from "@okfit/core";
2
- import { Context, Crypto, DateTime, Effect, FileSystem, Layer, Option, Path, PlatformError, Runtime, Schema } from "effect";
1
+ import { Actor, BundleLoadError, ConceptId, Diagnostic, DiagnosticRange, DiagnosticSeverity, GraphLink, GraphNode, LinkGraph, LoadedBundle, LoadedConcept, OkfitConfig, OkfitConfigFile, StaleConcept, ValidationReport } from "@okfit/core";
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";
@@ -182,6 +182,24 @@ export declare class VerifySelectionError extends VerifySelectionError_base {
182
182
  readonly [Runtime.errorExitCode] = 64;
183
183
  get message(): string;
184
184
  }
185
+ declare const DocumentPathError_base: Schema.Class<DocumentPathError, Schema.TaggedStruct<"DocumentPathError", {
186
+ readonly path: Schema.String;
187
+ readonly reason: Schema.Literals<readonly ["not-relative", "escapes-bundle", "not-markdown", "duplicate", "no-directory"]>;
188
+ }>, import("effect/Cause").YieldableError>;
189
+ /**
190
+ * An unsaved-document input named a path the overlay cannot honestly shadow:
191
+ * absolute or backslashed (`not-relative`), outside the bundle
192
+ * (`escapes-bundle`), not a markdown file (`not-markdown`), the same file as
193
+ * an earlier input after normalization (`duplicate`), or under a directory that
194
+ * does not exist on disk, so the bundle walk would never reach it
195
+ * (`no-directory`). Exit 64: a usage error.
196
+ *
197
+ * @public
198
+ */
199
+ export declare class DocumentPathError extends DocumentPathError_base {
200
+ readonly [Runtime.errorExitCode] = 64;
201
+ get message(): string;
202
+ }
185
203
  //#endregion
186
204
  //#region src/config/layer.d.ts
187
205
  /**
@@ -357,6 +375,42 @@ interface ContextResult {
357
375
  */
358
376
  export declare const runContext: (options: ContextRunOptions) => Effect.Effect<ContextResult, never, FileSystem.FileSystem | Path.Path>;
359
377
  //#endregion
378
+ //#region src/external/ExternalReferences.d.ts
379
+ /**
380
+ * Reachability of an external reference: `unknown` means never checked.
381
+ *
382
+ * @public
383
+ */
384
+ export declare const ReferenceState: Schema.Literals<readonly ["ok", "unreachable", "unknown"]>;
385
+ /**
386
+ * One reachability answer. `checkedAt` is `None` when nothing was checked: the
387
+ * engine never reads a Clock to invent a timestamp.
388
+ *
389
+ * @public
390
+ */
391
+ export declare const ReferenceCheck: Schema.Struct<{
392
+ readonly url: Schema.String;
393
+ readonly state: Schema.Literals<readonly ["ok", "unreachable", "unknown"]>;
394
+ readonly checkedAt: Schema.Option<Schema.DateTimeUtc>;
395
+ }>;
396
+ /** @public */
397
+ export type ReferenceCheck = typeof ReferenceCheck.Type;
398
+ /** @public */
399
+ interface ExternalReferencesShape {
400
+ readonly check: (url: string) => Effect.Effect<ReferenceCheck>;
401
+ }
402
+ declare const ExternalReferences_base: Context.ServiceClass<ExternalReferences, "@okfit/engine/ExternalReferences", ExternalReferencesShape>;
403
+ /**
404
+ * External-reference reachability (spec 4.4). Phase 2 ships only `layerNoop`;
405
+ * phase 7 adds an HTTP layer over a store-backed cache.
406
+ *
407
+ * @public
408
+ */
409
+ export declare class ExternalReferences extends ExternalReferences_base {
410
+ /** Answers `unknown`, never checked, for every url. */
411
+ static readonly layerNoop: Layer.Layer<ExternalReferences>;
412
+ }
413
+ //#endregion
360
414
  //#region src/graph/run.d.ts
361
415
  /** @public */
362
416
  interface GraphRunOptions {
@@ -442,6 +496,95 @@ export declare const targetPaths: (options: ScaffoldOptions) => ReadonlyArray<st
442
496
  */
443
497
  export declare const files: (options: ScaffoldOptions) => Effect.Effect<ReadonlyArray<ScaffoldFile>, MarkdownParseError | FrontmatterWriteError>;
444
498
  //#endregion
499
+ //#region src/overlay/documents.d.ts
500
+ /**
501
+ * One unsaved document: `path` is bundle-relative posix (`metrics/churn.md`),
502
+ * `text` the full file contents.
503
+ *
504
+ * @public
505
+ */
506
+ interface DocumentInput {
507
+ readonly path: string;
508
+ readonly text: string;
509
+ }
510
+ /**
511
+ * Resolve a bundle-relative posix path to the absolute key `Bundle.load` will
512
+ * read (`path.join(root, relative)`, which also normalizes `.`/`..`), or say
513
+ * why it cannot be.
514
+ *
515
+ * @public
516
+ */
517
+ export declare const resolveDocumentPath: (path: Path.Path, root: string, relative: string) => Result.Result<string, DocumentPathError>;
518
+ /**
519
+ * Run `self` with `documents` shadowing the ambient `FileSystem` under `root`
520
+ * (spec 4.5). Every path is validated before `self` starts; the first bad one
521
+ * fails `DocumentPathError` and `self` never runs. Beyond `resolveDocumentPath`'s
522
+ * lexical rules, each document's parent directory must exist on the underlying
523
+ * `FileSystem` (`no-directory`): the bundle walk lists one existing directory at a
524
+ * time, so a draft under a missing directory would otherwise never be read while
525
+ * validation silently checked disk. An empty list runs `self` unchanged. The
526
+ * overlay is private to this call.
527
+ *
528
+ * @public
529
+ */
530
+ export declare const provideDocuments: (root: string, documents: ReadonlyArray<DocumentInput>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E | DocumentPathError, R | FileSystem.FileSystem | Path.Path>;
531
+ //#endregion
532
+ //#region src/overlay/layer.d.ts
533
+ /**
534
+ * One open, possibly unsaved, document: its full text and the editor's version.
535
+ *
536
+ * @public
537
+ */
538
+ interface OverlayDocument {
539
+ readonly text: string;
540
+ readonly version: number;
541
+ }
542
+ /**
543
+ * The open-document map. Keys are absolute, normalized paths, the same strings
544
+ * `Bundle.load` builds with `path.join(root, file)`. Callers normalize; the
545
+ * overlay compares strings verbatim.
546
+ *
547
+ * @public
548
+ */
549
+ interface OverlayDocumentsShape {
550
+ /** Open or replace `path`; `version` defaults to 0. */
551
+ readonly open: (path: string, text: string, version?: number) => Effect.Effect<void>;
552
+ /** Replace `path`'s text; `version` defaults to the previous version plus one (0 plus one when not open). */
553
+ readonly change: (path: string, text: string, version?: number) => Effect.Effect<void>;
554
+ /** Forget `path`; later reads fall through to the underlying filesystem. */
555
+ readonly close: (path: string) => Effect.Effect<void>;
556
+ readonly get: (path: string) => Effect.Effect<Option.Option<OverlayDocument>>;
557
+ readonly entries: () => Effect.Effect<ReadonlyArray<readonly [string, OverlayDocument]>>;
558
+ }
559
+ declare const OverlayDocuments_base: Context.ServiceClass<OverlayDocuments, "@okfit/engine/OverlayDocuments", OverlayDocumentsShape>;
560
+ /**
561
+ * Editor buffers that shadow the filesystem (spec 3.2). Mutable by design: an
562
+ * LSP server changes it on every keystroke-level `didChange`.
563
+ *
564
+ * @public
565
+ */
566
+ export declare class OverlayDocuments extends OverlayDocuments_base {
567
+ /** A fresh, empty overlay. */
568
+ static readonly make: () => OverlayDocumentsShape;
569
+ /** One fresh overlay per layer build. */
570
+ static readonly layer: Layer.Layer<OverlayDocuments>;
571
+ }
572
+ /**
573
+ * Wrap `underlying` so `readFile`, `readFileString`, `exists` and `stat` answer
574
+ * from `documents` for open paths, and a non-recursive `readDirectory` also lists
575
+ * open documents whose parent is the directory read (so a brand-new unsaved file
576
+ * is walked). Every other member, and every path not open, is `underlying`'s.
577
+ *
578
+ * @public
579
+ */
580
+ export declare const makeOverlayFileSystem: (underlying: FileSystem.FileSystem, documents: OverlayDocumentsShape) => FileSystem.FileSystem;
581
+ /**
582
+ * The ambient `FileSystem`, shadowed by the ambient `OverlayDocuments`.
583
+ *
584
+ * @public
585
+ */
586
+ export declare const layerOverlayFileSystem: Layer.Layer<FileSystem.FileSystem, never, FileSystem.FileSystem | OverlayDocuments>;
587
+ //#endregion
445
588
  //#region src/platform.d.ts
446
589
  /**
447
590
  * K-9/K-11: `AppDirs.layer(options)` requires `Xdg | FileSystem | Path`, so
@@ -1203,6 +1346,115 @@ export declare const verifyBatchEnvelope: (input: {
1203
1346
  readonly distribution?: Distribution;
1204
1347
  }) => VerifyBatchEnvelope;
1205
1348
  //#endregion
1349
+ //#region src/session/BundleSession.d.ts
1350
+ /**
1351
+ * One bundle root's fixed inputs; `config` and `profile` come from the caller's
1352
+ * `resolveProjectConfig`.
1353
+ *
1354
+ * @public
1355
+ */
1356
+ interface BundleSessionOptions {
1357
+ readonly root: string;
1358
+ readonly config: OkfitConfig;
1359
+ readonly profile: Option.Option<Profile>;
1360
+ }
1361
+ /**
1362
+ * `edit` skips the git tier of the provenance lint (`skipProvenance: true`); `full` runs every tier.
1363
+ *
1364
+ * @public
1365
+ */
1366
+ type RevalidateTier = "edit" | "full";
1367
+ /** @public */
1368
+ interface RevalidateOptions {
1369
+ /** Read once by the caller per revalidate; the engine never reads a Clock. */
1370
+ readonly now: DateTime.Utc;
1371
+ readonly tier: RevalidateTier;
1372
+ }
1373
+ /** @public */
1374
+ interface RevalidateResult {
1375
+ /**
1376
+ * Only files whose diagnostic set differs from the previous revalidate, keyed by
1377
+ * bundle-relative posix path (`""` for bundle-level); a file whose set became
1378
+ * empty maps to `[]`. Includes files that are not open.
1379
+ */
1380
+ readonly changed: ReadonlyMap<string, ReadonlyArray<RenderedDiagnostic>>;
1381
+ readonly bundle: LoadedBundle;
1382
+ }
1383
+ /**
1384
+ * Exactly `run`'s failure union.
1385
+ *
1386
+ * @public
1387
+ */
1388
+ type RevalidateError = BundleLoadError | GitHistoryError | GitCommandError | UnknownRefError | PlatformError.PlatformError;
1389
+ /** @public */
1390
+ interface WatchedFilesOutcome {
1391
+ /** A config discovery file changed: re-resolve config and replace this session. */
1392
+ readonly configChanged: boolean;
1393
+ }
1394
+ /**
1395
+ * What `BundleSession.make` captures once.
1396
+ *
1397
+ * @public
1398
+ */
1399
+ type BundleSessionServices = FileSystem.FileSystem | Path.Path | Git | GitHistory | Crypto.Crypto;
1400
+ /** @public */
1401
+ interface BundleSessionShape {
1402
+ /** Absolute, resolved bundle root. */
1403
+ readonly root: string;
1404
+ /**
1405
+ * Shadow `path` with an editor buffer. A document whose parent directory does
1406
+ * not exist on disk is not walked until that directory exists.
1407
+ */
1408
+ readonly open: (path: string, text: string, version?: number) => Effect.Effect<void>;
1409
+ readonly change: (path: string, text: string, version?: number) => Effect.Effect<void>;
1410
+ readonly close: (path: string) => Effect.Effect<void>;
1411
+ readonly watchedFilesChanged: (paths: ReadonlyArray<string>) => Effect.Effect<WatchedFilesOutcome>;
1412
+ readonly revalidate: (options: RevalidateOptions) => Effect.Effect<RevalidateResult, RevalidateError>;
1413
+ readonly bundle: () => Effect.Effect<Option.Option<LoadedBundle>>;
1414
+ readonly graph: () => Effect.Effect<Option.Option<LinkGraph>>;
1415
+ readonly config: () => OkfitConfig;
1416
+ }
1417
+ declare const BundleSession_base: Context.ServiceClass<BundleSession, "@okfit/engine/BundleSession", BundleSessionShape>;
1418
+ /**
1419
+ * One bundle root's editor state (spec 4): open-document overlay, last loaded
1420
+ * bundle and graph, and last published diagnostics per file. Debounce is NOT
1421
+ * here: the caller (the LSP server) schedules revalidates; overlapping calls are
1422
+ * serialized so every change is reported exactly once.
1423
+ *
1424
+ * @public
1425
+ */
1426
+ export declare class BundleSession extends BundleSession_base {
1427
+ /** Build a session; the returned operations need no services. */
1428
+ static readonly make: (options: BundleSessionOptions) => Effect.Effect<BundleSessionShape, never, BundleSessionServices>;
1429
+ }
1430
+ //#endregion
1431
+ //#region src/session/concept.d.ts
1432
+ /**
1433
+ * The loaded concept at `absolutePath`, if any. `absolutePath` must already sit under
1434
+ * `bundle.root` (a real resolved path, never re-resolved here: `@okfit/core`'s own
1435
+ * `internal/posixPath.ts` avoids `path.resolve`/`path.relative` on purpose, because both read
1436
+ * `process.cwd()`, and this package holds the same line). Strips `bundle.root` as a plain
1437
+ * string prefix, then applies the same `ConceptId.fromPath` normalisation `withFallbackRange`
1438
+ * uses: `None` for a path outside the bundle, a reserved file (`index.md`, `log.md`), a
1439
+ * non-markdown file, or a markdown file that never decoded into a concept.
1440
+ *
1441
+ * @public
1442
+ */
1443
+ export declare const conceptFor: (bundle: LoadedBundle, absolutePath: string) => Option.Option<LoadedConcept>;
1444
+ //#endregion
1445
+ //#region src/session/range.d.ts
1446
+ /**
1447
+ * Give a range-less diagnostic somewhere to point (spec 4.3): the concept's
1448
+ * frontmatter block when the file loaded as a concept with frontmatter, else a
1449
+ * zero-length range at line 0. Ranged and bundle-level (`file === ""`)
1450
+ * diagnostics are returned unchanged. A file whose frontmatter failed to decode
1451
+ * is not a loaded concept and falls to line 0; core ranges its own decode
1452
+ * diagnostics already.
1453
+ *
1454
+ * @public
1455
+ */
1456
+ export declare const withFallbackRange: (bundle: LoadedBundle, diagnostic: RenderedDiagnostic) => RenderedDiagnostic;
1457
+ //#endregion
1206
1458
  //#region src/stale/run.d.ts
1207
1459
  /** @public */
1208
1460
  interface StaleRunOptions {
@@ -1442,5 +1694,5 @@ export declare const runVerifyBatch: (options: VerifyBatchOptions) => Effect.Eff
1442
1694
  */
1443
1695
  export declare const ENGINE_VERSION: string;
1444
1696
  //#endregion
1445
- export type { ContextResult, ContextRunOptions, DiagnosticSource, DiscoveredConfig, Distribution, GraphRunOptions, GraphRunResult, RenderedDiagnostic, ResolveProjectConfigInput, ResolvedProjectConfig, RunOptions, RunResult, ScaffoldFile, ScaffoldOptions, StaleRunOptions, StaleRunResult, SyncMode, SyncModeResult, SyncOptions, SyncResult, Tally, VerifyBatchOptions, VerifyBatchResult, VerifyBatchSkipReason, VerifyOptions, VerifyResult };
1697
+ export type { BundleSessionOptions, BundleSessionServices, BundleSessionShape, ContextResult, ContextRunOptions, DiagnosticSource, DiscoveredConfig, Distribution, DocumentInput, ExternalReferencesShape, GraphRunOptions, GraphRunResult, OverlayDocument, OverlayDocumentsShape, RenderedDiagnostic, ResolveProjectConfigInput, ResolvedProjectConfig, RevalidateError, RevalidateOptions, RevalidateResult, RevalidateTier, RunOptions, RunResult, ScaffoldFile, ScaffoldOptions, StaleRunOptions, StaleRunResult, SyncMode, SyncModeResult, SyncOptions, SyncResult, Tally, VerifyBatchOptions, VerifyBatchResult, VerifyBatchSkipReason, VerifyOptions, VerifyResult, WatchedFilesOutcome };
1446
1698
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import { resolveBundleRoot, resolveProjectRoot } from "./config/anchor.js";
2
- import { ConfigMalformedError, ConfigPathNotFoundError, InitOverwriteError, SyncStagedLogError, VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError } from "./errors.js";
2
+ import { ConfigMalformedError, ConfigPathNotFoundError, DocumentPathError, InitOverwriteError, 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";
6
+ import { ExternalReferences, ReferenceCheck, ReferenceState } from "./external/ExternalReferences.js";
6
7
  import { runGraph } from "./graph/run.js";
7
8
  import { CONFIG_RELATIVE_PATH, configValue, files, targetPaths } from "./init/scaffold.js";
9
+ import { OverlayDocuments, layerOverlayFileSystem, makeOverlayFileSystem } from "./overlay/layer.js";
10
+ import { provideDocuments, resolveDocumentPath } from "./overlay/documents.js";
8
11
  import { OkfitPlatform } from "./platform.js";
9
12
  import { ContextEnvelope, ContextField, ContextFieldValue, ContextTag, ContextType, contextEnvelope } from "./render/context.js";
10
13
  import { DistributionField } from "./render/distribution.js";
@@ -17,8 +20,11 @@ import { StaleEnvelope, StaleItem, StaleSummary, staleEnvelope } from "./render/
17
20
  import { SkipReason, runSync } from "./sync/run.js";
18
21
  import { SyncEnvelope, SyncModeEnvelope, syncEnvelope } from "./render/sync.js";
19
22
  import { VerifyBatchEnvelope, VerifyEnvelope, verifyBatchEnvelope, verifyEnvelope } from "./render/verify.js";
20
- import { runStale } from "./stale/run.js";
21
23
  import { Now, run } from "./validate/run.js";
24
+ import { conceptFor } from "./session/concept.js";
25
+ import { withFallbackRange } from "./session/range.js";
26
+ import { BundleSession } from "./session/BundleSession.js";
27
+ import { runStale } from "./stale/run.js";
22
28
  import { runVerify, runVerifyBatch } from "./verify/run.js";
23
29
 
24
- export { CONFIG_RELATIVE_PATH, ConfigMalformedError, ConfigPathNotFoundError, ContextEnvelope, ContextField, ContextFieldValue, ContextTag, ContextType, DEFAULT_PROFILE_NAME, DistributionField, ENGINE_VERSION, GraphEdgeEnvelope, GraphEnvelope, GraphNodeEnvelope, GraphSummary, InitOverwriteError, JsonDiagnostic, JsonEnvelope, JsonErrorEnvelope, JsonSummary, Now, OkfitPlatform, SkipReason, StaleEnvelope, StaleItem, StaleSummary, SyncEnvelope, SyncModeEnvelope, SyncStagedLogError, VerifyBatchEnvelope, VerifyConceptNotFoundError, VerifyEnvelope, VerifySelectionError, VerifyUnsupportedFrontmatterError, buildConfigLayer, collect, configValue, contextEnvelope, files, forDiagnostics, graphEnvelope, json, jsonError, provideConfig, resolveBundleRoot, resolveProjectConfig, resolveProjectRoot, run, runContext, runGraph, runStale, runSync, runVerify, runVerifyBatch, sort, staleEnvelope, syncEnvelope, tally, targetPaths, verifyBatchEnvelope, verifyEnvelope };
30
+ 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, 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 };
@@ -0,0 +1,61 @@
1
+ import { DocumentPathError } from "../errors.js";
2
+ import { OverlayDocuments, makeOverlayFileSystem } from "./layer.js";
3
+ import { Effect, FileSystem, Path, Result } from "effect";
4
+
5
+ //#region src/overlay/documents.ts
6
+ /**
7
+ * Resolve a bundle-relative posix path to the absolute key `Bundle.load` will
8
+ * read (`path.join(root, relative)`, which also normalizes `.`/`..`), or say
9
+ * why it cannot be.
10
+ *
11
+ * @public
12
+ */
13
+ const resolveDocumentPath = (path, root, relative) => {
14
+ const reject = (reason) => Result.fail(new DocumentPathError({
15
+ path: relative,
16
+ reason
17
+ }));
18
+ if (relative === "" || relative.includes("\\") || path.isAbsolute(relative)) return reject("not-relative");
19
+ if (!relative.endsWith(".md")) return reject("not-markdown");
20
+ const absolute = path.join(root, relative);
21
+ const back = path.relative(root, absolute);
22
+ if (back === "" || back === ".." || back.startsWith(`..${path.sep}`) || path.isAbsolute(back)) return reject("escapes-bundle");
23
+ return Result.succeed(absolute);
24
+ };
25
+ /**
26
+ * Run `self` with `documents` shadowing the ambient `FileSystem` under `root`
27
+ * (spec 4.5). Every path is validated before `self` starts; the first bad one
28
+ * fails `DocumentPathError` and `self` never runs. Beyond `resolveDocumentPath`'s
29
+ * lexical rules, each document's parent directory must exist on the underlying
30
+ * `FileSystem` (`no-directory`): the bundle walk lists one existing directory at a
31
+ * time, so a draft under a missing directory would otherwise never be read while
32
+ * validation silently checked disk. An empty list runs `self` unchanged. The
33
+ * overlay is private to this call.
34
+ *
35
+ * @public
36
+ */
37
+ const provideDocuments = (root, documents) => (self) => documents.length === 0 ? self : Effect.gen(function* () {
38
+ const path = yield* Path.Path;
39
+ const underlying = yield* FileSystem.FileSystem;
40
+ const overlay = OverlayDocuments.make();
41
+ const seen = /* @__PURE__ */ new Set();
42
+ for (const document of documents) {
43
+ const resolved = resolveDocumentPath(path, root, document.path);
44
+ if (Result.isFailure(resolved)) return yield* resolved.failure;
45
+ if (seen.has(resolved.success)) return yield* new DocumentPathError({
46
+ path: document.path,
47
+ reason: "duplicate"
48
+ });
49
+ seen.add(resolved.success);
50
+ const parent = path.dirname(resolved.success);
51
+ if (!(yield* underlying.exists(parent).pipe(Effect.flatMap((exists) => exists ? Effect.map(underlying.stat(parent), (info) => info.type === "Directory") : Effect.succeed(false)), Effect.orElseSucceed(() => false)))) return yield* new DocumentPathError({
52
+ path: document.path,
53
+ reason: "no-directory"
54
+ });
55
+ yield* overlay.open(resolved.success, document.text);
56
+ }
57
+ return yield* self.pipe(Effect.provideService(FileSystem.FileSystem, makeOverlayFileSystem(underlying, overlay)));
58
+ });
59
+
60
+ //#endregion
61
+ export { provideDocuments, resolveDocumentPath };
@@ -0,0 +1,109 @@
1
+ import { ByteSize, Context, Effect, FileSystem, Layer, Option } from "effect";
2
+
3
+ //#region src/overlay/layer.ts
4
+ /**
5
+ * Editor buffers that shadow the filesystem (spec 3.2). Mutable by design: an
6
+ * LSP server changes it on every keystroke-level `didChange`.
7
+ *
8
+ * @public
9
+ */
10
+ var OverlayDocuments = class OverlayDocuments extends Context.Service()("@okfit/engine/OverlayDocuments") {
11
+ /** A fresh, empty overlay. */
12
+ static make = () => {
13
+ const documents = /* @__PURE__ */ new Map();
14
+ return {
15
+ open: (path, text, version = 0) => Effect.sync(() => {
16
+ documents.set(path, {
17
+ text,
18
+ version
19
+ });
20
+ }),
21
+ change: (path, text, version) => Effect.sync(() => {
22
+ documents.set(path, {
23
+ text,
24
+ version: version ?? (documents.get(path)?.version ?? 0) + 1
25
+ });
26
+ }),
27
+ close: (path) => Effect.sync(() => {
28
+ documents.delete(path);
29
+ }),
30
+ get: (path) => Effect.sync(() => Option.fromUndefinedOr(documents.get(path))),
31
+ entries: () => Effect.sync(() => [...documents.entries()])
32
+ };
33
+ };
34
+ /** One fresh overlay per layer build. */
35
+ static layer = Layer.sync(OverlayDocuments, () => OverlayDocuments.make());
36
+ };
37
+ const encoder = new TextEncoder();
38
+ /** Parent directory and base name of an absolute path, on either separator. */
39
+ const parentAndName = (path) => {
40
+ const index = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
41
+ return [index <= 0 ? path.slice(0, index + 1) : path.slice(0, index), path.slice(index + 1)];
42
+ };
43
+ const trimTrailingSeparators = (path) => path.length > 1 ? path.replace(/[\\/]+$/, "") : path;
44
+ /** A `File.Info` for a document that exists only in the overlay. */
45
+ const overlayInfo = (text) => ({
46
+ type: "File",
47
+ mtime: Option.none(),
48
+ atime: Option.none(),
49
+ birthtime: Option.none(),
50
+ dev: 0,
51
+ ino: Option.none(),
52
+ mode: 420,
53
+ nlink: Option.none(),
54
+ uid: Option.none(),
55
+ gid: Option.none(),
56
+ rdev: Option.none(),
57
+ size: ByteSize.bytes(encoder.encode(text).byteLength),
58
+ blksize: Option.none(),
59
+ blocks: Option.none()
60
+ });
61
+ /**
62
+ * Wrap `underlying` so `readFile`, `readFileString`, `exists` and `stat` answer
63
+ * from `documents` for open paths, and a non-recursive `readDirectory` also lists
64
+ * open documents whose parent is the directory read (so a brand-new unsaved file
65
+ * is walked). Every other member, and every path not open, is `underlying`'s.
66
+ *
67
+ * @public
68
+ */
69
+ const makeOverlayFileSystem = (underlying, documents) => FileSystem.FileSystem.of({
70
+ ...underlying,
71
+ readFile: (path) => Effect.flatMap(documents.get(path), Option.match({
72
+ onNone: () => underlying.readFile(path),
73
+ onSome: (document) => Effect.succeed(encoder.encode(document.text))
74
+ })),
75
+ readFileString: (path, encoding) => Effect.flatMap(documents.get(path), Option.match({
76
+ onNone: () => underlying.readFileString(path, encoding),
77
+ onSome: (document) => Effect.succeed(document.text)
78
+ })),
79
+ exists: (path) => Effect.flatMap(documents.get(path), Option.match({
80
+ onNone: () => underlying.exists(path),
81
+ onSome: () => Effect.succeed(true)
82
+ })),
83
+ stat: (path) => Effect.flatMap(documents.get(path), Option.match({
84
+ onNone: () => underlying.stat(path),
85
+ onSome: (document) => underlying.stat(path).pipe(Effect.map((info) => ({
86
+ ...info,
87
+ size: ByteSize.bytes(encoder.encode(document.text).byteLength)
88
+ })), Effect.catchTag("PlatformError", (error) => error.reason._tag === "NotFound" ? Effect.succeed(overlayInfo(document.text)) : Effect.fail(error)))
89
+ })),
90
+ readDirectory: (path, options) => options?.recursive === true ? underlying.readDirectory(path, options) : Effect.gen(function* () {
91
+ const names = yield* underlying.readDirectory(path, options);
92
+ const directory = trimTrailingSeparators(path);
93
+ const extra = (yield* documents.entries()).map(([key]) => parentAndName(key)).filter(([parent, name]) => parent === directory && !names.includes(name)).map(([, name]) => name);
94
+ return extra.length === 0 ? names : [...names, ...extra];
95
+ })
96
+ });
97
+ /**
98
+ * The ambient `FileSystem`, shadowed by the ambient `OverlayDocuments`.
99
+ *
100
+ * @public
101
+ */
102
+ const layerOverlayFileSystem = Layer.effect(FileSystem.FileSystem, Effect.gen(function* () {
103
+ const underlying = yield* FileSystem.FileSystem;
104
+ const documents = yield* OverlayDocuments;
105
+ return makeOverlayFileSystem(underlying, documents);
106
+ }));
107
+
108
+ //#endregion
109
+ export { OverlayDocuments, layerOverlayFileSystem, makeOverlayFileSystem };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okfit/engine",
3
- "version": "0.7.4",
3
+ "version": "0.8.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,21 +34,21 @@
34
34
  "./package.json": "./package.json"
35
35
  },
36
36
  "dependencies": {
37
- "@effect/platform-node": "4.0.0-rc.116",
38
- "@effected/app": "^0.17.0",
39
- "@effected/config-file": "^0.11.1",
40
- "@effected/git": "^0.16.0",
41
- "@effected/glob": "^0.7.0",
42
- "@effected/jsonc": "^0.12.0",
43
- "@effected/markdown": "^0.11.0",
44
- "@effected/store": "^0.9.0",
45
- "@effected/toml": "^0.8.0",
46
- "@effected/walker": "^0.11.0",
47
- "@effected/xdg": "^0.6.1",
48
- "@effected/yaml": "^0.16.0",
49
- "@okfit/core": "0.7.3",
50
- "@okfit/profiles": "0.7.4",
51
- "effect": "4.0.0-rc.116"
37
+ "@effect/platform-node": "4.0.0-rc.117",
38
+ "@effected/app": "^0.18.0",
39
+ "@effected/config-file": "^0.12.0",
40
+ "@effected/git": "^0.17.0",
41
+ "@effected/glob": "^0.8.0",
42
+ "@effected/jsonc": "^0.13.0",
43
+ "@effected/markdown": "^0.12.0",
44
+ "@effected/store": "^0.10.0",
45
+ "@effected/toml": "^0.9.0",
46
+ "@effected/walker": "^0.12.0",
47
+ "@effected/xdg": "^0.7.0",
48
+ "@effected/yaml": "^0.17.0",
49
+ "@okfit/core": "0.8.0",
50
+ "@okfit/profiles": "0.8.0",
51
+ "effect": "4.0.0-rc.117"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=24.11.0"
@@ -0,0 +1,67 @@
1
+ import { PROJECT_CONFIG_NAMES } from "../init/scaffold.js";
2
+ import { OverlayDocuments, makeOverlayFileSystem } from "../overlay/layer.js";
3
+ import { collect, sort } from "../render/sort.js";
4
+ import { run } from "../validate/run.js";
5
+ import { diffDiagnostics, groupByFile } from "./diff.js";
6
+ import { withFallbackRange } from "./range.js";
7
+ import { Graph } from "@okfit/core";
8
+ import { Context, Effect, FileSystem, Option, Path, Ref, Semaphore } from "effect";
9
+
10
+ //#region src/session/BundleSession.ts
11
+ const CONFIG_BASENAMES = new Set(PROJECT_CONFIG_NAMES.map((name) => name.slice(name.lastIndexOf("/") + 1)));
12
+ /**
13
+ * One bundle root's editor state (spec 4): open-document overlay, last loaded
14
+ * bundle and graph, and last published diagnostics per file. Debounce is NOT
15
+ * here: the caller (the LSP server) schedules revalidates; overlapping calls are
16
+ * serialized so every change is reported exactly once.
17
+ *
18
+ * @public
19
+ */
20
+ var BundleSession = class BundleSession extends Context.Service()("@okfit/engine/BundleSession") {
21
+ /** Build a session; the returned operations need no services. */
22
+ static make = (options) => Effect.gen(function* () {
23
+ const path = yield* Path.Path;
24
+ const underlying = yield* FileSystem.FileSystem;
25
+ const context = yield* Effect.context();
26
+ const root = path.resolve(options.root);
27
+ const overlay = OverlayDocuments.make();
28
+ const fileSystem = makeOverlayFileSystem(underlying, overlay);
29
+ const semaphore = yield* Semaphore.make(1);
30
+ const published = yield* Ref.make(/* @__PURE__ */ new Map());
31
+ const loaded = yield* Ref.make(Option.none());
32
+ const revalidateNow = Effect.fn("BundleSession.revalidate")(function* (revalidate) {
33
+ const result = yield* run({
34
+ root,
35
+ config: options.config,
36
+ profile: options.profile,
37
+ now: revalidate.now,
38
+ skipProvenance: revalidate.tier === "edit"
39
+ }).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideContext(context));
40
+ const rendered = sort(collect(result.report.conformance, result.report.lint, result.profileDiagnostics).map((diagnostic) => withFallbackRange(result.bundle, diagnostic)));
41
+ const next = groupByFile(rendered);
42
+ const changed = yield* Ref.modify(published, (previous) => [diffDiagnostics(previous, next), next]);
43
+ yield* Ref.set(loaded, Option.some({
44
+ bundle: result.bundle,
45
+ graph: Graph.fromBundle(result.bundle)
46
+ }));
47
+ return {
48
+ changed,
49
+ bundle: result.bundle
50
+ };
51
+ });
52
+ return BundleSession.of({
53
+ root,
54
+ open: (file, text, version) => overlay.open(path.resolve(file), text, version),
55
+ change: (file, text, version) => overlay.change(path.resolve(file), text, version),
56
+ close: (file) => overlay.close(path.resolve(file)),
57
+ watchedFilesChanged: (paths) => Effect.succeed({ configChanged: paths.some((file) => CONFIG_BASENAMES.has(path.basename(file))) }),
58
+ revalidate: (revalidate) => semaphore.withPermit(revalidateNow(revalidate)),
59
+ bundle: () => Effect.map(Ref.get(loaded), Option.map((state) => state.bundle)),
60
+ graph: () => Effect.map(Ref.get(loaded), Option.map((state) => state.graph)),
61
+ config: () => options.config
62
+ });
63
+ });
64
+ };
65
+
66
+ //#endregion
67
+ export { BundleSession };
@@ -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 };
@@ -0,0 +1,44 @@
1
+ //#region src/session/diff.ts
2
+ /**
3
+ * Group `diagnostics` by `file`, keeping input order within each file.
4
+ *
5
+ * @public
6
+ */
7
+ const groupByFile = (diagnostics) => {
8
+ const grouped = /* @__PURE__ */ new Map();
9
+ for (const diagnostic of diagnostics) {
10
+ const list = grouped.get(diagnostic.file);
11
+ if (list === void 0) grouped.set(diagnostic.file, [diagnostic]);
12
+ else list.push(diagnostic);
13
+ }
14
+ return grouped;
15
+ };
16
+ const fingerprint = (list) => list.map((d) => JSON.stringify([
17
+ d.source,
18
+ d.code,
19
+ d.severity,
20
+ d.message,
21
+ d.range?.offset ?? null,
22
+ d.range?.length ?? null,
23
+ d.range?.line ?? null,
24
+ d.range?.character ?? null
25
+ ])).join("\n");
26
+ /**
27
+ * The files whose diagnostic list differs between `previous` and `next`,
28
+ * structurally and in order, mapped to their `next` list; a file present only in
29
+ * `previous` maps to `[]` (its set became empty). Each diagnostic compares by
30
+ * source, code, severity, message and its range's offset, length, line and
31
+ * character: across an edit the text changes, so an unchanged offset can still
32
+ * sit on a different line.
33
+ *
34
+ * @public
35
+ */
36
+ const diffDiagnostics = (previous, next) => {
37
+ const changed = /* @__PURE__ */ new Map();
38
+ for (const [file, list] of next) if (fingerprint(previous.get(file) ?? []) !== fingerprint(list)) changed.set(file, list);
39
+ for (const file of previous.keys()) if (!next.has(file)) changed.set(file, []);
40
+ return changed;
41
+ };
42
+
43
+ //#endregion
44
+ export { diffDiagnostics, groupByFile };
@@ -0,0 +1,37 @@
1
+ import { conceptFor } from "./concept.js";
2
+ import { DiagnosticRange } from "@okfit/core";
3
+ import { Option } from "effect";
4
+
5
+ //#region src/session/range.ts
6
+ const FILE_START = DiagnosticRange.make({
7
+ offset: 0,
8
+ length: 0,
9
+ line: 0,
10
+ character: 0
11
+ });
12
+ /**
13
+ * Give a range-less diagnostic somewhere to point (spec 4.3): the concept's
14
+ * frontmatter block when the file loaded as a concept with frontmatter, else a
15
+ * zero-length range at line 0. Ranged and bundle-level (`file === ""`)
16
+ * diagnostics are returned unchanged. A file whose frontmatter failed to decode
17
+ * is not a loaded concept and falls to line 0; core ranges its own decode
18
+ * diagnostics already.
19
+ *
20
+ * @public
21
+ */
22
+ const withFallbackRange = (bundle, diagnostic) => {
23
+ if (diagnostic.range !== void 0 || diagnostic.file === "") return diagnostic;
24
+ const concept = Option.getOrUndefined(conceptFor(bundle, `${bundle.root}/${diagnostic.file}`));
25
+ const node = concept?.document.frontmatter;
26
+ if (concept === void 0 || node === void 0) return {
27
+ ...diagnostic,
28
+ range: FILE_START
29
+ };
30
+ return {
31
+ ...diagnostic,
32
+ range: DiagnosticRange.fromOffset(concept.document.source, node.position.start.offset, node.position.end.offset - node.position.start.offset)
33
+ };
34
+ };
35
+
36
+ //#endregion
37
+ export { withFallbackRange };
@@ -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/version.js CHANGED
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * @public
13
13
  */
14
- const ENGINE_VERSION = "0.7.4";
14
+ const ENGINE_VERSION = "0.8.0";
15
15
 
16
16
  //#endregion
17
17
  export { ENGINE_VERSION };