@okfit/profiles 0.8.3 → 0.10.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/Trust.js ADDED
@@ -0,0 +1,156 @@
1
+ import { Derivation } from "./Derivation.js";
2
+ import { Actor, Derive, Diagnostic, DiagnosticRange, OkfitConfig, Timestamp } from "@okfit/core";
3
+ import { DateTime, Effect, Schema } from "effect";
4
+
5
+ //#region src/Trust.ts
6
+ const encodeAt = Schema.encodeSync(Timestamp);
7
+ /**
8
+ * Why a human verification no longer binds: the content changed, the entry has
9
+ * no `integrity.verified` binding, a later human entry is not the bound one, or
10
+ * the bound actor fails a non-empty `[actors].humans` allow-list.
11
+ *
12
+ * @public
13
+ */
14
+ const DriftReason = Schema.Literals([
15
+ "content-changed",
16
+ "unbound",
17
+ "entry-superseded",
18
+ "actor-not-allowed"
19
+ ]);
20
+ /** Only `human:` entries bind; machine entries neither bind nor break a binding. */
21
+ const latestHumanOf = (entries) => entries.filter((entry) => Actor.isHuman(entry.by)).reduce((latest, entry) => latest === void 0 || DateTime.isGreaterThanOrEqualTo(entry.at, latest.at) ? entry : latest, void 0);
22
+ const latestHuman = (concept) => latestHumanOf(concept.frontmatter.verified ?? []);
23
+ const inputOf = (concept, config) => ({
24
+ verified: concept.frontmatter.verified ?? [],
25
+ binding: concept.frontmatter.integrity?.verified,
26
+ generated: concept.frontmatter.generated,
27
+ humans: config.actors?.humans ?? []
28
+ });
29
+ /** The binding when it names the latest human entry; `undefined` when none, unbound or superseded. */
30
+ const boundEntry = (input) => {
31
+ const latest = latestHumanOf(input.verified);
32
+ const binding = input.binding;
33
+ if (latest === void 0 || binding === void 0) return void 0;
34
+ return binding.by === latest.by && DateTime.Equivalence(binding.at, latest.at) ? binding : void 0;
35
+ };
36
+ const needsDigest = (input) => {
37
+ const bound = boundEntry(input);
38
+ return bound !== void 0 && Actor.allowed(bound.by, input.humans);
39
+ };
40
+ const evaluate = (input, content) => {
41
+ if (latestHumanOf(input.verified) === void 0) return { integrity: "none" };
42
+ const binding = input.binding;
43
+ if (binding === void 0) return {
44
+ integrity: "drifted",
45
+ drift: { reason: "unbound" }
46
+ };
47
+ if (boundEntry(input) === void 0) return {
48
+ integrity: "drifted",
49
+ drift: { reason: "entry-superseded" }
50
+ };
51
+ if (!Actor.allowed(binding.by, input.humans)) return {
52
+ integrity: "drifted",
53
+ drift: { reason: "actor-not-allowed" }
54
+ };
55
+ if (content === void 0) throw new Error("Trust.evaluate: a bound verification needs a content digest");
56
+ if (content === binding.content) return { integrity: "current" };
57
+ const generated = input.generated;
58
+ return {
59
+ integrity: "drifted",
60
+ drift: {
61
+ reason: "content-changed",
62
+ ...generated?.at !== void 0 && DateTime.isGreaterThan(generated.at, binding.at) ? {
63
+ editedBy: generated.by,
64
+ editedAt: encodeAt(generated.at)
65
+ } : {}
66
+ }
67
+ };
68
+ };
69
+ const signal = Effect.fn("Trust.signal")(function* (concept, config, now) {
70
+ const tier = Derive.trustTier(concept.frontmatter);
71
+ const stale = Derive.isStale(concept.frontmatter, now);
72
+ const input = inputOf(concept, config);
73
+ const content = needsDigest(input) ? yield* Derivation.contentDigest(concept.frontmatter.raw, concept.document.source) : void 0;
74
+ return {
75
+ tier,
76
+ stale,
77
+ ...evaluate(input, content)
78
+ };
79
+ });
80
+ const signals = Effect.fn("Trust.signals")(function* (bundle, config, now) {
81
+ const out = /* @__PURE__ */ new Map();
82
+ for (const [id, concept] of bundle.concepts) out.set(id, yield* signal(concept, config, now));
83
+ return out;
84
+ });
85
+ const messageFor = (concept, drift) => {
86
+ const latest = latestHuman(concept);
87
+ const by = latest?.by ?? "a human";
88
+ const at = latest === void 0 ? "an unknown time" : encodeAt(latest.at);
89
+ if (drift.reason === "unbound") return `verified by ${by} at ${at}, but no integrity.verified binding records what was verified; re-run okfit verify`;
90
+ if (drift.reason === "entry-superseded") return `the latest human verification (${by} at ${at}) is not the one integrity.verified binds; re-run okfit verify`;
91
+ const binding = concept.frontmatter.integrity?.verified;
92
+ if (drift.reason === "actor-not-allowed") return `verified by ${binding?.by ?? by}, who is not in [actors].humans; a listed human re-runs okfit verify`;
93
+ const edited = drift.editedBy === void 0 ? "" : ` (edited by ${drift.editedBy} at ${drift.editedAt})`;
94
+ return `content changed since ${binding?.by ?? by} verified it at ${binding === void 0 ? at : encodeAt(binding.at)}${edited}; re-run okfit verify`;
95
+ };
96
+ /**
97
+ * Where a `verified-drift` finding points (spec section 6): the binding when one exists,
98
+ * else the latest human `verified` entry -- by its list index when `verified` is a
99
+ * sequence, the key itself when it is a single bare mapping.
100
+ */
101
+ const rangePath = (concept) => {
102
+ if (concept.frontmatter.integrity?.verified !== void 0) return ["integrity", "verified"];
103
+ const latest = latestHuman(concept);
104
+ const index = latest === void 0 ? -1 : (concept.frontmatter.verified ?? []).indexOf(latest);
105
+ return Array.isArray(concept.frontmatter.raw.verified) && index >= 0 ? ["verified", index] : ["verified"];
106
+ };
107
+ const lint = Effect.fn("Trust.lint")(function* (bundle, config, now, given) {
108
+ const severity = OkfitConfig.severityFor(config, "verified-drift");
109
+ if (severity === "off") return [];
110
+ const diagnostics = [];
111
+ for (const [id, concept] of bundle.concepts) {
112
+ const status = Derive.status(concept.frontmatter);
113
+ if (status === "draft" || status === "deprecated") continue;
114
+ const result = given?.get(id) ?? (yield* signal(concept, config, now));
115
+ if (result.drift === void 0) continue;
116
+ const range = DiagnosticRange.forFrontmatterPath(concept.document, rangePath(concept));
117
+ diagnostics.push(Diagnostic.make({
118
+ file: concept.path,
119
+ code: "verified-drift",
120
+ severity,
121
+ message: messageFor(concept, result.drift),
122
+ ...range === void 0 ? {} : { range }
123
+ }));
124
+ }
125
+ return diagnostics;
126
+ });
127
+ /**
128
+ * The integrity trust signal: whether a human verification still binds the
129
+ * content it covered, and the `verified-drift` lint built on it. A signal, not
130
+ * a tamper seal. Named `Trust` so it does not collide with core's `Integrity`
131
+ * schema.
132
+ *
133
+ * @public
134
+ */
135
+ var Trust = class {
136
+ constructor() {}
137
+ /** The signal for one concept under `config`; hashes only when a human entry is bound by an allowed actor. */
138
+ static signal = signal;
139
+ /** The signal for every concept in a bundle, keyed by concept id. */
140
+ static signals = signals;
141
+ /** Pure integrity decision; no hashing. `content` may be `undefined` only when {@link Trust.needsDigest} is false. */
142
+ static evaluate = evaluate;
143
+ /** True iff the latest human entry is bound by `integrity.verified` and its actor passes the allow-list, i.e. a digest is needed. */
144
+ static needsDigest = needsDigest;
145
+ /** The {@link EvaluateInput} of a loaded concept. */
146
+ static inputOf = inputOf;
147
+ /**
148
+ * `verified-drift` diagnostics for drifted concepts (drafts and deprecated concepts are exempt: `okfit verify` refuses them); `[]` (no hashing) when the severity is `off`.
149
+ * Given `signals` (from {@link Trust.signals}) it reuses each concept's entry and hashes nothing for it;
150
+ * a concept absent from `signals` falls back to computing its own signal (hashing if it needs a digest).
151
+ */
152
+ static lint = lint;
153
+ };
154
+
155
+ //#endregion
156
+ export { DriftReason, Trust };
package/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Context, Crypto, DateTime, Effect, FileSystem, Layer, Option, Path, PlatformError, Schema } from "effect";
2
2
  import { Git, GitCommandError, NotARepositoryError, UnknownRefError } from "@effected/git";
