@effected/github-actions 0.8.0 → 0.9.1

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/CheckDocument.js CHANGED
@@ -29,29 +29,112 @@ var CheckReport = class extends Schema.Class("CheckReport")({
29
29
  url: Schema.optionalKey(Schema.String)
30
30
  }) {};
31
31
  /**
32
- * Raised when the check document cannot be regenerated or written.
32
+ * Raised when the check document cannot be regenerated, read back or written.
33
33
  *
34
34
  * @remarks
35
35
  * `render` — projecting the registry onto the document failed, almost always
36
36
  * because a reported `detail` contains a line the region scanner would read
37
37
  * as a marker; the underlying `ManagedDocumentError` rides in `cause`.
38
- * `sink` — the consumer's write effect failed; its error rides in `cause`.
39
- * Callers of `flush` branch on `kind`; the background reconciler never fails
40
- * a fiber with this — it logs and retries on the next report.
38
+ * `read` — the sink's read-back of the live document failed; its error rides
39
+ * in `cause`. `sink` — the consumer's write effect failed; its error rides in
40
+ * `cause`. Callers of `flush` branch on `kind`; the background reconciler
41
+ * never fails a fiber with this — it logs and retries on the next report.
41
42
  *
42
43
  * @public
43
44
  */
44
45
  var CheckDocumentError = class extends Schema.TaggedError()("CheckDocumentError", {
45
- /** Which half failed: regenerating the document, or writing it. */
46
- kind: Schema.Literals(["render", "sink"]),
46
+ /** Which phase failed: regenerating the document, reading it back, or writing it. */
47
+ kind: Schema.Literals([
48
+ "render",
49
+ "read",
50
+ "sink"
51
+ ]),
47
52
  /** The underlying failure. */
48
53
  cause: Schema.optionalKey(Schema.Defect())
49
54
  }) {
50
55
  get message() {
51
- return this.kind === "render" ? "The check document could not be regenerated from the reported state" : "Writing the check document failed";
56
+ return this.kind === "render" ? "The check document could not be regenerated from the reported state" : this.kind === "read" ? "Reading the check document's current text failed" : "Writing the check document failed";
52
57
  }
53
58
  };
