@brainervirus/workit-core 8.28.0 → 8.28.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-core",
3
- "version": "8.28.0",
3
+ "version": "8.28.1",
4
4
  "private": false,
5
5
  "description": "Workit shared core — task, policy, evidence, review, decision, worker, and writer state for agentic coding workflows",
6
6
  "keywords": [
@@ -318,6 +318,16 @@ export const validateCommitMessagePolicy = (
318
318
  };
319
319
  };
320
320
 
321
+ /**
322
+ * A TypeError or ReferenceError while resolving a policy is a bug in this
323
+ * runtime (or a host too old for it), not a config problem the user can
324
+ * repair: it propagates so the hook's failure decision applies instead of a
325
+ * misleading "repair or select a valid workspace policy".
326
+ */
327
+ const rethrowRuntimeBug = (error: unknown): void => {
328
+ if (error instanceof TypeError || error instanceof ReferenceError) throw error;
329
+ };
330
+
321
331
  export const validateBranchNameFor = (
322
332
  workspaceRoot: string,
323
333
  name: string,
@@ -326,6 +336,7 @@ export const validateBranchNameFor = (
326
336
  const resolved = branchPolicySnapshotFor(workspaceRoot);
327
337
  return validateBranchNamePolicy(name, resolved.namePolicy, resolved.source);
328
338
  } catch (error) {
339
+ rethrowRuntimeBug(error);
329
340
  const correction = "repair or select a valid workspace policy before creating a branch";
330
341
  return {
331
342
  ok: false,
@@ -347,6 +358,7 @@ export const validateCommitMessageFor = (
347
358
  const resolved = commitPolicySnapshotFor(workspaceRoot);
348
359
  return validateCommitMessagePolicy(message, resolved.policy, resolved.source);
349
360
  } catch (error) {
361
+ rethrowRuntimeBug(error);
350
362
  const correction = "repair or select a valid workspace policy before committing";
351
363
  return {
352
364
  ok: false,
@@ -184,9 +184,11 @@ export function flatOperationJsonSchema(
184
184
  // Reads advertise only what reads take: no revisions, and task reads only their filters.
185
185
  const readField = (name: string) =>
186
186
  name === "taskId" || (family === "task" ? READ_TASK_FIELDS.has(name) : family === "policy");
187
+ // Only advertise what some listed action takes (derived from the contract).
188
+ const taken = (name: string) =>
189
+ family === "policy" || actions.some((action) => actionKeys(family, action)?.accepted.has(name));
187
190
  for (const [name, field] of Object.entries({ ...COMMON, ...spec.fields }))
188
- if (options.readOnly ? readField(name) : !(family === "state" && name === "expectedRevision"))
189
- properties[name] = property(field);
191
+ if (options.readOnly ? readField(name) : taken(name)) properties[name] = property(field);
190
192
  return {
191
193
  $schema: "https://json-schema.org/draft/2020-12/schema",
192
194
  type: "object",
@@ -482,6 +484,129 @@ export function normalizeOperationInput(
482
484
  return request;
483
485
  }
484
486
 
487
+ // ---------------------------------------------------------------------------
488
+ // per-action keys, derived from the contract
489
+
490
+ type ZodLike = {
491
+ _zod: { def: { type: string; shape?: Record<string, ZodLike>; innerType?: ZodLike } };
492
+ };
493
+ const defOf = (node: ZodLike) => node._zod.def;
494
+ const unwrap = (node: ZodLike): ZodLike => {
495
+ let inner = node;
496
+ while (["optional", "nullable"].includes(defOf(inner).type) && defOf(inner).innerType)
497
+ inner = defOf(inner).innerType!;
498
+ return inner;
499
+ };
500
+ /** Contract keys the flat form renames. */
501
+ const FLAT_NAME: Record<string, string> = {
502
+ authorityRefs: "refs",
503
+ contentRefs: "refs",
504
+ statedChoice: "choice",
505
+ reviewContext: "reviewSession",
506
+ };
507
+ const NOT_FLAT = new Set(["schemaVersion", "action", "workspaceId"]);
508
+
509
+ export type ActionKeys = { accepted: Set<string>; required: string[] };
510
+
511
+ /**
512
+ * What one action of a family takes, read from the advertised contract:
513
+ * every top-level key, the keys of its nested objects (flat form, renamed per
514
+ * FLAT_NAME), and the required scalars. Lists, scopes and nullables are
515
+ * defaulted by normalization, so they are accepted but never required.
516
+ */
517
+ export function actionKeys(family: OperationFamily, action: string): ActionKeys | null {
518
+ const options = (advertisedOperationSchemas[family] as unknown as { options: ZodLike[] }).options;
519
+ const option = options.find((item) => {
520
+ const literal = defOf(item).shape?.action as unknown as { _zod: { def: { values: string[] } } };
521
+ return literal._zod.def.values.includes(action);
522
+ });
523
+ if (!option) return null;
524
+ const out: ActionKeys = { accepted: new Set(), required: [] };
525
+ const walk = (shape: Record<string, ZodLike>, nested: boolean): void => {
526
+ for (const [key, node] of Object.entries(shape)) {
527
+ if (NOT_FLAT.has(key) || (nested && key === "taskId")) continue;
528
+ const inner = unwrap(node);
529
+ const type = defOf(inner).type;
530
+ const needed = !["optional", "nullable"].includes(defOf(node).type);
531
+ if (key === "scope")
532
+ for (const name of ["scope", "paths", "exclusions"]) out.accepted.add(name);
533
+ else if (type === "object" && key !== "bundle" && !(key in FLAT_NAME)) {
534
+ out.accepted.add(key);
535
+ walk(defOf(inner).shape!, true);
536
+ } else {
537
+ const name = FLAT_NAME[key] ?? key;
538
+ out.accepted.add(name);
539
+ if (needed && type !== "array" && type !== "record" && !out.required.includes(name))
540
+ out.required.push(name);
541
+ }
542
+ }
543
+ };
544
+ walk(defOf(option).shape!, false);
545
+ return out;
546
+ }
547
+
548
+ /** A minimal valid flat payload for one action: required scalars with placeholders. */
549
+ export function exampleOperation(family: OperationFamily, action: string): Record<string, unknown> {
550
+ const keys = actionKeys(family, action);
551
+ const example: Record<string, unknown> = { action };
552
+ for (const name of keys?.required ?? [])
553
+ example[name] = FLAT[family].fields[name]?.enum?.[0] ?? `<${name}>`;
554
+ if (family === "policy" && !keys?.required.length && action !== "explain")
555
+ example.riskTier = "normal";
556
+ return example;
557
+ }
558
+
559
+ const oneOf = (values: readonly string[]): string => values.join(" | ");
560
+
561
+ /** Every invalid_input the hosts return: the exact fix for this family and action. */
562
+ function invalidOperation(
563
+ family: OperationFamily,
564
+ input: unknown,
565
+ error: ZodErrorLike,
566
+ ): Result<never> {
567
+ const request = isObject(input) ? input : {};
568
+ const action = typeof request.action === "string" ? request.action : "";
569
+ const actions = OPERATION_ACTIONS[family];
570
+ if (!actions.includes(action))
571
+ return failure("invalid_input", `${family}: action must be one of ${oneOf(actions)}`, {
572
+ fields: [{ path: "action", reason: `got ${JSON.stringify(request.action ?? null)}` }],
573
+ });
574
+ const own = actionKeys(family, action)!;
575
+ const fields: { path: string; reason: string }[] = [];
576
+ const sentence: string[] = [];
577
+ for (const issue of error.issues) {
578
+ if (issue.code === "unrecognized_keys") {
579
+ for (const key of issue.keys) {
580
+ const home = actions.find(
581
+ (other) => other !== action && actionKeys(family, other)!.accepted.has(key),
582
+ );
583
+ const reason = home
584
+ ? `${key} belongs to action: ${home}`
585
+ : `${key} is not a field of ${family} ${action}; it takes ${[...own.accepted].join(", ")}`;
586
+ fields.push({ path: key, reason });
587
+ sentence.push(reason);
588
+ }
589
+ continue;
590
+ }
591
+ const last = String(issue.path.at(-1) ?? "");
592
+ const name = Object.entries(FLAT_NAME).find(([contract]) => contract === last)?.[1] ?? last;
593
+ const missing = issue.code === "invalid_type" && /received undefined/.test(issue.message);
594
+ // A nested container (binding, intent, ...) is absent because its flat fields are.
595
+ const absent = own.required.filter((key) => !(key in request));
596
+ const needs = missing && issue.path.length === 1 && absent.length ? absent.join(", ") : name;
597
+ const reason = missing ? `${family} ${action} requires ${needs}` : `${name}: ${issue.message}`;
598
+ fields.push({ path: issue.path.join("."), reason });
599
+ sentence.push(reason);
600
+ }
601
+ const example = JSON.stringify(exampleOperation(family, action));
602
+ return failure("invalid_input", `${[...new Set(sentence)].join("; ")}. Example: ${example}`, {
603
+ fields,
604
+ });
605
+ }
606
+ type ZodErrorLike = {
607
+ issues: { code: string; keys: string[]; path: PropertyKey[]; message: string }[];
608
+ };
609
+
485
610
  /**
486
611
  * Host-side validation before the engine runs: normalize, then parse against
487
612
  * the advertised contract (taskId optional: the engine resolves the branch's
@@ -497,10 +622,5 @@ export function parseAdvertisedOperation(
497
622
  return failure("unsupported_version", "unsupported schema version", { operation: family });
498
623
  const parsed = advertisedOperationSchemas[family].safeParse(request);
499
624
  if (parsed.success) return success(null, null, request);
500
- return failure("invalid_input", "operation input is invalid", {
501
- fields: parsed.error.issues.map((issue) => ({
502
- path: issue.code === "unrecognized_keys" ? issue.keys.join(".") : issue.path.join("."),
503
- reason: issue.message,
504
- })),
505
- });
625
+ return invalidOperation(family, request, parsed.error as unknown as ZodErrorLike);
506
626
  }