3
- import { Actor, Diagnostic, DiagnosticRange, LoadedBundle, OkfitConfig } from "@okfit/core";
3
+ import { Actor, Diagnostic, DiagnosticRange, Digest, LoadedBundle, LoadedConcept, OkfitConfig, TrustTier } from "@okfit/core";
4
4
  import { ChildProcessSpawner } from "effect/process";
5
5
  //#region src/BodyProvenance.d.ts
6
6
  /**
@@ -60,6 +60,22 @@ export type BodyProvenance = typeof BodyProvenance.Type;
60
60
  */
61
61
  type UncommittedReason = typeof BodyUncommitted.fields.reason.Type;
62
62
  //#endregion
63
+ //#region src/Canonical.d.ts
64
+ /**
65
+ * Canonical encoding for okfit's content digest.
66
+ *
67
+ * @public
68
+ */
69
+ export declare class Canonical {
70
+ private constructor();
71
+ /** Top-level frontmatter keys excluded from the content digest. */
72
+ static readonly EXCLUDED_KEYS: ReadonlySet<string>;
73
+ /** Canonical JSON: sorted keys at every depth, array order kept; a `bigint` is its bare decimal literal. */
74
+ static readonly json: (value: unknown) => string;
75
+ /** The exact string a content digest hashes. */
76
+ static readonly contentInput: (raw: Readonly<Record<string, unknown>>, source: string) => string;
77
+ }
78
+ //#endregion
63
79
  //#region src/GitHistory.d.ts