54
59
  /**
60
+ * A run's identity for staleness ordering: when it started and which run it
61
+ * is.
62
+ *
63
+ * @remarks
64
+ * Two delayed workflows can race for one document — a re-run's stale pass
65
+ * must not overwrite a newer run's regions. The ordering is **total**:
66
+ * `at` compares first (as epoch milliseconds when both sides `Date.parse`
67
+ * cleanly, lexically otherwise), `runId` breaks ties (numerically when both
68
+ * parse to finite numbers, lexically otherwise), and two equal stamps compare
69
+ * as at-least-as-recent — a run may refine its own regions.
70
+ *
71
+ * @public
72
+ */
73
+ var CheckDocumentStamp = class extends Schema.Class("CheckDocumentStamp")({
74
+ /** When the run started — ideally an ISO-8601 instant. */
75
+ at: Schema.String,
76
+ /** The run's identifier — on GitHub, `GITHUB_RUN_ID`. */
77
+ runId: Schema.String
78
+ }) {
79
+ /**
80
+ * Is `incoming` allowed to overwrite regions stamped `existing`?
81
+ *
82
+ * @remarks
83
+ * Total and reflexive: equal stamps answer `true`, so a run always may
84
+ * rewrite what it wrote itself. Only a strictly older `incoming` answers
85
+ * `false`.
86
+ */
87
+ static isAtLeastAsRecent(incoming, existing) {
88
+ const at = compareAt(incoming.at, existing.at);
89
+ if (at !== 0) return at > 0;
90
+ return compareRunId(incoming.runId, existing.runId) >= 0;
91
+ }
92
+ };
93
+ /** Epoch order when both sides parse as dates, lexical otherwise. */
94
+ const compareAt = (left, right) => {
95
+ const leftEpoch = Date.parse(left);
96
+ const rightEpoch = Date.parse(right);
97
+ if (!Number.isNaN(leftEpoch) && !Number.isNaN(rightEpoch)) return leftEpoch === rightEpoch ? 0 : leftEpoch < rightEpoch ? -1 : 1;
98
+ return left === right ? 0 : left < right ? -1 : 1;
99
+ };
100
+ /**
101
+ * Numeric order when both sides are non-blank finite numbers, lexical
102
+ * otherwise.
103
+ *
104
+ * @remarks
105
+ * The blank guard exists because `Number("")` and `Number(" ")` are both a
106
+ * finite `0` — without it a blank `runId` (plausible when `GITHUB_RUN_ID` is
107
+ * unset in a local or self-hosted run) would compare numerically equal to
108
+ * `"0"` and outrank `"-1"`. Blank sides fall to the lexical branch, which
109
+ * stays total and deterministic.
110
+ */
111
+ const compareRunId = (left, right) => {
112
+ if (left.trim() !== "" && right.trim() !== "") {
113
+ const leftNumber = Number(left);
114
+ const rightNumber = Number(right);
115
+ if (Number.isFinite(leftNumber) && Number.isFinite(rightNumber)) return leftNumber === rightNumber ? 0 : leftNumber < rightNumber ? -1 : 1;
116
+ }
117
+ return left === right ? 0 : left < right ? -1 : 1;
118
+ };
119
+ /** The stamp a document's own regions carry, when they carry one. */
120
+ const stampOf = (meta) => {
121
+ const at = meta.at;
122
+ const runId = meta.runId;
123
+ return at === void 0 || runId === void 0 ? void 0 : {
124
+ at,
125
+ runId
126
+ };
127
+ };
128
+ /** The most recent stamp among a document's own regions, if any carry one. */
129
+ const maxStampOf = (regions) => {
130
+ let max;
131
+ for (const region of regions) {
132
+ const stamp = stampOf(region.meta);
133
+ if (stamp !== void 0 && (max === void 0 || CheckDocumentStamp.isAtLeastAsRecent(stamp, max))) max = stamp;
134
+ }
135
+ return max;
136
+ };
137
+ /**
55
138
  * A living document of check state: an in-process registry the run reports
56
139
  * into, debounced onto a marker-delimited document, written through a narrow
57
140
  * sink.
@@ -121,26 +204,58 @@ var CheckDocument = class CheckDocument extends Context.Service()("@effected/git
121
204
  const versionRef = yield* Ref.make(0);
122
205
  const signal = yield* Latch.make(false);
123
206
  const gate = yield* Semaphore.make(1);
207
+ const passTimeout = options.sinkTimeout ?? "30 seconds";
208
+ const sink = typeof options.sink === "function" ? { write: options.sink } : options.sink;
209
+ const stamp = options.stamp;
210
+ const stampMeta = stamp === void 0 ? void 0 : {
211
+ at: stamp.at,
212
+ runId: stamp.runId
213
+ };
214
+ const staleLoggedRef = yield* Ref.make(false);
124
215
  const reconcile = Effect.fn("CheckDocument.reconcile")(function* () {
125
216
  const checks = yield* Ref.get(checksRef);
126
- const current = yield* Ref.get(writtenRef);
127
- const next = yield* (yield* ManagedDocument.parse({
217
+ const current = sink.read === void 0 ? yield* Ref.get(writtenRef) : (yield* sink.read.pipe(Effect.timeout(passTimeout), Effect.mapError((cause) => new CheckDocumentError({
218
+ kind: "read",
219
+ cause
220
+ })))) ?? "";
221
+ const document = yield* ManagedDocument.parse({
128
222
  namespace: options.namespace,
129
223
  key: options.key,
130
224
  text: current
131
225
  }).pipe(Effect.mapError((cause) => new CheckDocumentError({
132
226
  kind: "render",
133
227
  cause
134
- })))).withRegions(options.render(checks)).pipe(Effect.mapError((cause) => new CheckDocumentError({
228
+ })));
229
+ if (stamp !== void 0) {
230
+ const existing = maxStampOf(document.regions);
231
+ if (existing !== void 0 && !CheckDocumentStamp.isAtLeastAsRecent(stamp, existing)) {
232
+ yield* Effect.annotateCurrentSpan("outcome", "stale");
233
+ yield* ((yield* Ref.getAndSet(staleLoggedRef, true)) ? Effect.logDebug : Effect.logInfo)("CheckDocument pass dropped: the document carries a more recent run's regions", {
234
+ incomingAt: stamp.at,
235
+ incomingRunId: stamp.runId,
236
+ existingAt: existing.at,
237
+ existingRunId: existing.runId
238
+ });
239
+ return "stale";
240
+ }
241
+ yield* Ref.set(staleLoggedRef, false);
242
+ }
243
+ const entries = options.render(checks);
244
+ const next = yield* document.withRegions(stampMeta === void 0 ? entries : entries.map(([key, content]) => [
245
+ key,
246
+ content,
247
+ stampMeta
248
+ ])).pipe(Effect.mapError((cause) => new CheckDocumentError({
135
249
  kind: "render",
136
250
  cause
137
251
  })));
138
- if (next.text === current) return;
139
- yield* options.sink(next.text).pipe(Effect.timeout(options.sinkTimeout ?? "30 seconds"), Effect.mapError((cause) => new CheckDocumentError({
252
+ if (next.text === current) return "unchanged";
253
+ yield* sink.write(next.text).pipe(Effect.timeout(passTimeout), Effect.mapError((cause) => new CheckDocumentError({
140
254
  kind: "sink",
141
255
  cause
142
256
  })));
143
257
  yield* Ref.set(writtenRef, next.text);
258
+ return "written";
144
259
  });
145
260
  const flush = gate.withPermits(1)(reconcile());
146
261
  const daemon = Effect.gen(function* () {
@@ -190,4 +305,4 @@ const unstubbed = (member) => {
190
305
  };
191
306
 
192
307
  //#endregion
193
- export { CheckDocument, CheckDocumentError, CheckReport };
308
+ export { CheckDocument, CheckDocumentError, CheckDocumentStamp, CheckReport };
@@ -31,10 +31,14 @@ const REGION_DIALECT = SectionDialect.make({
31
31
  * The four structural kinds (`unterminatedRegion`, `orphanedEnd`,
32
32
  * `overlappingRegions`, `duplicateRegion`) surface at parse time and name an
33
33
  * ambiguity whose silent resolution would corrupt content a human wrote by
34
- * hand. The two declaration kinds surface when regions are applied:
34
+ * hand. The three declaration kinds surface when regions are applied:
35
35
  * `markerInContent` refuses region content that itself contains a line the
36
- * scanner would read as a region marker, and `duplicateDeclaration` refuses
37
- * one call that states two intentions for the same region.
36
+ * scanner would read as a region marker, `duplicateDeclaration` refuses
37
+ * one call that states two intentions for the same region, and
38
+ * `invalidAttribute` refuses region metadata whose name is outside the
39
+ * `[A-Za-z][A-Za-z0-9_-]*` grammar or whose value contains `"` or a line
40
+ * break — either would write a marker the region scanner could not read
41
+ * back verbatim.
38
42
  *
39
43
  * @public
40
44
  */
@@ -46,17 +50,21 @@ var ManagedDocumentError = class extends Schema.TaggedError()("ManagedDocumentEr
46
50
  "overlappingRegions",
47
51
  "duplicateRegion",
48
52
  "markerInContent",
49
- "duplicateDeclaration"
53
+ "duplicateDeclaration",
54
+ "invalidAttribute"
50
55
  ]),
51
56
  /** 1-based line of the offending marker, for the structural kinds. */
52
57
  line: Schema.optionalKey(Schema.Number),
53
58
  /** The region key involved, when the failure names one. */
54
- key: Schema.optionalKey(Schema.String)
59
+ key: Schema.optionalKey(Schema.String),
60
+ /** The offending metadata attribute's name, for `invalidAttribute`. */
61
+ attribute: Schema.optionalKey(Schema.String)
55
62
  }) {
56
63
  get message() {
57
64
  const which = this.key === void 0 ? "" : ` for region "${this.key}"`;
65
+ const named = this.attribute === void 0 ? "" : ` (attribute "${this.attribute}")`;
58
66
  const where = this.line === void 0 ? "" : ` at line ${this.line}`;
59
- return `${KIND_PROSE[this.kind]}${which}${where}`;
67
+ return `${KIND_PROSE[this.kind]}${which}${named}${where}`;
60
68
  }
61
69
  };
