@abelxiaoxing/cadence 1.2.2 → 1.2.3

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/src/contracts.ts CHANGED
@@ -1102,8 +1102,258 @@ function validateTaskBoundary(
1102
1102
  }
1103
1103
 
1104
1104
  export type ImplementGraphContractDiagnostic =
1105
- | { code: "invalid-implement-graph" }
1106
- | { code: "invalid-output-path"; outputId?: string };
1105
+ | {
1106
+ code: "invalid-implement-graph";
1107
+ taskId?: string;
1108
+ phase?: ImplementationPhase;
1109
+ field?: string;
1110
+ category?: string;
1111
+ outputId?: string;
1112
+ }
1113
+ | {
1114
+ code: "invalid-output-path";
1115
+ outputId?: string;
1116
+ field: "outputs.path";
1117
+ category: "path";
1118
+ };
1119
+
1120
+ function phaseBoundaryContractDiagnostic(
1121
+ value: unknown,
1122
+ phase: ImplementationPhase,
1123
+ ): Omit<
1124
+ Extract<
1125
+ ImplementGraphContractDiagnostic,
1126
+ { code: "invalid-implement-graph" }
1127
+ >,
1128
+ "code"
1129
+ > | null {
1130
+ const prefix = `phases.${phase}`;
1131
+ if (
1132
+ !hasExactKeys(
1133
+ value,
1134
+ ["read", "write", "delete", "verification", "verificationInputs"],
1135
+ ["verificationLock"],
1136
+ )
1137
+ ) {
1138
+ return { phase, field: prefix, category: "shape" };
1139
+ }
1140
+ if (!validatePathSet(value.read)) {
1141
+ return { phase, field: `${prefix}.read`, category: "path-set" };
1142
+ }
1143
+ if (!validatePathSet(value.write) || value.write.some(isAgentsPath)) {
1144
+ return { phase, field: `${prefix}.write`, category: "path-set" };
1145
+ }
1146
+ if (!validatePathSet(value.delete) || value.delete.some(isAgentsPath)) {
1147
+ return { phase, field: `${prefix}.delete`, category: "path-set" };
1148
+ }
1149
+ const deletions = value.delete as string[];
1150
+ if (value.write.some((candidate) => deletions.includes(candidate))) {
1151
+ return { phase, field: prefix, category: "write-delete-overlap" };
1152
+ }
1153
+ if (!validateVerificationInputBindings(value.verificationInputs)) {
1154
+ return {
1155
+ phase,
1156
+ field: `${prefix}.verificationInputs`,
1157
+ category: "verification-input-binding",
1158
+ };
1159
+ }
1160
+ if (
1161
+ value.verificationLock !== undefined &&
1162
+ !isValidRelativePath(value.verificationLock)
1163
+ ) {
1164
+ return {
1165
+ phase,
1166
+ field: `${prefix}.verificationLock`,
1167
+ category: "path",
1168
+ };
1169
+ }
1170
+ const verification = validateVerificationContract(value.verification);
1171
+ if (!verification.ok) {
1172
+ return {
1173
+ phase,
1174
+ field: `${prefix}.verification`,
1175
+ category: "verification-contract",
1176
+ };
1177
+ }
1178
+ const classification = {
1179
+ red: "expected-red",
1180
+ green: "expected-green",
1181
+ refactor: "expected-refactor",
1182
+ } as const;
1183
+ if (verification.value.classification !== classification[phase]) {
1184
+ return {
1185
+ phase,
1186
+ field: `${prefix}.verification.classification`,
1187
+ category: "verification-classification",
1188
+ };
1189
+ }
1190
+ return null;
1191
+ }
1192
+
1193
+ function impactClosureCategory(reason: string): string {
1194
+ const categories: Record<string, string> = {
1195
+ "invalid impact closure": "shape",
1196
+ "invalid impact closure related test": "related-test",
1197
+ "current-task related test is outside the write set":
1198
+ "current-task-outside-write-set",
1199
+ "regression task identity is required": "regression-task-identity",
1200
+ "unexpected regression task identity": "regression-task-identity",
1201
+ "public impact closure is incomplete": "public-impact-incomplete",
1202
+ "affected suite contains no existing test evidence":
1203
+ "existing-test-evidence",
1204
+ };
1205
+ return categories[reason] ?? "contract";
1206
+ }
1207
+
1208
+ function taskBoundaryContractDiagnostic(
1209
+ value: Record<string, unknown>,
1210
+ snapshot: unknown,
1211
+ ): Omit<
1212
+ Extract<
1213
+ ImplementGraphContractDiagnostic,
1214
+ { code: "invalid-implement-graph" }
1215
+ >,
1216
+ "code"
1217
+ > | null {
1218
+ const taskId = validIdentifier(value.taskId) ? value.taskId : undefined;
1219
+ const diagnostic = (
1220
+ field: string,
1221
+ category: string,
1222
+ phase?: ImplementationPhase,
1223
+ ) => ({
1224
+ ...(taskId ? { taskId } : {}),
1225
+ ...(phase ? { phase } : {}),
1226
+ field,
1227
+ category,
1228
+ });
1229
+ if (
1230
+ !hasExactKeys(value, [
1231
+ "changeId",
1232
+ "taskId",
1233
+ "dependsOn",
1234
+ "objective",
1235
+ "context",
1236
+ "roots",
1237
+ "phases",
1238
+ "scheduling",
1239
+ "agents",
1240
+ "approvedDependencies",
1241
+ "impactClosure",
1242
+ ])
1243
+ ) {
1244
+ return diagnostic("task", "shape");
1245
+ }
1246
+ if (!validIdentifier(value.changeId)) {
1247
+ return diagnostic("changeId", "identifier");
1248
+ }
1249
+ if (!validIdentifier(value.taskId)) {
1250
+ return diagnostic("taskId", "identifier");
1251
+ }
1252
+ if (!validateIdentifierSet(value.dependsOn)) {
1253
+ return diagnostic("dependsOn", "identifier-set");
1254
+ }
1255
+ if (
1256
+ typeof value.objective !== "string" ||
1257
+ value.objective.length === 0 ||
1258
+ value.objective.length > 4096
1259
+ ) {
1260
+ return diagnostic("objective", "text-boundary");
1261
+ }
1262
+ if (
1263
+ !hasExactKeys(value.context, ["agents", "contract"]) ||
1264
+ typeof value.context.agents !== "string" ||
1265
+ typeof value.context.contract !== "string"
1266
+ ) {
1267
+ return diagnostic("context", "shape");
1268
+ }
1269
+ if (!validateRoots(value.roots)) {
1270
+ return diagnostic("roots", "path-set");
1271
+ }
1272
+ if (!hasExactKeys(value.phases, ["red", "green"], ["refactor"])) {
1273
+ return diagnostic("phases", "shape");
1274
+ }
1275
+ for (const phase of IMPLEMENTATION_PHASES) {
1276
+ const boundary = value.phases[phase];
1277
+ if (phase === "refactor" && boundary === undefined) continue;
1278
+ const phaseDiagnostic = phaseBoundaryContractDiagnostic(boundary, phase);
1279
+ if (phaseDiagnostic)
1280
+ return { ...diagnostic("phases", "contract"), ...phaseDiagnostic };
1281
+ }
1282
+ if (
1283
+ !hasExactKeys(value.scheduling, ["conflicts", "resources"]) ||
1284
+ !validatePathSet(value.scheduling.conflicts) ||
1285
+ !validatePathSet(value.scheduling.resources)
1286
+ ) {
1287
+ return diagnostic("scheduling", "path-set");
1288
+ }
1289
+ if (
1290
+ !hasExactKeys(value.agents, ["impact", "managedOnly"], ["target"]) ||
1291
+ !(AGENTS_IMPACTS as readonly unknown[]).includes(value.agents.impact) ||
1292
+ value.agents.managedOnly !== true ||
1293
+ (value.agents.impact === "none"
1294
+ ? value.agents.target !== undefined
1295
+ : !isAgentsPath(value.agents.target))
1296
+ ) {
1297
+ return diagnostic("agents", "contract");
1298
+ }
1299
+ if (validateApprovedDependencies(value.approvedDependencies) !== null) {
1300
+ return diagnostic("approvedDependencies", "dependency-set");
1301
+ }
1302
+ const roots = value.roots as string[];
1303
+ for (const phase of IMPLEMENTATION_PHASES) {
1304
+ const boundary = value.phases[phase] as PhaseBoundary | undefined;
1305
+ if (!boundary) continue;
1306
+ for (const [field, candidates] of [
1307
+ ["read", boundary.read],
1308
+ ["write", boundary.write],
1309
+ ["delete", boundary.delete],
1310
+ ["verification", verificationInputPaths(boundary.verification)],
1311
+ ] as const) {
1312
+ if (candidates.some((candidate) => !isWithinRoots(candidate, roots))) {
1313
+ return diagnostic(`phases.${phase}.${field}`, "outside-roots", phase);
1314
+ }
1315
+ }
1316
+ if (boundary.verification.kind !== undefined) {
1317
+ const declared = new Set([...boundary.read, ...boundary.write]);
1318
+ if (
1319
+ verificationInputPaths(boundary.verification).some(
1320
+ (input) => !declared.has(input),
1321
+ )
1322
+ ) {
1323
+ return diagnostic(
1324
+ `phases.${phase}.verification`,
1325
+ "verification-input-not-declared",
1326
+ phase,
1327
+ );
1328
+ }
1329
+ }
1330
+ }
1331
+ const taskPhases = value.phases as Record<
1332
+ ImplementationPhase,
1333
+ PhaseBoundary | undefined
1334
+ >;
1335
+ const phases = IMPLEMENTATION_PHASES.flatMap((phase) => {
1336
+ const boundary = taskPhases[phase];
1337
+ return boundary ? [boundary] : [];
1338
+ });
1339
+ const declared = {
1340
+ read: [...new Set(phases.flatMap((entry) => entry.read))],
1341
+ write: [
1342
+ ...new Set(phases.flatMap((entry) => [...entry.write, ...entry.delete])),
1343
+ ],
1344
+ };
1345
+ const impactReason = validateImpactClosure(
1346
+ value.impactClosure,
1347
+ declared,
1348
+ snapshot,
1349
+ );
1350
+ if (impactReason !== null) {
1351
+ return diagnostic("impactClosure", impactClosureCategory(impactReason));
1352
+ }
1353
+ return validateTaskBoundary(value, snapshot)
1354
+ ? null
1355
+ : diagnostic("task", "contract");
1356
+ }
1107
1357
 
