@afokapu/atdd-flow 0.3.28 → 0.3.29

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/threads.ts +34 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-flow",
3
- "version": "0.3.28",
3
+ "version": "0.3.29",
4
4
  "description": "Filesystem-first Desk registry for projects, seats, tasks, threads, and handoffs.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/threads.ts CHANGED
@@ -90,6 +90,18 @@ type PendingReference = { thread: string; message: string; created_at: string; p
90
90
  type PendingSegment = { schema: "atdd-flow/pi-inbox-segment/v1"; entries: PendingReference[]; next?: string };
91
91
  type PendingQueue = { schema: "atdd-flow/pi-inbox-queue/v1"; head?: string; tail?: string };
92
92
 
93
+ function isPendingQueue(value: unknown): value is Required<PendingQueue> {
94
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
95
+ const queue = value as PendingQueue;
96
+ return queue.schema === "atdd-flow/pi-inbox-queue/v1"
97
+ && typeof queue.head === "string" && queue.head.length > 0
98
+ && typeof queue.tail === "string" && queue.tail.length > 0;
99
+ }
100
+
101
+ function missingFile(error: unknown) {
102
+ return (error as NodeJS.ErrnoException).code === "ENOENT";
103
+ }
104
+
93
105
  function inboxDirectory(root: string, address: string) {
94
106
  return join(root, ".atdd-flow", "pi-inbox", encodeURIComponent(address));
95
107
  }
@@ -129,8 +141,16 @@ export async function enqueueNativeMail(root: string, address: string, reference
129
141
  return withInboxLock(directory, async () => {
130
142
  const statePath = join(directory, "queue.yaml");
131
143
  let queue: PendingQueue;
132
- try { queue = await readYaml<PendingQueue>(statePath); }
133
- catch { queue = { schema: "atdd-flow/pi-inbox-queue/v1" }; }
144
+ try {
145
+ queue = await readYaml<PendingQueue>(statePath);
146
+ if (!isPendingQueue(queue)) throw new Error(`Malformed pending inbox queue ${statePath}`);
147
+ } catch (error) {
148
+ // A reconciler deletes the index once it drains the final segment. Only
149
+ // that absent-index state may be initialized by normal enqueue; parse,
150
+ // permission, and competing-state failures remain fail-closed.
151
+ if (!missingFile(error)) throw error;
152
+ queue = { schema: "atdd-flow/pi-inbox-queue/v1" };
153
+ }
134
154
  const entriesPath = (segment: string) => join(directory, "pending", `${segment}.yaml`);
135
155
  const compare = (left: PendingReference, right: PendingReference) => left.created_at.localeCompare(right.created_at) || left.message.localeCompare(right.message);
136
156
  const capacity = 32;
@@ -176,10 +196,20 @@ export async function enqueueNativeMail(root: string, address: string, reference
176
196
  });
177
197
  }
178
198
 
179
- async function publishNativeMail(root: string, address: string, _segment: string, message: string) {
199
+ export async function publishNativeMail(root: string, address: string, _segment: string, message: string) {
180
200
  const directory = inboxDirectory(root, address);
181
201
  await withInboxLock(directory, async () => {
182
- const queue = await readYaml<PendingQueue>(join(directory, "queue.yaml"));
202
+ const statePath = join(directory, "queue.yaml");
203
+ let queue: PendingQueue;
204
+ try { queue = await readYaml<PendingQueue>(statePath); }
205
+ catch (error) {
206
+ // A native reconciler may consume the newly authoritative message and
207
+ // drain the queue between prepare and this advisory publish marker.
208
+ // Its deletion is a successful exactly-once delivery, not an error.
209
+ if (missingFile(error)) return;
210
+ throw error;
211
+ }
212
+ if (!isPendingQueue(queue)) throw new Error(`Malformed pending inbox queue ${statePath}`);
183
213
  let segment = queue.head;
184
214
  while (segment) {
185
215
  const path = join(directory, "pending", `${segment}.yaml`);