@yansirplus/cli 0.5.17

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.
Files changed (47) hide show
  1. package/PUBLIC_API.md +22 -0
  2. package/README.md +34 -0
  3. package/dist/build/agent-authoring/config.d.ts +177 -0
  4. package/dist/build/agent-authoring/config.js +607 -0
  5. package/dist/build/agent-authoring/manifest-compiler.d.ts +159 -0
  6. package/dist/build/agent-authoring/manifest-compiler.js +737 -0
  7. package/dist/build/agent-authoring/shared.d.ts +10 -0
  8. package/dist/build/agent-authoring/shared.js +57 -0
  9. package/dist/build/agent-authoring/static-target.d.ts +59 -0
  10. package/dist/build/agent-authoring/static-target.js +1857 -0
  11. package/dist/build/agent-authoring.d.ts +9 -0
  12. package/dist/build/agent-authoring.js +5 -0
  13. package/dist/build/build-cli.d.ts +2 -0
  14. package/dist/build/build-cli.js +264 -0
  15. package/dist/check/algorithmic/architecture-checks.mjs +971 -0
  16. package/dist/check/algorithmic/client-boundary-checks.mjs +337 -0
  17. package/dist/check/algorithmic/convergence-smoke-checks.mjs +608 -0
  18. package/dist/check/algorithmic/distribution-checks.mjs +919 -0
  19. package/dist/check/algorithmic/owner-checks.mjs +647 -0
  20. package/dist/check/algorithmic/package-boundary-checks.mjs +985 -0
  21. package/dist/check/algorithmic/projection-boundary-checks.mjs +302 -0
  22. package/dist/check/algorithmic/repo-surface-checks.mjs +267 -0
  23. package/dist/check/algorithmic/runtime-structural-checks.mjs +264 -0
  24. package/dist/check/algorithmic/source-alias-checks.mjs +106 -0
  25. package/dist/check/algorithmic/static-target-checks.mjs +447 -0
  26. package/dist/check/algorithmic-checks.mjs +482 -0
  27. package/dist/check/check-coverage.mjs +231 -0
  28. package/dist/check/command-runner.mjs +22 -0
  29. package/dist/check/default-gate.mjs +51 -0
  30. package/dist/check/gate-selector.mjs +305 -0
  31. package/dist/check/manifest-rules.mjs +223 -0
  32. package/dist/check/package-graph.mjs +464 -0
  33. package/dist/generate/generate-agent-docs.mjs +435 -0
  34. package/dist/generate/generate-carrier-reference.mjs +514 -0
  35. package/dist/generate/generate-docs.mjs +345 -0
  36. package/dist/generate/generate-effect-skill-manifests.mjs +193 -0
  37. package/dist/generate/project-docs-site.mjs +190 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.js +25 -0
  40. package/dist/lib/agent-docs-model.mjs +888 -0
  41. package/dist/lib/boundary-rules.mjs +63 -0
  42. package/dist/lib/capability-routes.mjs +354 -0
  43. package/dist/lib/projection-sink.mjs +113 -0
  44. package/dist/lib/public-api-model.mjs +306 -0
  45. package/dist/main.mjs +233 -0
  46. package/dist/runner.mjs +127 -0
  47. package/package.json +32 -0