64
80
  declare const PathHistoryEntry_base: Schema.Class<PathHistoryEntry, Schema.Struct<{
65
81
  readonly sha: Schema.String;
@@ -178,6 +194,31 @@ declare const AgentActorUnconfiguredError_base: Schema.Class<AgentActorUnconfigu
178
194
  export declare class AgentActorUnconfiguredError extends AgentActorUnconfiguredError_base {
179
195
  get message(): string;
180
196
  }
197
+ declare const ActorConfigInvalidError_base: Schema.Class<ActorConfigInvalidError, Schema.TaggedStruct<"ActorConfigInvalidError", {
198
+ readonly value: Schema.String;
199
+ }>, import("effect/Cause").YieldableError>;
200
+ /**
201
+ * `git config okfit.actor` is set but is not a `human:<id>` actor without `@`
202
+ * or glob characters. The value stays on the person's machine; okfit never
203
+ * writes it anywhere.
204
+ *
205
+ * @public
206
+ */
207
+ export declare class ActorConfigInvalidError extends ActorConfigInvalidError_base {
208
+ get message(): string;
209
+ }
210
+ declare const HumanActorNotAllowedError_base: Schema.Class<HumanActorNotAllowedError, Schema.TaggedStruct<"HumanActorNotAllowedError", {
211
+ readonly actor: Schema.String;
212
+ }>, import("effect/Cause").YieldableError>;
213
+ /**
214
+ * The resolved human is not on a non-empty `[actors].humans` allow-list, so it
215
+ * may not verify or revoke (spec section 8).
216
+ *
217
+ * @public
218
+ */
219
+ export declare class HumanActorNotAllowedError extends HumanActorNotAllowedError_base {
220
+ get message(): string;
221
+ }
181
222
  /**
182
223
  * Options for {@link Derivation.generatedAt} (P-39).
183
224
  *
@@ -214,7 +255,13 @@ interface GeneratedByOptions {
214
255
  *
215
256
  * @public
216
257
  */
217
- type GeneratedByError = AgentActorUnconfiguredError | HumanActorUnresolvedError | GitCommandError | NotARepositoryError | UnknownRefError;
258
+ type GeneratedByError = ActorConfigInvalidError | AgentActorUnconfiguredError | HumanActorUnresolvedError | GitCommandError | NotARepositoryError | UnknownRefError;
259
+ /**
260
+ * Everything {@link Derivation.verifier} can fail with.
261
+ *
262
+ * @public
263
+ */
264
+ type VerifierError = GeneratedByError | HumanActorNotAllowedError;
218
265
  /**
219
266
  * Trimmed `user.name` / `user.email` as `Git.configGet` answers them; a key is absent when git has no value.
220
267
  *
@@ -240,11 +287,32 @@ export declare class Derivation {
240
287
  */
241
288
  static readonly body: (text: string) => string;
242
289
  /**
243
- * Lowercase 64-char hex sha256 of `Derivation.body(text)`, through `effect`'s platform-agnostic
244
- * `Crypto` service (issue #19). Same normalization as `body`, so a frontmatter-only edit or a
245
- * `core.autocrlf` worktree never changes the digest.
290
+ * Full lowercase 64-char hex sha256 of a string. Tests only.
291
+ *
292
+ * @internal
246
293
  */
247
- static readonly bodyDigest: (text: string) => Effect.Effect<string, PlatformError.PlatformError, Crypto.Crypto>;
294
+ static readonly digestFull: (text: string) => Effect.Effect<string, PlatformError.PlatformError, Crypto.Crypto>;
295
+ /**
296
+ * The okfit digest of a string: the first 16 lowercase hex characters of its SHA-256, through
297
+ * `effect`'s platform-agnostic `Crypto` service.
298
+ */
299
+ static readonly digest: (text: string) => Effect.Effect<Digest, PlatformError.PlatformError, Crypto.Crypto>;
300
+ /**
301
+ * 16-hex digest of `Derivation.body(source)` (issue #19). Same normalization as `body`, so a
302
+ * frontmatter-only edit or a `core.autocrlf` worktree never changes the digest.
303
+ */
304
+ static readonly bodyDigest: (source: string) => Effect.Effect<Digest, PlatformError.PlatformError, Crypto.Crypto>;
305
+ /**
306
+ * The body digest recorded for a generated concept: `integrity.generated.body`, else the legacy
307
+ * `generated.body_sha256` cut to its first 16 hex characters. `undefined` when neither is present.
308
+ * Pure; every "is the stamped body still current" comparison reads through this.
309
+ */
310
+ static readonly recordedBody: (concept: LoadedConcept) => string | undefined;
311
+ /**
312
+ * 16-hex digest of the canonical content input (`Canonical.contentInput`): authored frontmatter
313
+ * plus the normalised body, insensitive to style, key order and CRLF.
314
+ */
315
+ static readonly contentDigest: (raw: Readonly<Record<string, unknown>>, source: string) => Effect.Effect<Digest, PlatformError.PlatformError, Crypto.Crypto>;
248
316
  /**
249
317
  * Spec 5.4 rule 2 as P-2 (DERIVE option C). Steps:
250
318
  * 1. `root = Git.repoRoot(dirname(file))`: a file outside any repository surfaces `NotARepositoryError`;
@@ -265,18 +333,30 @@ export declare class Derivation {
265
333
  static readonly generatedAt: (options: GeneratedAtOptions) => Effect.Effect<BodyProvenance, GeneratedAtError, Git | GitHistory | FileSystem.FileSystem | Path.Path>;
266
334
  /**
267
335
  * P-13 / P-14, pure. Candidates: the email local part (returned as written, lower-cased only for matching)
268
- * and the `user.name` slug. Order, first hit wins: (1) a `human:` entry of `humans` whose id equals a
269
- * candidate case-insensitively, returned verbatim; (2) `human:<local>`; (3) `human:<slug>`; (4) none.
336
+ * and the `user.name` slug. Order, first hit wins: (1) a literal `human:` entry of `humans` (glob entries,
337
+ * those with `*` or `?`, are skipped) whose id equals a candidate case-insensitively, returned verbatim; (2) `human:<local>`; (3) `human:<slug>`; (4) none.
270
338
  * The result is branded through core's `Actor` codec and always passes it.
271
339
  */
272
- static readonly humanActorId: (identity: GitIdentity, humans: ReadonlyArray<Actor>) => Option.Option<Actor>;
340
+ static readonly humanActorId: (identity: GitIdentity, humans: ReadonlyArray<string>) => Option.Option<Actor>;
273
341
  /**
274
342
  * Spec 5.4 rule 1 (P-16, P-45). `"agent"`: `config.actors.agent`, else {@link AgentActorUnconfiguredError}
275
- * (P-17). `"human"`: `Git.configGet(cwd, "user.name")` and `Git.configGet(cwd, "user.email")` in merged
343
+ * (P-17). `"human"`: first `Git.configGet(cwd, "okfit.actor")`, which wins when set and must be a
344
+ * `human:<id>` with no `@` or glob ({@link ActorConfigInvalidError} otherwise), returned in the spelling of an exact `[actors].humans` entry that equals it case-insensitively (P-14); else `Git.configGet(cwd, "user.name")` and `Git.configGet(cwd, "user.email")` in merged
276
345
  * scope (P-15) through {@link Derivation.humanActorId}; none is {@link HumanActorUnresolvedError}.
277
346
  * Answers only "who is writing now" (P-40).
278
347
  */
279
348
  static readonly generatedBy: (options: GeneratedByOptions) => Effect.Effect<Actor, GeneratedByError, Git>;
349
+ /**
350
+ * The human allowed to verify now: {@link Derivation.generatedBy} for a human
351
+ * writer, then the `[actors].humans` allow-list (`Actor.allowed`): empty or
352
+ * absent allows anyone, otherwise the actor must match an entry or this fails
353
+ * {@link HumanActorNotAllowedError}. `okfit verify`, `--revoke` and the LSP
354
+ * verify actions all resolve through this.
355
+ */
356
+ static readonly verifier: (options: {
357
+ readonly cwd: string;
358
+ readonly config: OkfitConfig;
359
+ }) => Effect.Effect<Actor, VerifierError, Git>;
280
360
  /**
281
361
  * P-19, pure: `from` plus `config.lifecycle.default_stale_after`, falling back to `OkfitConfig.DEFAULTS`'
282
362
  * 90 days. `from` is the caller's: `generated.at` when committed, else its own `now`.
@@ -354,6 +434,41 @@ export declare const ProfileDiagnostic: Schema.Struct<{
354
434
  * @public
355
435
  */
356
436
  export type ProfileDiagnostic = typeof ProfileDiagnostic.Type;
437
+ /**
438
+ * One Surface concept a docs preset writes: a bundle-relative `file` and the
439
+ * frontmatter and body to write there. `resource` is relative to the file.
440
+ * The `site` preset's `url` is an `https://example.invalid` placeholder the
441
+ * docs skill replaces.
442
+ *
443
+ * @public
444
+ */
445
+ interface SurfaceTemplate {
446
+ readonly file: string;
447
+ readonly frontmatter: {
448
+ readonly type: "Surface";
449
+ readonly status: "draft";
450
+ readonly title: string;
451
+ readonly description: string;
452
+ readonly kind: "site" | "repo" | "readme";
453
+ readonly audience: "users" | "contributors" | "agents";
454
+ readonly resource: string;
455
+ readonly links_to?: string;
456
+ readonly url?: string;
457
+ };
458
+ readonly body: string;
459
+ }
460
+ /**
461
+ * A named set of Surface templates for one repository shape. `additive` is
462
+ * true only for `site`, which layers on another preset instead of replacing it.
463
+ *
464
+ * @public
465
+ */
466
+ interface DocsPreset {
467
+ readonly name: "npm-package" | "monorepo-router" | "monorepo-shared-docs" | "site";
468
+ readonly description: string;
469
+ readonly additive: boolean;
470
+ readonly surfaces: ReadonlyArray<SurfaceTemplate>;
471
+ }
357
472
  /**
358
473
  * A named profile (P-38): a partial `OkfitConfig` the CLI merges as
359
474
  * `DEFAULTS < profile < file`, a scaffolding `Layout`, and a pure `check`
@@ -365,6 +480,7 @@ interface Profile {
365
480
  readonly name: ProfileName;
366
481
  readonly config: OkfitConfig;
367
482
  readonly layout: Layout;
483
+ readonly docsPresets: ReadonlyArray<DocsPreset>;
368
484
  readonly check: (bundle: LoadedBundle) => ReadonlyArray<ProfileDiagnostic>;
369
485
  }
370
486
  //#endregion
@@ -395,17 +511,20 @@ export declare class Profiles {
395
511
  * like every other lint (S-7). `Profile.check` and `ProfileDiagnosticCode`
396
512
  * are unchanged.
397
513
  *
398
- * Two tiers per concept (issue #19, body-digest design): when
399
- * `generated.body_sha256` is present, drift is decided ENTIRELY by comparing it to
514
+ * Two tiers per concept (issue #19, body-digest design): when a body digest
515
+ * is recorded (`integrity.generated.body`, else the 16-hex prefix of a legacy
516
+ * `generated.body_sha256`, via `Derivation.recordedBody`), drift is decided
517
+ * ENTIRELY by comparing it to
400
518
  * `Derivation.bodyDigest` of the concept's current source -- no git call at
401
519
  * all, so this works for a dirty worktree too (that is the point: an edited
402
520
  * body is caught immediately, before it is even committed) and survives a
403
521
  * squash/rebase merge that only rewrites commit dates, since the body
404
- * itself, and therefore its digest, never changed. When `body_sha256` is
405
- * absent (an un-migrated concept), this falls back to the original
522
+ * itself, and therefore its digest, never changed. When no digest is recorded
523
+ * at all (a concept `okfit sync` has never stamped), this falls back to the original
406
524
  * git-derived date comparison against `Derivation.generatedAt`, preserving
407
525
  * `--skip-provenance`'s meaning for bundles that have not run `okfit sync`
408
- * since this field was introduced.
526
+ * since the digest was introduced. The drift message names the field the
527
+ * recorded value came from, so a legacy concept reads `generated.body_sha256`.
409
528
  *
410
529
  * The tier-2 fallback returns `[]` the moment a `NotARepositoryError`
411
530
  * surfaces: a bundle outside git has nothing to derive for any concept still
@@ -423,8 +542,8 @@ export declare class Profiles {
423
542
  * calling at all; the two agree by construction (both skip the walk).
424
543
  *
425
544
  * A third, optional options argument carries `skipGitTier`: when `true`, a
426
- * concept that falls through to tier 2 (no `generated.body_sha256`
427
- * recorded) is skipped entirely -- no `Derivation.generatedAt` call, no
545
+ * concept that falls through to tier 2 (no body digest recorded) is
546
+ * skipped entirely -- no `Derivation.generatedAt` call, no
428
547
  * git spawn at all -- while tier 1 still runs, unaffected, for every
429
548
  * concept that DOES carry a digest. This is a runtime skip, not a
430
549
  * type-level narrowing: the R channel still lists `Git | GitHistory` even
@@ -442,5 +561,140 @@ export declare class Provenance {
442
561
  }) => Effect.Effect<ReadonlyArray<Diagnostic>, GitHistoryError | GitCommandError | UnknownRefError | PlatformError.PlatformError, Git | GitHistory | FileSystem.FileSystem | Path.Path | Crypto.Crypto>;
443
562
  }
444
563
  //#endregion
445
- export type { GeneratedAtError, GeneratedAtOptions, GeneratedByError, GeneratedByOptions, GitHistoryShape, GitIdentity, Layout, LayoutDirectory, PathLogOptions, Profile, ProfileName, UncommittedReason, Writer };
564
+ //#region src/Publications.d.ts
565
+ /**
566
+ * The concept type name of a documentation surface.
567
+ *
568
+ * @public
569
+ */
570
+ export declare const SURFACE_TYPE: "Surface";
571
+ /**
572
+ * The concept type name of a published page rendered from bundle concepts.
573
+ *
574
+ * @public
575
+ */
576
+ export declare const PUBLICATION_TYPE: "Publication";
577
+ /**
578
+ * One entry of a Publication's `renders` list, with its position.
579
+ *
580
+ * @public
581
+ */
582
+ interface RendersEntry {
583
+ readonly index: number;
584
+ readonly path: string;
585
+ readonly body?: string;
586
+ }
587
+ /**
588
+ * A profiles-owned lint over Publication concepts: `publication-orphan` when a
589
+ * `surface` or `renders[].path` does not resolve to a bundle concept (or
590
+ * `renders` is malformed), and `publication-drift` when a rendered source's
591
+ * body digest no longer matches the stamped `renders[].body`. Total over
592
+ * severity: returns `[]` before any digest work when both codes are `"off"`.
593
+ *
594
+ * @public
595
+ */
596
+ export declare class Publications {
597
+ private constructor();
598
+ /** Resolves a raw reference against a bundle-relative concept path; `""` when it escapes the bundle. */
599
+ static readonly resolveRef: (from: string, raw: string) => string;
600
+ /** The `renders` entries of a Publication, or `undefined` when malformed. */
601
+ static readonly rendersOf: (concept: LoadedConcept) => ReadonlyArray<RendersEntry> | undefined;
602
+ /** Lints every Publication in `bundle`; diagnostics are sorted by file then code. */
603
+ static readonly lint: (bundle: LoadedBundle, config: OkfitConfig) => Effect.Effect<ReadonlyArray<Diagnostic>, PlatformError.PlatformError, Crypto.Crypto>;
604
+ }
605
+ //#endregion
606
+ //#region src/Trust.d.ts
607
+ /**
608
+ * Whether a concept's human verification still covers its content: `none` (no
609
+ * `human:` entry in `verified`), `current` (the latest human entry is bound by
610
+ * `integrity.verified` and the content digest still matches), or `drifted`.
611
+ *
612
+ * @public
613
+ */
614
+ type IntegrityState = "current" | "drifted" | "none";
615
+ /**
616
+ * Why a human verification no longer binds: the content changed, the entry has
617
+ * no `integrity.verified` binding, a later human entry is not the bound one, or
618
+ * the bound actor fails a non-empty `[actors].humans` allow-list.
619
+ *
620
+ * @public
621
+ */
622
+ export declare const DriftReason: Schema.Literals<readonly ["content-changed", "unbound", "entry-superseded", "actor-not-allowed"]>;
623
+ /**
624
+ * The type of {@link (DriftReason:variable)}.
625
+ * @public
626
+ */
627
+ export type DriftReason = typeof DriftReason.Type;
628
+ /**
629
+ * The trust signal for one concept: its tier, whether its human verification
630
+ * still binds, and staleness. `drift` is present only when `integrity` is
631
+ * `drifted`.
632
+ *
633
+ * @public
634
+ */
635
+ interface ConceptSignal {
636
+ readonly tier: TrustTier;
637
+ readonly integrity: IntegrityState;
638
+ readonly drift?: {
639
+ readonly reason: DriftReason;
640
+ readonly editedBy?: Actor;
641
+ readonly editedAt?: string;
642
+ };
643
+ readonly stale: boolean;
644
+ }
645
+ /**
646
+ * What the integrity decision reads from a concept: its `verified` entries, the
647
+ * `integrity.verified` binding, and `generated`. Built by {@link Trust.inputOf}.
648
+ *
649
+ * @public
650
+ */
651
+ interface EvaluateInput {
652
+ readonly verified: ReadonlyArray<{
653
+ readonly by: Actor;
654
+ readonly at: DateTime.Utc;
655
+ }>;
656
+ readonly binding: {
657
+ readonly by: Actor;
658
+ readonly at: DateTime.Utc;
659
+ readonly content: string;
660
+ } | undefined;
661
+ readonly generated: {
662
+ readonly by: Actor;
663
+ readonly at?: DateTime.Utc;
664
+ } | undefined;
665
+ /** The `[actors].humans` allow-list; empty allows anyone. */
666
+ readonly humans: ReadonlyArray<string>;
667
+ }
668
+ /**
669
+ * The integrity trust signal: whether a human verification still binds the
670
+ * content it covered, and the `verified-drift` lint built on it. A signal, not
671
+ * a tamper seal. Named `Trust` so it does not collide with core's `Integrity`
672
+ * schema.
673
+ *
674
+ * @public
675
+ */
676
+ export declare class Trust {
677
+ private constructor();
678
+ /** The signal for one concept under `config`; hashes only when a human entry is bound by an allowed actor. */
679
+ static readonly signal: (concept: LoadedConcept, config: OkfitConfig, now: DateTime.Utc) => Effect.Effect<ConceptSignal, PlatformError.PlatformError, Crypto.Crypto>;
680
+ /** The signal for every concept in a bundle, keyed by concept id. */
681
+ static readonly signals: (bundle: LoadedBundle, config: OkfitConfig, now: DateTime.Utc) => Effect.Effect<Map<string, ConceptSignal>, PlatformError.PlatformError, Crypto.Crypto>;
682
+ /** Pure integrity decision; no hashing. `content` may be `undefined` only when {@link Trust.needsDigest} is false. */
683
+ static readonly evaluate: (input: EvaluateInput, content: string | undefined) => {
684
+ readonly integrity: IntegrityState;
685
+ readonly drift?: NonNullable<ConceptSignal["drift"]>;
686
+ };
687
+ /** True iff the latest human entry is bound by `integrity.verified` and its actor passes the allow-list, i.e. a digest is needed. */
688
+ static readonly needsDigest: (input: EvaluateInput) => boolean;
689
+ /** The {@link EvaluateInput} of a loaded concept. */
690
+ static readonly inputOf: (concept: LoadedConcept, config: OkfitConfig) => EvaluateInput;
691
+ /**
692
+ * `verified-drift` diagnostics for drifted concepts (drafts and deprecated concepts are exempt: `okfit verify` refuses them); `[]` (no hashing) when the severity is `off`.
693
+ * Given `signals` (from {@link Trust.signals}) it reuses each concept's entry and hashes nothing for it;
694
+ * a concept absent from `signals` falls back to computing its own signal (hashing if it needs a digest).
695
+ */
696
+ static readonly lint: (bundle: LoadedBundle, config: OkfitConfig, now: DateTime.Utc, signals?: ReadonlyMap<string, ConceptSignal>) => Effect.Effect<Array<Diagnostic>, PlatformError.PlatformError, Crypto.Crypto>;
697
+ }
698
+ //#endregion
699
+ export type { ConceptSignal, DocsPreset, EvaluateInput, GeneratedAtError, GeneratedAtOptions, GeneratedByError, GeneratedByOptions, GitHistoryShape, GitIdentity, IntegrityState, Layout, LayoutDirectory, PathLogOptions, Profile, ProfileName, RendersEntry, SurfaceTemplate, UncommittedReason, VerifierError, Writer };
446
700
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  import { BodyCommitted, BodyProvenance, BodyUncommitted } from "./BodyProvenance.js";
2
+ import { Canonical } from "./Canonical.js";
2
3
  import { GitHistory, GitHistoryError, PathHistoryEntry } from "./GitHistory.js";
3
- import { AgentActorUnconfiguredError, Derivation, HumanActorUnresolvedError } from "./Derivation.js";
4
+ import { ActorConfigInvalidError, AgentActorUnconfiguredError, Derivation, HumanActorNotAllowedError, HumanActorUnresolvedError } from "./Derivation.js";
4
5
  import { PROFILE_NAMES, ProfileDiagnostic, ProfileDiagnosticCode } from "./Profile.js";
5
6
  import { Profiles } from "./Profiles.js";
6
7
  import { Provenance } from "./Provenance.js";
8
+ import { PUBLICATION_TYPE, Publications, SURFACE_TYPE } from "./Publications.js";
9
+ import { DriftReason, Trust } from "./Trust.js";
7
10
 
8
- export { AgentActorUnconfiguredError, BodyCommitted, BodyProvenance, BodyUncommitted, Derivation, GitHistory, GitHistoryError, HumanActorUnresolvedError, PROFILE_NAMES, PathHistoryEntry, ProfileDiagnostic, ProfileDiagnosticCode, Profiles, Provenance };
11
+ export { ActorConfigInvalidError, AgentActorUnconfiguredError, BodyCommitted, BodyProvenance, BodyUncommitted, Canonical, Derivation, DriftReason, GitHistory, GitHistoryError, HumanActorNotAllowedError, HumanActorUnresolvedError, PROFILE_NAMES, PUBLICATION_TYPE, PathHistoryEntry, ProfileDiagnostic, ProfileDiagnosticCode, Profiles, Provenance, Publications, SURFACE_TYPE, Trust };
@@ -0,0 +1,13 @@
1
+ import { FrontmatterSource } from "@effected/markdown";
2
+
3
+ //#region src/internal/body.ts
4
+ const normalise = (text) => text.replace(/\r\n?/g, "\n").trimEnd();
5
+ /**
6
+ * The normalised body of a concept source (`FrontmatterSource.split(text).body`, then P-6 normalisation).
7
+ *
8
+ * @internal
9
+ */
10
+ const body = (text) => normalise(FrontmatterSource.split(text).body);
11
+
12
+ //#endregion
13
+ export { body };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okfit/profiles",
3
- "version": "0.8.3",
3
+ "version": "0.10.0",
4
4
  "private": false,
5
5
  "description": "Named OKF configuration profiles for okfit, starting with the software-project profile.",
6
6
  "keywords": [
@@ -35,9 +35,9 @@
35
35
  "./package.json": "./package.json"
36
36
  },
37
37
  "peerDependencies": {
38
- "@effected/git": "^0.20.0",
39
- "@effected/markdown": "^0.15.0",
40
- "@okfit/core": "^0.8.4",
38
+ "@effected/git": "^0.21.0",
39
+ "@effected/markdown": "^0.16.0",
40
+ "@okfit/core": "^0.10.0",
41
41
  "effect": "^4.0.0"
42
42
  },
43
43
  "engines": {
@@ -0,0 +1,51 @@
1
+ import { CONTRIBUTOR_GUIDES, DOCS_PACKAGE, DOCS_PACKAGES, DOCS_REPO, README_PACKAGE, README_PACKAGES, README_PACKAGES_ONE_PAGE, README_PACKAGE_ONE_PAGE, README_ROOT, README_ROOT_ONE_PAGE, SITE } from "./surfaces.js";
2
+
3
+ //#region src/presets/index.ts
4
+ /**
5
+ * The docs-surface presets the `software-project` profile offers, one per
6
+ * repository shape. Three replace a repository's surfaces; `site` is
7
+ * additive and swaps the README templates for one-page variants that link to
8
+ * the site.
9
+ *
10
+ * @public
11
+ */
12
+ const DOCS_PRESETS = [
13
+ {
14
+ name: "npm-package",
15
+ description: "A single package published to npm: a README surface that links to a docs/ folder surface.",
16
+ additive: false,
17
+ surfaces: [README_PACKAGE, DOCS_PACKAGE]
18
+ },
19
+ {
20
+ name: "monorepo-router",
21
+ description: "A monorepo whose root README routes to per-package READMEs.",
22
+ additive: false,
23
+ surfaces: [README_ROOT, README_PACKAGES]
24
+ },
25
+ {
26
+ name: "monorepo-shared-docs",
27
+ description: "A monorepo with routing READMEs plus a shared root docs/ folder, contributor guides and per-package docs/ folders read on the forge.",
28
+ additive: false,
29
+ surfaces: [
30
+ README_ROOT,
31
+ README_PACKAGES,
32
+ DOCS_REPO,
33
+ DOCS_PACKAGES,
34
+ CONTRIBUTOR_GUIDES
35
+ ]
36
+ },
37
+ {
38
+ name: "site",
39
+ description: "A documentation website plus one-page README variants that link to it; keep the variants that match the repository shape.",
40
+ additive: true,
41
+ surfaces: [
42
+ SITE,
43
+ README_ROOT_ONE_PAGE,
44
+ README_PACKAGES_ONE_PAGE,
45
+ README_PACKAGE_ONE_PAGE
46
+ ]
47
+ }
48
+ ];
49
+
50
+ //#endregion
51
+ export { DOCS_PRESETS };