@okfit/engine 0.11.1 → 0.12.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
2
  import { splice, spliceTopLevelScalar } from "../verify/splice.js";
3
- import { Effect, Runtime, Schema } from "effect";
4
- import { MarkdownEdit } from "@effected/markdown";
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
  /**
@@ -70,6 +71,61 @@ var FrontmatterEdits = class {
70
71
  const edit = splice(target, entry, documentNewline(text));
71
72
  return [shift(edit, bom.length)];
72
73
  });
74
+ /**
75
+ * Edits that stamp `renders[i].body_sha256` with `digests[i]`: an existing value is replaced in
76
+ * place, a missing key is inserted on the line after `renders[i].path` at that key's column.
77
+ * Block-style `renders` only; flow style fails with `UnsupportedFrontmatterError`.
78
+ */
79
+ static rendersDigests = (source, digests) => Effect.gen(function* () {
80
+ const { text, bom } = stripBom(source);
81
+ const unsupported = (shape) => new UnsupportedFrontmatterError({
82
+ key: "renders",
83
+ shape
84
+ });
85
+ const parsed = MarkdownDocument.parseResult(text, { frontmatter: true });
86
+ if (Result.isFailure(parsed)) return yield* unsupported("unparseable");
87
+ const document = parsed.success;
88
+ const block = DiagnosticRange.forFrontmatterPath(document, []);
89
+ const rendersSpan = DiagnosticRange.forFrontmatterPath(document, ["renders"]);
90
+ if (block === void 0 || rendersSpan === void 0 || rendersSpan.offset === block.offset) return yield* unsupported("absent");
91
+ if (text.charAt(rendersSpan.offset) === "[") return yield* unsupported("flow-sequence");
92
+ const newline = documentNewline(text);
93
+ const edits = [];
94
+ for (const [index, digest] of digests.entries()) {
95
+ const digestSpan = DiagnosticRange.forFrontmatterPath(document, [
96
+ "renders",
97
+ index,
98
+ "body_sha256"
99
+ ]);
100
+ if (digestSpan !== void 0 && digestSpan.offset !== block.offset) {
101
+ const content = digestSpan.length === 0 ? ` ${digest}` : digest;
102
+ edits.push(shift(MarkdownEdit.make({
103
+ offset: digestSpan.offset,
104
+ length: digestSpan.length,
105
+ content
106
+ }), bom.length));
107
+ continue;
108
+ }
109
+ const pathSpan = DiagnosticRange.forFrontmatterPath(document, [
110
+ "renders",
111
+ index,
112
+ "path"
113
+ ]);
114
+ if (pathSpan === void 0 || pathSpan.offset === block.offset) return yield* unsupported("missing-path");
115
+ const lineStart = text.lastIndexOf("\n", pathSpan.offset - 1) + 1;
116
+ const prefix = /^[ ]*(?:-[ ]+)*(?:"path"|'path'|path)[ ]*:[ ]*$/u.exec(text.slice(lineStart, pathSpan.offset));
117
+ if (prefix === null) return yield* unsupported("flow-mapping");
118
+ const column = /^[ ]*(?:-[ ]+)*/u.exec(prefix[0])?.[0].length ?? 0;
119
+ const newlineAt = text.indexOf("\n", pathSpan.offset + pathSpan.length);
120
+ const lineEnd = newlineAt === -1 ? text.length : newlineAt > 0 && text.charAt(newlineAt - 1) === "\r" ? newlineAt - 1 : newlineAt;
121
+ edits.push(shift(MarkdownEdit.make({
122
+ offset: lineEnd,
123
+ length: 0,
124
+ content: `${newline}${" ".repeat(column)}body_sha256: ${digest}`
125
+ }), bom.length));
126
+ }
127
+ return edits;
128
+ });
73
129
  };
74
130
 
75
131
  //#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,12 @@ 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 stamp `renders[i].body_sha256` with `digests[i]`: an existing value is replaced in
484
+ * place, a missing key is inserted on the line after `renders[i].path` at that key's column.
485
+ * Block-style `renders` only; flow style fails with `UnsupportedFrontmatterError`.
486
+ */
487
+ static readonly rendersDigests: (source: string, digests: ReadonlyArray<string>) => Effect.Effect<ReadonlyArray<MarkdownEdit>, YamlParseError | UnsupportedFrontmatterError>;
469
488
  }
470
489
  //#endregion
471
490
  //#region src/external/ExternalReferences.d.ts
@@ -1480,6 +1499,22 @@ export declare const SyncModeEnvelope: Schema.Struct<{
1480
1499
  }>;
1481
1500
  /** @public */
1482
1501
  export type SyncModeEnvelope = typeof SyncModeEnvelope.Type;
1502
+ /**
1503
+ * One `okfit sync --publication` result: the Publication id, whether its
1504
+ * file changed, and the digest stamped for each rendered source.
1505
+ *
1506
+ * @public
1507
+ */
1508
+ export declare const PublicationEnvelope: Schema.Struct<{
1509
+ readonly id: Schema.String;
1510
+ readonly written: Schema.Boolean;
1511
+ readonly digests: Schema.$Array<Schema.Struct<{
1512
+ readonly path: Schema.String;
1513
+ readonly body_sha256: Schema.String;
1514
+ }>>;
1515
+ }>;
1516
+ /** @public */
1517
+ export type PublicationEnvelope = typeof PublicationEnvelope.Type;
1483
1518
  /**
1484
1519
  * Contract §14 note 5: `schema: 1` is INFERRED, not in the design's own
1485
1520
  * §2 JSON example — every other envelope in this codebase
@@ -1529,6 +1564,15 @@ export declare const SyncEnvelope: Schema.Struct<{
1529
1564
  readonly reason: Schema.Literals<readonly ["untracked", "dirty", "unborn", "generated-missing", "generated-unsupported", "log-unparseable"]>;
1530
1565
  }>>;
1531
1566
  }>;
1567
+ /** Present only for `okfit sync --publication`; then the three modes are all `selected: false`. */
1568
+ readonly publication: Schema.optionalKey<Schema.Struct<{
1569
+ readonly id: Schema.String;
1570
+ readonly written: Schema.Boolean;
1571
+ readonly digests: Schema.$Array<Schema.Struct<{
1572
+ readonly path: Schema.String;
1573
+ readonly body_sha256: Schema.String;
1574
+ }>>;
1575
+ }>>;
1532
1576
  }>;