@@ -0,0 +1,63 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
5
+
6
+ export const readBoundaryRulesSource = (root, failures = []) => {
7
+ const file = "docs/agent/boundary-rules.source.json";
8
+ try {
9
+ const value = JSON.parse(fs.readFileSync(path.join(root, file), "utf8"));
10
+ if (!isRecord(value)) {
11
+ failures.push(`${file}: boundary rules source must be an object`);
12
+ return null;
13
+ }
14
+ return value;
15
+ } catch (cause) {
16
+ const reason = cause instanceof Error ? cause.message : String(cause);
17
+ failures.push(`${file}: unable to read boundary rules source: ${reason}`);
18
+ return null;
19
+ }
20
+ };
21
+
22
+ export const commandGroupContainsRule = (source, groupId, ruleId, seen = new Set()) => {
23
+ if (!isRecord(source.commandGroups) || seen.has(groupId)) return false;
24
+ const steps = source.commandGroups[groupId];
25
+ if (!Array.isArray(steps)) return false;
26
+ seen.add(groupId);
27
+ return steps.some((step) => {
28
+ if (!isRecord(step)) return false;
29
+ if (step.type === "rule") return step.id === ruleId;
30
+ if (step.type === "group" && typeof step.id === "string") {
31
+ return commandGroupContainsRule(source, step.id, ruleId, seen);
32
+ }
33
+ return false;
34
+ });
35
+ };
36
+
37
+ export const collectBoundaryRuleMembershipFailures = (root, specs) => {
38
+ const failures = [];
39
+ const source = readBoundaryRulesSource(root, failures);
40
+ if (source === null) return failures;
41
+
42
+ const rules = Array.isArray(source.rules) ? source.rules : [];
43
+ for (const spec of specs) {
44
+ const rule = rules.find((entry) => isRecord(entry) && entry.id === spec.ruleId);
45
+ if (rule === undefined) {
46
+ failures.push(`docs/agent/boundary-rules.source.json: missing boundary rule ${spec.ruleId}`);
47
+ continue;
48
+ }
49
+ if (spec.commandGroup !== undefined && rule.commandGroup !== spec.commandGroup) {
50
+ failures.push(
51
+ `docs/agent/boundary-rules.source.json: ${spec.ruleId} must declare commandGroup ${spec.commandGroup}`,
52
+ );
53
+ }
54
+ for (const groupId of spec.reachableFrom ?? []) {
55
+ if (!commandGroupContainsRule(source, groupId, spec.ruleId)) {
56
+ failures.push(
57
+ `docs/agent/boundary-rules.source.json: ${groupId} must include boundary rule ${spec.ruleId}`,
58
+ );
59
+ }
60
+ }
61
+ }
62
+ return failures;
63
+ };
@@ -0,0 +1,354 @@
1
+ export const generatedCapabilityRuleFields = new Set([
2
+ "allowedPrimitivePackages",
3
+ "coordinationCapabilityKind",
4
+ "coordinationPackage",
5
+ "coverage",
6
+ "docs",
7
+ "invariants",
8
+ "sourceFactOwners",
9
+ "testEvidence",
10
+ ]);
11
+
12
+ const unique = (values) => [...new Set(values)].sort((left, right) => left.localeCompare(right));
13
+
14
+ const coordinationCapabilityKinds = new Set(["composer", "facade", "profile", "projection"]);
15
+
16
+ const consumerFacingCapabilityKinds = new Set(["composer", "facade", "profile"]);
17
+
18
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
19
+
20
+ const stringArray = (value) =>
21
+ Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry.length > 0);
22
+
23
+ const authoredCapabilityRuleSourceFields = new Set(["schemaVersion", "rules"]);
24
+
25
+ const resolvePrefixOwner = (prefix, namespaceOwners) => {
26
+ const candidates = namespaceOwners.filter(
27
+ (owner) => prefix.startsWith(owner.prefix) || owner.prefix.startsWith(prefix),
28
+ );
29
+ const owners = unique(candidates.map((candidate) => candidate.owner));
30
+ if (owners.length !== 1) {
31
+ return { ok: false, owners, candidates };
32
+ }
33
+ return {
34
+ ok: true,
35
+ owner: owners[0],
36
+ declarations: candidates
37
+ .filter((candidate) => candidate.owner === owners[0])
38
+ .map((candidate) => ({
39
+ prefix: candidate.prefix,
40
+ owner: candidate.owner,
41
+ filePath: candidate.filePath,
42
+ })),
43
+ };
44
+ };
45
+
46
+ const primitiveEvidence = (primitive) => {
47
+ if (primitive.testEvidence?.tests !== undefined) {
48
+ return {
49
+ primitive: primitive.id,
50
+ tests: primitive.testEvidence.tests,
51
+ };
52
+ }
53
+ return {
54
+ primitive: primitive.id,
55
+ noTestReason: primitive.testEvidence?.noTestReason ?? "missing evidence source",
56
+ };
57
+ };
58
+
59
+ export const buildCapabilityRouteProjection = ({
60
+ source,
61
+ recipes = [],
62
+ primitives,
63
+ invariants,
64
+ rootScripts,
65
+ namespaceOwners,
66
+ }) => {
67
+ const failures = [];
68
+ const primitiveById = new Map(primitives.map((primitive) => [primitive.id, primitive]));
69
+ const invariantById = new Map(invariants.map((invariant) => [invariant.id, invariant]));
70
+ const scriptNames = new Set(Object.keys(rootScripts));
71
+
72
+ if (!isObject(source)) {
73
+ return { failures: ["capability rules source must be an object"], routes: [] };
74
+ }
75
+ for (const field of Object.keys(source)) {
76
+ if (!authoredCapabilityRuleSourceFields.has(field)) {
77
+ failures.push(`capability rules source must not author field ${field}`);
78
+ }
79
+ }
80
+ if (source.schemaVersion !== 1) failures.push("capability rules schemaVersion must be 1");
81
+ if (!Array.isArray(source.rules)) failures.push("capability rules source must contain rules[]");
82
+
83
+ const rules = Array.isArray(source.rules) ? source.rules : [];
84
+ const seenPrimitives = new Set();
85
+ const routes = [];
86
+
87
+ for (const [index, rule] of rules.entries()) {
88
+ const owner =
89
+ isObject(rule) && typeof rule.primitive === "string" ? rule.primitive : `rule[${index}]`;
90
+
91
+ if (!isObject(rule)) {
92
+ failures.push(`${owner} must be an object`);
93
+ continue;
94
+ }
95
+
96
+ for (const field of Object.keys(rule)) {
97
+ if (generatedCapabilityRuleFields.has(field)) {
98
+ failures.push(`${owner} must not author generated field ${field}`);
99
+ }
100
+ }
101
+
102
+ const required = [
103
+ "primitive",
104
+ "intents",
105
+ "sourceFactPrefixes",
106
+ "allowedPrimitives",
107
+ "forbiddenWrites",
108
+ "gates",
109
+ ];
110
+ for (const field of required) {
111
+ if (!(field in rule)) failures.push(`${owner} missing ${field}`);
112
+ }
113
+
114
+ if (typeof rule.primitive !== "string" || rule.primitive.length === 0) {
115
+ failures.push(`${owner} primitive must be a non-empty string`);
116
+ continue;
117
+ }
118
+ if (seenPrimitives.has(rule.primitive))
119
+ failures.push(`duplicate capability rule ${rule.primitive}`);
120
+ seenPrimitives.add(rule.primitive);
121
+
122
+ const primitive = primitiveById.get(rule.primitive);
123
+ if (primitive === undefined) {
124
+ failures.push(`${rule.primitive} references unknown primitive`);
125
+ }
126
+
127
+ if (!stringArray(rule.intents) || rule.intents.length === 0) {
128
+ failures.push(`${rule.primitive} intents must be a non-empty string array`);
129
+ }
130
+ if (!stringArray(rule.sourceFactPrefixes) || rule.sourceFactPrefixes.length === 0) {
131
+ failures.push(`${rule.primitive} sourceFactPrefixes must be a non-empty string array`);
132
+ }
133
+ if (!stringArray(rule.allowedPrimitives) || rule.allowedPrimitives.length === 0) {
134
+ failures.push(`${rule.primitive} allowedPrimitives must be a non-empty string array`);
135
+ } else if (!rule.allowedPrimitives.includes(rule.primitive)) {
136
+ failures.push(`${rule.primitive} allowedPrimitives must include its coordination primitive`);
137
+ }
138
+ if (!Array.isArray(rule.forbiddenWrites)) {
139
+ failures.push(`${rule.primitive} forbiddenWrites must be an array`);
140
+ }
141
+ if (!stringArray(rule.gates) || rule.gates.length === 0) {
142
+ failures.push(`${rule.primitive} gates must be a non-empty boundary gate array`);
143
+ }
144
+
145
+ const allowedPrimitives = stringArray(rule.allowedPrimitives) ? rule.allowedPrimitives : [];
146
+ const allowedPrimitiveRecords = [];
147
+ for (const allowedPrimitive of allowedPrimitives) {
148
+ const record = primitiveById.get(allowedPrimitive);
149
+ if (record === undefined) {
150
+ failures.push(
151
+ `${rule.primitive} allowedPrimitives references unknown primitive ${allowedPrimitive}`,
152
+ );
153
+ } else {
154
+ allowedPrimitiveRecords.push(record);
155
+ }
156
+ }
157
+
158
+ const sourceFactOwners = [];
159
+ for (const prefix of stringArray(rule.sourceFactPrefixes) ? rule.sourceFactPrefixes : []) {
160
+ const resolved = resolvePrefixOwner(prefix, namespaceOwners);
161
+ if (!resolved.ok) {
162
+ failures.push(
163
+ `${rule.primitive} sourceFactPrefixes ${JSON.stringify(prefix)} must resolve to exactly one owner; observed ${JSON.stringify(resolved.owners)}`,
164
+ );
165
+ } else {
166
+ sourceFactOwners.push({
167
+ prefix,
168
+ owner: resolved.owner,
169
+ declarations: resolved.declarations,
170
+ });
171
+ }
172
+ }
173
+
174
+ const forbiddenWrites = Array.isArray(rule.forbiddenWrites) ? rule.forbiddenWrites : [];
175
+ for (const [writeIndex, write] of forbiddenWrites.entries()) {
176
+ const writeOwner = `${rule.primitive} forbiddenWrites[${writeIndex}]`;
177
+ if (!isObject(write)) {
178
+ failures.push(`${writeOwner} must be an object`);
179
+ continue;
180
+ }
181
+ for (const field of ["actor", "action", "target", "invariant"]) {
182
+ if (!(field in write)) failures.push(`${writeOwner} missing ${field}`);
183
+ }
184
+ if (typeof write.actor !== "string" || write.actor.length === 0) {
185
+ failures.push(`${writeOwner} actor must be a non-empty string`);
186
+ }
187
+ if (typeof write.action !== "string" || write.action.length === 0) {
188
+ failures.push(`${writeOwner} action must be a non-empty string`);
189
+ }
190
+ if (!isObject(write.target)) {
191
+ failures.push(`${writeOwner} target must be an object`);
192
+ } else {
193
+ if (!["eventPrefix", "surface", "material"].includes(write.target.kind)) {
194
+ failures.push(`${writeOwner} target.kind must be eventPrefix, surface, or material`);
195
+ }
196
+ if (typeof write.target.value !== "string" || write.target.value.length === 0) {
197
+ failures.push(`${writeOwner} target.value must be a non-empty string`);
198
+ }
199
+ if (write.target.kind === "eventPrefix" && typeof write.target.value === "string") {
200
+ const resolved = resolvePrefixOwner(write.target.value, namespaceOwners);
201
+ if (!resolved.ok) {
202
+ failures.push(
203
+ `${writeOwner} target ${JSON.stringify(write.target.value)} must resolve to exactly one owner; observed ${JSON.stringify(resolved.owners)}`,
204
+ );
205
+ }
206
+ }
207
+ }
208
+ if (typeof write.invariant !== "string" || !invariantById.has(write.invariant)) {
209
+ failures.push(
210
+ `${writeOwner} references unknown invariant ${JSON.stringify(write.invariant)}`,
211
+ );
212
+ }
213
+ }
214
+
215
+ for (const gate of stringArray(rule.gates) ? rule.gates : []) {
216
+ if (!scriptNames.has(gate))
217
+ failures.push(`${rule.primitive} references unknown boundary gate ${gate}`);
218
+ }
219
+
220
+ const sourceOwnerNames = unique(sourceFactOwners.map((entry) => entry.owner));
221
+ if (
222
+ sourceOwnerNames.length > 1 &&
223
+ primitive !== undefined &&
224
+ !coordinationCapabilityKinds.has(primitive.capabilityKind)
225
+ ) {
226
+ failures.push(
227
+ `${rule.primitive} spans ${sourceOwnerNames.length} fact owners but coordination primitive kind ${JSON.stringify(
228
+ primitive.capabilityKind,
229
+ )} is not one of ${JSON.stringify([...coordinationCapabilityKinds])}`,
230
+ );
231
+ }
232
+
233
+ if (
234
+ primitive === undefined ||
235
+ failures.some((failure) => failure.startsWith(`${rule.primitive} `))
236
+ ) {
237
+ continue;
238
+ }
239
+
240
+ const invariantIds = unique([
241
+ ...allowedPrimitiveRecords.flatMap((record) => record.invariants),
242
+ ...forbiddenWrites.flatMap((write) =>
243
+ typeof write?.invariant === "string" && invariantById.has(write.invariant)
244
+ ? [write.invariant]
245
+ : [],
246
+ ),
247
+ ]);
248
+
249
+ routes.push({
250
+ primitive: rule.primitive,
251
+ intents: rule.intents,
252
+ coordinationPackage: primitive.package,
253
+ coordinationCapabilityKind: primitive.capabilityKind,
254
+ sourceFactPrefixes: rule.sourceFactPrefixes,
255
+ sourceFactOwners,
256
+ allowedPrimitives,
257
+ allowedPrimitivePackages: unique(allowedPrimitiveRecords.map((record) => record.package)),
258
+ forbiddenWrites,
259
+ gates: rule.gates,
260
+ invariants: invariantIds,
261
+ docs: unique([
262
+ ...allowedPrimitiveRecords.map((record) => record.docs),
263
+ ...invariantIds.flatMap((id) => {
264
+ const invariant = invariantById.get(id);
265
+ return invariant === undefined ? [] : [invariant.docs];
266
+ }),
267
+ ]),
268
+ testEvidence: allowedPrimitiveRecords.map(primitiveEvidence),
269
+ });
270
+ }
271
+
272
+ const coverage = buildCapabilityRouteCoverage({ routes, recipes, primitives });
273
+ failures.push(...coverage.failures);
274
+
275
+ return {
276
+ failures,
277
+ routes:
278
+ failures.length === 0
279
+ ? routes.sort((left, right) => left.primitive.localeCompare(right.primitive))
280
+ : [],
281
+ coverage: failures.length === 0 ? coverage.summary : { recipes: [], primitives: [] },
282
+ };
283
+ };
284
+
285
+ const routeMatchesRecipe = (route, recipe) => {
286
+ const recipePrimitives = new Set(recipe.primitives);
287
+ return (
288
+ recipePrimitives.has(route.primitive) ||
289
+ route.allowedPrimitives.some((primitive) => recipePrimitives.has(primitive))
290
+ );
291
+ };
292
+
293
+ const routePrimitivesForRecipe = (routes, recipe) =>
294
+ routes
295
+ .filter((route) => routeMatchesRecipe(route, recipe))
296
+ .map((route) => route.primitive)
297
+ .sort((left, right) => left.localeCompare(right));
298
+
299
+ const buildCapabilityRouteCoverage = ({ routes, recipes, primitives }) => {
300
+ const failures = [];
301
+ const coveredPrimitiveIds = new Set(
302
+ routes.flatMap((route) => [route.primitive, ...route.allowedPrimitives]),
303
+ );
304
+ const recipeCoverage = recipes.map((recipe) => {
305
+ const routePrimitives = routePrimitivesForRecipe(routes, recipe);
306
+ const noRouteReason =
307
+ typeof recipe.noRouteReason === "string" ? recipe.noRouteReason.trim() : undefined;
308
+ if (routePrimitives.length === 0 && noRouteReason === undefined) {
309
+ failures.push(`${recipe.id} must have a capability route or noRouteReason`);
310
+ }
311
+ if (routePrimitives.length > 0 && noRouteReason !== undefined) {
312
+ failures.push(`${recipe.id} has both route coverage and noRouteReason`);
313
+ }
314
+ return {
315
+ id: recipe.id,
316
+ routePrimitives,
317
+ ...(noRouteReason === undefined ? {} : { noRouteReason }),
318
+ };
319
+ });
320
+
321
+ const primitiveCoverage = primitives
322
+ .filter((primitive) => consumerFacingCapabilityKinds.has(primitive.capabilityKind))
323
+ .map((primitive) => {
324
+ const routePrimitives = routes
325
+ .filter(
326
+ (route) =>
327
+ route.primitive === primitive.id || route.allowedPrimitives.includes(primitive.id),
328
+ )
329
+ .map((route) => route.primitive)
330
+ .sort((left, right) => left.localeCompare(right));
331
+ const noRouteReason =
332
+ typeof primitive.noRouteReason === "string" ? primitive.noRouteReason.trim() : undefined;
333
+ if (!coveredPrimitiveIds.has(primitive.id) && noRouteReason === undefined) {
334
+ failures.push(`${primitive.id} must have a capability route or noRouteReason`);
335
+ }
336
+ if (coveredPrimitiveIds.has(primitive.id) && noRouteReason !== undefined) {
337
+ failures.push(`${primitive.id} has both route coverage and noRouteReason`);
338
+ }
339
+ return {
340
+ id: primitive.id,
341
+ capabilityKind: primitive.capabilityKind,
342
+ routePrimitives,
343
+ ...(noRouteReason === undefined ? {} : { noRouteReason }),
344
+ };
345
+ });
346
+
347
+ return {
348
+ failures,
349
+ summary: {
350
+ recipes: recipeCoverage,
351
+ primitives: primitiveCoverage,
352
+ },
353
+ };
354
+ };
@@ -0,0 +1,113 @@
1
+ const isNonEmptyString = (value) => typeof value === "string" && value.trim().length > 0;
2
+
3
+ const sourceRefIssue = (source) => {
4
+ if (!isNonEmptyString(source.kind)) return "projection source kind must be non-empty";
5
+ if (!isNonEmptyString(source.ref)) return "projection source ref must be non-empty";
6
+ if (source.hash !== undefined && !isNonEmptyString(source.hash)) {
7
+ return "projection source hash must be non-empty when present";
8
+ }
9
+ if (source.kind !== "source-set" && source.sources !== undefined) {
10
+ return "projection source children require source-set kind";
11
+ }
12
+ if (
13
+ source.kind === "source-set" &&
14
+ (!Array.isArray(source.sources) || source.sources.length === 0)
15
+ ) {
16
+ return "projection source-set must include at least one source";
17
+ }
18
+ for (const child of source.sources ?? []) {
19
+ const issue = sourceRefIssue(child);
20
+ if (issue !== undefined) return `${source.ref}: ${issue}`;
21
+ }
22
+ return undefined;
23
+ };
24
+
25
+ export const defineProjectionSpec = (spec) => {
26
+ if (!isNonEmptyString(spec.id)) throw new Error("projection id must be non-empty");
27
+ if (!Number.isInteger(spec.version) || spec.version < 1) {
28
+ throw new Error("projection version must be a positive integer");
29
+ }
30
+ const sourceIssue = sourceRefIssue(spec.source);
31
+ if (sourceIssue !== undefined) throw new Error(sourceIssue);
32
+ return spec;
33
+ };
34
+
35
+ const provenanceOf = (spec) => ({
36
+ projection: {
37
+ id: spec.id,
38
+ version: spec.version,
39
+ },
40
+ source: spec.source,
41
+ });
42
+
43
+ const projectionOk = (provenance, output) => ({ _tag: "ok", output, provenance });
44
+
45
+ const projectionFailure = (provenance, reason, issues) => ({
46
+ _tag: "failure",
47
+ reason,
48
+ provenance,
49
+ ...(issues === undefined ? {} : { issues }),
50
+ });
51
+
52
+ const isThenable = (value) =>
53
+ value !== null &&
54
+ (typeof value === "object" || typeof value === "function") &&
55
+ "then" in value &&
56
+ typeof value.then === "function";
57
+
58
+ const isProjectionResult = (value) =>
59
+ value !== null &&
60
+ typeof value === "object" &&
61
+ "_tag" in value &&
62
+ (value._tag === "ok" || value._tag === "failure");
63
+
64
+ const project = (spec, input) => {
65
+ const provenance = provenanceOf(spec);
66
+ const context = {
67
+ provenance,
68
+ ok: (output) => projectionOk(provenance, output),
69
+ failure: (reason, issues) => projectionFailure(provenance, reason, issues),
70
+ };
71
+
72
+ try {
73
+ const result = spec.project(input, context);
74
+ if (isThenable(result)) return projectionFailure(provenance, "projection_returned_thenable");
75
+ if (!isProjectionResult(result))
76
+ return projectionFailure(provenance, "projection_result_invalid");
77
+ return result._tag === "ok"
78
+ ? projectionOk(provenance, result.output)
79
+ : projectionFailure(provenance, result.reason, result.issues);
80
+ } catch {
81
+ return projectionFailure(provenance, "projection_threw");
82
+ }
83
+ };
84
+
85
+ const defaultEquals = (actual, expected) => Object.is(actual, expected);
86
+
87
+ export const checkProjectionSink = async (spec, input, sink) => {
88
+ const result = project(spec, input);
89
+ if (result._tag === "failure") return { _tag: "projection_failed", result };
90
+
91
+ const current = await sink.read();
92
+ if (current._tag === "found" && (sink.equals ?? defaultEquals)(current.output, result.output)) {
93
+ return { _tag: "current", result, actual: current.output };
94
+ }
95
+
96
+ return {
97
+ _tag: "stale",
98
+ result,
99
+ expected: result.output,
100
+ actual: current,
101
+ };
102
+ };
103
+
104
+ export const runProjectionSink = async (spec, input, sink) => {
105
+ const checked = await checkProjectionSink(spec, input, sink);
106
+ if (checked._tag !== "stale") return checked;
107
+ await sink.write(checked.expected);
108
+ return {
109
+ _tag: "updated",
110
+ result: checked.result,
111
+ previous: checked.actual,
112
+ };
113
+ };