@okfit/engine 0.9.4 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/errors.js +56 -6
- package/index.d.ts +409 -18
- package/index.js +6 -3
- package/package.json +18 -18
- package/query/ConceptQuery.js +102 -0
- package/render/query.js +104 -0
- package/render/verify.js +12 -1
- package/verify/run.js +127 -56
- package/verify/select.js +86 -0
- package/verify/splice.js +25 -1
- package/version.js +1 -1
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveBundleRoot, resolveProjectRoot } from "./config/anchor.js";
|
|
2
|
-
import { ConfigMalformedError, ConfigPathNotFoundError, DocumentPathError, InitOverwriteError, SyncStagedLogError, VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError } from "./errors.js";
|
|
2
|
+
import { ConfigMalformedError, ConfigPathNotFoundError, DocumentPathError, InitOverwriteError, QueryConceptNotFoundError, QuerySelectionError, QueryUnknownVocabularyError, 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";
|
|
@@ -10,6 +10,7 @@ import { CONFIG_RELATIVE_PATH, configValue, files, targetPaths } from "./init/sc
|
|
|
10
10
|
import { OverlayDocuments, layerOverlayFileSystem, makeOverlayFileSystem } from "./overlay/layer.js";
|
|
11
11
|
import { provideDocuments, resolveDocumentPath } from "./overlay/documents.js";
|
|
12
12
|
import { OkfitPlatform } from "./platform.js";
|
|
13
|
+
import { ConceptQuery } from "./query/ConceptQuery.js";
|
|
13
14
|
import { ContextEnvelope, ContextField, ContextFieldValue, ContextTag, ContextType, contextEnvelope } from "./render/context.js";
|
|
14
15
|
import { DistributionField } from "./render/distribution.js";
|
|
15
16
|
import { forDiagnostics, tally } from "./render/exit.js";
|
|
@@ -17,6 +18,7 @@ import { ENGINE_VERSION } from "./version.js";
|
|
|
17
18
|
import { GraphEdgeEnvelope, GraphEnvelope, GraphNodeEnvelope, GraphSummary, graphEnvelope } from "./render/graph.js";
|
|
18
19
|
import { collect, sort } from "./render/sort.js";
|
|
19
20
|
import { JsonDiagnostic, JsonEnvelope, JsonErrorEnvelope, JsonSummary, json, jsonError } from "./render/json.js";
|
|
21
|
+
import { ConceptSummary, QueryGetEnvelope, QueryLink, QueryListEnvelope, QueryNeighbor, QueryNeighborsEnvelope, queryGetEnvelope, queryListEnvelope, queryNeighborsEnvelope, toConceptSummary } from "./render/query.js";
|
|
20
22
|
import { StaleEnvelope, StaleItem, StaleSummary, staleEnvelope } from "./render/stale.js";
|
|
21
23
|
import { SkipReason, runSync } from "./sync/run.js";
|
|
22
24
|
import { SyncEnvelope, SyncModeEnvelope, syncEnvelope } from "./render/sync.js";
|
|
@@ -26,6 +28,7 @@ import { conceptFor } from "./session/concept.js";
|
|
|
26
28
|
import { withFallbackRange } from "./session/range.js";
|
|
27
29
|
import { BundleSession } from "./session/BundleSession.js";
|
|
28
30
|
import { runStale } from "./stale/run.js";
|
|
29
|
-
import {
|
|
31
|
+
import { resolveBatchTypes, selectAttestable, selectPickerCandidates } from "./verify/select.js";
|
|
32
|
+
import { loadPickerCandidates, runVerify, runVerifyBatch, runVerifyIds } from "./verify/run.js";
|
|
30
33
|
|
|
31
|
-
export { BundleSession, CONFIG_RELATIVE_PATH, 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, 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, makeOverlayFileSystem, provideConfig, provideDocuments, resolveBundleRoot, resolveDocumentPath, resolveProjectConfig, resolveProjectRoot, run, runContext, runGraph, runStale, runSync, runVerify, runVerifyBatch, sort, staleEnvelope, syncEnvelope, tally, targetPaths, verifyBatchEnvelope, verifyEnvelope, withFallbackRange };
|
|
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@okfit/engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The okfit engine: platform layer, config discovery, and the validate, verify, sync, init and context programs shared by the okfit CLI and MCP server.",
|
|
6
6
|
"keywords": [
|
|
@@ -34,23 +34,23 @@
|
|
|
34
34
|
"./package.json": "./package.json"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@effect/platform-node": "4.0.0
|
|
38
|
-
"@effected/app": "^0.
|
|
39
|
-
"@effected/config-file": "^0.
|
|
40
|
-
"@effected/engine": "^0.
|
|
41
|
-
"@effected/git": "^0.
|
|
42
|
-
"@effected/glob": "^0.
|
|
43
|
-
"@effected/jsonc": "^0.
|
|
44
|
-
"@effected/markdown": "^0.
|
|
45
|
-
"@effected/schemastore": "^0.
|
|
46
|
-
"@effected/store": "^0.
|
|
47
|
-
"@effected/toml": "^0.
|
|
48
|
-
"@effected/walker": "^0.
|
|
49
|
-
"@effected/xdg": "^0.
|
|
50
|
-
"@effected/yaml": "^0.
|
|
51
|
-
"@okfit/core": "0.8.
|
|
52
|
-
"@okfit/profiles": "0.8.
|
|
53
|
-
"effect": "4.0.0
|
|
37
|
+
"@effect/platform-node": "^4.0.0",
|
|
38
|
+
"@effected/app": "^0.20.0",
|
|
39
|
+
"@effected/config-file": "^0.14.0",
|
|
40
|
+
"@effected/engine": "^0.3.0",
|
|
41
|
+
"@effected/git": "^0.20.0",
|
|
42
|
+
"@effected/glob": "^0.10.0",
|
|
43
|
+
"@effected/jsonc": "^0.15.0",
|
|
44
|
+
"@effected/markdown": "^0.15.0",
|
|
45
|
+
"@effected/schemastore": "^0.18.0",
|
|
46
|
+
"@effected/store": "^0.12.0",
|
|
47
|
+
"@effected/toml": "^0.11.0",
|
|
48
|
+
"@effected/walker": "^0.15.0",
|
|
49
|
+
"@effected/xdg": "^0.9.0",
|
|
50
|
+
"@effected/yaml": "^0.19.0",
|
|
51
|
+
"@okfit/core": "0.8.4",
|
|
52
|
+
"@okfit/profiles": "0.8.3",
|
|
53
|
+
"effect": "^4.0.0"
|
|
54
54
|
},
|
|
55
55
|
"engines": {
|
|
56
56
|
"node": ">=24.11.0"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { QueryConceptNotFoundError, QueryUnknownVocabularyError } from "../errors.js";
|
|
2
|
+
import { ConceptId, Derive, Graph } from "@okfit/core";
|
|
3
|
+
import { Effect, Option } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/query/ConceptQuery.ts
|
|
6
|
+
const byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
7
|
+
const resolve = (bundle, rawId) => Effect.gen(function* () {
|
|
8
|
+
const normalized = ConceptId.normalize(rawId);
|
|
9
|
+
if (Option.isNone(normalized)) return yield* Effect.fail(new QueryConceptNotFoundError({
|
|
10
|
+
id: rawId,
|
|
11
|
+
reason: "empty-id"
|
|
12
|
+
}));
|
|
13
|
+
const id = normalized.value;
|
|
14
|
+
const concept = bundle.concepts.get(id);
|
|
15
|
+
if (concept === void 0) return yield* Effect.fail(new QueryConceptNotFoundError({
|
|
16
|
+
id: rawId,
|
|
17
|
+
reason: "not-a-concept"
|
|
18
|
+
}));
|
|
19
|
+
return {
|
|
20
|
+
id,
|
|
21
|
+
concept
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Read-only queries over a loaded bundle, shared by `okfit query` and the MCP
|
|
26
|
+
* tools.
|
|
27
|
+
*
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
const ConceptQuery = {
|
|
31
|
+
/** Validates types then tags against config (first unknown fails), then filters and sorts by id. */
|
|
32
|
+
list: (bundle, config, filter) => Effect.gen(function* () {
|
|
33
|
+
const declaredTypes = Object.keys(config.types ?? {}).toSorted();
|
|
34
|
+
const declaredTags = Object.keys(config.tags ?? {}).toSorted();
|
|
35
|
+
const types = filter.types ?? [];
|
|
36
|
+
for (const type of types) if (!declaredTypes.includes(type)) return yield* Effect.fail(new QueryUnknownVocabularyError({
|
|
37
|
+
kind: "type",
|
|
38
|
+
requested: type,
|
|
39
|
+
valid: declaredTypes
|
|
40
|
+
}));
|
|
41
|
+
const requestedTags = filter.tags ?? [];
|
|
42
|
+
for (const tag of requestedTags) if (!declaredTags.includes(tag)) return yield* Effect.fail(new QueryUnknownVocabularyError({
|
|
43
|
+
kind: "tag",
|
|
44
|
+
requested: tag,
|
|
45
|
+
valid: declaredTags
|
|
46
|
+
}));
|
|
47
|
+
const statuses = filter.statuses ?? [];
|
|
48
|
+
return [...bundle.concepts.values()].filter((concept) => {
|
|
49
|
+
if (types.length > 0 && !types.includes(concept.frontmatter.type)) return false;
|
|
50
|
+
const tags = concept.frontmatter.tags ?? [];
|
|
51
|
+
if (!requestedTags.every((tag) => tags.includes(tag))) return false;
|
|
52
|
+
if (statuses.length > 0 && !statuses.includes(Derive.status(concept.frontmatter))) return false;
|
|
53
|
+
if (filter.verified !== void 0) {
|
|
54
|
+
if ((concept.frontmatter.verified ?? []).length > 0 !== filter.verified) return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}).toSorted(byId);
|
|
58
|
+
}),
|
|
59
|
+
/** One concept by tolerant id, with its outgoing links. */
|
|
60
|
+
get: (bundle, id) => Effect.gen(function* () {
|
|
61
|
+
const resolved = yield* resolve(bundle, id);
|
|
62
|
+
const graph = Graph.fromBundle(bundle);
|
|
63
|
+
const links = graph.edges.filter((edge) => edge.from === resolved.id).map((edge) => ({
|
|
64
|
+
to: edge.to,
|
|
65
|
+
kind: Option.match(graph.node(edge.to), {
|
|
66
|
+
onNone: () => "missing",
|
|
67
|
+
onSome: (node) => node.kind
|
|
68
|
+
}),
|
|
69
|
+
source: edge.data.source,
|
|
70
|
+
...edge.data.field === void 0 ? {} : { field: edge.data.field }
|
|
71
|
+
}));
|
|
72
|
+
return {
|
|
73
|
+
concept: resolved.concept,
|
|
74
|
+
links
|
|
75
|
+
};
|
|
76
|
+
}),
|
|
77
|
+
/** Outgoing and incoming graph neighbours of one concept. */
|
|
78
|
+
neighbors: (bundle, id) => Effect.gen(function* () {
|
|
79
|
+
const resolved = yield* resolve(bundle, id);
|
|
80
|
+
const graph = Graph.fromBundle(bundle);
|
|
81
|
+
const project = (node) => {
|
|
82
|
+
if (node.kind !== "concept") return {
|
|
83
|
+
id: node.id,
|
|
84
|
+
kind: node.kind,
|
|
85
|
+
concept: null
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
id: node.id,
|
|
89
|
+
kind: node.kind,
|
|
90
|
+
concept: bundle.concepts.get(node.id) ?? null
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
id: resolved.id,
|
|
95
|
+
outgoing: graph.successors(resolved.id).map(project),
|
|
96
|
+
incoming: graph.predecessors(resolved.id).map(project)
|
|
97
|
+
};
|
|
98
|
+
})
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
//#endregion
|
|
102
|
+
export { ConceptQuery };
|
package/render/query.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { DistributionField } from "./distribution.js";
|
|
2
|
+
import { ENGINE_VERSION } from "../version.js";
|
|
3
|
+
import { Derive, GraphNodeKind, Status } from "@okfit/core";
|
|
4
|
+
import { Schema } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/render/query.ts
|
|
7
|
+
/** The seven-field shape every list-like query result carries (N-16). @public */
|
|
8
|
+
const ConceptSummary = Schema.Struct({
|
|
9
|
+
id: Schema.String,
|
|
10
|
+
type: Schema.String,
|
|
11
|
+
title: Schema.String,
|
|
12
|
+
description: Schema.NullOr(Schema.String),
|
|
13
|
+
status: Status,
|
|
14
|
+
tags: Schema.Array(Schema.String),
|
|
15
|
+
path: Schema.String
|
|
16
|
+
});
|
|
17
|
+
/** Project one loaded concept into its summary. @public */
|
|
18
|
+
const toConceptSummary = (concept) => ({
|
|
19
|
+
id: concept.id,
|
|
20
|
+
type: concept.frontmatter.type,
|
|
21
|
+
title: Derive.title(concept),
|
|
22
|
+
description: concept.frontmatter.description ?? null,
|
|
23
|
+
status: Derive.status(concept.frontmatter),
|
|
24
|
+
tags: [...concept.frontmatter.tags ?? []],
|
|
25
|
+
path: concept.path
|
|
26
|
+
});
|
|
27
|
+
/** `okfit query list --format json`'s envelope, schema 1, snake_case. @public */
|
|
28
|
+
const QueryListEnvelope = Schema.Struct({
|
|
29
|
+
schema: Schema.Literal(1),
|
|
30
|
+
okfit_version: Schema.String,
|
|
31
|
+
engine_version: Schema.String,
|
|
32
|
+
distribution: DistributionField,
|
|
33
|
+
total: Schema.Int,
|
|
34
|
+
items: Schema.Array(ConceptSummary)
|
|
35
|
+
});
|
|
36
|
+
/** One outgoing link of a concept, as `okfit query get` reports it. @public */
|
|
37
|
+
const QueryLink = Schema.Struct({
|
|
38
|
+
to: Schema.String,
|
|
39
|
+
kind: GraphNodeKind,
|
|
40
|
+
source: Schema.Literals(["body", "frontmatter"]),
|
|
41
|
+
field: Schema.optionalKey(Schema.String)
|
|
42
|
+
});
|
|
43
|
+
/** `okfit query get --format json`'s envelope, schema 1, snake_case. @public */
|
|
44
|
+
const QueryGetEnvelope = Schema.Struct({
|
|
45
|
+
schema: Schema.Literal(1),
|
|
46
|
+
okfit_version: Schema.String,
|
|
47
|
+
engine_version: Schema.String,
|
|
48
|
+
distribution: DistributionField,
|
|
49
|
+
concept: Schema.Struct({
|
|
50
|
+
...ConceptSummary.fields,
|
|
51
|
+
frontmatter: Schema.Record(Schema.String, Schema.Unknown),
|
|
52
|
+
links: Schema.Array(QueryLink)
|
|
53
|
+
})
|
|
54
|
+
});
|
|
55
|
+
/** One neighbour of a concept; `summary` is `null` for a non-concept node. @public */
|
|
56
|
+
const QueryNeighbor = Schema.Struct({
|
|
57
|
+
id: Schema.String,
|
|
58
|
+
kind: GraphNodeKind,
|
|
59
|
+
summary: Schema.NullOr(ConceptSummary)
|
|
60
|
+
});
|
|
61
|
+
/** `okfit query neighbors --format json`'s envelope, schema 1, snake_case. @public */
|
|
62
|
+
const QueryNeighborsEnvelope = Schema.Struct({
|
|
63
|
+
schema: Schema.Literal(1),
|
|
64
|
+
okfit_version: Schema.String,
|
|
65
|
+
engine_version: Schema.String,
|
|
66
|
+
distribution: DistributionField,
|
|
67
|
+
id: Schema.String,
|
|
68
|
+
outgoing: Schema.Array(QueryNeighbor),
|
|
69
|
+
incoming: Schema.Array(QueryNeighbor)
|
|
70
|
+
});
|
|
71
|
+
/** Build the list envelope; `items` is the already-paged, already-sorted summaries. @public */
|
|
72
|
+
const queryListEnvelope = (input) => ({
|
|
73
|
+
schema: 1,
|
|
74
|
+
okfit_version: input.okfitVersion,
|
|
75
|
+
engine_version: ENGINE_VERSION,
|
|
76
|
+
distribution: input.distribution ?? null,
|
|
77
|
+
total: input.total,
|
|
78
|
+
items: input.items
|
|
79
|
+
});
|
|
80
|
+
/** Build the get envelope from a summary, the raw frontmatter and the outgoing links. @public */
|
|
81
|
+
const queryGetEnvelope = (input) => ({
|
|
82
|
+
schema: 1,
|
|
83
|
+
okfit_version: input.okfitVersion,
|
|
84
|
+
engine_version: ENGINE_VERSION,
|
|
85
|
+
distribution: input.distribution ?? null,
|
|
86
|
+
concept: {
|
|
87
|
+
...input.concept,
|
|
88
|
+
frontmatter: input.frontmatter,
|
|
89
|
+
links: input.links
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
/** Build the neighbors envelope. @public */
|
|
93
|
+
const queryNeighborsEnvelope = (input) => ({
|
|
94
|
+
schema: 1,
|
|
95
|
+
okfit_version: input.okfitVersion,
|
|
96
|
+
engine_version: ENGINE_VERSION,
|
|
97
|
+
distribution: input.distribution ?? null,
|
|
98
|
+
id: input.id,
|
|
99
|
+
outgoing: input.outgoing,
|
|
100
|
+
incoming: input.incoming
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
export { ConceptSummary, QueryGetEnvelope, QueryLink, QueryListEnvelope, QueryNeighbor, QueryNeighborsEnvelope, queryGetEnvelope, queryListEnvelope, queryNeighborsEnvelope, toConceptSummary };
|
package/render/verify.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DistributionField } from "./distribution.js";
|
|
2
2
|
import { ENGINE_VERSION } from "../version.js";
|
|
3
|
+
import { Status } from "@okfit/core";
|
|
3
4
|
import { Schema } from "effect";
|
|
4
5
|
|
|
5
6
|
//#region src/render/verify.ts
|
|
@@ -23,6 +24,11 @@ const VerifyEnvelope = Schema.Struct({
|
|
|
23
24
|
by: Schema.String,
|
|
24
25
|
at: Schema.String
|
|
25
26
|
}),
|
|
27
|
+
/** Issue #185: the status change `--stable`/`--draft` made; `null` when neither flag was given. */
|
|
28
|
+
status: Schema.NullOr(Schema.Struct({
|
|
29
|
+
from: Schema.NullOr(Status),
|
|
30
|
+
to: Schema.Literals(["stable", "draft"])
|
|
31
|
+
})),
|
|
26
32
|
dry_run: Schema.Boolean,
|
|
27
33
|
exit_code: Schema.Literal(0)
|
|
28
34
|
});
|
|
@@ -45,6 +51,7 @@ const verifyEnvelope = (input) => ({
|
|
|
45
51
|
by: input.by,
|
|
46
52
|
at: input.at
|
|
47
53
|
},
|
|
54
|
+
status: input.status,
|
|
48
55
|
dry_run: input.dryRun,
|
|
49
56
|
exit_code: 0
|
|
50
57
|
});
|
|
@@ -69,7 +76,11 @@ const VerifyBatchEnvelope = Schema.Struct({
|
|
|
69
76
|
})),
|
|
70
77
|
skipped: Schema.Array(Schema.Struct({
|
|
71
78
|
id: Schema.String,
|
|
72
|
-
reason: Schema.Literals([
|
|
79
|
+
reason: Schema.Literals([
|
|
80
|
+
"draft",
|
|
81
|
+
"deprecated",
|
|
82
|
+
"already-verified"
|
|
83
|
+
])
|
|
73
84
|
})),
|
|
74
85
|
dry_run: Schema.Boolean,
|
|
75
86
|
exit_code: Schema.Literal(0)
|
package/verify/run.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { VerifyConceptNotFoundError,
|
|
2
|
-
import { documentNewline, locate, stripBom } from "./locate.js";
|
|
3
|
-
import { splice } from "./splice.js";
|
|
1
|
+
import { VerifyConceptNotFoundError, VerifyUnsupportedFrontmatterError } from "../errors.js";
|
|
2
|
+
import { documentNewline, locate, locateTopLevelScalar, stripBom } from "./locate.js";
|
|
3
|
+
import { mergeSameOffset, splice, spliceTopLevelScalar } from "./splice.js";
|
|
4
|
+
import { resolveBatchTypes, selectAttestable, selectPickerCandidates } from "./select.js";
|
|
4
5
|
import { Bundle, ConceptId, Timestamp } from "@okfit/core";
|
|
5
6
|
import { Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
6
7
|
import { Derivation } from "@okfit/profiles";
|
|
@@ -24,25 +25,39 @@ const UNDECODABLE_CODES = /* @__PURE__ */ new Set([
|
|
|
24
25
|
"type-missing"
|
|
25
26
|
]);
|
|
26
27
|
/** Steps 9-14 of runVerify: read, locate (fail closed), splice; no write. */
|
|
27
|
-
const prepareVerify = Effect.fn("okfit/verify/prepareVerify")(function* (bundle, concept, actor, at) {
|
|
28
|
+
const prepareVerify = Effect.fn("okfit/verify/prepareVerify")(function* (bundle, concept, actor, at, status) {
|
|
28
29
|
const fs = yield* FileSystem.FileSystem;
|
|
29
30
|
const absolutePath = (yield* Path.Path).join(bundle.root, concept.path);
|
|
30
|
-
const source = yield* fs.
|
|
31
|
+
const source = new TextDecoder("utf-8", { ignoreBOM: true }).decode(yield* fs.readFile(absolutePath));
|
|
31
32
|
const { text, bom } = stripBom(source);
|
|
32
33
|
const located = yield* locate(text);
|
|
33
34
|
if (located._tag === "unsupported") return yield* new VerifyUnsupportedFrontmatterError({
|
|
34
35
|
id: concept.id,
|
|
35
|
-
shape: located.shape
|
|
36
|
+
shape: located.shape,
|
|
37
|
+
key: "verified"
|
|
36
38
|
});
|
|
37
39
|
const priorAt = (concept.frontmatter.verified ?? []).filter((entry) => entry.by === actor).map((entry) => Schema.encodeSync(Timestamp)(entry.at));
|
|
38
|
-
const
|
|
40
|
+
const newline = documentNewline(text);
|
|
41
|
+
const verifiedEdit = splice(located, {
|
|
39
42
|
by: actor,
|
|
40
43
|
at
|
|
41
|
-
},
|
|
44
|
+
}, newline);
|
|
45
|
+
let statusEdit;
|
|
46
|
+
if (status !== void 0 && concept.frontmatter.status !== status) {
|
|
47
|
+
const target = yield* locateTopLevelScalar(text, "status");
|
|
48
|
+
if (target._tag === "unsupported") return yield* new VerifyUnsupportedFrontmatterError({
|
|
49
|
+
id: concept.id,
|
|
50
|
+
shape: target.shape,
|
|
51
|
+
key: "status"
|
|
52
|
+
});
|
|
53
|
+
statusEdit = spliceTopLevelScalar(target, "status", status, newline);
|
|
54
|
+
}
|
|
55
|
+
const edits = mergeSameOffset([verifiedEdit, ...statusEdit === void 0 ? [] : [statusEdit]].toSorted((a, b) => a.offset - b.offset || (a === statusEdit ? -1 : b === statusEdit ? 1 : 0)));
|
|
42
56
|
return {
|
|
43
57
|
absolutePath,
|
|
44
|
-
finalText: bom + MarkdownEdit.applyAll(text,
|
|
45
|
-
fragment:
|
|
58
|
+
finalText: bom + MarkdownEdit.applyAll(text, edits),
|
|
59
|
+
fragment: verifiedEdit.content,
|
|
60
|
+
statusFragment: statusEdit?.content ?? null,
|
|
46
61
|
priorAt
|
|
47
62
|
};
|
|
48
63
|
});
|
|
@@ -56,21 +71,11 @@ const writeVerified = Effect.fn("okfit/verify/writeVerified")(function* (absolut
|
|
|
56
71
|
yield* fs.chmod(tempPath, original.mode);
|
|
57
72
|
yield* fs.rename(tempPath, target).pipe(Effect.onError(() => fs.remove(tempPath, { force: true }).pipe(Effect.ignore)));
|
|
58
73
|
});
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
* frontmatter and writes it back atomically.
|
|
63
|
-
*
|
|
64
|
-
* Nothing here reads `status`, `stale_after`, or a type's
|
|
65
|
-
* `require_verified`: `verify` is mechanical, not config-aware (V-4, V-5).
|
|
66
|
-
*
|
|
67
|
-
* @internal
|
|
68
|
-
*/
|
|
69
|
-
const runVerify = Effect.fn("okfit/verify/runVerify")(function* (options) {
|
|
70
|
-
const bundle = yield* Bundle.load({ root: options.bundleRoot });
|
|
71
|
-
const normalized = ConceptId.normalize(options.id);
|
|
74
|
+
/** Steps 7a-7d of runVerify, shared with `runVerifyIds`: tolerant id, reserved file, located concept. */
|
|
75
|
+
const resolveConcept = (bundle, rawId) => Effect.gen(function* () {
|
|
76
|
+
const normalized = ConceptId.normalize(rawId);
|
|
72
77
|
if (Option.isNone(normalized)) return yield* new VerifyConceptNotFoundError({
|
|
73
|
-
id:
|
|
78
|
+
id: rawId,
|
|
74
79
|
root: bundle.root,
|
|
75
80
|
reason: "not-a-concept"
|
|
76
81
|
});
|
|
@@ -93,13 +98,32 @@ const runVerify = Effect.fn("okfit/verify/runVerify")(function* (options) {
|
|
|
93
98
|
}
|
|
94
99
|
});
|
|
95
100
|
}
|
|
101
|
+
return {
|
|
102
|
+
id,
|
|
103
|
+
concept
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
/**
|
|
107
|
+
* Contract §2.3 steps 6–15. Loads the bundle, resolves the id, resolves the
|
|
108
|
+
* human actor from git, splices exactly one entry into the concept's
|
|
109
|
+
* frontmatter and writes it back atomically.
|
|
110
|
+
*
|
|
111
|
+
* Nothing here reads `stale_after` or a type's `require_verified`; `status`
|
|
112
|
+
* is read only to skip a no-op edit under `options.status` (#185). `verify`
|
|
113
|
+
* is mechanical, not config-aware (V-4, V-5).
|
|
114
|
+
*
|
|
115
|
+
* @internal
|
|
116
|
+
*/
|
|
117
|
+
const runVerify = Effect.fn("okfit/verify/runVerify")(function* (options) {
|
|
118
|
+
const bundle = yield* Bundle.load({ root: options.bundleRoot });
|
|
119
|
+
const { id, concept } = yield* resolveConcept(bundle, options.id);
|
|
96
120
|
const actor = yield* Derivation.generatedBy({
|
|
97
121
|
writer: "human",
|
|
98
122
|
cwd: options.projectRoot,
|
|
99
123
|
config: options.config
|
|
100
124
|
});
|
|
101
125
|
const at = Schema.encodeSync(Timestamp)(options.at);
|
|
102
|
-
const prepared = yield* prepareVerify(bundle, concept, actor, at);
|
|
126
|
+
const prepared = yield* prepareVerify(bundle, concept, actor, at, options.status);
|
|
103
127
|
if (!options.dryRun) yield* writeVerified(prepared.absolutePath, prepared.finalText);
|
|
104
128
|
return {
|
|
105
129
|
id,
|
|
@@ -109,13 +133,18 @@ const runVerify = Effect.fn("okfit/verify/runVerify")(function* (options) {
|
|
|
109
133
|
at,
|
|
110
134
|
priorAt: prepared.priorAt,
|
|
111
135
|
dryRun: options.dryRun,
|
|
112
|
-
fragment: prepared.fragment
|
|
136
|
+
fragment: prepared.fragment,
|
|
137
|
+
status: options.status === void 0 ? null : {
|
|
138
|
+
from: concept.frontmatter.status ?? null,
|
|
139
|
+
to: options.status
|
|
140
|
+
},
|
|
141
|
+
statusFragment: prepared.statusFragment
|
|
113
142
|
};
|
|
114
143
|
});
|
|
115
144
|
/**
|
|
116
145
|
* Issue #138. Attests every concept selected by `options.types` (or, when
|
|
117
146
|
* empty, every type whose declaration sets `require_verified = true`) that
|
|
118
|
-
* is not a draft and does not already carry a `verified` entry by the
|
|
147
|
+
* is not a draft, is not deprecated (#143) and does not already carry a `verified` entry by the
|
|
119
148
|
* resolved actor. Every candidate is located FIRST -- an unsupported
|
|
120
149
|
* `verified` shape on any one of them fails the whole batch with nothing
|
|
121
150
|
* written, mirroring `runVerify`'s own fail-closed rule at batch scale.
|
|
@@ -124,43 +153,66 @@ const runVerify = Effect.fn("okfit/verify/runVerify")(function* (options) {
|
|
|
124
153
|
*/
|
|
125
154
|
const runVerifyBatch = Effect.fn("okfit/verify/runVerifyBatch")(function* (options) {
|
|
126
155
|
const bundle = yield* Bundle.load({ root: options.bundleRoot });
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
156
|
+
const types = yield* resolveBatchTypes(options.config, options.types);
|
|
157
|
+
const actor = yield* Derivation.generatedBy({
|
|
158
|
+
writer: "human",
|
|
159
|
+
cwd: options.projectRoot,
|
|
160
|
+
config: options.config
|
|
161
|
+
});
|
|
162
|
+
const at = Schema.encodeSync(Timestamp)(options.at);
|
|
163
|
+
const selection = selectAttestable(bundle, types, actor);
|
|
164
|
+
const prepared = [];
|
|
165
|
+
for (const concept of selection.candidates) prepared.push({
|
|
166
|
+
id: concept.id,
|
|
167
|
+
conceptPath: concept.path,
|
|
168
|
+
...yield* prepareVerify(bundle, concept, actor, at, void 0)
|
|
169
|
+
});
|
|
170
|
+
if (!options.dryRun) for (const entry of prepared) yield* writeVerified(entry.absolutePath, entry.finalText);
|
|
171
|
+
return {
|
|
172
|
+
bundleRoot: bundle.root,
|
|
173
|
+
by: actor,
|
|
174
|
+
at,
|
|
175
|
+
dryRun: options.dryRun,
|
|
176
|
+
verified: prepared.map(({ id, conceptPath, fragment }) => ({
|
|
177
|
+
id,
|
|
178
|
+
conceptPath,
|
|
179
|
+
fragment
|
|
180
|
+
})),
|
|
181
|
+
skipped: selection.skipped
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
/**
|
|
185
|
+
* Issue #214. Attests an explicit list of concept ids in one all-or-nothing
|
|
186
|
+
* batch. Every id is resolved exactly as {@link runVerify} resolves it, and
|
|
187
|
+
* every concept is located and spliced BEFORE any file is written, so one
|
|
188
|
+
* unknown id, reserved file or unsupported `verified`/`status` shape fails the
|
|
189
|
+
* whole call with the tree untouched. An explicit pick is never second-guessed:
|
|
190
|
+
* drafts, deprecated and already-attested concepts are all attested, and
|
|
191
|
+
* `skipped` is always empty. Duplicate ids (after normalisation) are written
|
|
192
|
+
* once. With `promote`, a draft's `status` becomes `stable` in the same write.
|
|
193
|
+
*
|
|
194
|
+
* @public
|
|
195
|
+
*/
|
|
196
|
+
const runVerifyIds = Effect.fn("okfit/verify/runVerifyIds")(function* (options) {
|
|
197
|
+
const bundle = yield* Bundle.load({ root: options.bundleRoot });
|
|
136
198
|
const actor = yield* Derivation.generatedBy({
|
|
137
199
|
writer: "human",
|
|
138
200
|
cwd: options.projectRoot,
|
|
139
201
|
config: options.config
|
|
140
202
|
});
|
|
141
203
|
const at = Schema.encodeSync(Timestamp)(options.at);
|
|
142
|
-
const
|
|
204
|
+
const concepts = /* @__PURE__ */ new Map();
|
|
205
|
+
for (const rawId of options.ids) {
|
|
206
|
+
const { id, concept } = yield* resolveConcept(bundle, rawId);
|
|
207
|
+
concepts.set(id, concept);
|
|
208
|
+
}
|
|
143
209
|
const prepared = [];
|
|
144
|
-
for (const [id, concept] of
|
|
145
|
-
|
|
146
|
-
if (concept.frontmatter.status === "draft") {
|
|
147
|
-
skipped.push({
|
|
148
|
-
id,
|
|
149
|
-
reason: "draft"
|
|
150
|
-
});
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
if ((concept.frontmatter.verified ?? []).some((entry) => entry.by === actor)) {
|
|
154
|
-
skipped.push({
|
|
155
|
-
id,
|
|
156
|
-
reason: "already-verified"
|
|
157
|
-
});
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
210
|
+
for (const [id, concept] of concepts) {
|
|
211
|
+
const status = options.promote === true && concept.frontmatter.status === "draft" ? "stable" : void 0;
|
|
160
212
|
prepared.push({
|
|
161
213
|
id,
|
|
162
214
|
conceptPath: concept.path,
|
|
163
|
-
...yield* prepareVerify(bundle, concept, actor, at)
|
|
215
|
+
...yield* prepareVerify(bundle, concept, actor, at, status)
|
|
164
216
|
});
|
|
165
217
|
}
|
|
166
218
|
if (!options.dryRun) for (const entry of prepared) yield* writeVerified(entry.absolutePath, entry.finalText);
|
|
@@ -174,9 +226,28 @@ const runVerifyBatch = Effect.fn("okfit/verify/runVerifyBatch")(function* (optio
|
|
|
174
226
|
conceptPath,
|
|
175
227
|
fragment
|
|
176
228
|
})),
|
|
177
|
-
skipped
|
|
229
|
+
skipped: []
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
/**
|
|
233
|
+
* The interactive verify picker's starting point: loads the bundle, resolves
|
|
234
|
+
* the human actor from git exactly as {@link runVerify} does, and selects the
|
|
235
|
+
* rows that actor may still attest. Reads only; nothing is written.
|
|
236
|
+
*
|
|
237
|
+
* @public
|
|
238
|
+
*/
|
|
239
|
+
const loadPickerCandidates = Effect.fn("okfit/verify/loadPickerCandidates")(function* (options) {
|
|
240
|
+
const bundle = yield* Bundle.load({ root: options.bundleRoot });
|
|
241
|
+
const by = yield* Derivation.generatedBy({
|
|
242
|
+
writer: "human",
|
|
243
|
+
cwd: options.projectRoot,
|
|
244
|
+
config: options.config
|
|
245
|
+
});
|
|
246
|
+
return {
|
|
247
|
+
by,
|
|
248
|
+
candidates: selectPickerCandidates(bundle, options.config, by)
|
|
178
249
|
};
|
|
179
250
|
});
|
|
180
251
|
|
|
181
252
|
//#endregion
|
|
182
|
-
export { runVerify, runVerifyBatch };
|
|
253
|
+
export { loadPickerCandidates, runVerify, runVerifyBatch, runVerifyIds };
|