@tomflow/proflow-task-orchestration 0.1.12 → 0.1.14

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.
@@ -0,0 +1,2 @@
1
+ /** Only owner-generated, flat Markdown files under the workspace are accepted. */
2
+ export declare function productDocumentAbsolute(workspaceRoot: string, filePath: string): string;
@@ -0,0 +1,24 @@
1
+ import { lstatSync, realpathSync } from "node:fs";
2
+ import { resolve, sep } from "node:path";
3
+ /** Only owner-generated, flat Markdown files under the workspace are accepted. */
4
+ export function productDocumentAbsolute(workspaceRoot, filePath) {
5
+ if (!/^product-requirements\/[a-f0-9]{24}\.md$/.test(filePath))
6
+ throw new Error("PRODUCT_DOCUMENT_PATH_INVALID");
7
+ const workspace = realpathSync(workspaceRoot);
8
+ const directory = resolve(workspace, "product-requirements");
9
+ const absolute = resolve(workspace, filePath);
10
+ for (const path of [directory, absolute]) {
11
+ try {
12
+ const stat = lstatSync(path);
13
+ if (stat.isSymbolicLink() || (path === directory ? !stat.isDirectory() : !stat.isFile()))
14
+ throw new Error("PRODUCT_DOCUMENT_PATH_INVALID");
15
+ if (!realpathSync(path).startsWith(`${workspace}${sep}`))
16
+ throw new Error("PRODUCT_DOCUMENT_PATH_INVALID");
17
+ }
18
+ catch (error) {
19
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
20
+ throw error;
21
+ }
22
+ }
23
+ return absolute;
24
+ }
@@ -77,6 +77,7 @@ export interface CompleteNodeResult {
77
77
  nextNodeId: string | null;
78
78
  }