62
70
  const KIND_PROSE = {
@@ -65,7 +73,8 @@ const KIND_PROSE = {
65
73
  overlappingRegions: "Managed region opens inside another",
66
74
  duplicateRegion: "Managed region appears twice",
67
75
  markerInContent: "Region content contains a managed-region marker",
68
- duplicateDeclaration: "Region was declared twice in one call"
76
+ duplicateDeclaration: "Region was declared twice in one call",
77
+ invalidAttribute: "Region metadata has an invalid attribute name or value"
69
78
  };
70
79
  /** Map a templates scan failure onto this surface's vocabulary. */
71
80
  const STRUCTURAL_KIND = {
@@ -173,7 +182,29 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
173
182
  const found = this.ownRegions().find((entry) => entry.key === key);
174
183
  return found === void 0 ? Option.none() : Option.some(found.content);
175
184
  }
176
- /** Every region this document carries, in document order. */
185
+ /**
186
+ * One region's content and metadata together, if the document carries it.
187
+ *
188
+ * @remarks
189
+ * `meta` is the region's `name="value"` marker attributes, empty when the
190
+ * marker carries none. Metadata never participates in region
191
+ * addressability — this looks up by key exactly as
192
+ * {@link ManagedDocument.region} does.
193
+ */
194
+ entry(key) {
195
+ const found = this.ownRegions().find((candidate) => candidate.key === key);
196
+ return found === void 0 ? Option.none() : Option.some({
197
+ content: found.content,
198
+ meta: found.meta
199
+ });
200
+ }
201
+ /**
202
+ * Every region this document carries, in document order.
203
+ *
204
+ * @remarks
205
+ * `meta` is always present: a region whose marker carries no attributes
206
+ * reports an empty record, so callers never branch on absence.
207
+ */
177
208
  get regions() {
178
209
  return this.ownRegions();
179
210
  }
@@ -182,12 +213,22 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
182
213
  *
183
214
  * @remarks
184
215
  * The synchronous primitive; {@link ManagedDocument.withRegions} derives
185
- * from it. Each entry pairs a region key with its full new content. A
186
- * region already present is **replaced in place**; a region not present
187
- * yet is added; regions not named are left exactly as they are; and every
188
- * byte outside a managed region survives untouched. Applying entries that
189
- * change nothing returns byte-identical text, which is what lets a caller
190
- * compare-and-skip instead of issuing a write.
216
+ * from it. Each entry pairs a region key with its full new content, plus
217
+ * optional metadata. A region already present is **replaced in place**; a
218
+ * region not present yet is added; regions not named are left exactly as
219
+ * they are; and every byte outside a managed region survives untouched.
220
+ * Applying entries that change nothing returns byte-identical text, which
221
+ * is what lets a caller compare-and-skip instead of issuing a write.
222
+ *
223
+ * `meta` becomes `name="value"` attributes on the region's BEGIN marker.
224
+ * It round-trips verbatim through {@link ManagedDocument.entry}, survives
225
+ * writes by parties that do not know about it — a region not named in a
226
+ * call keeps its content *and* its metadata untouched — and never
227
+ * participates in region addressability: a region is found by key alone,
228
+ * so changing metadata updates the region in place. A two-element entry
229
+ * behaves exactly as it always has. Names must match
230
+ * `[A-Za-z][A-Za-z0-9_-]*` and values may not contain `"` or a line
231
+ * break; violations fail typed as `invalidAttribute`.
191
232
  *
192
233
  * Region keys share the name grammar `namespace` and `key` obey; a key
193
234
  * outside it is a wiring error and throws at construction rather than
@@ -203,17 +244,18 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
203
244
  ...failure.key === void 0 ? {} : { key: failure.key }
204
245
  }));
205
246
  }
206
- const declared = entries.map(([key, content]) => SectionId.make({
247
+ const declared = entries.map(([key, content, meta]) => SectionId.make({
207
248
  key: this.wireKey(key),
208
249
  commentStyle: CommentStyle.html
209
- }).section(content));
250
+ }).section(content, meta));
210
251
  const reconciled = parsed.success.reconcile(declared);
211
252
  if (Result.isFailure(reconciled)) {
212
253
  const failure = reconciled.failure;
213
254
  if (failure.reason === "unknownCommentStyle") throw new Error("ManagedDocument: the region dialect rejected its own comment style");
214
255
  return Result.fail(new ManagedDocumentError({
215
256
  kind: failure.reason,
216
- key: this.regionKeyOf(failure.key)
257
+ key: this.regionKeyOf(failure.key),
258
+ ...failure.attribute === void 0 ? {} : { attribute: failure.attribute }
217
259
  }));
218
260
  }
219
261
  const withSentinel = this.ensureSentinel(reconciled.success.text, parsed.success.eol);
@@ -259,7 +301,8 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
259
301
  const prefix = `${this.namespace}.${this.key}.`;
260
302
  return parsed.success.sections.filter((placed) => placed.section.key.startsWith(prefix)).map((placed) => ({
261
303
  key: placed.section.key.slice(prefix.length),
262
- content: placed.section.content
304
+ content: placed.section.content,
305
+ meta: placed.section.attributes
263
306
  }));
264
307
  }