1108
1358
  export function validateImplementGraphBoundary(value: unknown):
1109
1359
  | { ok: true; value: ImplementGraphBoundary }
@@ -1137,36 +1387,29 @@ export function validateImplementGraphBoundary(value: unknown):
1137
1387
  return {
1138
1388
  ok: false,
1139
1389
  reason: "invalid Implement task boundary",
1140
- diagnostic: { code: "invalid-implement-graph" },
1390
+ diagnostic: {
1391
+ code: "invalid-implement-graph",
1392
+ field: "tasks",
1393
+ category: "shape",
1394
+ },
1141
1395
  };
1142
1396
  }
1143
1397
  const task = candidate as Record<string, unknown>;
1144
- const phases = task.phases as Record<string, unknown> | undefined;
1145
- if (
1146
- Object.hasOwn(task, "changeId") ||
1147
- !Object.hasOwn(task, "dependsOn") ||
1148
- !validateIdentifierSet(task.dependsOn) ||
1149
- !phases ||
1150
- !["red", "green"].every((phase) => {
1151
- const boundary = phases[phase];
1152
- return (
1153
- boundary !== null &&
1154
- typeof boundary === "object" &&
1155
- !Array.isArray(boundary) &&
1156
- Object.hasOwn(boundary, "verificationInputs")
1398
+ const diagnostic = Object.hasOwn(task, "changeId")
1399
+ ? {
1400
+ ...(validIdentifier(task.taskId) ? { taskId: task.taskId } : {}),
1401
+ field: "changeId",
1402
+ category: "unexpected-field",
1403
+ }
1404
+ : taskBoundaryContractDiagnostic(
1405
+ { changeId: value.changeId, ...task },
1406
+ undefined,
1157
1407
  );
1158
- }) ||
1159
- (phases.refactor !== undefined &&
1160
- (!phases.refactor ||
1161
- typeof phases.refactor !== "object" ||
1162
- Array.isArray(phases.refactor) ||
1163
- !Object.hasOwn(phases.refactor, "verificationInputs"))) ||
1164
- !validateTaskBoundary({ changeId: value.changeId, ...task }, undefined)
1165
- ) {
1408
+ if (diagnostic) {
1166
1409
  return {
1167
1410
  ok: false,
1168
1411
  reason: "invalid Implement task boundary",
1169
- diagnostic: { code: "invalid-implement-graph" },
1412
+ diagnostic: { code: "invalid-implement-graph", ...diagnostic },
1170
1413
  };
1171
1414
  }
1172
1415
  }
@@ -1191,6 +1434,8 @@ export function validateImplementGraphBoundary(value: unknown):
1191
1434
  diagnostic: {
1192
1435
  code: "invalid-output-path",
1193
1436
  ...(validIdentifier(output.id) ? { outputId: output.id } : {}),
1437
+ field: "outputs.path",
1438
+ category: "path",
1194
1439
  },
1195
1440
  };
