@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/Canonical.js +68 -0
- package/Derivation.js +86 -15
- package/Provenance.js +14 -9
- package/Publications.js +135 -0
- package/README.md +52 -4
- package/SoftwareProject.js +67 -1
- package/Trust.js +156 -0
- package/index.d.ts +272 -18
- package/index.js +5 -2
- package/internal/body.js +13 -0
- package/package.json +4 -4
- package/presets/index.js +51 -0
- package/presets/surfaces.js +318 -0
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
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
290
|
+
* Full lowercase 64-char hex sha256 of a string. Tests only.
|
|
291
|
+
*
|
|
292
|
+
* @internal
|
|
246
293
|
*/
|
|
247
|
-
static readonly
|
|
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`
|
|
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<
|
|
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, "
|
|
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.
|
|
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
|
|
405
|
-
*
|
|
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
|
|
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
|
|
427
|
-
*
|
|
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
|
-
|
|
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 };
|
package/internal/body.js
ADDED
|
@@ -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.
|
|
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.
|
|
39
|
-
"@effected/markdown": "^0.
|
|
40
|
-
"@okfit/core": "^0.
|
|
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": {
|
package/presets/index.js
ADDED
|
@@ -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 };
|