@akagilnc/pi-workflow-roles 0.1.4035 → 0.1.4193

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 (108) hide show
  1. package/dist/acp-host/production-host.js +2368 -1155
  2. package/dist/archivist-record-entry.js +204 -20
  3. package/dist/archivist-record-topology.js +36 -16
  4. package/dist/auditor-dossier-tool.js +2 -1
  5. package/dist/diarist-contracts.js +96 -0
  6. package/dist/doctor-auditor.js +1 -1
  7. package/dist/doctor-evidence.js +3 -2
  8. package/dist/dossier-resolution.js +21 -8
  9. package/dist/gatekeeper-role.js +34 -27
  10. package/dist/headless-host/description.js +7 -8
  11. package/dist/headless-host/production-host.js +2375 -1149
  12. package/dist/host-contracts.js +16 -0
  13. package/dist/inspector-contracts.js +8 -0
  14. package/dist/migrate-book-topology.js +278917 -0
  15. package/dist/navigator-public-session.js +22 -25
  16. package/dist/notary-source-run.js +7 -4
  17. package/dist/packaged-role-registry.js +1 -1
  18. package/dist/pi/durable-principal.js +9 -6
  19. package/dist/pi/role-turn-host.js +43 -13
  20. package/dist/public-cli/auto-resume.js +2 -5
  21. package/dist/public-cli/case-dossier-delivery.js +60 -1
  22. package/dist/public-cli/diarist-run.js +27 -25
  23. package/dist/public-cli/inspector-run.js +17 -3
  24. package/dist/public-cli/instruction-seat-run.js +17 -17
  25. package/dist/public-cli/invocation.js +127 -63
  26. package/dist/public-cli/main.js +1685 -696
  27. package/dist/public-cli/notary-run.js +7 -4
  28. package/dist/public-cli/option-definitions.js +2 -2
  29. package/dist/public-cli/post-admission.js +113 -71
  30. package/dist/public-cli/run-lifecycle.js +209 -58
  31. package/dist/public-cli/seat-ticket-binding.js +3 -6
  32. package/dist/public-cli/settlement.js +29 -10
  33. package/dist/public-role-summons.js +8 -0
  34. package/dist/role-run-placement.js +84 -0
  35. package/dist/role-run-relocation.js +358 -0
  36. package/dist/run-terminal-artifacts.js +2 -1
  37. package/dist/run-ticket-number.js +51 -32
  38. package/dist/sitian-appender.js +11 -26
  39. package/dist/submission-ledger.js +106 -25
  40. package/dist/ticket-provenance.js +3 -1
  41. package/dist/user-dialogue-stdin.js +33 -0
  42. package/extensions/role-runtime.ts +1 -0
  43. package/package.json +3 -2
  44. package/resources/836-deleted-machine-instruction-inventory.md +1 -1
  45. package/scripts/build-package.mjs +34 -2
  46. package/souls/fixer.md +3 -2
  47. package/src/analyst-ledger.ts +14 -15
  48. package/src/archivist-record-entry.ts +285 -38
  49. package/src/archivist-record-topology.ts +43 -18
  50. package/src/auditor-dossier-tool.ts +2 -1
  51. package/src/book-topology-migration-placement.ts +493 -0
  52. package/src/book-topology-migration.ts +220 -0
  53. package/src/book-topology-migrators-t11.ts +856 -0
  54. package/src/book-topology-mixed-volume-migrators.ts +470 -0
  55. package/src/book-topology-partition-migrators.ts +25 -0
  56. package/src/book-topology-record-class-migrators.ts +779 -0
  57. package/src/book-topology-runs-migrator.ts +244 -0
  58. package/src/diarist-contracts.ts +116 -0
  59. package/src/diarist-role.ts +6 -0
  60. package/src/diarist.ts +2 -0
  61. package/src/doctor-auditor.ts +1 -1
  62. package/src/doctor-evidence.ts +4 -1
  63. package/src/doctor-role.ts +1 -1
  64. package/src/dossier-resolution.ts +26 -8
  65. package/src/external-host-turn-loop.ts +9 -0
  66. package/src/gatekeeper-pass-envelope.ts +6 -0
  67. package/src/gatekeeper-role.ts +47 -28
  68. package/src/headless-host/description.ts +8 -11
  69. package/src/headless-host/role-turn-host.ts +20 -5
  70. package/src/host-contracts.ts +22 -3
  71. package/src/inspector-contracts.ts +7 -0
  72. package/src/judge-role.ts +4 -0
  73. package/src/navigator-public-session.ts +33 -30
  74. package/src/navigator-work-context.ts +3 -4
  75. package/src/notary-role.ts +1 -0
  76. package/src/notary-source-run.ts +9 -4
  77. package/src/packaged-role-registry.ts +1 -1
  78. package/src/pi/adapter.ts +19 -4
  79. package/src/pi/durable-principal.ts +9 -13
  80. package/src/pi/role-turn-host.ts +48 -14
  81. package/src/public-cli/auto-resume.ts +2 -6
  82. package/src/public-cli/case-dossier-delivery.ts +86 -1
  83. package/src/public-cli/countersign-run.ts +182 -32
  84. package/src/public-cli/diarist-run.ts +34 -26
  85. package/src/public-cli/inspector-run.ts +26 -2
  86. package/src/public-cli/instruction-seat-run.ts +24 -20
  87. package/src/public-cli/invocation.ts +194 -58
  88. package/src/public-cli/notary-run.ts +13 -4
  89. package/src/public-cli/option-definitions.ts +2 -2
  90. package/src/public-cli/post-admission.ts +147 -78
  91. package/src/public-cli/run-lifecycle.ts +258 -65
  92. package/src/public-cli/seat-ticket-binding.ts +3 -6
  93. package/src/public-cli/settlement.ts +38 -10
  94. package/src/public-role-summons.ts +31 -7
  95. package/src/role-envelope.ts +9 -30
  96. package/src/role-run-placement.ts +128 -0
  97. package/src/role-run-relocation.ts +491 -0
  98. package/src/role-runtime-dependencies.ts +1 -0
  99. package/src/role-runtime.ts +106 -24
  100. package/src/run-terminal-artifacts.ts +3 -1
  101. package/src/run-ticket-number.ts +80 -20
  102. package/src/sitian-appender.ts +25 -28
  103. package/src/submission-ledger.ts +148 -30
  104. package/src/ticket-provenance.ts +4 -1
  105. package/src/ticket-trajectory.ts +22 -35
  106. package/src/user-dialogue-stdin.ts +37 -0
  107. package/src/worker-role.ts +8 -4
  108. package/src/worker-submission-gates.ts +6 -6