1196
1441
  }
@@ -1207,7 +1452,12 @@ export function validateImplementGraphBoundary(value: unknown):
1207
1452
  return {
1208
1453
  ok: false,
1209
1454
  reason: "invalid Implement graph output",
1210
- diagnostic: { code: "invalid-implement-graph" },
1455
+ diagnostic: {
1456
+ code: "invalid-implement-graph",
1457
+ ...(validIdentifier(output.id) ? { outputId: output.id } : {}),
1458
+ field: "outputs",
1459
+ category: "shape",
1460
+ },
1211
1461
  };
1212
1462
  }
1213
1463
  }
@@ -1271,11 +1521,7 @@ export function validatePacketEnvelope(
1271
1521
  ) {
1272
1522
  return { ok: false, reason: "invalid Design run identity" };
1273
1523
  }
1274
- if (
1275
- typeof packet.id !== "string" ||
1276
- packet.id.length === 0 ||
1277
- packet.id.length > 128
1278
- ) {
1524
+ if (!validIdentifier(packet.id)) {
1279
1525
  return { ok: false, reason: "invalid request id" };
1280
1526
  }
1281
1527
  if (
@@ -1729,7 +1975,7 @@ function validateDesignEvidenceResult(r: Record<string, unknown>): {
1729
1975
  if (
1730
1976
  !validIdentifier(r.id) ||
1731
1977
  r.packet_id !== r.id ||
1732
- !validIdentifier(r.module_name)
1978
+ !isValidRelativePath(r.module_name)
1733
1979
  ) {
1734
1980
  return { ok: false, reason: "invalid Design evidence identity" };
1735
1981
  }
@@ -13,6 +13,68 @@ export const CONTROL_COMMANDS = [
13
13
 
14
14
  export const CONTROL_STAGES = ["abel-implement"] as const;
15
15
 
16
+ const CONTROL_SCHEMA_PROPERTIES = {
17
+ command: {
18
+ type: "string",
19
+ enum: ["start", "status", "resume", "rebind", "cancel", "discard"],
20
+ },
21
+ stage: { type: "string", enum: ["abel-implement"] },
22
+ change: { type: "string" },
23
+ operationId: { type: "string" },
24
+ deliveryRevision: { type: "integer", minimum: 1 },
25
+ receiptHash: { type: "string" },
26
+ routeId: { type: "string" },
27
+ } as const;
28
+
29
+ type ControlSchemaField = Exclude<
30
+ keyof typeof CONTROL_SCHEMA_PROPERTIES,
31
+ "command"
32
+ >;
33
+
34
+ const commandSchema = (
35
+ command: ControlCommandName,
36
+ fields: readonly ControlSchemaField[],
37
+ ) => ({
38
+ type: "object" as const,
39
+ properties: {
40
+ command: { type: "string" as const, enum: [command] },
41
+ ...Object.fromEntries(
42
+ fields.map((field) => [field, CONTROL_SCHEMA_PROPERTIES[field]]),
43
+ ),
44
+ },
45
+ required: ["command", ...fields],
46
+ additionalProperties: false,
47
+ });
48
+
49
+ /**
50
+ * Provider-facing schema for the closed Implement command union.
51
+ *
52
+ * The anyOf branches express command-specific required fields, including the
53
+ * paired optional receipt arguments on resume. Exact keys and provider-added
54
+ * nullable padding remain enforced by the canonicalizer and validator below.
55
+ */
56
+ export const CONTROL_COMMAND_PARAMETERS = {
57
+ type: "object",
58
+ properties: CONTROL_SCHEMA_PROPERTIES,
59
+ required: ["command", "stage", "change"],
60
+ anyOf: [
61
+ commandSchema("start", ["stage", "change", "operationId"]),
62
+ commandSchema("status", ["stage", "change"]),
63
+ commandSchema("resume", ["stage", "change", "operationId"]),
64
+ commandSchema("resume", [
65
+ "stage",
66
+ "change",
67
+ "operationId",
68
+ "deliveryRevision",
69
+ "receiptHash",
70
+ ]),
71
+ commandSchema("rebind", ["stage", "change", "operationId", "routeId"]),
72
+ commandSchema("cancel", ["stage", "change", "operationId"]),
73
+ commandSchema("discard", ["stage", "change", "operationId"]),
74
+ ],
75
+ additionalProperties: false,
76
+ } as const;
77
+
16
78
  export type ControlCommandName = (typeof CONTROL_COMMANDS)[number];
17
79
  export type ControlStage = (typeof CONTROL_STAGES)[number];
18
80