79
79
  export interface DocumentResult {
80
+ documentRef: string;
80
81
  taskId: string;
81
82
  nodeId: string | null;
82
83
  documentType: string;
@@ -84,6 +85,11 @@ export interface DocumentResult {
84
85
  contentHash: string;
85
86
  sizeBytes: number;
86
87
  taskVersion: number;
88
+ revision: number;
89
+ sourceNodeRunNo: number | null;
90
+ sourceDocumentRef: string | null;
91
+ sourceDocumentRevision: number | null;
92
+ updatedByRef: string;
87
93
  updatedAt: string;
88
94
  content?: string;
89
95
  }
@@ -150,10 +156,18 @@ export declare function createTaskServices(options: {
150
156
  task: Pick<Task, "taskId" | "title" | "objective" | "status" | "version">;
151
157
  node: Pick<TaskNode, "nodeId" | "title" | "objective" | "status" | "version" | "runNo" | "requiredAgentPackageRef" | "workerRef" | "inputDocuments" | "outputDocuments">;
152
158
  documents: Array<{
159
+ documentRef: string;
153
160
  documentType: string;
154
161
  path: string;
155
162
  contentHash: string;
156
163
  sizeBytes: number;
164
+ revision: number;
165
+ sourceNodeId: string | null;
166
+ sourceNodeRunNo: number | null;
167
+ sourceDocumentRef: string | null;
168
+ sourceDocumentRevision: number | null;
169
+ updatedByRef: string;
170
+ updatedAt: string;
157
171
  }>;
158
172
  }>;
159
173
  listPendingMessages: (raw: unknown) => TaskResult<{
@@ -3,7 +3,7 @@ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readdirSync, rea
3
3
  import { dirname, join } from "node:path";
4
4
  import { ZodError } from "zod";
5
5
  import { validatePublicInput } from "./contracts.js";
6
- import { requiredTaskAgentPackageRefs } from "./model.js";
6
+ import { requiredTaskAgentPackageRefs, taskDocumentRef } from "./model.js";
7
7
  const allowedDocumentTypes = [
8
8
  "REQUIREMENT",
9
9
  "PRD",
@@ -590,9 +590,14 @@ export function createTaskServices(options) {
590
590
  for (const item of validated.initialDocuments) {
591
591
  const path = relativeDocumentPath(taskId, item.documentType);
592
592
  tx.documents.upsert({
593
+ documentRef: taskDocumentRef(taskId, item.documentType),
593
594
  taskId,
594
595
  documentType: item.documentType,
595
596
  sourceNodeId: null,
597
+ sourceNodeRunNo: null,
598
+ revision: 1,
599
+ sourceDocumentRef: null,
600
+ sourceDocumentRevision: null,
596
601
  filePath: path,
597
602
  contentHash: contentHash(item.content),
598
603
  updatedByRef: validated.actorRef,
@@ -848,6 +853,12 @@ export function createTaskServices(options) {
848
853
  const doc = tx.documents.get(task.taskId, type);
849
854
  if (!doc || !existsSync(join(workspaceRoot, doc.filePath)))
850
855
  throw new DomainError("NODE_OUTPUT_MISSING", "A required output document is missing.");
856
+ if (doc.documentRef !== taskDocumentRef(task.taskId, type) ||
857
+ doc.sourceNodeId !== node.nodeId ||
858
+ doc.sourceNodeRunNo !== node.runNo ||
859
+ doc.updatedByRef !== input.actorRef ||
860
+ doc.revision < 1)
861
+ throw new DomainError("NODE_OUTPUT_EVIDENCE_INVALID", "A required output document is not evidence from the current Node run.");
851
862
  if (contentHash(readFileSync(join(workspaceRoot, doc.filePath), "utf8")) !== doc.contentHash)
852
863
  throw new DomainError("DOCUMENT_INDEX_MISMATCH", "A required output document does not match its index.");
853
864
  }
@@ -1199,10 +1210,18 @@ export function createTaskServices(options) {
1199
1210
  .map((type) => tx.documents.get(task.taskId, type))
1200
1211
  .filter((item) => item !== undefined)
1201
1212
  .map((document) => ({
1213
+ documentRef: document.documentRef,
1202
1214
  documentType: document.documentType,
1203
1215
  path: document.filePath,
1204
1216
  contentHash: document.contentHash,
1205
1217
  sizeBytes: statSync(join(workspaceRoot, document.filePath)).size,
1218
+ revision: document.revision,
1219
+ sourceNodeId: document.sourceNodeId,
1220
+ sourceNodeRunNo: document.sourceNodeRunNo,
1221
+ sourceDocumentRef: document.sourceDocumentRef,
1222
+ sourceDocumentRevision: document.sourceDocumentRevision,
1223
+ updatedByRef: document.updatedByRef,
1224
+ updatedAt: document.updatedAt,
1206
1225
  }));
1207
1226
  return {
1208
1227
  task: {
@@ -1247,6 +1266,7 @@ export function createTaskServices(options) {
1247
1266
  const content = readFileSync(join(workspaceRoot, document.filePath), "utf8");
1248
1267
  const task = requireTask(tx, input.taskId);
1249
1268
  return {
1269
+ documentRef: document.documentRef,
1250
1270
  taskId: input.taskId,
1251
1271
  nodeId: document.sourceNodeId,
1252
1272
  documentType: type,
@@ -1254,6 +1274,11 @@ export function createTaskServices(options) {
1254
1274
  contentHash: document.contentHash,
1255
1275
  sizeBytes: statSync(join(workspaceRoot, document.filePath)).size,
1256
1276
  taskVersion: task.version,
1277
+ revision: document.revision,
1278
+ sourceNodeRunNo: document.sourceNodeRunNo,
1279
+ sourceDocumentRef: document.sourceDocumentRef,
1280
+ sourceDocumentRevision: document.sourceDocumentRevision,
1281
+ updatedByRef: document.updatedByRef,
1257
1282
  updatedAt: document.updatedAt,
1258
1283
  content,
1259
1284
  };
@@ -1301,9 +1326,23 @@ export function createTaskServices(options) {
1301
1326
  const result = command("putTaskDocument", raw, (tx, validated, timestamp) => {
1302
1327
  const task = requireTask(tx, validated.taskId);
1303
1328
  checkVersion(task.version, validated.expectedTaskVersion, "TASK_VERSION_CONFLICT");
1304
- if (validated.nodeId !== null)
1305
- requireNode(tx, task.taskId, validated.nodeId);
1306
1329
  const validatedType = ensureType(validated.documentType);
1330
+ let sourceNodeRunNo = null;
1331
+ if (validated.nodeId !== null) {
1332
+ const producer = requireNode(tx, task.taskId, validated.nodeId);
1333
+ if (task.status !== "ACTIVE" ||
1334
+ task.currentNodeId !== producer.nodeId ||
1335
+ producer.status !== "IN_PROGRESS")
1336
+ throw new DomainError("NODE_NOT_CURRENT", "Only the current IN_PROGRESS Node may produce Task evidence.");
1337
+ if (!matchesWorker(validated.actorRef, producer.workerRef))
1338
+ throw new DomainError("WORKER_MISMATCH", "Only the bound current Worker may produce Task evidence.");
1339
+ if (!producer.outputDocuments.includes(validatedType))
1340
+ throw new DomainError("NODE_OUTPUT_DOCUMENT_NOT_DECLARED", "Document type is not declared as an output of the current Node.");
1341
+ sourceNodeRunNo = producer.runNo;
1342
+ }
1343
+ else if (task.status !== "PENDING") {
1344
+ throw new DomainError("TASK_INVALID_STATE", "Node-less Task documents can only be written while the Task is PENDING.");
1345
+ }
1307
1346
  const path = relativeDocumentPath(task.taskId, validatedType);
1308
1347
  const staged = `${documentUpdateRecoveryPath(task.taskId, validatedType, validated.idempotencyKey)}/next.md`;
1309
1348
  // BEGIN IMMEDIATE + optimistic version validation happens before the
@@ -1317,10 +1356,18 @@ export function createTaskServices(options) {
1317
1356
  updatedAt: timestamp,
1318
1357
  };
1319
1358
  tx.tasks.update(updated);
1359
+ const currentDocument = tx.documents.get(task.taskId, validatedType);
1360
+ const documentRevision = (currentDocument?.revision ?? 0) + 1;
1361
+ const documentRef = taskDocumentRef(task.taskId, validatedType);
1320
1362
  tx.documents.upsert({
1363
+ documentRef,
1321
1364
  taskId: task.taskId,
1322
1365
  documentType: validatedType,
1323
1366
  sourceNodeId: validated.nodeId,
1367
+ sourceNodeRunNo,
1368
+ revision: documentRevision,
1369
+ sourceDocumentRef: null,
1370
+ sourceDocumentRevision: null,
1324
1371
  filePath: path,
1325
1372
  contentHash: contentHash(validated.content),
1326
1373
  updatedByRef: validated.actorRef,
@@ -1336,9 +1383,15 @@ export function createTaskServices(options) {
1336
1383
  actorRef: validated.actorRef,
1337
1384
  taskVersion: readied.version,
1338
1385
  nodeVersion: null,
1339
- payload: { documentType: validatedType },
1386
+ payload: {
1387
+ documentType: validatedType,
1388
+ documentRef,
1389
+ revision: documentRevision,
1390
+ sourceNodeRunNo,
1391
+ },
1340
1392
  }, timestamp);
1341
1393
  return {
1394
+ documentRef,
1342
1395
  taskId: task.taskId,
1343
1396
  nodeId: validated.nodeId,
1344
1397
  documentType: validatedType,
@@ -1346,6 +1399,11 @@ export function createTaskServices(options) {
1346
1399
  contentHash: contentHash(validated.content),
1347
1400
  sizeBytes: Buffer.byteLength(validated.content),
1348
1401
  taskVersion: readied.version,
1402
+ revision: documentRevision,
1403
+ sourceNodeRunNo,
1404
+ sourceDocumentRef: null,
1405
+ sourceDocumentRevision: null,
1406
+ updatedByRef: validated.actorRef,
1349
1407
  updatedAt: timestamp,
1350
1408
  };
1351
1409
  });
@@ -1373,31 +1431,16 @@ export function createTaskServices(options) {
1373
1431
  promoteDocument(staged, relativeDocumentPath(input.taskId, type));
1374
1432
  }
1375
1433
  cleanupCreateStage(input.taskId);
1376
- const result = store.transaction((tx) => {
1434
+ const result = store.read((tx) => {
1377
1435
  requireTask(tx, input.taskId);
1378
- let reconciled = 0;
1379
- for (const type of allowedDocumentTypes) {
1380
- const path = relativeDocumentPath(input.taskId, type);
1381
- const absolute = join(workspaceRoot, path);
1436
+ for (const document of tx.documents.listByTask(input.taskId)) {
1437
+ const absolute = join(workspaceRoot, document.filePath);
1382
1438
  if (!existsSync(absolute))
1383
- continue;
1384
- const content = readFileSync(absolute, "utf8");
1385
- const current = tx.documents.get(input.taskId, type);
1386
- const digest = contentHash(content);
1387
- if (!current || current.contentHash !== digest) {
1388
- tx.documents.upsert({
1389
- taskId: input.taskId,
1390
- documentType: type,
1391
- sourceNodeId: current?.sourceNodeId ?? null,
1392
- filePath: path,
1393
- contentHash: digest,
1394
- updatedByRef: input.actorRef,
1395
- updatedAt: now(),
1396
- });
1397
- reconciled++;
1398
- }
1439
+ throw new DomainError("DOCUMENT_INDEX_MISMATCH", "Canonical Task document is missing.");
1440
+ if (contentHash(readFileSync(absolute, "utf8")) !== document.contentHash)
1441
+ throw new DomainError("DOCUMENT_INDEX_MISMATCH", "Canonical Task document does not match durable evidence metadata.");
1399
1442
  }
1400
- return { reconciled };
1443
+ return { reconciled: 0 };
1401
1444
  });
1402
1445
  return success(result);
1403
1446
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomflow/proflow-task-orchestration",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -23,9 +23,9 @@
23
23
  "@tomflow/proflow-module-contract": "^0.1.13"
24
24
  },
25
25
  "devDependencies": {
26
- "@tomflow/proflow-deployment-conformance": "^0.1.13",
26
+ "@tomflow/proflow-task-store-sqlite": "^0.1.13",
27
27
  "@tomflow/proflow-task-migration-runner": "^0.1.11",
28
- "@tomflow/proflow-task-store-sqlite": "^0.1.12"
28
+ "@tomflow/proflow-deployment-conformance": "^0.1.13"
29
29
  },
30
30
  "description": "Owns the Task & Orchestration public contract, domain rules and application services.",
31
31
  "keywords": [
@@ -3,7 +3,7 @@
3
3
  "contractVersion": "1.0.0",
4
4
  "moduleRef": "task-orchestration",
5
5
  "packageName": "@tomflow/proflow-task-orchestration",
6
- "moduleVersion": "0.1.12",
6
+ "moduleVersion": "0.1.14",
7
7
  "kind": "library",
8
8
  "templateVersion": "1.0.0",
9
9
  "platformCompatibility": ">=1.0.0 <2.0.0",