@@ -11,7 +11,6 @@ import { isAbsolute, join, resolve } from "node:path";
11
11
 
12
12
  import {
13
13
  buildResumeContinuationPrompt,
14
- GATE_DOSSIER_POINTER_PREFIX,
15
14
  RESUME_TRANSPORT_ENVELOPE,
16
15
  type PublicResumeRequest,
17
16
  type SameTicketSummonsMaterials,
@@ -37,7 +36,11 @@ import type {
37
36
  RoleTurnResult,
38
37
  SessionCustomEntryAppender,
39
38
  } from "../host-contracts.ts";
40
- import { projectCaseDossierPointerSection } from "./case-dossier-delivery.ts";
39
+ import { isOfficerReviewSeat } from "../host-contracts.ts";
40
+ import {
41
+ deliverCaseDossierAsAttachment,
42
+ projectCaseDossierPointerSection,
43
+ } from "./case-dossier-delivery.ts";
41
44
 
42
45
  /** Original error bytes, never relabeled — a secondary fact riding beside a classified cause. */
43
46
  function describeCaughtError(error: unknown): { name?: string; message: string; code?: string | number } {
@@ -58,6 +61,18 @@ function appendContinuationSection(
58
61
  ? { kind: "initial", prompt }
59
62
  : { kind: "resume", prompt };
60
63
  }
64
+
65
+ /**
66
+ * Nested gate summons (station child) on an officer seat: dialogue content is
67
+ * peer words only (#879). ADR 0081 case dossier still hangs via the existing
68
+ * attachments freeze seam — never into the peer body, never RoleTurnRequest.materials.
69
+ */
70
+ function isStationChildOfficerDialogue(
71
+ role: string,
72
+ env: { readonly stationChild?: boolean },
73
+ ): boolean {
74
+ return env.stationChild === true && isOfficerReviewSeat(role);
75
+ }
61
76
  import { projectHostTransitionPriorNative } from "../host-transition-prior-native.ts";
62
77
  import type { CredentialProviders, SeatModelConfig } from "./config.ts";
63
78
  import {
@@ -130,9 +145,9 @@ function withOnceSuccessfulBeforeDispatch<
130
145
  let succeeded = false;
131
146
  return {
132
147
  ...adapters,
133
- beforeDispatch: async (admitted) => {
148
+ beforeDispatch: async (admitted, lease) => {
134
149
  if (succeeded) return;
135
- await hook(admitted);
150
+ await hook(admitted, lease);
136
151
  succeeded = true;
137
152
  },
138
153
  };
@@ -265,7 +280,13 @@ export type PostAdmissionAdapters<
265
280
  result: RoleTurnResult;
266
281
  sessionFile: string;
267
282
  }) => Promise<RoleTurnKnownFailure | undefined>;
268
- beforeDispatch?: (admitted: A) => Promise<void> | void;
283
+ beforeDispatch?: (admitted: A, lease: RunWriterLease) => Promise<void> | void;
284
+ /**
285
+ * After the host turn settles and before the writer lease is released.
286
+ * Post-turn bind/relocate (e.g. Diarist first assert) must run here so the
287
+ * held lease follows the run directory and failures stay controlled.
288
+ */
289
+ afterDispatch?: (admitted: A, lease: RunWriterLease) => Promise<void> | void;
269
290
  };
270
291
 
271
292
  export type ControlledFailureInput = {
@@ -555,6 +576,60 @@ export async function dispatchPostAdmissionTurn<
555
576
  const deferredPersist = persistRunState ? {} : { needsPersist: true as const };
556
577
  const shouldPresent =
557
578
  adapters.shouldPresentSettled ?? ((terminal: T) => isLawfulTypedTerminalOutcome(terminal.roleOutcome));
579
+ type DispatchOutcome = {
580
+ exitCode: number;
581
+ admitted: A;
582
+ terminal?: T;
583
+ skipAutoResume?: true;
584
+ turnDispatched?: true;
585
+ needsPersist?: true;
586
+ };
587
+ /**
588
+ * Once-only post-host-turn hook (Diarist bind/relocate, etc.) under the still-held
589
+ * writer lease. Every exit after the host turn has started must pass here before
590
+ * lease release — success and failure alike. When a primary failure terminal already
591
+ * exists, keep that cause and leave relocate failure on the shared diagnostic channel.
592
+ */
593
+ let afterDispatchApplied = false;
594
+ const finishAfterTurn = async (result: DispatchOutcome): Promise<DispatchOutcome> => {
595
+ if (adapters.afterDispatch === undefined || afterDispatchApplied) return result;
596
+ afterDispatchApplied = true;
597
+ try {
598
+ await adapters.afterDispatch(admitted, lease);
599
+ return result;
600
+ } catch (error) {
601
+ const primaryFailure =
602
+ result.terminal !== undefined
603
+ && !isLawfulTypedTerminalOutcome(result.terminal.roleOutcome);
604
+ if (primaryFailure) {
605
+ await recordBestEffortPostDispatchDiagnostic(
606
+ admitted,
607
+ env,
608
+ `afterDispatch failed beside primary terminal (best-effort continue): ${describeErrorIdentity(error)}`,
609
+ io,
610
+ );
611
+ return result;
612
+ }
613
+ return {
614
+ ...(await presentControlledFailure(
615
+ admitted,
616
+ {
617
+ timedOut: false,
618
+ code: null,
619
+ stderr: "",
620
+ thrown: error,
621
+ },
622
+ adapters,
623
+ env.principalAuthority,
624
+ io,
625
+ persistRunState,
626
+ )) as { exitCode: number; admitted: A; terminal: T },
627
+ ...(result.turnDispatched === true ? { turnDispatched: true as const } : {}),
628
+ ...(result.skipAutoResume === true ? { skipAutoResume: true as const } : {}),
629
+ ...deferredPersist,
630
+ };
631
+ }
632
+ };
558
633
  try {
559
634
  const missingCredential = missingCredentialPreDispatchFailure(
560
635
  env.model,
@@ -629,7 +704,7 @@ export async function dispatchPostAdmissionTurn<
629
704
  // (#840 父子不层叠).
630
705
  if (adapters.beforeDispatch !== undefined) {
631
706
  try {
632
- await adapters.beforeDispatch(admitted);
707
+ await adapters.beforeDispatch(admitted, lease);
633
708
  } catch (error) {
634
709
  const settled = (await presentControlledFailure(
635
710
  admitted,
@@ -654,9 +729,9 @@ export async function dispatchPostAdmissionTurn<
654
729
  // Turn request is assembled after beforeDispatch so this turn sees whatever it
655
730
  // settled — the seat's ticket bind re-projection and any court diarist station
656
731
  // writes (#742). Case dossier delivery (ADR 0081 / #709) rides here once for
657
- // every public entry: first call, same-ticket re-summons and manual resume
658
- // alike. System refs append their own neutral section; caller frozen
659
- // attachments and the seat's own prompt bytes are never rewritten.
732
+ // every public entry. #879: station-child officer dialogue keeps peer body
733
+ // intact — 起居录 hangs via existing attachments freeze (not prompt wrap,
734
+ // not RoleTurnRequest.materials). Other entries keep the neutral prompt section.
660
735
  let turnRequest: RoleTurnRequest =
661
736
  env.signal === undefined ? request : { ...request, signal: env.signal };
662
737
  if (env.stationChild !== undefined) {
@@ -665,19 +740,33 @@ export async function dispatchPostAdmissionTurn<
665
740
  if (hostTransition !== undefined) {
666
741
  turnRequest = { ...turnRequest, hostTransition };
667
742
  }
668
- const dossierSection = await projectCaseDossierPointerSection({
669
- ticketNumber: admitted.ticketNumber,
670
- projectRoot: admitted.projectRoot,
671
- home: env.home,
672
- });
673
- if (dossierSection !== undefined) {
674
- turnRequest = {
675
- ...turnRequest,
676
- continuation: appendContinuationSection(
677
- turnRequest.continuation,
678
- dossierSection,
679
- ),
680
- };
743
+ if (isStationChildOfficerDialogue(admitted.role, env)) {
744
+ // 0081 non-body face: freeze pointer section under run/attachments/.
745
+ // Peer dialogue continuation.prompt stays parent payload only — the seat
746
+ // consumes the freeze via loadCaseDossierReadingMaterial → existing
747
+ // agent-start readingMaterial / systemPrompt.materials fold (not prompt splice,
748
+ // not RoleTurnRequest.materials).
749
+ await deliverCaseDossierAsAttachment({
750
+ ticketNumber: admitted.ticketNumber,
751
+ projectRoot: admitted.projectRoot,
752
+ home: env.home,
753
+ runDirectory: admitted.runDirectory,
754
+ });
755
+ } else {
756
+ const dossierSection = await projectCaseDossierPointerSection({
757
+ ticketNumber: admitted.ticketNumber,
758
+ projectRoot: admitted.projectRoot,
759
+ home: env.home,
760
+ });
761
+ if (dossierSection !== undefined) {
762
+ turnRequest = {
763
+ ...turnRequest,
764
+ continuation: appendContinuationSection(
765
+ turnRequest.continuation,
766
+ dossierSection,
767
+ ),
768
+ };
769
+ }
681
770
  }
682
771
 
683
772
  // Authoritative host write happens here, at the real dispatch boundary —
@@ -708,7 +797,7 @@ export async function dispatchPostAdmissionTurn<
708
797
  io,
709
798
  persistRunState,
710
799
  );
711
- return { ...settled, turnDispatched: true as const, ...deferredPersist };
800
+ return await finishAfterTurn({ ...settled, turnDispatched: true as const, ...deferredPersist });
712
801
  }
713
802
 
714
803
  // `result.stderr` stays live in memory for the real classification below
@@ -912,7 +1001,7 @@ export async function dispatchPostAdmissionTurn<
912
1001
  io,
913
1002
  persistRunState,
914
1003
  );
915
- return { ...settledFailure, turnDispatched: true as const, ...deferredPersist };
1004
+ return await finishAfterTurn({ ...settledFailure, turnDispatched: true as const, ...deferredPersist });
916
1005
  }
917
1006
  if (settledOutcome !== undefined) {
918
1007
  if (persistRunState) {
@@ -943,12 +1032,12 @@ export async function dispatchPostAdmissionTurn<
943
1032
  io,
944
1033
  persistRunState,
945
1034
  );
946
- return { ...failed, turnDispatched: true as const, ...deferredPersist };
1035
+ return await finishAfterTurn({ ...failed, turnDispatched: true as const, ...deferredPersist });
947
1036
  }
948
1037
  }
949
1038
  // Lawful persist + present is the caller's stop seam (auto-resume loop /
950
1039
  // manual resume), not this retried host-turn function.
951
- return { ...settledOutcome, ...deferredPersist };
1040
+ return await finishAfterTurn({ ...settledOutcome, ...deferredPersist });
952
1041
  }
953
1042
 
954
1043
  // Host/runner true failure coexists with already-recorded payloads — never wash as accepted.
@@ -984,7 +1073,7 @@ export async function dispatchPostAdmissionTurn<
984
1073
  io,
985
1074
  persistRunState,
986
1075
  );
987
- return { ...failed, turnDispatched: true as const, ...deferredPersist };
1076
+ return await finishAfterTurn({ ...failed, turnDispatched: true as const, ...deferredPersist });
988
1077
  }
989
1078
 
990
1079
  // Nothing else was wrong, but the durable stderr mirror itself failed to
@@ -1004,7 +1093,7 @@ export async function dispatchPostAdmissionTurn<
1004
1093
  io,
1005
1094
  persistRunState,
1006
1095
  );
1007
- return { ...failed, turnDispatched: true as const, ...deferredPersist };
1096
+ return await finishAfterTurn({ ...failed, turnDispatched: true as const, ...deferredPersist });
1008
1097
  }