265
308
  /**
package/README.md CHANGED
@@ -193,12 +193,40 @@ const program = Effect.gen(function* () {
193
193
  const doc = yield* CheckDocument;
194
194
  yield* doc.report("build", CheckReport.make({ state: "running" }));
195
195
  yield* doc.report("build", CheckReport.make({ state: "pass", outcome: "clean" }));
196
- yield* doc.flush;
196
+ return yield* doc.flush;
197
197
  });
198
- // one write carrying the final state; a render byte-identical to the last one writes nothing
198
+ // "written" — one write carrying the final state
199
+ // "unchanged" — the render was byte-identical to the last one, so nothing was sent
200
+ // "stale" — a newer run owns the document; this pass was dropped (see below)
199
201
  ```
200
202
 
201
- The debounce is trailing with a max-wait, so a burst of reports coalesces into one write carrying the *final* state while a steady stream still surfaces progress. `CheckState` is the vocabulary both of them speak, and it is deliberately wider than GitHub's conclusion set — `running` is a state rather than the absence of one, and `user_interaction_required` names a pipeline waiting on a human. `projectCheckState` maps each onto the wire form GitHub wants, as a status plus a conclusion exactly when the status is `completed`.
203
+ The debounce is trailing with a max-wait, so a burst of reports coalesces into one write carrying the *final* state while a steady stream still surfaces progress.
204
+
205
+ A region can also carry metadata: `withRegions` takes `[key, content, meta]` triples, the pairs ride the region's own marker, and `entry(key)` reads them back verbatim. Metadata survives a writer that knows nothing about it — a region a call does not name keeps its content *and* its metadata — and never takes part in addressing a region, so changing it updates that region in place rather than growing a second one.
206
+
207
+ `CheckDocument` uses exactly that to survive two runs writing one document — a re-run overlapping the delayed run it replaced. Give the layer this run's `stamp` and a sink that can `read` as well as `write`, and each pass reconciles against the live document instead of a private shadow of what this process last wrote:
208
+
209
+ ```ts
210
+ import { CheckDocument } from "@effected/github-actions";
211
+ import type { Effect } from "effect";
212
+
213
+ declare const startedAt: string;
214
+ declare const runId: string;
215
+ declare const readComment: Effect.Effect<string | undefined>;
216
+ declare const writeComment: (text: string) => Effect.Effect<void>;
217
+
218
+ const layer = CheckDocument.layer({
219
+ namespace: "release-bot",
220
+ key: "checks",
221
+ render: (checks) => [...checks].map(([name, report]) => [name, report.state] as const),
222
+ sink: { write: writeComment, read: readComment },
223
+ stamp: { at: startedAt, runId }, // a per-run constant — mint it once at startup
224
+ });
225
+ ```
226
+
227
+ Every region the run writes then carries its `at` and `runId`, and a pass whose stamp is strictly older than the newest stamp already on the document is dropped rather than written — `flush` answers `"stale"` and the reconciler logs the drop once, at the transition. Equal stamps pass, so a run always may refine its own regions. Keeping the stamp constant is what preserves write suppression: the same run rendering the same state still produces byte-identical text.
228
+
229
+ `CheckState` is the vocabulary both of them speak, and it is deliberately wider than GitHub's conclusion set — `running` is a state rather than the absence of one, and `user_interaction_required` names a pipeline waiting on a human. `projectCheckState` maps each onto the wire form GitHub wants, as a status plus a conclusion exactly when the status is `completed`.
202
230
 
203
231
  ## Testing
204
232
 
@@ -239,9 +267,9 @@ const TestOutputs = ActionOutputs.layerTest({
239
267
  - `OidcTokenIssuer` — the runner's OIDC token service, with unverified claim decoding for provenance predicates.
240
268
  - `ActionsIdentityToken` / `ActionsProvenance` — the `@effected/sbom` seam: `layer` serves that package's `IdentityToken` contract from the runner's issuer, and `capture` builds a `SlsaProvenance` predicate out of the runner's OIDC claims.
241
269
  - `GitHubMarkdown` — the escaping-safe writer for GitHub surfaces: `table`, `tableFor(schema)`, `heading`, `link`, `code`, `codeBlock`, `list`, `details` and `raw`, and the package's only importer of `@effected/markdown`.
242
- - `ManagedDocument` — marker-delimited named regions in text a human also edits, for a sticky comment or a managed pull request description; content outside the regions survives byte-for-byte.
270
+ - `ManagedDocument` — marker-delimited named regions in text a human also edits, for a sticky comment or a managed pull request description; content outside the regions survives byte-for-byte, and each region can carry marker metadata that round-trips through `entry(key)` without ever addressing the region.
243
271
  - `CheckState` / `projectCheckState` — the kit's check vocabulary, wider than GitHub's conclusions, and its projection onto the check-run wire form.
244
- - `CheckDocument` — the debounced check-to-document reconciler: report states as they change, and one write lands carrying the final one.
272
+ - `CheckDocument` — the debounced check-to-document reconciler: report states as they change, and one write lands carrying the final one. `stamp` plus a readable sink lets two runs share one document, dropping a strictly older run's pass instead of clobbering the newer one; `flush` reports `written`, `unchanged` or `stale`.
245
273
  - `ToolInstaller` — download, extract and cache a toolchain in the runner's tool cache, with no `@actions/tool-cache` dependency.
246
274
  - `BlobStore` — durable `get`/`put`/`has` over bytes plus a metadata channel, backed by `layerS3` (SigV4-signed, no `@aws-sdk/*` dependency) or `GitHubCacheBlobStore.layer` (the runner's own cache); `layerMemory` runs the real envelope framing for tests.
247
275
 
package/index.d.ts CHANGED
@@ -2412,41 +2412,108 @@ declare const CheckReport_base: Schema.Class<CheckReport, Schema.Struct<{
2412
2412
  */
2413
2413
  declare class CheckReport extends CheckReport_base {}
2414
2414
  declare const CheckDocumentError_base: Schema.Class<CheckDocumentError, Schema.TaggedStruct<"CheckDocumentError", {
2415
- /** Which half failed: regenerating the document, or writing it. */
2416
- readonly kind: Schema.Literals<readonly ["render", "sink"]>;
2415
+ /** Which phase failed: regenerating the document, reading it back, or writing it. */
2416
+ readonly kind: Schema.Literals<readonly ["render", "read", "sink"]>;
2417
2417
  /** The underlying failure. */
2418
2418
  readonly cause: Schema.optionalKey<Schema.Defect>;
2419
2419
  }>, import("effect/Cause").YieldableError>;
2420
2420
  /**
2421
- * Raised when the check document cannot be regenerated or written.
2421
+ * Raised when the check document cannot be regenerated, read back or written.
2422
2422
  *
2423
2423
  * @remarks
2424
2424
  * `render` — projecting the registry onto the document failed, almost always
2425
2425
  * because a reported `detail` contains a line the region scanner would read
2426
2426
  * as a marker; the underlying `ManagedDocumentError` rides in `cause`.
2427
- * `sink` — the consumer's write effect failed; its error rides in `cause`.
2428
- * Callers of `flush` branch on `kind`; the background reconciler never fails
2429
- * a fiber with this — it logs and retries on the next report.
2427
+ * `read` — the sink's read-back of the live document failed; its error rides
2428
+ * in `cause`. `sink` — the consumer's write effect failed; its error rides in
2429
+ * `cause`. Callers of `flush` branch on `kind`; the background reconciler
2430
+ * never fails a fiber with this — it logs and retries on the next report.
2430
2431
  *
2431
2432
  * @public
2432
2433
  */
