@okfit/engine 0.11.1 → 0.13.0

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