1009
1098
 
1010
1099
  const noReceipt = await attachRecordedSubmissions(
@@ -1035,16 +1124,16 @@ export async function dispatchPostAdmissionTurn<
1035
1124
  io,
1036
1125
  persistRunState,
1037
1126
  );
1038
- return { ...failed, turnDispatched: true as const, ...deferredPersist };
1127
+ return await finishAfterTurn({ ...failed, turnDispatched: true as const, ...deferredPersist });
1039
1128
  }
1040
1129
  }
1041
- return {
1130
+ return await finishAfterTurn({
1042
1131
  exitCode: exitCodeForTerminalOutcome(noReceipt.roleOutcome),
1043
1132
  admitted,
1044
1133
  terminal: noReceipt,
1045
1134
  turnDispatched: true as const,
1046
1135
  ...deferredPersist,
1047
- };
1136
+ });
1048
1137
  } finally {
1049
1138
  try {
1050
1139
  await lease.release();
@@ -1065,16 +1154,16 @@ export async function dispatchPostAdmissionTurn<
1065
1154
  }
1066
1155
 
1067
1156
  /**
1068
- * Shared resume continuation projection (#471 / #600 / #633 / #637 / #755):
1157
+ * Shared resume continuation projection (#471 / #600 / #633 / #637 / #755 / #879):
1069
1158
  * seat-table model/engine/timeout axes, restored correlation, and either
1070
1159
  * - manual resume (no same-ticket summons): package envelope / optional caller
1071
1160
  * message, with engine-axis handbook via buildResumeContinuationPrompt, or
1072
1161
  * - same-ticket summons (审核循环续话): caller/peer words + optional frozen
1073
- * attachment paths only — no「重新读」、no engine handbook packaging
1074
- * (#750/#755), whether or not attachments are present.
1162
+ * attachment paths only — no「请重读」、no code-authored content substitute,
1163
+ * no engine handbook packaging (#750/#755/#879).
1075
1164
  * Caller message wins as prompt base when supplied (bytes unchanged, including
1076
- * blank/whitespace); else summons instruction. Attachment projection must not
1077
- * re-interpret the caller message as instructionEmpty.
1165
+ * blank/whitespace); else summons instruction (parent payload or reask).
1166
+ * Binding pointer stays on summons.sourceRunPath / activation — not dialogue content.
1078
1167
  * Seats add only their activation projection. Call prepareSummonsResumeMaterials
1079
1168
  * first when request.summons carries instruction or attachment paths.
1080
1169
  */
@@ -1088,32 +1177,25 @@ export function resumeTurnRequestProjectionOptions(
1088
1177
  readonly attachments: readonly { frozenPath: string }[];
1089
1178
  },
1090
1179
  ): RoleTurnRequestProjectionOptions {
1091
- const officerSourcePath = request.summons?.sourceRunPath;
1092
- const withReread = (body: string): string => {
1093
- if (
1094
- officerSourcePath === undefined
1095
- || (admitted.role !== "notary"
1096
- && admitted.role !== "inspector"
1097
- && admitted.role !== "auditor")
1098
- ) {
1099
- return body;
1100
- }
1101
- if (body.startsWith("请重读")) return body;
1102
- return `请重读\n${body}`;
1103
- };
1180
+ // #879: officer dialogue content is caller/peer words only — no「请重读」,
1181
+ // no code-authored constant substitute, no attachment-list wrap of peer body.
1182
+ // Binding pointer stays on summons sourceRunPath / activation / attachments.
1183
+ const officerDialogue = isStationChildOfficerDialogue(admitted.role, env);
1104
1184
  let prompt: string;
1105
1185
  if (request.message !== undefined) {
1106
1186
  if (summonsPrepared !== undefined) {
1107
- // #755: same-ticket review / open-court — caller words + optional paths.
1108
- // Attachments are not a gate: message-only summons must stay plain too.
1109
- prompt = withReread(buildInstructionTransportPrompt({
1110
- instruction: request.message,
1111
- instructionEmpty: false,
1112
- attachments: summonsPrepared.attachments,
1113
- }));
1187
+ // #755: same-ticket review / open-court — caller words.
1188
+ // #879 station-child officer: words only (attachments are independent freeze).
1189
+ prompt = officerDialogue
1190
+ ? request.message
1191
+ : buildInstructionTransportPrompt({
1192
+ instruction: request.message,
1193
+ instructionEmpty: false,
1194
+ attachments: summonsPrepared.attachments,
1195
+ });
1114
1196
  } else if (request.summons !== undefined) {
1115
1197
  // #755: same-ticket summons without prepared materials — caller words only.
1116
- prompt = withReread(request.message);
1198
+ prompt = request.message;
1117
1199
  } else {
1118
1200
  // Bare manual resume — outsourcing engine axis keeps handbook (#600/#736).
1119
1201
  prompt = buildResumeContinuationPrompt({
@@ -1123,28 +1205,15 @@ export function resumeTurnRequestProjectionOptions(
1123
1205
  });
1124
1206
  }
1125
1207
  } else if (summonsPrepared !== undefined) {
1126
- // #755: same-ticket review summons — instruction/attachments only.
1127
- prompt = withReread(buildInstructionTransportPrompt(summonsPrepared));
1208
+ // #879 station-child officer: instruction bytes === peer body/reask (no wrap).
1209
+ // Other seats keep #755 instruction + optional attachment path listing.
1210
+ prompt = officerDialogue
1211
+ ? (summonsPrepared.instructionEmpty ? "" : summonsPrepared.instruction)
1212
+ : buildInstructionTransportPrompt(summonsPrepared);
1128
1213
  } else if (request.summons !== undefined) {
1129
- // #755: same-ticket summons with no instruction/attachments (e.g. notary
1130
- // source-run pointer). #836: officer resume opening adds「请重读」+ path pointer.
1131
- const path = request.summons.sourceRunPath;
1132
- if (
1133
- path !== undefined &&
1134
- (admitted.role === "notary" ||
1135
- admitted.role === "inspector" ||
1136
- admitted.role === "auditor")
1137
- ) {
1138
- prompt = `请重读\n${GATE_DOSSIER_POINTER_PREFIX}${path}`;
1139
- } else if (
1140
- admitted.role === "notary" ||
1141
- admitted.role === "inspector" ||
1142
- admitted.role === "auditor"
1143
- ) {
1144
- prompt = "请重读";
1145
- } else {
1146
- prompt = "";
1147
- }
1214
+ // #879: same-ticket summons with no instruction (e.g. notary source-run binding
1215
+ // only). Pointer is activation/sourceRun material — not dialogue content.
1216
+ prompt = "";
1148
1217
  } else {
1149
1218
  // Bare manual resume — outsourcing engine axis keeps handbook (#600/#736).
1150
1219
  prompt = buildResumeContinuationPrompt({