2433
2434
  declare class CheckDocumentError extends CheckDocumentError_base {
2434
2435
  get message(): string;
2435
2436
  }
2437
+ declare const CheckDocumentStamp_base: Schema.Class<CheckDocumentStamp, Schema.Struct<{
2438
+ /** When the run started — ideally an ISO-8601 instant. */
2439
+ readonly at: Schema.String;
2440
+ /** The run's identifier — on GitHub, `GITHUB_RUN_ID`. */
2441
+ readonly runId: Schema.String;
2442
+ }>, {}>;
2443
+ /**
2444
+ * A run's identity for staleness ordering: when it started and which run it
2445
+ * is.
2446
+ *
2447
+ * @remarks
2448
+ * Two delayed workflows can race for one document — a re-run's stale pass
2449
+ * must not overwrite a newer run's regions. The ordering is **total**:
2450
+ * `at` compares first (as epoch milliseconds when both sides `Date.parse`
2451
+ * cleanly, lexically otherwise), `runId` breaks ties (numerically when both
2452
+ * parse to finite numbers, lexically otherwise), and two equal stamps compare
2453
+ * as at-least-as-recent — a run may refine its own regions.
2454
+ *
2455
+ * @public
2456
+ */
2457
+ declare class CheckDocumentStamp extends CheckDocumentStamp_base {
2458
+ /**
2459
+ * Is `incoming` allowed to overwrite regions stamped `existing`?
2460
+ *
2461
+ * @remarks
2462
+ * Total and reflexive: equal stamps answer `true`, so a run always may
2463
+ * rewrite what it wrote itself. Only a strictly older `incoming` answers
2464
+ * `false`.
2465
+ */
2466
+ static isAtLeastAsRecent(incoming: {
2467
+ readonly at: string;
2468
+ readonly runId: string;
2469
+ }, existing: {
2470
+ readonly at: string;
2471
+ readonly runId: string;
2472
+ }): boolean;
2473
+ }
2474
+ /**
2475
+ * What one reconcile pass did.
2476
+ *
2477
+ * @remarks
2478
+ * `written` — the render changed and the sink wrote it. `unchanged` — the
2479
+ * render was byte-identical, so no write was issued. `stale` — the document
2480
+ * already carries regions from a more recent run, so the pass was dropped
2481
+ * without writing (only possible when {@link CheckDocumentOptions.stamp} is
2482
+ * set). A plain literal union: callers that ignored `flush`'s old `void`
2483
+ * success stay compile-compatible.
2484
+ *
2485
+ * @public
2486
+ */
2487
+ type CheckFlushOutcome = "written" | "unchanged" | "stale";
2436
2488
  /**
2437
2489
  * How a rendered document leaves the process: the narrow sink contract.
2438
2490
  *
2439
2491
  * @remarks
2440
- * One rendered document in, one write effect out — nothing else. That is
2441
- * what lets the sticky PR comment (`PullRequestComment.upsert`), the PR
2442
- * description (`PATCH` on the pull request) and a test's recording `Ref` all
2443
- * fit the same reconciler. The success value is ignored; a failure surfaces
2444
- * as {@link CheckDocumentError} with `kind: "sink"`. Provide any services the
2445
- * write needs when constructing the sink — the reconciler runs it as given.
2492
+ * The function form is one rendered document in, one write effect out —
2493
+ * nothing else. That is what lets the sticky PR comment
2494
+ * (`PullRequestComment.upsert`), the PR description (`PATCH` on the pull
2495
+ * request) and a test's recording `Ref` all fit the same reconciler. The
2496
+ * write's success value is ignored; a failure surfaces as
2497
+ * {@link CheckDocumentError} with `kind: "sink"`. Provide any services the
2498
+ * effects need when constructing the sink — the reconciler runs them as
2499
+ * given.
2500
+ *
2501
+ * The object form adds an optional `read`: the live document's current text,
2502
+ * fetched at the start of every pass and used as the base to reconcile
2503
+ * against instead of the last text this process wrote. `undefined` means
2504
+ * "nothing there yet" — a fresh document. That is what lets two runs share
2505
+ * one document: each pass sees what the *other* run wrote, not a private
2506
+ * shadow. A failed read surfaces as `kind: "read"`. A bare function is
2507
+ * exactly `{ write }`.
2446
2508
  *
2447
2509
  * @public
2448
2510
  */
2449
- type CheckDocumentSink = (rendered: string) => Effect.Effect<unknown, unknown>;
2511
+ type CheckDocumentSink = ((rendered: string) => Effect.Effect<unknown, unknown>) | {
2512
+ /** Write one rendered document. */
2513
+ readonly write: (rendered: string) => Effect.Effect<unknown, unknown>;
2514
+ /** The live document's current text, or `undefined` for none yet. */
2515
+ readonly read?: Effect.Effect<string | undefined, unknown> | undefined;
2516
+ };
2450
2517
  /**
2451
2518
  * Options for {@link CheckDocument.layer}.
2452
2519
  *
@@ -2477,6 +2544,27 @@ interface CheckDocumentOptions {
2477
2544
  readonly render: (checks: ReadonlyMap<string, CheckReport>) => ReadonlyArray<readonly [key: string, content: string]>;
2478
2545
  /** Where the rendered document goes. */
2479
2546
  readonly sink: CheckDocumentSink;