1533
1577
  /** @public */
1534
1578
  export type SyncEnvelope = typeof SyncEnvelope.Type;
@@ -1543,7 +1587,8 @@ export declare const syncEnvelope: (input: {
1543
1587
  readonly okfitVersion: string;
1544
1588
  readonly root: string;
1545
1589
  readonly dryRun: boolean;
1546
- readonly result: SyncResult;
1590
+ readonly result?: SyncResult;
1591
+ readonly publication?: PublicationEnvelope;
1547
1592
  readonly distribution?: Distribution;
1548
1593
  }) => SyncEnvelope;
1549
1594
  //#endregion
@@ -1853,6 +1898,49 @@ interface StaleRunResult {
1853
1898
  */
1854
1899
  export declare const runStale: (options: StaleRunOptions) => Effect.Effect<StaleRunResult, BundleLoadError, FileSystem.FileSystem | Path.Path>;
1855
1900
  //#endregion
1901
+ //#region src/sync/publication.d.ts
1902
+ declare const PublicationNotFoundError_base: Schema.Class<PublicationNotFoundError, Schema.TaggedStruct<"PublicationNotFoundError", {
1903
+ readonly id: Schema.String;
1904
+ readonly publication: Schema.optionalKey<Schema.String>;
1905
+ }>, import("effect/Cause").YieldableError>;
1906
+ /**
1907
+ * No concept answers to `id` -- or one of a Publication's `renders` paths
1908
+ * resolves to no concept, in which case `id` names that missing source.
1909
+ *
1910
+ * @public
1911
+ */
1912
+ export declare class PublicationNotFoundError extends PublicationNotFoundError_base {
1913
+ readonly [Runtime.errorExitCode] = 64;
1914
+ get message(): string;
1915
+ }
1916
+ declare const NotAPublicationError_base: Schema.Class<NotAPublicationError, Schema.TaggedStruct<"NotAPublicationError", {
1917
+ readonly id: Schema.String;
1918
+ readonly type: Schema.String;
1919
+ }>, import("effect/Cause").YieldableError>;
1920
+ /**
1921
+ * The concept at `id` exists but is not a Publication (`type` is what it is).
1922
+ *
1923
+ * @public
1924
+ */
1925
+ export declare class NotAPublicationError extends NotAPublicationError_base {
1926
+ readonly [Runtime.errorExitCode] = 64;
1927
+ get message(): string;
1928
+ }
1929
+ /**
1930
+ * Restamps one Publication's `renders[].body_sha256` digests from the current
1931
+ * body of each rendered source concept, writing the Publication file
1932
+ * atomically unless `dryRun`. `written` is whether the text changed (so a
1933
+ * `dryRun` that would change the file reports `true` without writing).
1934
+ * A malformed `renders` fails `UnsupportedFrontmatterError`.
1935
+ *
1936
+ * @public
1937
+ */
1938
+ export declare const stampPublication: (options: {
1939
+ readonly bundleRoot: string;
1940
+ readonly id: string;
1941
+ readonly dryRun: boolean;
1942
+ }) => Effect.Effect<PublicationEnvelope, PublicationNotFoundError | NotAPublicationError | BundleLoadError | YamlParseError | UnsupportedFrontmatterError | PlatformError.PlatformError, FileSystem.FileSystem | Path.Path | Crypto.Crypto>;
1943
+ //#endregion
1856
1944
  //#region src/validate/run.d.ts
1857
1945
  declare const Now_base: Context.ServiceClass<Now, "@okfit/engine/Now", DateTime.Utc>;
1858
1946
  /**
@@ -1920,6 +2008,10 @@ interface RunResult {
1920
2008
  * the same `report.lint` array every other core lint diagnostic already
1921
2009
  * does.
1922
2010
  *
2011
+ * It then appends `lintResources`, `Publications.lint` (`publication-drift`
2012
+ * and `publication-orphan`, only under the `software-project` profile) and `lintSurfaces` (`surface-unmatched`), each
2013
+ * of which gates itself on its own severity.
2014
+ *
1923
2015
  * K-52 holds structurally: `Bundle.load`'s failure short-circuits the
1924
2016
  * generator, so `profile.check` never runs on a bundle that did not load.
1925
2017
  *
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.12.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": [
@@ -48,8 +48,8 @@
48
48
  "@effected/walker": "^0.15.0",
49
49
  "@effected/xdg": "^0.9.0",
50
50
  "@effected/yaml": "^0.19.0",
51
- "@okfit/core": "0.8.5",
52
- "@okfit/profiles": "0.8.3",
51
+ "@okfit/core": "0.9.0",
52
+ "@okfit/profiles": "0.9.0",
53
53
  "effect": "^4.0.0"
54
54
  },
55
55
  "engines": {
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/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
  /**
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.12.0";
15
15
 
16
16
  //#endregion
17
17
  export { ENGINE_VERSION };