2547
+ /**
2548
+ * This run's identity, for staleness ordering against other runs.
2549
+ *
2550
+ * @remarks
2551
+ * A **per-run constant** — mint it once at startup, never per pass. When
2552
+ * present, every region a pass writes carries `at`/`runId` metadata, and a
2553
+ * pass whose stamp is strictly older than the most recent stamp already on
2554
+ * the document is **dropped** rather than written, so a delayed re-run can
2555
+ * never clobber a newer run's regions. Equal stamps pass: a run may refine
2556
+ * its own regions.
2557
+ *
2558
+ * The constant is what preserves write suppression: the same run rendering
2559
+ * the same state produces byte-identical text (stamp included), so no
2560
+ * write is issued. The accepted corner is that a content-identical pass
2561
+ * therefore does not refresh the document's stamp to "now" — suppression
2562
+ * wins — which is sound because only strictly-older stamps drop.
2563
+ */
2564
+ readonly stamp?: {
2565
+ readonly at: string;
2566
+ readonly runId: string;
2567
+ } | undefined;
2480
2568
  /**
2481
2569
  * Debounce parameters: `quiet` is the trailing settle window, `maxWait`
2482
2570
  * the staleness bound under a steady stream of reports. Defaults: 500
@@ -2487,8 +2575,9 @@ interface CheckDocumentOptions {
2487
2575
  readonly maxWait?: Duration.Input | undefined;
2488
2576
  } | undefined;
2489
2577
  /**
2490
- * How long one sink write may run before the pass fails with
2491
- * `kind: "sink"`. Default: 30 seconds.
2578
+ * How long one sink write — or one read-back, when the sink carries a
2579
+ * `read` — may run before the pass fails with `kind: "sink"` (or
2580
+ * `kind: "read"`). Default: 30 seconds.
2492
2581
  *
2493
2582
  * @remarks
2494
2583
  * The reconciler serializes passes behind one permit, and the finalizer's
@@ -2524,9 +2613,11 @@ interface CheckDocumentShape {
2524
2613
  * @remarks
2525
2614
  * Rendering unchanged state issues no write. Call it at the end of a run;
2526
2615
  * the layer's finalizer also runs it, so the last burst of reports lands
2527
- * even when the scope closes inside the quiet window.
2616
+ * even when the scope closes inside the quiet window. The outcome says
2617
+ * what the pass did — `written`, `unchanged` or `stale` — and callers
2618
+ * that ignore it stay exactly as they were.
2528
2619
  */
2529
- readonly flush: Effect.Effect<void, CheckDocumentError>;
2620
+ readonly flush: Effect.Effect<CheckFlushOutcome, CheckDocumentError>;
2530
2621
  }
2531
2622
  declare const CheckDocument_base: Context.ServiceClass<CheckDocument, "@effected/github-actions/CheckDocument", CheckDocumentShape>;
2532
2623
  /**
@@ -3610,11 +3701,13 @@ declare class GitHubToken {
3610
3701
  //#region src/ManagedDocument.d.ts
3611
3702
  declare const ManagedDocumentError_base: Schema.Class<ManagedDocumentError, Schema.TaggedStruct<"ManagedDocumentError", {
3612
3703
  /** Which ambiguity or refusal was found. */
3613
- readonly kind: Schema.Literals<readonly ["unterminatedRegion", "orphanedEnd", "overlappingRegions", "duplicateRegion", "markerInContent", "duplicateDeclaration"]>;
3704
+ readonly kind: Schema.Literals<readonly ["unterminatedRegion", "orphanedEnd", "overlappingRegions", "duplicateRegion", "markerInContent", "duplicateDeclaration", "invalidAttribute"]>;
3614
3705
  /** 1-based line of the offending marker, for the structural kinds. */
3615
3706
  readonly line: Schema.optionalKey<Schema.Number>;
3616
3707
  /** The region key involved, when the failure names one. */
3617
3708
  readonly key: Schema.optionalKey<Schema.String>;
3709
+ /** The offending metadata attribute's name, for `invalidAttribute`. */
3710
+ readonly attribute: Schema.optionalKey<Schema.String>;
3618
3711
  }>, import("effect/Cause").YieldableError>;
3619
3712
  /**
3620
3713
  * Raised when a managed document cannot be read or regenerated without
@@ -3624,10 +3717,14 @@ declare const ManagedDocumentError_base: Schema.Class<ManagedDocumentError, Sche
3624
3717
  * The four structural kinds (`unterminatedRegion`, `orphanedEnd`,
3625
3718
  * `overlappingRegions`, `duplicateRegion`) surface at parse time and name an
3626
3719
  * ambiguity whose silent resolution would corrupt content a human wrote by
3627
- * hand. The two declaration kinds surface when regions are applied:
3720
+ * hand. The three declaration kinds surface when regions are applied:
3628
3721
  * `markerInContent` refuses region content that itself contains a line the
3629
- * scanner would read as a region marker, and `duplicateDeclaration` refuses
3630
- * one call that states two intentions for the same region.
3722
+ * scanner would read as a region marker, `duplicateDeclaration` refuses
3723
+ * one call that states two intentions for the same region, and
3724
+ * `invalidAttribute` refuses region metadata whose name is outside the
3725
+ * `[A-Za-z][A-Za-z0-9_-]*` grammar or whose value contains `"` or a line
3726
+ * break — either would write a marker the region scanner could not read
3727
+ * back verbatim.
3631
3728
  *
3632
3729
  * @public
3633
3730
  */
@@ -3729,28 +3826,58 @@ declare class ManagedDocument extends ManagedDocument_base {
3729
3826
  matches(text: string): boolean;
3730
3827
  /** The content of one region, if the document carries it. */
3731
3828
  region(key: string): Option.Option<string>;
3732
- /** Every region this document carries, in document order. */
3829
+ /**
3830
+ * One region's content and metadata together, if the document carries it.
3831
+ *
3832
+ * @remarks
3833
+ * `meta` is the region's `name="value"` marker attributes, empty when the
3834
+ * marker carries none. Metadata never participates in region
3835
+ * addressability — this looks up by key exactly as
3836
+ * {@link ManagedDocument.region} does.
3837
+ */
3838
+ entry(key: string): Option.Option<{
3839
+ readonly content: string;
3840
+ readonly meta: Readonly<Record<string, string>>;
3841
+ }>;
3842
+ /**
3843
+ * Every region this document carries, in document order.
3844
+ *
3845
+ * @remarks
3846
+ * `meta` is always present: a region whose marker carries no attributes
3847
+ * reports an empty record, so callers never branch on absence.
3848
+ */
3733
3849
  get regions(): ReadonlyArray<{
3734
3850
  readonly key: string;
3735
3851
  readonly content: string;
3852
+ readonly meta: Readonly<Record<string, string>>;
3736
3853
  }>;
3737
3854
  /**
3738
3855
  * The document with these regions replaced from current state.
3739
3856
  *
3740
3857
  * @remarks
3741
3858
  * The synchronous primitive; {@link ManagedDocument.withRegions} derives
3742
- * from it. Each entry pairs a region key with its full new content. A
3743
- * region already present is **replaced in place**; a region not present
3744
- * yet is added; regions not named are left exactly as they are; and every
3745
- * byte outside a managed region survives untouched. Applying entries that
3746
- * change nothing returns byte-identical text, which is what lets a caller
3747
- * compare-and-skip instead of issuing a write.
3859
+ * from it. Each entry pairs a region key with its full new content, plus
3860
+ * optional metadata. A region already present is **replaced in place**; a
3861
+ * region not present yet is added; regions not named are left exactly as
3862
+ * they are; and every byte outside a managed region survives untouched.
3863
+ * Applying entries that change nothing returns byte-identical text, which
3864
+ * is what lets a caller compare-and-skip instead of issuing a write.
3865
+ *
3866
+ * `meta` becomes `name="value"` attributes on the region's BEGIN marker.
3867
+ * It round-trips verbatim through {@link ManagedDocument.entry}, survives
3868
+ * writes by parties that do not know about it — a region not named in a
3869
+ * call keeps its content *and* its metadata untouched — and never
3870
+ * participates in region addressability: a region is found by key alone,
3871
+ * so changing metadata updates the region in place. A two-element entry
3872
+ * behaves exactly as it always has. Names must match
3873
+ * `[A-Za-z][A-Za-z0-9_-]*` and values may not contain `"` or a line
3874
+ * break; violations fail typed as `invalidAttribute`.
3748
3875
  *
3749
3876
  * Region keys share the name grammar `namespace` and `key` obey; a key
3750
3877
  * outside it is a wiring error and throws at construction rather than
3751
3878
  * surfacing in the typed channel.
3752
3879
  */
3753
- withRegionsResult(entries: ReadonlyArray<readonly [key: string, content: string]>): Result.Result<ManagedDocument, ManagedDocumentError>;
3880
+ withRegionsResult(entries: ReadonlyArray<readonly [key: string, content: string, meta?: Readonly<Record<string, string>>]>): Result.Result<ManagedDocument, ManagedDocumentError>;
3754
3881
  /**
3755
3882
  * The document with these regions replaced, in `Effect`.
3756
3883
  *
@@ -3758,7 +3885,7 @@ declare class ManagedDocument extends ManagedDocument_base {
3758
3885
  * Defined in terms of {@link ManagedDocument.withRegionsResult} —
3759
3886
  * synchronous callers can use that variant directly.
3760
3887
  */
3761
- withRegions(entries: ReadonlyArray<readonly [key: string, content: string]>): Effect.Effect<ManagedDocument, ManagedDocumentError>;
3888
+ withRegions(entries: ReadonlyArray<readonly [key: string, content: string, meta?: Readonly<Record<string, string>>]>): Effect.Effect<ManagedDocument, ManagedDocumentError>;
3762
3889
  /** The marker key a region is written under: `namespace.key.region`. */
3763
3890
  private wireKey;
3764
3891
  /** The region key back out of a wire key, for error reporting. */
@@ -4413,5 +4540,5 @@ declare class Secret {
4413
4540
  static readonly adopt: (name: string) => Config.Config<Redacted.Redacted<string>>;
4414
4541
  }
4415
4542
  //#endregion
4416
- export { Action, ActionCache, ActionCacheError, type ActionCacheShape, ActionEnvironment, ActionEnvironmentError, type ActionEnvironmentShape, ActionInput, ActionLogger, type ActionLoggerShape, ActionOutputError, ActionOutputs, type ActionOutputsShape, type ActionRunOptions, ActionRuntime, type ActionServices, ActionState, ActionStateError, type ActionStateShape, ActionsIdentityToken, ActionsProvenance, AmbientPackageManager, type AnnotationProperties, Artifact, ArtifactError, type ArtifactItem, type ArtifactRef, type ArtifactShape, type Blob, BlobEnvelope, BlobEnvelopeError, BlobStore, BlobStoreError, type BlobStoreShape, BlobTransferError, CacheKey, CacheKeyError, CachedPackageManager, CheckDocument, CheckDocumentError, type CheckDocumentOptions, type CheckDocumentShape, type CheckDocumentSink, CheckReport, type CheckRunConclusion, type CheckRunProjection, CheckState, ChildEnv, type ClientLayerOptions, type DataBlobTransfer, DetachedProcess, DetachedProcessError, type DetachedProcessOps, type DetachedSpawnOptions, type DownloadOptions, type DownloadResult, DryRun, type DryRunShape, type ExtractOptions, type FileBlobTransfer, GitHubCacheBlobStore, GitHubContext, type GitHubHeadingDepth, type GitHubListOptions, GitHubMarkdown, type GitHubRowSchema, type GitHubSchemaTable, type GitHubSchemaTableColumn, type GitHubSchemaTableColumns, type GitHubSchemaTableFormatRequiredKeys, type GitHubSchemaTableFormattedColumn, type GitHubSchemaTableOptions, GitHubToken, GitHubTokenError, InstalledPackageManager, ManagedDocument, ManagedDocumentError, type ManagedDocumentSource, OidcClaims, OidcTokenError, OidcTokenIssuer, type OidcTokenIssuerShape, type PackageManagerInstallOptions, PackageManagerInstaller, PackageManagerInstallerError, type PackageManagerInstallerShape, type PathPrependEnv, type PathPrependOptions, ProcessId, type ProvisionFileOptions, type ProvisionOptions, type ProvisionedFile, type ReadOptions, type ReadinessOptions, RunnerContext, type S3Config, Secret, type ToolDownloadOptions, ToolInstaller, ToolInstallerError, type ToolInstallerShape, type UploadOptions, type UploadResult, type WithBufferOptions, type WithStepOptions, WorkflowCommand, describeCause, projectCheckState };
4543
+ export { Action, ActionCache, ActionCacheError, type ActionCacheShape, ActionEnvironment, ActionEnvironmentError, type ActionEnvironmentShape, ActionInput, ActionLogger, type ActionLoggerShape, ActionOutputError, ActionOutputs, type ActionOutputsShape, type ActionRunOptions, ActionRuntime, type ActionServices, ActionState, ActionStateError, type ActionStateShape, ActionsIdentityToken, ActionsProvenance, AmbientPackageManager, type AnnotationProperties, Artifact, ArtifactError, type ArtifactItem, type ArtifactRef, type ArtifactShape, type Blob, BlobEnvelope, BlobEnvelopeError, BlobStore, BlobStoreError, type BlobStoreShape, BlobTransferError, CacheKey, CacheKeyError, CachedPackageManager, CheckDocument, CheckDocumentError, type CheckDocumentOptions, type CheckDocumentShape, type CheckDocumentSink, CheckDocumentStamp, type CheckFlushOutcome, CheckReport, type CheckRunConclusion, type CheckRunProjection, CheckState, ChildEnv, type ClientLayerOptions, type DataBlobTransfer, DetachedProcess, DetachedProcessError, type DetachedProcessOps, type DetachedSpawnOptions, type DownloadOptions, type DownloadResult, DryRun, type DryRunShape, type ExtractOptions, type FileBlobTransfer, GitHubCacheBlobStore, GitHubContext, type GitHubHeadingDepth, type GitHubListOptions, GitHubMarkdown, type GitHubRowSchema, type GitHubSchemaTable, type GitHubSchemaTableColumn, type GitHubSchemaTableColumns, type GitHubSchemaTableFormatRequiredKeys, type GitHubSchemaTableFormattedColumn, type GitHubSchemaTableOptions, GitHubToken, GitHubTokenError, InstalledPackageManager, ManagedDocument, ManagedDocumentError, type ManagedDocumentSource, OidcClaims, OidcTokenError, OidcTokenIssuer, type OidcTokenIssuerShape, type PackageManagerInstallOptions, PackageManagerInstaller, PackageManagerInstallerError, type PackageManagerInstallerShape, type PathPrependEnv, type PathPrependOptions, ProcessId, type ProvisionFileOptions, type ProvisionOptions, type ProvisionedFile, type ReadOptions, type ReadinessOptions, RunnerContext, type S3Config, Secret, type ToolDownloadOptions, ToolInstaller, ToolInstallerError, type ToolInstallerShape, type UploadOptions, type UploadResult, type WithBufferOptions, type WithStepOptions, WorkflowCommand, describeCause, projectCheckState };
4417
4544
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -18,7 +18,7 @@ import { GitHubCacheBlobStore } from "./BlobStore.githubCache.js";
18
18
  import { CacheKey, CacheKeyError } from "./CacheKey.js";
19
19
  import { CheckState, projectCheckState } from "./CheckState.js";
20
20
  import { ManagedDocument, ManagedDocumentError } from "./ManagedDocument.js";
21
- import { CheckDocument, CheckDocumentError, CheckReport } from "./CheckDocument.js";
21
+ import { CheckDocument, CheckDocumentError, CheckDocumentStamp, CheckReport } from "./CheckDocument.js";
22
22
  import { ChildEnv } from "./ChildEnv.js";
23
23
  import { DetachedProcess, DetachedProcessError, ProcessId } from "./DetachedProcess.js";
24
24
  import { DryRun } from "./DryRun.js";
@@ -27,4 +27,4 @@ import { GitHubToken, GitHubTokenError } from "./GitHubToken.js";
27
27
  import { ToolInstaller, ToolInstallerError } from "./ToolInstaller.js";
28
28
  import { AmbientPackageManager, CachedPackageManager, InstalledPackageManager, PackageManagerInstaller, PackageManagerInstallerError } from "./PackageManagerInstaller.js";
29
29
 
30
- export { Action, ActionCache, ActionCacheError, ActionEnvironment, ActionEnvironmentError, ActionInput, ActionLogger, ActionOutputError, ActionOutputs, ActionRuntime, ActionState, ActionStateError, ActionsIdentityToken, ActionsProvenance, AmbientPackageManager, Artifact, ArtifactError, BlobEnvelope, BlobEnvelopeError, BlobStore, BlobStoreError, BlobTransferError, CacheKey, CacheKeyError, CachedPackageManager, CheckDocument, CheckDocumentError, CheckReport, CheckState, ChildEnv, DetachedProcess, DetachedProcessError, DryRun, GitHubCacheBlobStore, GitHubContext, GitHubMarkdown, GitHubToken, GitHubTokenError, InstalledPackageManager, ManagedDocument, ManagedDocumentError, OidcClaims, OidcTokenError, OidcTokenIssuer, PackageManagerInstaller, PackageManagerInstallerError, ProcessId, RunnerContext, Secret, ToolInstaller, ToolInstallerError, WorkflowCommand, describeCause, projectCheckState };
30
+ export { Action, ActionCache, ActionCacheError, ActionEnvironment, ActionEnvironmentError, ActionInput, ActionLogger, ActionOutputError, ActionOutputs, ActionRuntime, ActionState, ActionStateError, ActionsIdentityToken, ActionsProvenance, AmbientPackageManager, Artifact, ArtifactError, BlobEnvelope, BlobEnvelopeError, BlobStore, BlobStoreError, BlobTransferError, CacheKey, CacheKeyError, CachedPackageManager, CheckDocument, CheckDocumentError, CheckDocumentStamp, CheckReport, CheckState, ChildEnv, DetachedProcess, DetachedProcessError, DryRun, GitHubCacheBlobStore, GitHubContext, GitHubMarkdown, GitHubToken, GitHubTokenError, InstalledPackageManager, ManagedDocument, ManagedDocumentError, OidcClaims, OidcTokenError, OidcTokenIssuer, PackageManagerInstaller, PackageManagerInstallerError, ProcessId, RunnerContext, Secret, ToolInstaller, ToolInstallerError, WorkflowCommand, describeCause, projectCheckState };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/github-actions",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "private": false,
5
5
  "description": "The GitHub Actions runtime: env, inputs, outputs, state, logging, cache, artifacts, blob storage, OIDC and the token bridge.",
6
6
  "keywords": [
@@ -39,12 +39,12 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@azure/storage-blob": "^12.33.0",
42
- "@effected/github": "^0.5.0",
42
+ "@effected/github": "^0.7.0",
43
43
  "@effected/glob": "^0.4.0",
44
44
  "@effected/markdown": "^0.6.0",
45
- "@effected/npm": "^0.10.0",
46
- "@effected/sbom": "^0.4.0",
47
- "@effected/templates": "^0.3.0"
45
+ "@effected/npm": "^0.11.0",
46
+ "@effected/sbom": "^0.4.1",
47
+ "@effected/templates": "^0.4.0"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@effect/platform-node": "4.0.0-rc.109",