@unisonlabs/sixminute-admin 0.1.1246 → 0.1.1247-next

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.
@@ -21,11 +21,7 @@ import { homedir } from "node:os";
21
21
  import { fileURLToPath } from "node:url";
22
22
 
23
23
  // tools/admin/src/build-info.ts
24
- var BUILT = { version: "0.1.1246", commit: "7f74de0c794bae81e8d332483c119d0ffcb5f5e0", surface: "bb42e8a273612558" };
25
-
26
- // tools/admin/src/run.ts
27
- import { rmSync as rmSync3 } from "node:fs";
28
- import { join as join2, resolve } from "node:path";
24
+ var BUILT = { version: "0.1.1247-next", commit: "e6792ee2dfbf1cc15229a931746b3ac9695b62b4", surface: "e4b83a8db444655b" };
29
25
 
30
26
  // node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
31
27
  var exports_external = {};
@@ -4221,14 +4217,7 @@ var MatterPartySchema = exports_external.object({
4221
4217
  reference: RawWord,
4222
4218
  external_refs: exports_external.array(exports_external.string().min(1)).optional()
4223
4219
  });
4224
- var SyncSourceSchema = exports_external.enum([
4225
- "manual",
4226
- "clio",
4227
- "legalsense",
4228
- "basenet",
4229
- "urios",
4230
- "import"
4231
- ]);
4220
+ var SyncSourceSchema = exports_external.enum(["manual", "legalsense", "urios", "import"]);
4232
4221
  var ClientSchema = exports_external.object({
4233
4222
  id: ClientIdSchema,
4234
4223
  name: exports_external.string().min(1),
@@ -4343,64 +4332,6 @@ function readMatterRecord(raw) {
4343
4332
  };
4344
4333
  }
4345
4334
 
4346
- // packages/contracts/src/calendar.ts
4347
- var CalendarProviderSchema = exports_external.enum(["microsoft", "google"]);
4348
- var CalendarAccountKindSchema = exports_external.enum(["work", "personal"]);
4349
- var CalendarConnectionStatusSchema = exports_external.enum(["connected", "awaiting_admin", "needs_reconnect"]);
4350
- var CalendarConnectionSchema = exports_external.object({
4351
- provider: CalendarProviderSchema,
4352
- account_kind: CalendarAccountKindSchema,
4353
- account_email: exports_external.string(),
4354
- status: CalendarConnectionStatusSchema,
4355
- admin_consent_url: exports_external.string().nullable(),
4356
- last_synced_at: TimestampSchema.nullable()
4357
- });
4358
- var CalendarConnectRequestSchema = exports_external.object({
4359
- provider: CalendarProviderSchema,
4360
- state: exports_external.string().min(16),
4361
- code_challenge: exports_external.string().min(16)
4362
- });
4363
- var CalendarConnectAnswerSchema = exports_external.object({ authorize_url: exports_external.string().url() });
4364
- var CalendarConnectFinishSchema = exports_external.union([
4365
- exports_external.object({ state: exports_external.string().min(16), code: exports_external.string().min(1), code_verifier: exports_external.string().min(43).max(128) }).strict(),
4366
- exports_external.object({
4367
- state: exports_external.string().min(16),
4368
- error: exports_external.string().min(1).max(200),
4369
- error_description: exports_external.string().max(2000).optional(),
4370
- error_subcode: exports_external.string().max(200).optional()
4371
- }).strict()
4372
- ]);
4373
- var CalendarResponseSchema = exports_external.enum(["organiser", "accepted", "tentative", "declined", "none"]);
4374
- var CalendarGuestSchema = exports_external.object({
4375
- name: exports_external.string(),
4376
- email: exports_external.string().nullable(),
4377
- response: CalendarResponseSchema,
4378
- organiser: exports_external.boolean(),
4379
- me: exports_external.boolean()
4380
- });
4381
- var CalendarWhereSchema = exports_external.object({
4382
- kind: exports_external.enum(["call", "address", "none"]),
4383
- text: exports_external.string().nullable()
4384
- });
4385
- var CalendarEventSchema = exports_external.object({
4386
- id: exports_external.string().min(1),
4387
- title: exports_external.string(),
4388
- from: TimestampSchema,
4389
- to: TimestampSchema,
4390
- all_day: exports_external.boolean(),
4391
- status: exports_external.enum(["confirmed", "cancelled"]),
4392
- my_response: CalendarResponseSchema,
4393
- where: CalendarWhereSchema,
4394
- guests: exports_external.array(CalendarGuestSchema),
4395
- provider: CalendarProviderSchema,
4396
- web_link: exports_external.string().nullable(),
4397
- not_attended: exports_external.boolean(),
4398
- can_mark: exports_external.boolean()
4399
- });
4400
- var CalendarDaySchema = exports_external.object({
4401
- events: exports_external.array(CalendarEventSchema)
4402
- });
4403
-
4404
4335
  // packages/contracts/src/compat.ts
4405
4336
  var ActivityTypeKeySchema = exports_external.string().regex(/^[A-Za-z0-9_-]{1,40}$/, "an activity type key is 1 to 40 letters, digits, underscores or hyphens, such as A103 or draft-revise");
4406
4337
  var ActivityTypeSourceSchema = exports_external.enum(["published", "firm"]);
@@ -4603,8 +4534,8 @@ function partOnDay(part, day, zone) {
4603
4534
  }
4604
4535
 
4605
4536
  // packages/contracts/src/connector.ts
4606
- var ConnectorKindSchema = exports_external.enum(["urios", "legalsense", "partner", "fake"]);
4607
- var InitiativeSchema = exports_external.enum(["push", "poll"]);
4537
+ var ConnectorKindSchema = exports_external.enum(["urios", "legalsense", "fake"]);
4538
+ var InitiativeSchema = exports_external.enum(["push"]);
4608
4539
  var IdentityModeSchema = exports_external.enum(["mapping", "sign_in"]);
4609
4540
  var FieldDeclarationSchema = exports_external.object({
4610
4541
  key: AttributeKeySchema.refine((key) => !key.includes("__"), {
@@ -4621,7 +4552,7 @@ var CapabilitiesSchema = exports_external.object({
4621
4552
  change_feed: exports_external.enum(["modified_since", "full_list"]),
4622
4553
  narrative_max_chars: exports_external.number().int().positive().nullable(),
4623
4554
  note_max_chars: exports_external.number().int().positive().nullable().default(null),
4624
- insolvency_groups: exports_external.boolean(),
4555
+ insolvency_groups: exports_external.literal(false),
4625
4556
  matter_visibility: exports_external.enum(["firm", "member"]).default("firm"),
4626
4557
  reviewer: exports_external.enum(["self", "configurable"]).default("configurable"),
4627
4558
  fields: exports_external.array(FieldDeclarationSchema).superRefine((fields, ctx) => {
@@ -4701,7 +4632,6 @@ var PmsMatterSchema = exports_external.object({
4701
4632
  main_category: exports_external.string().nullable().optional(),
4702
4633
  phase: exports_external.string().nullable().optional(),
4703
4634
  folders: exports_external.array(exports_external.string()).optional(),
4704
- insolvency: exports_external.boolean().default(false),
4705
4635
  time_entry_blocked: exports_external.boolean().default(false)
4706
4636
  });
4707
4637
  var PmsTimekeeperSchema = exports_external.object({
@@ -4757,7 +4687,6 @@ var PmsListsSchema = exports_external.object({
4757
4687
  }).optional()
4758
4688
  });
4759
4689
  var MatterPmsRulesSchema = exports_external.object({
4760
- insolvency: exports_external.boolean().default(false),
4761
4690
  time_entry_blocked: exports_external.boolean().default(false)
4762
4691
  });
4763
4692
  var OutboundEntrySchema = exports_external.object({
@@ -4771,8 +4700,7 @@ var OutboundEntrySchema = exports_external.object({
4771
4700
  minutes: exports_external.number().int().nonnegative(),
4772
4701
  narrative: NarrativeSchema,
4773
4702
  internal_note: exports_external.string().nullable().default(null),
4774
- fields: exports_external.record(exports_external.string(), exports_external.object({ value: exports_external.string().nullable(), external_ref: exports_external.string().nullable() })),
4775
- recofa_category: exports_external.string().nullable().default(null)
4703
+ fields: exports_external.record(exports_external.string(), exports_external.object({ value: exports_external.string().nullable(), external_ref: exports_external.string().nullable() }))
4776
4704
  });
4777
4705
  var PmsEntryStateSchema = exports_external.object({
4778
4706
  external_id: exports_external.string().min(1),
@@ -4938,6 +4866,907 @@ var SyncProgressSchema = exports_external.object({
4938
4866
  failed: exports_external.boolean()
4939
4867
  });
4940
4868
 
4869
+ // packages/contracts/src/analytics/safe.ts
4870
+ var mark = (schema, kind) => schema.describe(`safe:${kind}`);
4871
+ var TELEMETRY_SECONDS_MAX = 86400;
4872
+ var TELEMETRY_DAY_OFFSET_MAX = 3660;
4873
+ var TELEMETRY_ENTRY_ID_MAX = 64;
4874
+ var count = () => mark(exports_external.number().int().nonnegative(), "count");
4875
+ var daySeconds = () => mark(exports_external.number().int().nonnegative().max(TELEMETRY_SECONDS_MAX), "day-seconds");
4876
+ var duration = () => mark(exports_external.number().int().nonnegative(), "duration");
4877
+ var signed = (min, max) => mark(exports_external.number().int().min(min).max(max), "signed");
4878
+ var dayOffset = () => signed(-TELEMETRY_DAY_OFFSET_MAX, TELEMETRY_DAY_OFFSET_MAX);
4879
+ var minutesDelta = () => mark(exports_external.number().int().min(-1440).max(1440).nullable(), "minutes-delta");
4880
+ var usd = () => mark(exports_external.number().nonnegative().finite(), "usd");
4881
+ var bool = () => exports_external.boolean();
4882
+ var oneOf = (values) => exports_external.enum(values);
4883
+ var day = () => mark(LocalDaySchema, "day");
4884
+ var version = () => mark(exports_external.string().regex(/^\d{1,9}\.\d{1,9}\.\d{1,9}$/), "version");
4885
+ var pageBuild = () => mark(exports_external.string().regex(/^\d{14}-[0-9A-Za-z]{1,40}$/, "expected YYYYMMDDHHMMSS-<commit>"), "page-build");
4886
+ var modelId = () => mark(exports_external.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,79}$/), "model-id");
4887
+ var word = () => mark(exports_external.string().regex(/^[A-Za-z0-9_.-]{1,48}$/), "word");
4888
+ var uuid = () => mark(exports_external.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i), "uuid");
4889
+ var sessionId = () => mark(exports_external.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, "expected a UUIDv7"), "session-id");
4890
+ var ids = {
4891
+ entry: () => mark(EntryIdSchema.max(TELEMETRY_ENTRY_ID_MAX), "id"),
4892
+ run: () => mark(RunIdSchema, "id"),
4893
+ firm: () => mark(TenantIdSchema, "id"),
4894
+ member: () => mark(MemberIdSchema, "id"),
4895
+ device: () => mark(DeviceIdSchema, "id")
4896
+ };
4897
+
4898
+ // packages/contracts/src/analytics/catalogue.ts
4899
+ var RawArchiveReasonSchema = oneOf(["person", "expired", "signed_out", "tenant_changed", "not_allowed", "refused", "disk_low"]);
4900
+ var TelemetryScreenSchema = oneOf(["day", "timeline", "entry", "settings", "sign_in", "onboarding", "permissions"]);
4901
+ var TelemetryCommandSchema = oneOf([
4902
+ "new-entry",
4903
+ "release-all",
4904
+ "settings",
4905
+ "exit",
4906
+ "undo",
4907
+ "redo",
4908
+ "cut",
4909
+ "copy",
4910
+ "paste",
4911
+ "select-all",
4912
+ "today",
4913
+ "previous-day",
4914
+ "next-day",
4915
+ "previous-entry",
4916
+ "next-entry",
4917
+ "actions",
4918
+ "capture",
4919
+ "privacy",
4920
+ "about",
4921
+ "folder",
4922
+ "release",
4923
+ "matter",
4924
+ "attribute",
4925
+ "merge",
4926
+ "delete",
4927
+ "open"
4928
+ ]);
4929
+ var GenerateFailureSchema = oneOf(["model", "deadline", "overlap", "other"]);
4930
+ var AppErrorKindSchema = oneOf(["boundary", "uncaught", "unhandled_rejection"]);
4931
+ var AppErrorClassSchema = oneOf(["Error", "TypeError", "RangeError", "ReferenceError", "SyntaxError", "ChunkLoadError", "AbortError", "other"]);
4932
+ var PageBuildSchema = pageBuild();
4933
+ var RELEASE_BLOCKERS = [
4934
+ "narrative_empty",
4935
+ "narrative_too_short",
4936
+ "matter",
4937
+ "internal_note_too_long",
4938
+ "connector_not_connected",
4939
+ "connector_sign_in",
4940
+ "connector_matter",
4941
+ "connector_timekeeper",
4942
+ "connector_narrative",
4943
+ "connector_matter_blocked",
4944
+ "connector_matter_not_found",
4945
+ "connector_matter_closed",
4946
+ "connector_person_not_found",
4947
+ "connector_person_inactive",
4948
+ "connector_not_accepted",
4949
+ "connector_reviewer",
4950
+ "attribute",
4951
+ "other"
4952
+ ];
4953
+ var SyncFailureSchema = oneOf(["tls", "login", "schema", "rights", "connect", "timeout", "nobody_signed_in", "dispatch", "error"]);
4954
+ var SourceHealthSchema = oneOf(["healthy", "failing", "needs_reconnect"]);
4955
+ var JobKindSchema = oneOf([
4956
+ "decide",
4957
+ "generate",
4958
+ "propose",
4959
+ "calendar_judge",
4960
+ "calendar_sync",
4961
+ "practice_sync",
4962
+ "day_accounts",
4963
+ "scrub_plaintext",
4964
+ "purge_expired",
4965
+ "research_purge",
4966
+ "sweep"
4967
+ ]);
4968
+ var SettingSchema = oneOf([
4969
+ "unit_minutes",
4970
+ "narrative_language",
4971
+ "narrative_max_chars",
4972
+ "narrative_guidance",
4973
+ "always_on",
4974
+ "exclusions",
4975
+ "name",
4976
+ "avatar",
4977
+ "entry_size",
4978
+ "time_zone",
4979
+ "launch_at_login",
4980
+ "theme"
4981
+ ]);
4982
+ var DeviceSettingSchema = oneOf(["unit_minutes", "narrative_language", "narrative_max_chars", "always_on", "launch_at_login", "theme", "exclusions", "entry_size"]);
4983
+ var Role = oneOf(["timekeeper", "billing", "admin"]);
4984
+ var Origin = oneOf(["engine", "hand"]);
4985
+ var DEVICE_COUNTER_NAMES = [
4986
+ "samples_received",
4987
+ "observations_written",
4988
+ "snapshots_written",
4989
+ "samples_idle",
4990
+ "samples_excluded",
4991
+ "samples_empty",
4992
+ "samples_out_of_order",
4993
+ "samples_late",
4994
+ "marks_late",
4995
+ "samples_dropped",
4996
+ "browser_breaker_trips",
4997
+ "clock_steps",
4998
+ "capture_seconds",
4999
+ "active_seconds",
5000
+ "idle_seconds",
5001
+ "locked_seconds",
5002
+ "excluded_seconds"
5003
+ ];
5004
+ function counterShape() {
5005
+ const shape = {};
5006
+ for (const name of DEVICE_COUNTER_NAMES)
5007
+ shape[name] = name.endsWith("_seconds") ? daySeconds() : count();
5008
+ return shape;
5009
+ }
5010
+ var DELIVERY = {
5011
+ via: oneOf(["release", "read_back"]),
5012
+ entry_id: ids.entry(),
5013
+ kind: ConnectorKindSchema,
5014
+ reason: DeliveryReasonSchema.nullable(),
5015
+ pms_status: word().nullable(),
5016
+ minutes: count(),
5017
+ seconds_since_released: duration(),
5018
+ changed_minutes: bool(),
5019
+ changed_day: bool(),
5020
+ changed_matter: bool(),
5021
+ changed_narrative: bool(),
5022
+ minutes_delta: minutesDelta(),
5023
+ direct: exports_external.literal(true)
5024
+ };
5025
+ var GENERATE = {
5026
+ trigger: oneOf(["press", "backstop"]),
5027
+ run_id: ids.run(),
5028
+ day_offset: dayOffset()
5029
+ };
5030
+ function spec(s) {
5031
+ return { ...s, properties: exports_external.object(s.properties).strict(), wire: s.wire };
5032
+ }
5033
+ var CATALOGUE = {
5034
+ userSignedIn: spec({
5035
+ name: "user signed in",
5036
+ owner: "cloud",
5037
+ expect: "daily",
5038
+ trigger: "POST /v1/me/provision answered a person in a firm",
5039
+ question: "Who uses Sixminute, and how did they start?",
5040
+ properties: { action: oneOf(["ready", "refresh"]), role: Role, first_sign_in: bool(), via: oneOf(["token", "invitation"]) }
5041
+ }),
5042
+ signInFailed: spec({
5043
+ name: "sign in failed",
5044
+ owner: "cloud",
5045
+ expect: "rare",
5046
+ trigger: "a sign-in ended in a refusal a person can act on",
5047
+ question: "Where does signing in break?",
5048
+ properties: { reason: oneOf(["no_firm", "choose_firm", "code_invalid"]) }
5049
+ }),
5050
+ tenantProvisioned: spec({
5051
+ name: "tenant provisioned",
5052
+ owner: "cloud",
5053
+ expect: "rare",
5054
+ trigger: "the first sign-in of a firm created its tenant row",
5055
+ question: "How many firms start?",
5056
+ properties: { via: oneOf(["token", "invitation"]) }
5057
+ }),
5058
+ memberInvited: spec({
5059
+ name: "member invited",
5060
+ owner: "cloud",
5061
+ expect: "rare",
5062
+ trigger: "an admin invited a colleague",
5063
+ question: "Do firms bring their colleagues?",
5064
+ properties: { role: Role }
5065
+ }),
5066
+ memberJoined: spec({
5067
+ name: "member joined",
5068
+ owner: "cloud",
5069
+ expect: "rare",
5070
+ trigger: "a membership row was created",
5071
+ question: "How fast does a firm fill?",
5072
+ properties: { role: Role, via: oneOf(["token", "invitation"]) }
5073
+ }),
5074
+ memberChanged: spec({
5075
+ name: "member changed",
5076
+ owner: "cloud",
5077
+ expect: "rare",
5078
+ trigger: "an admin moved a role or the active flag, or withdrew an invitation",
5079
+ question: "How do firms manage their people?",
5080
+ properties: { change: oneOf(["role", "reactivated", "deactivated", "invitation_revoked"]) }
5081
+ }),
5082
+ deviceRevoked: spec({
5083
+ name: "device revoked",
5084
+ owner: "cloud",
5085
+ expect: "rare",
5086
+ trigger: "a person revoked a device that was not revoked",
5087
+ question: "How often are desktops replaced?",
5088
+ properties: {}
5089
+ }),
5090
+ applicationUpdated: spec({
5091
+ name: "Application updated",
5092
+ owner: "cloud",
5093
+ expect: "weekly",
5094
+ trigger: "a device called with another version than the one it last called with",
5095
+ question: "How fast does a release reach every desktop?",
5096
+ properties: { from_version: version(), to_version: version() }
5097
+ }),
5098
+ clientOutdated: spec({
5099
+ name: "client outdated",
5100
+ owner: "cloud",
5101
+ expect: "rare",
5102
+ trigger: "the floor refused a desktop's registration",
5103
+ question: "Which old desktops are still in the field?",
5104
+ properties: { version: version() }
5105
+ }),
5106
+ installerDownloaded: spec({
5107
+ name: "installer downloaded",
5108
+ owner: "cloud",
5109
+ expect: "weekly",
5110
+ trigger: "the download route served an installer a person pressed for",
5111
+ question: "How many installs start from the site?",
5112
+ properties: { os: oneOf(["macos", "windows"]), arch: oneOf(["arm64", "x64", "aarch64", "x86_64", "universal"]), version: version(), via: oneOf(["latest", "direct"]) }
5113
+ }),
5114
+ settingChanged: spec({
5115
+ name: "setting changed",
5116
+ owner: "cloud",
5117
+ expect: "weekly",
5118
+ trigger: "a firm's or a person's setting row changed, or a desktop relayed its own",
5119
+ question: "Which settings matter to people?",
5120
+ properties: { setting: SettingSchema, scope: oneOf(["firm", "profile", "device"]) },
5121
+ wire: { name: "setting.changed", properties: exports_external.object({ setting: DeviceSettingSchema, scope: oneOf(["device"]) }) }
5122
+ }),
5123
+ entryDrafted: spec({
5124
+ name: "entry drafted",
5125
+ owner: "cloud",
5126
+ expect: "daily",
5127
+ trigger: "an entry was inserted: a person's by hand, or the engine's by a Generate",
5128
+ question: "How much is drafted, and of what shape?",
5129
+ properties: {
5130
+ entry_id: ids.entry(),
5131
+ origin: Origin,
5132
+ minutes: count(),
5133
+ has_matter: bool(),
5134
+ attribute_count: count(),
5135
+ narrative_chars: count(),
5136
+ day_offset: dayOffset(),
5137
+ run_id: ids.run()
5138
+ }
5139
+ }),
5140
+ entryEdited: spec({
5141
+ name: "entry edited",
5142
+ owner: "cloud",
5143
+ expect: "daily",
5144
+ trigger: "a person's edit changed a draft",
5145
+ question: "How much work is reviewing?",
5146
+ properties: {
5147
+ entry_id: ids.entry(),
5148
+ origin: Origin,
5149
+ status: oneOf(["draft", "released"]),
5150
+ is_first_edit: bool(),
5151
+ changed_narrative: bool(),
5152
+ changed_matter: bool(),
5153
+ changed_time: bool(),
5154
+ changed_attributes: bool(),
5155
+ changed_note: bool(),
5156
+ minutes_delta: signed(-1440, 1440),
5157
+ matter_change: oneOf(["set", "changed", "cleared"]).nullable(),
5158
+ attributes_changed_count: count(),
5159
+ seconds_since_drafted: duration()
5160
+ }
5161
+ }),
5162
+ entryReleased: spec({
5163
+ name: "entry released",
5164
+ owner: "cloud",
5165
+ expect: "daily",
5166
+ trigger: "a person released a draft",
5167
+ question: "How good are the drafts people release?",
5168
+ properties: {
5169
+ entry_id: ids.entry(),
5170
+ origin: Origin,
5171
+ is_unchanged: bool(),
5172
+ changed_matter: bool(),
5173
+ changed_narrative: bool(),
5174
+ changed_time: bool(),
5175
+ changed_attributes: bool(),
5176
+ changed_note: bool(),
5177
+ matter_outcome: oneOf(["kept", "filled", "replaced", "cleared"]),
5178
+ narrative_change: oneOf(["none", "light", "heavy", "rewritten"]),
5179
+ minutes: count(),
5180
+ recorded_minutes: count(),
5181
+ override: bool(),
5182
+ judged_minutes: count().nullable(),
5183
+ minutes_delta: minutesDelta(),
5184
+ attributes_changed_count: count(),
5185
+ lineage: oneOf(["original", "split", "merged"]),
5186
+ seconds_since_drafted: duration(),
5187
+ days_after_work_day: dayOffset(),
5188
+ is_delivered: bool(),
5189
+ connector_kind: ConnectorKindSchema.nullable()
5190
+ }
5191
+ }),
5192
+ releaseBlocked: spec({
5193
+ name: "release blocked",
5194
+ owner: "cloud",
5195
+ expect: "weekly",
5196
+ trigger: "a release answered a refusal, once per kind of blocker",
5197
+ question: "What stops people releasing?",
5198
+ properties: { entry_id: ids.entry(), blocker: oneOf(RELEASE_BLOCKERS) }
5199
+ }),
5200
+ entrySplit: spec({
5201
+ name: "entry split",
5202
+ owner: "cloud",
5203
+ expect: "weekly",
5204
+ trigger: "a person split a draft",
5205
+ question: "How much of an entry do people take off?",
5206
+ properties: {
5207
+ entry_id: ids.entry(),
5208
+ new_entry_id: ids.entry(),
5209
+ origin: Origin,
5210
+ was_edited: bool(),
5211
+ first_minutes: count(),
5212
+ second_minutes: count()
5213
+ }
5214
+ }),
5215
+ entryMerged: spec({
5216
+ name: "entry merged",
5217
+ owner: "cloud",
5218
+ expect: "weekly",
5219
+ trigger: "a person joined drafts",
5220
+ question: "How big are the pieces people join?",
5221
+ properties: {
5222
+ entry_id: ids.entry(),
5223
+ entries: count(),
5224
+ minutes: count(),
5225
+ first_minutes: count(),
5226
+ second_minutes: count(),
5227
+ had_same_matter: bool(),
5228
+ was_edited: bool()
5229
+ }
5230
+ }),
5231
+ entryDeleted: spec({
5232
+ name: "entry deleted",
5233
+ owner: "cloud",
5234
+ expect: "weekly",
5235
+ trigger: "a person deleted one of their drafts",
5236
+ question: "Which drafts do people throw away?",
5237
+ properties: {
5238
+ entry_id: ids.entry(),
5239
+ origin: Origin,
5240
+ has_matter: bool(),
5241
+ was_edited: bool(),
5242
+ minutes: count(),
5243
+ seconds_since_drafted: duration()
5244
+ }
5245
+ }),
5246
+ deliveryReleased: spec({
5247
+ name: "delivery released",
5248
+ owner: "cloud",
5249
+ expect: "daily",
5250
+ trigger: "a delivery reached `released`: written by the release, or moved back by a read-back",
5251
+ question: "How many entries reach the practice system?",
5252
+ properties: DELIVERY
5253
+ }),
5254
+ deliveryAccepted: spec({
5255
+ name: "delivery accepted",
5256
+ owner: "cloud",
5257
+ expect: "weekly",
5258
+ trigger: "a read-back moved a delivery to `accepted`",
5259
+ question: "What share of releases does the firm accept?",
5260
+ properties: DELIVERY
5261
+ }),
5262
+ deliveryRejected: spec({
5263
+ name: "delivery rejected",
5264
+ owner: "cloud",
5265
+ expect: "rare",
5266
+ trigger: "a read-back moved a delivery to `rejected`",
5267
+ question: "What share of releases does the firm reject?",
5268
+ properties: DELIVERY
5269
+ }),
5270
+ deliveryEdited: spec({
5271
+ name: "delivery edited",
5272
+ owner: "cloud",
5273
+ expect: "weekly",
5274
+ trigger: "a read-back found a line edited, or edited further",
5275
+ question: "What does the firm change on a line?",
5276
+ properties: DELIVERY
5277
+ }),
5278
+ deliveryBilled: spec({
5279
+ name: "delivery billed",
5280
+ owner: "cloud",
5281
+ expect: "weekly",
5282
+ trigger: "the first read-back that says the line is billed",
5283
+ question: "How long until released time is billed?",
5284
+ properties: DELIVERY
5285
+ }),
5286
+ practiceConnectionChanged: spec({
5287
+ name: "practice connection changed",
5288
+ owner: "cloud",
5289
+ expect: "rare",
5290
+ trigger: "a firm's link to its practice system, or a member's own account at it, changed",
5291
+ question: "How do firms set up and keep their practice system?",
5292
+ properties: {
5293
+ kind: ConnectorKindSchema,
5294
+ change: oneOf(["connected", "team_confirmed", "settings_saved", "disconnected", "needs_reconnect", "synced_now", "signed_in", "signed_out", "session_failed"])
5295
+ }
5296
+ }),
5297
+ practiceSynced: spec({
5298
+ name: "practice synced",
5299
+ owner: "cloud",
5300
+ expect: "daily",
5301
+ trigger: "a practice sync run ended having written a row or failed (a quiet run counts in `jobs day`)",
5302
+ question: "Is the practice system's link healthy, and what does a sync cost?",
5303
+ properties: {
5304
+ kind: ConnectorKindSchema,
5305
+ duration_ms: duration(),
5306
+ rows_read: count(),
5307
+ rows_written: count(),
5308
+ pages: count(),
5309
+ whole: bool(),
5310
+ failure: SyncFailureSchema.nullable(),
5311
+ members: count(),
5312
+ members_failed: count(),
5313
+ pressed: bool()
5314
+ }
5315
+ }),
5316
+ calendarSynced: spec({
5317
+ name: "calendar synced",
5318
+ owner: "cloud",
5319
+ expect: "daily",
5320
+ trigger: "a person's calendar sync changed an event or failed (a quiet run counts in `jobs day`)",
5321
+ question: "Are calendars read, and do they change?",
5322
+ properties: { provider: oneOf(["microsoft", "google"]), outcome: oneOf(["synced", "failed"]), events_read: count(), events_written: count() }
5323
+ }),
5324
+ sourceHealthChanged: spec({
5325
+ name: "source health changed",
5326
+ owner: "cloud",
5327
+ expect: "rare",
5328
+ trigger: "a practice system or a calendar moved between healthy, failing and needs reconnect",
5329
+ question: "Which links are broken, and since when?",
5330
+ properties: { kind: oneOf(["practice", "calendar"]), from: SourceHealthSchema, to: SourceHealthSchema }
5331
+ }),
5332
+ listChanged: spec({
5333
+ name: "list changed",
5334
+ owner: "cloud",
5335
+ expect: "weekly",
5336
+ trigger: "a matter or a client was created, edited or imported",
5337
+ question: "How do firms keep their lists?",
5338
+ properties: { list: oneOf(["matters", "clients"]), change: oneOf(["created", "edited", "imported"]), count: count() }
5339
+ }),
5340
+ generateStarted: spec({
5341
+ name: "generate started",
5342
+ owner: "cloud",
5343
+ expect: "daily",
5344
+ trigger: "a Generate run started its first attempt",
5345
+ question: "Do people press Generate, and over how much?",
5346
+ properties: { trigger: GENERATE.trigger, intervals: count(), recordings: count(), run_id: GENERATE.run_id, day_offset: GENERATE.day_offset },
5347
+ wire: { name: "generate.started" }
5348
+ }),
5349
+ generateFinished: spec({
5350
+ name: "generate finished",
5351
+ owner: "cloud",
5352
+ expect: "daily",
5353
+ trigger: "a Generate run ended done",
5354
+ question: "Does Generate work, and what does it cost?",
5355
+ properties: {
5356
+ trigger: GENERATE.trigger,
5357
+ entries: count(),
5358
+ blocks_joined: count(),
5359
+ seconds: count(),
5360
+ calls: count(),
5361
+ run_id: GENERATE.run_id,
5362
+ day_offset: GENERATE.day_offset,
5363
+ entries_clipped: count(),
5364
+ attempts: count()
5365
+ },
5366
+ wire: { name: "generate.finished" }
5367
+ }),
5368
+ generateFailed: spec({
5369
+ name: "generate failed",
5370
+ owner: "cloud",
5371
+ expect: "rare",
5372
+ trigger: "a Generate run failed for good",
5373
+ question: "Why does Generate fail?",
5374
+ properties: {
5375
+ trigger: GENERATE.trigger,
5376
+ step: oneOf(["decide", "join", "narrative", "write"]),
5377
+ run_id: GENERATE.run_id,
5378
+ day_offset: GENERATE.day_offset,
5379
+ failure: GenerateFailureSchema,
5380
+ attempts: count()
5381
+ },
5382
+ wire: { name: "generate.failed" }
5383
+ }),
5384
+ entryProposed: spec({
5385
+ name: "entry proposed",
5386
+ owner: "cloud",
5387
+ expect: "weekly",
5388
+ trigger: "a propose job answered",
5389
+ question: "Do the merge, split and drag proposals help?",
5390
+ properties: {
5391
+ ranges: count(),
5392
+ intervals: count(),
5393
+ how: oneOf(["chosen", "most", "tie", "none"]),
5394
+ tie_called: bool(),
5395
+ attribute_calls: count(),
5396
+ kept: oneOf(["no_text", "not_signed_in", "refused", "offline"]),
5397
+ profile_ms: duration(),
5398
+ read_ms: duration(),
5399
+ matter_ms: duration(),
5400
+ attributes_ms: duration(),
5401
+ narrative_ms: duration(),
5402
+ total_ms: duration(),
5403
+ run_id: ids.run()
5404
+ },
5405
+ wire: { name: "entry.proposed" }
5406
+ }),
5407
+ aiGeneration: spec({
5408
+ name: "$ai_generation",
5409
+ owner: "cloud",
5410
+ expect: "daily",
5411
+ trigger: "a `job_calls` row of a model call was written (a cache hit is no call)",
5412
+ question: "What do model calls cost, and do they fail?",
5413
+ properties: {
5414
+ $ai_model: modelId(),
5415
+ $ai_provider: word(),
5416
+ $ai_span_name: word(),
5417
+ $ai_input_tokens: count(),
5418
+ $ai_output_tokens: count(),
5419
+ $ai_cache_read_input_tokens: count(),
5420
+ $ai_cache_reporting_exclusive: bool(),
5421
+ $ai_total_cost_usd: usd(),
5422
+ $ai_cost_passthrough: bool(),
5423
+ $ai_latency: usd(),
5424
+ $ai_is_error: bool(),
5425
+ $ai_http_status: count(),
5426
+ $ai_trace_id: ids.run().or(uuid()).describe("safe:trace"),
5427
+ is_priced: bool(),
5428
+ run_kind: oneOf(["interval", "generate", "regenerate", "propose", "calendar_judge"])
5429
+ }
5430
+ }),
5431
+ modelCapHit: spec({
5432
+ name: "model cap hit",
5433
+ owner: "cloud",
5434
+ expect: "rare",
5435
+ trigger: "a member's daily cap refused a call or held a run",
5436
+ question: "Do the caps bite?",
5437
+ properties: {
5438
+ purpose: word(),
5439
+ day: day(),
5440
+ calls: count(),
5441
+ cost_usd: usd(),
5442
+ prompt_tokens: count(),
5443
+ completion_tokens: count(),
5444
+ run_kind: oneOf(["interval", "generate", "regenerate", "propose", "calendar_judge"])
5445
+ }
5446
+ }),
5447
+ jobsDay: spec({
5448
+ name: "jobs day",
5449
+ owner: "cloud",
5450
+ expect: "daily",
5451
+ trigger: "the first job of a subject to end on a UTC day counts that subject's runs of the day before, per kind",
5452
+ question: "How much background work runs, and how much of it fails?",
5453
+ properties: { kind: JobKindSchema, day: day(), runs: count(), failed: count() }
5454
+ }),
5455
+ dayAccounted: spec({
5456
+ name: "day accounted",
5457
+ owner: "cloud",
5458
+ expect: "daily",
5459
+ trigger: "the `day_accounts` job accounted a person's day two days after it ended",
5460
+ question: "Where did each person's day go?",
5461
+ properties: {
5462
+ day: day(),
5463
+ unobserved_seconds: daySeconds(),
5464
+ away_seconds: daySeconds(),
5465
+ work_undrafted_seconds: daySeconds(),
5466
+ drafted_seconds: daySeconds(),
5467
+ released_seconds: daySeconds(),
5468
+ day_seconds: duration(),
5469
+ devices: count(),
5470
+ capturing: bool(),
5471
+ late_contexts: count()
5472
+ }
5473
+ }),
5474
+ deviceCounters: spec({
5475
+ name: "device counters",
5476
+ owner: "host",
5477
+ expect: "daily",
5478
+ trigger: "a desktop posted its day's counters to POST /v1/telemetry",
5479
+ question: "Is capture healthy on each machine?",
5480
+ properties: { day: day(), ...counterShape() }
5481
+ }),
5482
+ captureStarted: spec({
5483
+ name: "capture started",
5484
+ owner: "host",
5485
+ expect: "daily",
5486
+ trigger: "a desktop turned capture on",
5487
+ question: "How do people start capturing?",
5488
+ properties: { trigger: oneOf(["button", "tray", "always_on", "launch", "sign_in"]), is_always_on: bool(), swallow_short_pauses: bool() },
5489
+ wire: { name: "capture.started" }
5490
+ }),
5491
+ captureStopped: spec({
5492
+ name: "capture stopped",
5493
+ owner: "host",
5494
+ expect: "daily",
5495
+ trigger: "a desktop turned capture off",
5496
+ question: "How long do people capture?",
5497
+ properties: { trigger: oneOf(["button", "tray", "quit", "update", "sign_out"]), seconds: count(), entries: count(), windows_failed: count() },
5498
+ wire: { name: "capture.stopped" }
5499
+ }),
5500
+ userSignedOut: spec({
5501
+ name: "user signed out",
5502
+ owner: "host",
5503
+ expect: "rare",
5504
+ trigger: "a desktop signed out",
5505
+ question: "Why do people sign out?",
5506
+ properties: { trigger: oneOf(["menu", "tray", "session_expired"]) },
5507
+ wire: { name: "user.signed_out" }
5508
+ }),
5509
+ permissionChanged: spec({
5510
+ name: "permission changed",
5511
+ owner: "host",
5512
+ expect: "rare",
5513
+ trigger: "a desktop's system permission changed",
5514
+ question: "Do people grant what capture needs?",
5515
+ properties: { permission: oneOf(["accessibility", "automation"]), is_granted: bool() },
5516
+ wire: { name: "permission.changed" }
5517
+ }),
5518
+ updateFailed: spec({
5519
+ name: "update failed",
5520
+ owner: "host",
5521
+ expect: "rare",
5522
+ trigger: "a desktop's update failed",
5523
+ question: "Where do updates break?",
5524
+ properties: { stage: oneOf(["check", "download", "apply"]) },
5525
+ wire: { name: "update.failed" }
5526
+ }),
5527
+ rawArchiveTurnedOn: spec({
5528
+ name: "raw archive turned on",
5529
+ owner: "host",
5530
+ expect: "rare",
5531
+ trigger: "a person switched the raw archive on",
5532
+ question: "Who keeps a raw archive?",
5533
+ properties: { reason: oneOf(["person"]) },
5534
+ wire: { name: "raw_archive.turned_on", beside: ["reason"] }
5535
+ }),
5536
+ rawArchiveTurnedOff: spec({
5537
+ name: "raw archive turned off",
5538
+ owner: "host",
5539
+ expect: "rare",
5540
+ trigger: "the raw archive was switched off",
5541
+ question: "Why does a raw archive stop?",
5542
+ properties: { reason: RawArchiveReasonSchema },
5543
+ wire: { name: "raw_archive.turned_off", beside: ["reason"] }
5544
+ }),
5545
+ applicationOpened: spec({
5546
+ name: "Application opened",
5547
+ owner: "page",
5548
+ expect: "daily",
5549
+ trigger: "the window opened",
5550
+ question: "How often do people open Sixminute?",
5551
+ properties: { trigger: oneOf(["launch", "login_item", "after_update", "tray"]), is_first_open: bool() },
5552
+ wire: { name: "application.opened" }
5553
+ }),
5554
+ applicationBecameActive: spec({
5555
+ name: "Application became active",
5556
+ owner: "page",
5557
+ expect: "daily",
5558
+ trigger: "the window came to the front",
5559
+ question: "How do people come back to the window?",
5560
+ properties: { trigger: oneOf(["launch", "tray", "dock", "notification", "shortcut"]) },
5561
+ wire: { name: "application.became_active" }
5562
+ }),
5563
+ applicationBackgrounded: spec({
5564
+ name: "Application backgrounded",
5565
+ owner: "page",
5566
+ expect: "daily",
5567
+ trigger: "the window left the front",
5568
+ question: "How long is the window in front?",
5569
+ properties: { foreground_seconds: daySeconds() },
5570
+ wire: { name: "application.backgrounded" }
5571
+ }),
5572
+ appUsage: spec({
5573
+ name: "app usage",
5574
+ owner: "page",
5575
+ expect: "daily",
5576
+ trigger: "a day's time in front of the window",
5577
+ question: "How much time do people spend in Sixminute?",
5578
+ properties: { day: day(), foreground_seconds: daySeconds(), sessions: count() },
5579
+ wire: { name: "app.usage" }
5580
+ }),
5581
+ screenViewed: spec({
5582
+ name: "$screen",
5583
+ owner: "page",
5584
+ expect: "daily",
5585
+ trigger: "a screen was shown",
5586
+ question: "Which screens do people use?",
5587
+ properties: {
5588
+ $screen_name: TelemetryScreenSchema,
5589
+ settings_page: oneOf([
5590
+ "profile",
5591
+ "calendar",
5592
+ "preferences",
5593
+ "appearance",
5594
+ "capture",
5595
+ "privacy",
5596
+ "general",
5597
+ "members",
5598
+ "timekeeping",
5599
+ "narratives",
5600
+ "practice-system",
5601
+ "matters",
5602
+ "member"
5603
+ ]),
5604
+ from_screen: TelemetryScreenSchema,
5605
+ day_offset: dayOffset()
5606
+ },
5607
+ wire: { name: "screen.viewed", rename: { screen: "$screen_name" } }
5608
+ }),
5609
+ commandUsed: spec({
5610
+ name: "command used",
5611
+ owner: "page",
5612
+ expect: "daily",
5613
+ trigger: "a command ran from a menu, the palette, a shortcut or a button",
5614
+ question: "Which commands do people use, and how?",
5615
+ properties: { command: TelemetryCommandSchema, via: oneOf(["menu", "palette", "shortcut", "row_menu", "tray", "button"]) },
5616
+ wire: { name: "command.used" }
5617
+ }),
5618
+ entryOpened: spec({
5619
+ name: "entry opened",
5620
+ owner: "page",
5621
+ expect: "daily",
5622
+ trigger: "an entry's page opened",
5623
+ question: "Which entries do people look into?",
5624
+ properties: { entry_id: ids.entry(), from_screen: TelemetryScreenSchema, status: oneOf(["draft", "released"]) },
5625
+ wire: { name: "entry.opened" }
5626
+ }),
5627
+ releaseClicked: spec({
5628
+ name: "release clicked",
5629
+ owner: "page",
5630
+ expect: "daily",
5631
+ trigger: "a person pressed release",
5632
+ question: "How do people release?",
5633
+ properties: { entries: count(), via: oneOf(["row", "release_all", "palette", "entry_page", "selection", "shortcut", "menu"]) },
5634
+ wire: { name: "release.clicked" }
5635
+ }),
5636
+ generateClicked: spec({
5637
+ name: "generate clicked",
5638
+ owner: "page",
5639
+ expect: "daily",
5640
+ trigger: "a person pressed Generate",
5641
+ question: "Where do people press Generate?",
5642
+ properties: { layout: oneOf(["timeline", "list"]) },
5643
+ wire: { name: "generate.clicked" }
5644
+ }),
5645
+ onboardingStepViewed: spec({
5646
+ name: "onboarding step viewed",
5647
+ owner: "page",
5648
+ expect: "weekly",
5649
+ trigger: "an onboarding step was shown",
5650
+ question: "Where does onboarding lose people?",
5651
+ properties: { step: oneOf(["capture", "entries", "permissions", "release"]) },
5652
+ wire: { name: "onboarding.step_viewed" }
5653
+ }),
5654
+ onboardingCompleted: spec({
5655
+ name: "onboarding completed",
5656
+ owner: "page",
5657
+ expect: "weekly",
5658
+ trigger: "onboarding ended",
5659
+ question: "How long does onboarding take?",
5660
+ properties: { seconds: duration() },
5661
+ wire: { name: "onboarding.completed" }
5662
+ }),
5663
+ appError: spec({
5664
+ name: "app error",
5665
+ owner: "page",
5666
+ expect: "rare",
5667
+ trigger: "the page caught an error",
5668
+ question: "Where does the page break?",
5669
+ properties: { kind: AppErrorKindSchema, error_class: AppErrorClassSchema, $screen_name: TelemetryScreenSchema, page_build: PageBuildSchema },
5670
+ wire: { name: "app.error", rename: { screen: "$screen_name" } }
5671
+ })
5672
+ };
5673
+ var EVENT_KEYS = Object.keys(CATALOGUE);
5674
+ var WIRE_KEYS = new Map(EVENT_KEYS.flatMap((key) => {
5675
+ const wire = CATALOGUE[key].wire;
5676
+ return wire === undefined ? [] : [[wire.name, key]];
5677
+ }));
5678
+ var TELEMETRY_WIRE_NAMES = [...WIRE_KEYS.keys()];
5679
+ function keyOfWire(name) {
5680
+ return WIRE_KEYS.get(name);
5681
+ }
5682
+ function wireSchemaOf(name) {
5683
+ const wire = CATALOGUE[keyOfWire(name)].wire;
5684
+ if (wire.properties !== undefined)
5685
+ return wire.properties;
5686
+ const back = new Map(Object.entries(wire.rename ?? {}).map(([wireKey, eventKey]) => [eventKey, wireKey]));
5687
+ const shape = {};
5688
+ for (const [key, schema] of Object.entries(CATALOGUE[keyOfWire(name)].properties.shape)) {
5689
+ if (!(wire.beside ?? []).includes(key))
5690
+ shape[back.get(key) ?? key] = schema;
5691
+ }
5692
+ return exports_external.object(shape);
5693
+ }
5694
+ // packages/contracts/src/analytics/common.ts
5695
+ var EventSourceSchema = oneOf(["api", "pipeline", "job", "daemon", "app"]);
5696
+ var OsSchema = oneOf(["macos", "windows", "ios", "android", "web"]);
5697
+ var COMMON_PROPERTIES = {
5698
+ source: EventSourceSchema,
5699
+ is_internal: bool(),
5700
+ app_version: version(),
5701
+ os: OsSchema,
5702
+ device_id: ids.device(),
5703
+ firm_id: ids.firm(),
5704
+ $session_id: sessionId()
5705
+ };
5706
+ // packages/contracts/src/calendar.ts
5707
+ var CalendarProviderSchema = exports_external.enum(["microsoft", "google"]);
5708
+ var CalendarAccountKindSchema = exports_external.enum(["work", "personal"]);
5709
+ var CalendarConnectionStatusSchema = exports_external.enum(["connected", "awaiting_admin", "needs_reconnect"]);
5710
+ var CalendarConnectionSchema = exports_external.object({
5711
+ provider: CalendarProviderSchema,
5712
+ account_kind: CalendarAccountKindSchema,
5713
+ account_email: exports_external.string(),
5714
+ status: CalendarConnectionStatusSchema,
5715
+ admin_consent_url: exports_external.string().nullable(),
5716
+ last_synced_at: TimestampSchema.nullable()
5717
+ });
5718
+ var CalendarConnectRequestSchema = exports_external.object({
5719
+ provider: CalendarProviderSchema,
5720
+ state: exports_external.string().min(16),
5721
+ code_challenge: exports_external.string().min(16)
5722
+ });
5723
+ var CalendarConnectAnswerSchema = exports_external.object({ authorize_url: exports_external.string().url(), state_token: exports_external.string().min(1).optional() });
5724
+ var CalendarConnectFinishSchema = exports_external.union([
5725
+ exports_external.object({
5726
+ state: exports_external.string().min(16),
5727
+ state_token: exports_external.string().min(1).max(4096).optional(),
5728
+ code: exports_external.string().min(1),
5729
+ code_verifier: exports_external.string().min(43).max(128)
5730
+ }).strict(),
5731
+ exports_external.object({
5732
+ state: exports_external.string().min(16),
5733
+ state_token: exports_external.string().min(1).max(4096).optional(),
5734
+ error: exports_external.string().min(1).max(200),
5735
+ error_description: exports_external.string().max(2000).optional(),
5736
+ error_subcode: exports_external.string().max(200).optional()
5737
+ }).strict()
5738
+ ]);
5739
+ var CalendarResponseSchema = exports_external.enum(["organiser", "accepted", "tentative", "declined", "none"]);
5740
+ var CalendarGuestSchema = exports_external.object({
5741
+ name: exports_external.string(),
5742
+ email: exports_external.string().nullable(),
5743
+ response: CalendarResponseSchema,
5744
+ organiser: exports_external.boolean(),
5745
+ me: exports_external.boolean()
5746
+ });
5747
+ var CalendarWhereSchema = exports_external.object({
5748
+ kind: exports_external.enum(["call", "address", "none"]),
5749
+ text: exports_external.string().nullable()
5750
+ });
5751
+ var CalendarEventSchema = exports_external.object({
5752
+ id: exports_external.string().min(1),
5753
+ title: exports_external.string(),
5754
+ from: TimestampSchema,
5755
+ to: TimestampSchema,
5756
+ all_day: exports_external.boolean(),
5757
+ status: exports_external.enum(["confirmed", "cancelled"]),
5758
+ my_response: CalendarResponseSchema,
5759
+ where: CalendarWhereSchema,
5760
+ guests: exports_external.array(CalendarGuestSchema),
5761
+ provider: CalendarProviderSchema,
5762
+ web_link: exports_external.string().nullable(),
5763
+ not_attended: exports_external.boolean(),
5764
+ can_mark: exports_external.boolean()
5765
+ });
5766
+ var CalendarDaySchema = exports_external.object({
5767
+ events: exports_external.array(CalendarEventSchema)
5768
+ });
5769
+
4941
5770
  // packages/contracts/src/evidence.ts
4942
5771
  var HostSchema = exports_external.string().transform((h) => h.trim().toLowerCase()).pipe(exports_external.string().max(253).regex(/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/, "expected a url host: lower-case labels, no scheme, no port and no path"));
4943
5772
  var WitnessSchema = exports_external.object({
@@ -5036,11 +5865,18 @@ var IntervalTextSchema = exports_external.object({
5036
5865
  // packages/contracts/src/api.ts
5037
5866
  var ProblemSchema = exports_external.object({
5038
5867
  type: exports_external.string().min(1),
5868
+ code: exports_external.string().min(1).optional(),
5039
5869
  title: exports_external.string().min(1),
5040
5870
  status: exports_external.number().int().min(400).max(599),
5041
5871
  detail: exports_external.string().optional(),
5042
5872
  instance: exports_external.string().optional(),
5043
- errors: exports_external.array(exports_external.object({ path: exports_external.string(), message: exports_external.string(), reason: exports_external.string().optional() })).optional()
5873
+ errors: exports_external.array(exports_external.object({
5874
+ path: exports_external.string(),
5875
+ message: exports_external.string(),
5876
+ reason: exports_external.string().optional(),
5877
+ rule: exports_external.string().min(1).optional(),
5878
+ params: exports_external.record(exports_external.union([exports_external.string(), exports_external.number()])).optional()
5879
+ })).optional()
5044
5880
  });
5045
5881
  var CursorSchema = exports_external.string().min(1);
5046
5882
  var PullQuerySchema = exports_external.object({
@@ -5108,10 +5944,9 @@ var EntrySizeSchema = exports_external.enum(["fewer", "balanced", "more"]);
5108
5944
  var MePatchSchema = exports_external.object({
5109
5945
  name: exports_external.string().trim().min(1).max(200).optional(),
5110
5946
  narrative_language: NarrativeLanguageSchema.nullable().optional(),
5111
- entry_size: EntrySizeSchema.nullable().optional()
5112
- }).refine((patch) => patch.name !== undefined || patch.narrative_language !== undefined || patch.entry_size !== undefined, {
5113
- message: "name, narrative_language or entry_size is required"
5114
- });
5947
+ entry_size: EntrySizeSchema.nullable().optional(),
5948
+ time_zone: exports_external.string().min(1).max(64).optional()
5949
+ }).refine((patch) => patch.name !== undefined || patch.narrative_language !== undefined || patch.entry_size !== undefined || patch.time_zone !== undefined, { message: "name, narrative_language, entry_size or time_zone is required" });
5115
5950
  var MemberSchema = exports_external.object({
5116
5951
  id: MemberIdSchema,
5117
5952
  user_id: UserIdSchema,
@@ -5168,14 +6003,19 @@ var MeSchema = exports_external.object({
5168
6003
  connector: MeConnectorSchema.nullable()
5169
6004
  }),
5170
6005
  member: MemberSchema.extend({
5171
- system_link: MeSystemLinkSchema.nullable().default(null)
6006
+ system_link: MeSystemLinkSchema.nullable().default(null),
6007
+ time_zone: exports_external.string().nullable().optional(),
6008
+ time_zone_next: exports_external.string().nullable().optional(),
6009
+ time_zone_next_from: LocalDaySchema.nullable().optional()
5172
6010
  }),
5173
6011
  permissions: exports_external.array(exports_external.string()).default([]),
5174
6012
  judgment_pipeline: JudgmentPipelineSchema.default("standard"),
5175
6013
  legalsense_connector: exports_external.boolean().default(false),
5176
6014
  calendar_offered: exports_external.boolean().optional(),
5177
6015
  swallow_short_pauses: exports_external.boolean().optional(),
5178
- pipeline_shadow: exports_external.boolean().optional()
6016
+ pipeline_shadow: exports_external.boolean().optional(),
6017
+ narrative_limit: exports_external.object({ limit: exports_external.number().int().positive(), by: exports_external.enum(["firm", "house", "system", "max"]) }).optional(),
6018
+ practice: exports_external.record(exports_external.unknown()).nullable().optional()
5179
6019
  });
5180
6020
  var ProvisionResultSchema = MeSchema.extend({
5181
6021
  action: exports_external.enum(["ready", "refresh"]).default("ready")
@@ -5297,10 +6137,24 @@ var EntryRowSchema = exports_external.object({
5297
6137
  entry: EntrySchema,
5298
6138
  delivery: DeliverySchema.nullable().optional()
5299
6139
  });
6140
+ var DayBlockerSchema = exports_external.object({
6141
+ path: exports_external.string().min(1),
6142
+ rule: exports_external.string().min(1),
6143
+ params: exports_external.record(exports_external.union([exports_external.string(), exports_external.number()])).optional()
6144
+ });
6145
+ var DayEntryRowSchema = EntryRowSchema.extend({
6146
+ editable: exports_external.boolean().optional(),
6147
+ blockers: exports_external.array(DayBlockerSchema).optional(),
6148
+ options: exports_external.record(exports_external.array(OptionSchema)).optional()
6149
+ });
5300
6150
  var DayReadSchema = exports_external.object({
5301
6151
  day: LocalDaySchema,
5302
6152
  unit_seconds: exports_external.number().int().positive(),
5303
- entries: exports_external.array(EntryRowSchema),
6153
+ zone: exports_external.string().optional(),
6154
+ t_start: TimestampSchema.optional(),
6155
+ t_end: TimestampSchema.optional(),
6156
+ provisional: exports_external.boolean().optional(),
6157
+ entries: exports_external.array(DayEntryRowSchema),
5304
6158
  intervals: exports_external.array(DayIntervalSchema),
5305
6159
  entry_intervals: exports_external.array(DayIntervalSchema).default([]),
5306
6160
  calendar: CalendarDaySchema.optional()
@@ -5600,185 +6454,8 @@ var CompletionWireResponseSchema = exports_external.object({
5600
6454
  priced: exports_external.boolean()
5601
6455
  })
5602
6456
  });
5603
- var RawArchiveReasonSchema = exports_external.enum([
5604
- "person",
5605
- "expired",
5606
- "signed_out",
5607
- "tenant_changed",
5608
- "not_allowed",
5609
- "refused",
5610
- "disk_low"
5611
- ]);
5612
6457
  var RAW_ARCHIVE_EVENT_NAMES = ["raw_archive.turned_on", "raw_archive.turned_off"];
5613
- var TelemetryScreenSchema = exports_external.enum(["day", "timeline", "entry", "settings", "sign_in", "onboarding", "permissions"]);
5614
- var TelemetryCommandSchema = exports_external.enum([
5615
- "new-entry",
5616
- "release-all",
5617
- "settings",
5618
- "exit",
5619
- "undo",
5620
- "redo",
5621
- "cut",
5622
- "copy",
5623
- "paste",
5624
- "select-all",
5625
- "today",
5626
- "previous-day",
5627
- "next-day",
5628
- "previous-entry",
5629
- "next-entry",
5630
- "actions",
5631
- "capture",
5632
- "privacy",
5633
- "about",
5634
- "folder",
5635
- "release",
5636
- "matter",
5637
- "attribute",
5638
- "merge",
5639
- "delete",
5640
- "open"
5641
- ]);
5642
- var Whole = exports_external.number().int().nonnegative();
5643
- var TELEMETRY_SECONDS_MAX = 86400;
5644
- var ForegroundSeconds = Whole.max(TELEMETRY_SECONDS_MAX);
5645
- var TELEMETRY_DAY_OFFSET_MAX = 3660;
5646
- var TELEMETRY_ENTRY_ID_MAX = 64;
5647
- var GenerateFailureSchema = exports_external.enum(["model", "deadline", "overlap", "other"]);
5648
- var AppErrorKindSchema = exports_external.enum(["boundary", "uncaught", "unhandled_rejection"]);
5649
- var AppErrorClassSchema = exports_external.enum([
5650
- "Error",
5651
- "TypeError",
5652
- "RangeError",
5653
- "ReferenceError",
5654
- "SyntaxError",
5655
- "ChunkLoadError",
5656
- "AbortError",
5657
- "other"
5658
- ]);
5659
- var PageBuildSchema = exports_external.string().regex(/^\d{14}-[0-9A-Za-z]{1,40}$/, "expected YYYYMMDDHHMMSS-<commit>");
5660
- var TELEMETRY_EVENT_PROPERTIES = {
5661
- "capture.started": exports_external.object({
5662
- trigger: exports_external.enum(["button", "tray", "always_on", "launch", "sign_in"]),
5663
- is_always_on: exports_external.boolean(),
5664
- swallow_short_pauses: exports_external.boolean().optional()
5665
- }),
5666
- "capture.stopped": exports_external.object({
5667
- trigger: exports_external.enum(["button", "tray", "quit", "update", "sign_out"]),
5668
- seconds: Whole,
5669
- entries: Whole,
5670
- windows_failed: Whole
5671
- }),
5672
- "user.signed_out": exports_external.object({ trigger: exports_external.enum(["menu", "tray", "session_expired"]) }),
5673
- "permission.changed": exports_external.object({ permission: exports_external.enum(["accessibility", "automation"]), is_granted: exports_external.boolean() }),
5674
- "update.failed": exports_external.object({ stage: exports_external.enum(["check", "download", "apply"]) }),
5675
- "setting.changed": exports_external.object({
5676
- setting: exports_external.enum([
5677
- "unit_minutes",
5678
- "narrative_language",
5679
- "narrative_max_chars",
5680
- "always_on",
5681
- "launch_at_login",
5682
- "theme",
5683
- "exclusions",
5684
- "entry_size"
5685
- ]),
5686
- scope: exports_external.enum(["device"])
5687
- }),
5688
- "raw_archive.turned_on": exports_external.object({}),
5689
- "raw_archive.turned_off": exports_external.object({}),
5690
- "generate.started": exports_external.object({
5691
- trigger: exports_external.enum(["press", "backstop"]),
5692
- intervals: Whole,
5693
- recordings: Whole,
5694
- run_id: RunIdSchema,
5695
- day_offset: exports_external.number().int().min(-TELEMETRY_DAY_OFFSET_MAX).max(TELEMETRY_DAY_OFFSET_MAX)
5696
- }),
5697
- "generate.finished": exports_external.object({
5698
- trigger: exports_external.enum(["press", "backstop"]),
5699
- entries: Whole,
5700
- blocks_joined: Whole,
5701
- seconds: Whole,
5702
- calls: Whole,
5703
- run_id: RunIdSchema,
5704
- day_offset: exports_external.number().int().min(-TELEMETRY_DAY_OFFSET_MAX).max(TELEMETRY_DAY_OFFSET_MAX),
5705
- entries_clipped: Whole,
5706
- attempts: Whole
5707
- }),
5708
- "generate.failed": exports_external.object({
5709
- trigger: exports_external.enum(["press", "backstop"]),
5710
- step: exports_external.enum(["decide", "join", "narrative", "write"]),
5711
- run_id: RunIdSchema,
5712
- day_offset: exports_external.number().int().min(-TELEMETRY_DAY_OFFSET_MAX).max(TELEMETRY_DAY_OFFSET_MAX),
5713
- failure: GenerateFailureSchema,
5714
- attempts: Whole
5715
- }),
5716
- "entry.proposed": exports_external.object({
5717
- ranges: Whole,
5718
- intervals: Whole,
5719
- how: exports_external.enum(["chosen", "most", "tie", "none"]),
5720
- tie_called: exports_external.boolean(),
5721
- attribute_calls: Whole,
5722
- kept: exports_external.enum(["no_text", "not_signed_in", "refused", "offline"]),
5723
- profile_ms: Whole,
5724
- read_ms: Whole,
5725
- matter_ms: Whole,
5726
- attributes_ms: Whole,
5727
- narrative_ms: Whole,
5728
- total_ms: Whole,
5729
- run_id: RunIdSchema
5730
- }),
5731
- "application.opened": exports_external.object({
5732
- trigger: exports_external.enum(["launch", "login_item", "after_update", "tray"]),
5733
- is_first_open: exports_external.boolean()
5734
- }),
5735
- "application.became_active": exports_external.object({ trigger: exports_external.enum(["launch", "tray", "dock", "notification", "shortcut"]) }),
5736
- "application.backgrounded": exports_external.object({ foreground_seconds: ForegroundSeconds }),
5737
- "app.usage": exports_external.object({ day: LocalDaySchema, foreground_seconds: ForegroundSeconds, sessions: Whole }),
5738
- "screen.viewed": exports_external.object({
5739
- screen: TelemetryScreenSchema,
5740
- settings_page: exports_external.enum([
5741
- "profile",
5742
- "calendar",
5743
- "preferences",
5744
- "appearance",
5745
- "capture",
5746
- "privacy",
5747
- "general",
5748
- "members",
5749
- "timekeeping",
5750
- "narratives",
5751
- "practice-system",
5752
- "matters",
5753
- "member"
5754
- ]),
5755
- from_screen: TelemetryScreenSchema,
5756
- day_offset: exports_external.number().int().min(-TELEMETRY_DAY_OFFSET_MAX).max(TELEMETRY_DAY_OFFSET_MAX)
5757
- }),
5758
- "command.used": exports_external.object({
5759
- command: TelemetryCommandSchema,
5760
- via: exports_external.enum(["menu", "palette", "shortcut", "row_menu", "tray", "button"])
5761
- }),
5762
- "entry.opened": exports_external.object({
5763
- entry_id: EntryIdSchema.max(TELEMETRY_ENTRY_ID_MAX),
5764
- from_screen: TelemetryScreenSchema,
5765
- status: exports_external.enum(["draft", "released"])
5766
- }),
5767
- "release.clicked": exports_external.object({
5768
- entries: Whole,
5769
- via: exports_external.enum(["row", "release_all", "palette", "entry_page", "selection", "shortcut", "menu"])
5770
- }),
5771
- "generate.clicked": exports_external.object({ layout: exports_external.enum(["timeline", "list"]) }),
5772
- "onboarding.step_viewed": exports_external.object({ step: exports_external.enum(["capture", "entries", "permissions", "release"]) }),
5773
- "onboarding.completed": exports_external.object({ seconds: Whole }),
5774
- "app.error": exports_external.object({
5775
- kind: AppErrorKindSchema,
5776
- error_class: AppErrorClassSchema,
5777
- screen: TelemetryScreenSchema,
5778
- page_build: PageBuildSchema
5779
- })
5780
- };
5781
- var TelemetryEventNameSchema = exports_external.enum(Object.keys(TELEMETRY_EVENT_PROPERTIES));
6458
+ var TelemetryEventNameSchema = exports_external.enum(TELEMETRY_WIRE_NAMES);
5782
6459
  var TelemetryPropertyValueSchema = exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()]);
5783
6460
  var TelemetrySessionIdSchema = exports_external.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, "expected a UUIDv7");
5784
6461
  var TelemetryEventSchema = exports_external.object({
@@ -5799,7 +6476,7 @@ var TelemetryEventSchema = exports_external.object({
5799
6476
  refuse("raw_archive.turned_on carries the reason person, and only that");
5800
6477
  }
5801
6478
  if (event.properties !== undefined) {
5802
- const allowed = TELEMETRY_EVENT_PROPERTIES[event.name].partial().strict().safeParse(event.properties);
6479
+ const allowed = wireSchemaOf(event.name).partial().strict().safeParse(event.properties);
5803
6480
  if (!allowed.success) {
5804
6481
  for (const issue of allowed.error.issues) {
5805
6482
  ctx.addIssue({ code: exports_external.ZodIssueCode.custom, path: ["properties", ...issue.path], message: issue.message });
@@ -5852,111 +6529,6 @@ var FirmInvitationsSchema = exports_external.object({ invitations: exports_exter
5852
6529
  var InvitationRevokedSchema = exports_external.object({
5853
6530
  invitation: exports_external.object({ id: exports_external.string(), email: exports_external.string() })
5854
6531
  });
5855
- // packages/contracts/src/admin-research.ts
5856
- var ResearchExportParamsSchema = exports_external.object({ tenant: TenantIdSchema });
5857
- var ResearchExportRequestSchema = exports_external.object({
5858
- from: LocalDaySchema,
5859
- to: LocalDaySchema
5860
- }).refine((range) => !Number.isNaN(Date.parse(`${range.from}T00:00:00.000Z`)), {
5861
- message: "from is not a real day",
5862
- path: ["from"]
5863
- }).refine((range) => !Number.isNaN(Date.parse(`${range.to}T00:00:00.000Z`)), {
5864
- message: "to is not a real day",
5865
- path: ["to"]
5866
- }).refine((range) => range.from < range.to, {
5867
- message: "from must be before to; the range is half-open and an empty one exports nothing",
5868
- path: ["to"]
5869
- });
5870
- var Base64Schema = exports_external.string().regex(/^[A-Za-z0-9+/]*={0,2}$/, "expected base64");
5871
- var SealedResearchCardSchema = exports_external.object({
5872
- id: exports_external.string(),
5873
- tenant_id: exports_external.string(),
5874
- member_id: exports_external.string(),
5875
- device_id: exports_external.string().nullable(),
5876
- purpose: exports_external.string(),
5877
- model: exports_external.string(),
5878
- created_at: exports_external.string(),
5879
- key_version: exports_external.number().int().positive(),
5880
- sealed_key: Base64Schema,
5881
- card_ct: Base64Schema,
5882
- answer_ct: Base64Schema
5883
- });
5884
- var JudgedPairEntrySchema = exports_external.object({
5885
- day: exports_external.string(),
5886
- time_zone: exports_external.string(),
5887
- timekeeper_id: exports_external.string(),
5888
- t_start: exports_external.string(),
5889
- t_end: exports_external.string(),
5890
- parts: exports_external.array(exports_external.object({ t_start: exports_external.string(), t_end: exports_external.string() })).nullable().default(null),
5891
- override_seconds: exports_external.number().int().nullable().default(null),
5892
- narrative: exports_external.string(),
5893
- matter_id: exports_external.string().nullable(),
5894
- attributes: exports_external.unknown(),
5895
- internal_note: exports_external.string().nullable()
5896
- });
5897
- var JudgedPairSchema = exports_external.object({
5898
- entry_id: exports_external.string(),
5899
- tenant_id: exports_external.string(),
5900
- member_id: exports_external.string(),
5901
- day: exports_external.string(),
5902
- status: exports_external.string(),
5903
- edited_at: exports_external.string().nullable(),
5904
- as_judged: exports_external.unknown(),
5905
- as_released_or_current: JudgedPairEntrySchema
5906
- });
5907
- var ExportHeadSchema = exports_external.object({
5908
- export_id: exports_external.string(),
5909
- tenant_id: exports_external.string(),
5910
- tenant_name: exports_external.string(),
5911
- from: exports_external.string(),
5912
- to: exports_external.string(),
5913
- operator: exports_external.string()
5914
- });
5915
- var ResearchExportConfidentialSchema = ExportHeadSchema.extend({
5916
- tier: exports_external.literal("confidential"),
5917
- rows: exports_external.literal(0)
5918
- });
5919
- var ResearchExportSealedSchema = ExportHeadSchema.extend({
5920
- tier: exports_external.literal("standard"),
5921
- rows: exports_external.number().int().nonnegative(),
5922
- cards: exports_external.number().int().nonnegative(),
5923
- pairs: exports_external.number().int().nonnegative(),
5924
- research_key_name: exports_external.string(),
5925
- sealed_cards: exports_external.array(SealedResearchCardSchema),
5926
- judged_pairs: exports_external.array(JudgedPairSchema)
5927
- });
5928
- var ResearchExportResultSchema = exports_external.discriminatedUnion("tier", [
5929
- ResearchExportConfidentialSchema,
5930
- ResearchExportSealedSchema
5931
- ]);
5932
- var ResearchContextExportSchema = exports_external.object({
5933
- export_id: exports_external.string(),
5934
- tenant_id: exports_external.string(),
5935
- tenant_name: exports_external.string(),
5936
- from: exports_external.string(),
5937
- to: exports_external.string(),
5938
- operator: exports_external.string(),
5939
- contexts: exports_external.array(exports_external.object({
5940
- member_id: exports_external.string(),
5941
- device_id: exports_external.string().optional(),
5942
- recording_id: exports_external.string(),
5943
- seq: exports_external.number().int(),
5944
- t_start: exports_external.string(),
5945
- t_end: exports_external.string(),
5946
- context: exports_external.unknown()
5947
- })),
5948
- steps: exports_external.array(exports_external.object({
5949
- run_id: exports_external.string(),
5950
- kind: exports_external.string(),
5951
- member_id: exports_external.string(),
5952
- subject_from: exports_external.string(),
5953
- name: exports_external.string(),
5954
- prompt_fingerprint: exports_external.string().nullable(),
5955
- usage: exports_external.unknown(),
5956
- output: exports_external.unknown(),
5957
- created_at: exports_external.string()
5958
- }))
5959
- });
5960
6532
  // packages/contracts/src/admin-tenants.ts
5961
6533
  var AdminTenantParamsSchema = exports_external.object({ tenant: TenantIdSchema });
5962
6534
  var AdminSecurityTierSchema = exports_external.enum(["standard", "confidential"]);
@@ -5997,10 +6569,6 @@ var AdminRawArchiveSetSchema = exports_external.object({ allowed: exports_extern
5997
6569
  var AdminTenantFlagsSchema = exports_external.object({
5998
6570
  tenant: exports_external.object({ id: TenantIdSchema, name: exports_external.string(), internal: exports_external.boolean(), raw_archive: exports_external.boolean() })
5999
6571
  });
6000
- var AdminResearchExportSetSchema = exports_external.object({ research_export: exports_external.boolean() });
6001
- var AdminResearchExportFlagSchema = exports_external.object({
6002
- tenant: exports_external.object({ id: TenantIdSchema, name: exports_external.string(), research_export: exports_external.boolean() })
6003
- });
6004
6572
  var AdminDemoSetSchema = exports_external.object({ enabled: exports_external.boolean() });
6005
6573
  var AdminDemoFlagSchema = exports_external.object({
6006
6574
  tenant: exports_external.object({ id: TenantIdSchema, name: exports_external.string(), demo: exports_external.boolean() })
@@ -6039,6 +6607,7 @@ var AdminEntriesPurgeResultSchema = exports_external.object({
6039
6607
  user: UserIdSchema,
6040
6608
  dry_run: exports_external.boolean(),
6041
6609
  rows: exports_external.object({ demo: exports_external.array(AdminPurgeRowSchema), unmatched: exports_external.array(AdminPurgeRowSchema) }),
6610
+ kept: exports_external.object({ demo: exports_external.array(AdminPurgeRowSchema) }).default({ demo: [] }),
6042
6611
  deleted: exports_external.object({ demo: exports_external.number().int(), unmatched: exports_external.number().int(), total: exports_external.number().int() }).nullable()
6043
6612
  });
6044
6613
  var AdminContextRetentionSetSchema = exports_external.object({ days: exports_external.number().int().min(1).max(365) });
@@ -6202,6 +6771,115 @@ var ClientOutdatedProblemSchema = ProblemSchema.extend({
6202
6771
  status: exports_external.literal(400),
6203
6772
  minimum: exports_external.string().regex(/^\d+\.\d+\.\d+$/)
6204
6773
  });
6774
+ // packages/contracts/src/compat/retirements.ts
6775
+ var PR4 = { pr: "pr4" };
6776
+ var MIRRORS = "services/api/migrations/0054_mirrors.sql";
6777
+ var COMPAT_PATHS = [
6778
+ { name: "the app.scope arm for transactions without a scope", file: "services/api/migrations/0053_scopes.sql", needle: "app_scope() is null", retiredBy: PR4 },
6779
+ { name: "the content-free as_judged marker", file: "services/api/migrations/0056_entry_guards.sql", needle: "as_judged = '{}'", retiredBy: PR4 },
6780
+ { name: "the calendar connect-state fallback to the old table", file: "services/api/src/features/calendar/store.ts", needle: "calendar_connect_states", retiredBy: PR4 },
6781
+ { name: "the worker's alias of PR 1's task path", file: "services/api/src/platform/jobs/service.ts", needle: "/internal/pipeline/run", retiredBy: PR4 },
6782
+ { name: "the 0.5 telemetry relay", file: "services/api/src/features/evidence/telemetry.ts", needle: 'path: "/v1/telemetry"', retiredBy: PR4 },
6783
+ { name: "the mirror triggers of the merged tables", file: MIRRORS, needle: "create trigger", retiredBy: PR4 },
6784
+ ...[
6785
+ "connector_timekeepers_mirror",
6786
+ "connector_sessions_mirror",
6787
+ "connector_links_mirror",
6788
+ "connector_pushes_mirror",
6789
+ "connector_deliveries_mirror",
6790
+ "deliveries_mirror",
6791
+ "calendar_event_marks_mirror",
6792
+ "calendar_events_mirror_insert",
6793
+ "calendar_events_mirror_update"
6794
+ ].map((trigger) => ({ name: `the mirror trigger ${trigger}`, file: MIRRORS, needle: `create trigger ${trigger}`, retiredBy: PR4 }))
6795
+ ];
6796
+ var markers = (marker, files) => files.map((file) => ({ file, marker }));
6797
+ var COMPAT_MARKERS = [
6798
+ ...markers("#180", [
6799
+ "boundaries.ts",
6800
+ "packages/client/test/t31-problems.test.ts",
6801
+ "packages/contracts/src/api.ts",
6802
+ "packages/contracts/src/compat.ts",
6803
+ "packages/contracts/src/config.ts",
6804
+ "packages/contracts/src/entry.ts",
6805
+ "packages/contracts/src/evidence.ts",
6806
+ "packages/contracts/src/index.ts",
6807
+ "packages/contracts/src/run.ts",
6808
+ "packages/contracts/src/vocabulary.ts",
6809
+ "packages/contracts/test/config.test.ts",
6810
+ "packages/contracts/test/desktop-lines.ts",
6811
+ "packages/contracts/test/entry.test.ts",
6812
+ "packages/contracts/test/t36-contracts.test.ts",
6813
+ "packages/contracts/test/vocabulary.test.ts",
6814
+ "services/api/Dockerfile",
6815
+ "services/api/openapi.json",
6816
+ "services/api/src/app.ts",
6817
+ "services/api/src/features/cells/routes.ts",
6818
+ "services/api/src/features/generate/proxy.ts",
6819
+ "services/api/src/features/lists/vocabulary-store.ts",
6820
+ "services/api/src/features/lists/vocabulary.ts",
6821
+ "services/api/src/features/people/me.ts",
6822
+ "services/api/src/features/update/config.ts",
6823
+ "services/api/src/features/update/slice.ts",
6824
+ "services/api/src/platform/http/config-version.ts",
6825
+ "services/api/src/platform/http/problems.ts",
6826
+ "services/api/src/platform/http/router.ts",
6827
+ "services/api/src/platform/models/bundle.ts",
6828
+ "services/api/src/platform/models/compat/activity-types.json",
6829
+ "services/api/src/platform/models/compat/activity-types.ts",
6830
+ "services/api/src/platform/models/compat/activity.md",
6831
+ "services/api/src/platform/models/compat/narrative.md",
6832
+ "services/api/src/platform/models/jev.ts",
6833
+ "services/api/src/platform/models/usage.ts",
6834
+ "services/api/test/admin-acting.test.ts",
6835
+ "services/api/test/compat-180.test.ts",
6836
+ "services/api/test/jev-route.test.ts",
6837
+ "services/api/test/t15-bundle.test.ts",
6838
+ "services/api/test/t15-lists.test.ts",
6839
+ "services/api/test/t34-vocabulary.test.ts",
6840
+ "services/api/test/t40-firm.test.ts",
6841
+ "services/api/test/t42-intervals.test.ts",
6842
+ "services/api/test/t53-bundle.test.ts"
6843
+ ]),
6844
+ ...markers("narrative-languages", ["services/api/src/app.ts", "services/api/src/features/update/legacy-languages.ts"]),
6845
+ ...markers("pr4", [
6846
+ "boundaries.ts",
6847
+ ".github/scripts/page-minimum.test.ts",
6848
+ ".github/scripts/page-minimum.ts",
6849
+ "apps/web/version.ts",
6850
+ "packages/client/src/copy/index.ts",
6851
+ "packages/client/src/data/failures-compat.ts",
6852
+ "packages/client/src/data/failures.ts",
6853
+ "packages/client/src/data/index.ts",
6854
+ "packages/client/src/data/mutations.ts",
6855
+ "packages/client/src/screens/time/grid.ts",
6856
+ "packages/client/src/screens/time/intervals.ts",
6857
+ "packages/client/src/screens/time/span.tsx",
6858
+ "packages/client/src/screens/time/timeline-blocks.tsx",
6859
+ "packages/client/test/t132-codes.test.ts",
6860
+ "packages/contracts/src/admin-tenants.ts",
6861
+ "packages/contracts/src/analytics/catalogue.ts",
6862
+ "packages/contracts/src/api.ts",
6863
+ "packages/contracts/src/connector.ts",
6864
+ "services/api/migrations/0054_mirrors.sql",
6865
+ "services/api/src/features/calendar/routes.ts",
6866
+ "services/api/src/features/calendar/store.ts",
6867
+ "services/api/src/features/entries/lifecycle.ts",
6868
+ "services/api/src/features/entries/store.ts",
6869
+ "services/api/src/features/lists/store.ts",
6870
+ "services/api/src/features/evidence/retention.ts",
6871
+ "services/api/src/features/evidence/telemetry.ts",
6872
+ "services/api/src/features/operator/tenants.ts",
6873
+ "services/api/src/platform/jobs/jobs.ts",
6874
+ "services/api/src/platform/jobs/ledger.ts",
6875
+ "services/api/src/platform/jobs/service.ts",
6876
+ "services/api/test/t134-scopes.test.ts",
6877
+ "services/api/test/t142-jobs.test.ts",
6878
+ "services/api/test/t90-calendar-connect.test.ts",
6879
+ "tools/release/src/assemble-page.ts"
6880
+ ]),
6881
+ ...markers("0.6.0", ["packages/client/src/data/page-update.ts"])
6882
+ ];
6205
6883
  // packages/contracts/src/config.ts
6206
6884
  var ConfigVersionSchema = exports_external.string().regex(/^[0-9a-f]{12}$/, "expected a 12-character lower-case hex fingerprint");
6207
6885
  var ConfigModelsSchema = exports_external.object({
@@ -6319,7 +6997,14 @@ var DemoObservationSchema = exports_external.object({
6319
6997
  }).strict();
6320
6998
  var DemoDecisionSchema = exports_external.object({ summary: exports_external.string().min(1), activity: exports_external.string().min(1).nullable(), matter: KeySchema.nullable() }).strict();
6321
6999
  var DemoIntervalSchema = exports_external.object({ from: ClockSchema, to: ClockSchema, observations: exports_external.array(DemoObservationSchema).default([]), decision: DemoDecisionSchema.optional() }).strict();
6322
- var DemoEntrySchema = exports_external.object({ from: ClockSchema, to: ClockSchema, matter: KeySchema.nullable(), narrative: exports_external.string().min(1), codes: exports_external.record(exports_external.string().min(1)).default({}) }).strict();
7000
+ var DemoEntrySchema = exports_external.object({
7001
+ from: ClockSchema,
7002
+ to: ClockSchema,
7003
+ matter: KeySchema.nullable(),
7004
+ narrative: exports_external.string(),
7005
+ codes: exports_external.record(exports_external.string().min(1)).default({}),
7006
+ released: exports_external.boolean().optional()
7007
+ }).strict();
6323
7008
  var DemoRecordingSchema = exports_external.object({ start: ClockSchema, stop: ClockSchema, intervals: exports_external.array(DemoIntervalSchema).min(1) }).strict();
6324
7009
  var minutes = (clock) => Number(clock.slice(0, 2)) * 60 + Number(clock.slice(3));
6325
7010
  function problemsOf(s) {
@@ -6382,6 +7067,14 @@ function problemsOf(s) {
6382
7067
  for (let k = first;k <= last; k++)
6383
7068
  covered.has(k) ? out.push([at, "entries do not overlap"]) : covered.add(k);
6384
7069
  });
7070
+ s.entries.forEach((e, i) => {
7071
+ if (e.released !== true)
7072
+ return;
7073
+ if (e.matter === null)
7074
+ out.push([["entries", i, "released"], "a released entry has a matter"]);
7075
+ if (e.narrative.trim() === "")
7076
+ out.push([["entries", i, "released"], "a released entry has a narrative"]);
7077
+ });
6385
7078
  const clock = (m) => `${String(Math.floor(m / 60)).padStart(2, "0")}:${String(m % 60).padStart(2, "0")}`;
6386
7079
  if (s.entries.length > 0)
6387
7080
  all.forEach((one, k) => one.work !== null && !covered.has(k) && out.push([["entries"], `the work at ${clock(one.from)} is in no entry`]));
@@ -6391,6 +7084,7 @@ var DemoScenarioSchema = exports_external.object({
6391
7084
  format: exports_external.literal(1),
6392
7085
  name: KeySchema,
6393
7086
  description: exports_external.string().default(""),
7087
+ weekday: exports_external.number().int().min(1).max(5).optional(),
6394
7088
  time_zone: exports_external.string().min(1),
6395
7089
  unit_minutes: exports_external.number().int().min(1).max(60),
6396
7090
  clients: exports_external.array(exports_external.object({ key: KeySchema, name: exports_external.string().min(1) }).strict()),
@@ -6407,22 +7101,55 @@ function replayGap(s) {
6407
7101
  return "every interval needs its decision";
6408
7102
  return s.entries.length === 0 ? "a replay needs the scenario's entries" : null;
6409
7103
  }
7104
+ var DEMO_MAX_DAYS = 31;
6410
7105
  var AdminDemoSeedSchema = exports_external.object({
6411
- scenario: DemoScenarioSchema,
7106
+ scenario: DemoScenarioSchema.optional(),
7107
+ scenarios: exports_external.array(DemoScenarioSchema).min(1).max(7).optional(),
6412
7108
  day: LocalDaySchema.optional(),
7109
+ anchor: LocalDaySchema.optional(),
7110
+ days: exports_external.number().int().min(1).max(DEMO_MAX_DAYS).optional(),
7111
+ members: exports_external.array(exports_external.string().email()).max(20).default([]),
6413
7112
  replay: exports_external.boolean().default(false),
6414
7113
  generate: exports_external.boolean().default(false)
7114
+ }).superRefine((body, ctx) => {
7115
+ if (body.scenario === undefined === (body.scenarios === undefined)) {
7116
+ ctx.addIssue({ code: exports_external.ZodIssueCode.custom, path: ["scenario"], message: "give scenario or scenarios, one of the two" });
7117
+ }
7118
+ const range = body.anchor !== undefined || body.days !== undefined || body.scenarios !== undefined;
7119
+ if (range && body.day !== undefined)
7120
+ ctx.addIssue({ code: exports_external.ZodIssueCode.custom, path: ["day"], message: "day seeds one day; anchor and days seed a range: give one form" });
7121
+ if (range && body.generate)
7122
+ ctx.addIssue({ code: exports_external.ZodIssueCode.custom, path: ["generate"], message: "a range writes its days' entries itself: generate is false" });
7123
+ if (range && !body.replay)
7124
+ ctx.addIssue({ code: exports_external.ZodIssueCode.custom, path: ["replay"], message: "a range of days is written from the scenarios' decisions and entries: replay is true" });
7125
+ const weekdays = (body.scenarios ?? []).map((one) => one.weekday);
7126
+ if (weekdays.length > 1 && weekdays.some((one) => one === undefined)) {
7127
+ ctx.addIssue({ code: exports_external.ZodIssueCode.custom, path: ["scenarios"], message: "with more than one scenario, each names its weekday" });
7128
+ }
7129
+ if (new Set(weekdays).size !== weekdays.length)
7130
+ ctx.addIssue({ code: exports_external.ZodIssueCode.custom, path: ["scenarios"], message: "a weekday is named twice" });
6415
7131
  });
6416
7132
  var AdminDemoDaySchema = exports_external.object({ day: LocalDaySchema, generate: exports_external.boolean().default(false) });
6417
7133
  var counts = { member_id: MemberIdSchema, day: LocalDaySchema };
6418
- var AdminDemoSeedResultSchema = exports_external.object({
6419
- ...counts,
7134
+ var SeededDaySchema = exports_external.object({
7135
+ day: LocalDaySchema,
6420
7136
  mode: exports_external.enum(["live", "replay"]),
6421
7137
  recordings: exports_external.number().int(),
6422
7138
  contexts: exports_external.object({ stored: exports_external.number().int(), repeated: exports_external.number().int() }),
6423
7139
  created: exports_external.object({ clients: exports_external.number().int(), matters: exports_external.number().int() }),
6424
7140
  generate_run: exports_external.string().nullable()
6425
7141
  });
7142
+ var AdminDemoRangeDaySchema = SeededDaySchema.extend({
7143
+ scenario: KeySchema,
7144
+ replaced: exports_external.boolean(),
7145
+ entries: exports_external.object({ written: exports_external.number().int(), released: exports_external.number().int().optional(), released_owed: exports_external.number().int() })
7146
+ });
7147
+ var AdminDemoInvitedSchema = exports_external.object({ email: exports_external.string(), outcome: exports_external.enum(["invited", "member", "invited_before"]) });
7148
+ var AdminDemoSeedResultSchema = SeededDaySchema.extend({
7149
+ member_id: MemberIdSchema,
7150
+ range: exports_external.array(AdminDemoRangeDaySchema).optional(),
7151
+ members: exports_external.array(AdminDemoInvitedSchema).optional()
7152
+ });
6426
7153
  var AdminDemoResetResultSchema = exports_external.object({
6427
7154
  ...counts,
6428
7155
  runs_deleted: exports_external.number().int(),
@@ -6717,16 +7444,16 @@ var LocalEventSchema = exports_external.discriminatedUnion("type", [
6717
7444
  var LocalHostEventSchema = exports_external.discriminatedUnion("name", [
6718
7445
  exports_external.object({
6719
7446
  name: exports_external.literal("permission.changed"),
6720
- properties: TELEMETRY_EVENT_PROPERTIES["permission.changed"].strict()
7447
+ properties: wireSchemaOf("permission.changed").strict()
6721
7448
  }).strict(),
6722
7449
  exports_external.object({
6723
7450
  name: exports_external.literal("update.failed"),
6724
- properties: TELEMETRY_EVENT_PROPERTIES["update.failed"].strict()
7451
+ properties: wireSchemaOf("update.failed").strict()
6725
7452
  }).strict(),
6726
7453
  exports_external.object({
6727
7454
  name: exports_external.literal("setting.changed"),
6728
7455
  properties: exports_external.object({
6729
- setting: TELEMETRY_EVENT_PROPERTIES["setting.changed"].shape.setting.extract(["launch_at_login", "theme"]),
7456
+ setting: wireSchemaOf("setting.changed").shape.setting.extract(["launch_at_login", "theme"]),
6730
7457
  scope: exports_external.literal("device")
6731
7458
  }).strict()
6732
7459
  }).strict()
@@ -6914,6 +7641,40 @@ var ContextSignalsV1Schema = exports_external.object({
6914
7641
  sent_at: TimestampSchema,
6915
7642
  observations: exports_external.array(exports_external.object({ id: ObservationIdSchema, signals: SignalsSchema })).min(1)
6916
7643
  });
7644
+ // packages/contracts/src/release.ts
7645
+ var VersionSchema = exports_external.string().regex(/^\d+\.\d+\.\d+$/, "a release is MAJOR.MINOR.N");
7646
+ var HostKeySchema = exports_external.union([exports_external.literal("unkeyed"), exports_external.string().regex(/^h:[0-9a-f]{8}$/)]);
7647
+ var DAY_MS = 24 * 60 * 60000;
7648
+ var ReleaseSchema = exports_external.object({
7649
+ version: VersionSchema,
7650
+ commit: exports_external.string().regex(/^[0-9a-f]{40}$/),
7651
+ key: HostKeySchema,
7652
+ promoted_at: exports_external.string().datetime(),
7653
+ superseded_at: exports_external.string().datetime().nullable(),
7654
+ state: exports_external.enum(["current", "superseded", "withdrawn"]),
7655
+ urgent: exports_external.boolean(),
7656
+ desktop: VersionSchema.nullable()
7657
+ });
7658
+ var ReleaseIndexSchema = exports_external.object({
7659
+ releases: exports_external.array(ReleaseSchema)
7660
+ });
7661
+ var UpdateReleaseQuerySchema = exports_external.object({
7662
+ key: HostKeySchema,
7663
+ version: VersionSchema.optional(),
7664
+ device: exports_external.string().min(1).max(200).optional()
7665
+ });
7666
+ var UpdateStateSchema = exports_external.enum(["current", "available", "required"]);
7667
+ var ReleaseSummarySchema = exports_external.object({
7668
+ version: VersionSchema,
7669
+ key: HostKeySchema,
7670
+ promoted_at: exports_external.string().datetime(),
7671
+ desktop: VersionSchema.nullable()
7672
+ });
7673
+ var UpdateReleaseResponseSchema = exports_external.object({
7674
+ current: ReleaseSummarySchema.nullable(),
7675
+ state: UpdateStateSchema,
7676
+ expired: exports_external.boolean()
7677
+ });
6917
7678
  // packages/contracts/src/run.ts
6918
7679
  var VerdictSchema = exports_external.enum(["matter", "unmatched", "absent"]);
6919
7680
  var WORK_VERDICTS = ["matter", "unmatched"];
@@ -6951,132 +7712,6 @@ var RunSchema = exports_external.object({
6951
7712
  message: "an absent run is never an entry, so it carries no proposed attributes and no judged text",
6952
7713
  path: ["attributes"]
6953
7714
  });
6954
- // packages/contracts/src/tools.ts
6955
- var TimeRangeSchema = exports_external.object({
6956
- from: TimestampSchema.optional(),
6957
- to: TimestampSchema.optional()
6958
- });
6959
- var ZoomSchema = exports_external.enum(["day", "entry", "observation"]);
6960
- var TimelineInputSchema = exports_external.object({
6961
- range: TimeRangeSchema,
6962
- zoom: ZoomSchema
6963
- });
6964
- var TimelineTotalsSchema = exports_external.object({
6965
- entries: exports_external.number().int().min(0),
6966
- observations: exports_external.number().int().min(0),
6967
- elapsed_minutes: exports_external.number().int().min(0),
6968
- away_minutes: exports_external.number().int().min(0),
6969
- span_seconds: exports_external.number().int().min(0)
6970
- });
6971
- var AwayGapSchema = exports_external.object({
6972
- from: TimestampSchema,
6973
- to: TimestampSchema,
6974
- minutes: exports_external.number().int().min(0),
6975
- reason: exports_external.enum(["no-signal", "lock", "sleep"]),
6976
- note: exports_external.string().nullable().default(null)
6977
- });
6978
- var EntryRefSchema = exports_external.object({
6979
- id: EntryIdSchema,
6980
- narrative: exports_external.string(),
6981
- t_start: TimestampSchema,
6982
- t_end: TimestampSchema,
6983
- elapsed_minutes: exports_external.number().int().min(0),
6984
- idle_ratio: exports_external.number().min(0).max(1),
6985
- matter_id: MatterIdSchema.nullable(),
6986
- assignment: AssignmentSchema.nullable(),
6987
- status: EntryStatusSchema,
6988
- observation_count: exports_external.number().int().min(0),
6989
- recording_id: RecordingIdSchema.nullable(),
6990
- owner: EntryOwnerSchema
6991
- });
6992
- var ObservationRefSchema = exports_external.object({
6993
- id: ObservationIdSchema,
6994
- t_start: TimestampSchema,
6995
- t_end: TimestampSchema,
6996
- app: exports_external.string(),
6997
- window_title: exports_external.string(),
6998
- url: exports_external.string().nullable(),
6999
- entry_id: EntryIdSchema.nullable()
7000
- });
7001
- var EvidenceRunSchema = exports_external.object({
7002
- kind: exports_external.enum(["run", "idle"]),
7003
- t_start: TimestampSchema,
7004
- t_end: TimestampSchema,
7005
- minutes: exports_external.number().int().min(0),
7006
- app: exports_external.string(),
7007
- title: exports_external.string(),
7008
- observation_ids: exports_external.array(ObservationIdSchema)
7009
- });
7010
- var TimelineDaySchema = exports_external.object({
7011
- id: DayIdSchema,
7012
- date: exports_external.string().regex(/^\d{4}-\d{2}-\d{2}$/),
7013
- entries: exports_external.array(EntryRefSchema),
7014
- away: exports_external.array(AwayGapSchema),
7015
- totals: TimelineTotalsSchema
7016
- });
7017
- var TimelineResultSchema = exports_external.discriminatedUnion("zoom", [
7018
- exports_external.object({
7019
- zoom: exports_external.literal("day"),
7020
- range: TimeRangeSchema,
7021
- days: exports_external.array(TimelineDaySchema),
7022
- totals: TimelineTotalsSchema
7023
- }),
7024
- exports_external.object({
7025
- zoom: exports_external.literal("entry"),
7026
- range: TimeRangeSchema,
7027
- entries: exports_external.array(EntryRefSchema),
7028
- totals: TimelineTotalsSchema
7029
- }),
7030
- exports_external.object({
7031
- zoom: exports_external.literal("observation"),
7032
- range: TimeRangeSchema,
7033
- observations: exports_external.array(ObservationRefSchema),
7034
- totals: TimelineTotalsSchema
7035
- })
7036
- ]);
7037
- var ReadInputSchema = exports_external.object({
7038
- id: exports_external.string().min(1)
7039
- });
7040
- var ReadObservationSchema = exports_external.object({
7041
- type: exports_external.literal("observation"),
7042
- id: ObservationIdSchema,
7043
- t_start: TimestampSchema,
7044
- t_end: TimestampSchema,
7045
- app: exports_external.string(),
7046
- window_title: exports_external.string(),
7047
- url: exports_external.string().nullable(),
7048
- signals: SignalsSchema,
7049
- snapshot_ids: exports_external.array(SnapshotIdSchema),
7050
- entry_id: EntryIdSchema.nullable(),
7051
- text: exports_external.string()
7052
- });
7053
- var ReadEntrySchema = exports_external.object({
7054
- type: exports_external.literal("entry"),
7055
- id: EntryIdSchema,
7056
- narrative: exports_external.string(),
7057
- t_start: TimestampSchema,
7058
- t_end: TimestampSchema,
7059
- elapsed_minutes: exports_external.number().int().min(0),
7060
- idle_ratio: exports_external.number().min(0).max(1),
7061
- matter_id: MatterIdSchema.nullable(),
7062
- assignment: AssignmentSchema.nullable(),
7063
- status: EntryStatusSchema,
7064
- observations: exports_external.array(ObservationRefSchema),
7065
- evidence: exports_external.array(EvidenceRunSchema)
7066
- });
7067
- var ReadDaySchema = exports_external.object({
7068
- type: exports_external.literal("day"),
7069
- id: DayIdSchema,
7070
- date: exports_external.string().regex(/^\d{4}-\d{2}-\d{2}$/),
7071
- entries: exports_external.array(EntryRefSchema),
7072
- away: exports_external.array(AwayGapSchema),
7073
- totals: TimelineTotalsSchema
7074
- });
7075
- var ReadResultSchema = exports_external.discriminatedUnion("type", [
7076
- ReadObservationSchema,
7077
- ReadEntrySchema,
7078
- ReadDaySchema
7079
- ]);
7080
7715
  // packages/contracts/src/trace.ts
7081
7716
  var TraceMessageSchema = exports_external.object({
7082
7717
  role: exports_external.string().min(1),
@@ -7145,50 +7780,13 @@ import { join } from "node:path";
7145
7780
 
7146
7781
  // tools/admin/src/files.ts
7147
7782
  import { randomUUID } from "node:crypto";
7148
- import { chmodSync, closeSync, mkdirSync, openSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
7149
- import { dirname } from "node:path";
7783
+ import { chmodSync, closeSync, mkdirSync, openSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
7150
7784
  function makePrivateDirectory(path) {
7151
7785
  mkdirSync(path, { recursive: true, mode: 448 });
7152
7786
  const mode = statSync(path).mode & 511;
7153
7787
  if ((mode & 63) !== 0)
7154
7788
  chmodSync(path, 448);
7155
7789
  }
7156
- function claimExportDirectory(path) {
7157
- const cannot = (err) => new AdminUsageError(`--out ${path} cannot be created: ${err.message}`);
7158
- try {
7159
- mkdirSync(dirname(path), { recursive: true, mode: 448 });
7160
- } catch (err) {
7161
- throw cannot(err);
7162
- }
7163
- try {
7164
- mkdirSync(path, { mode: 448 });
7165
- return { created: true };
7166
- } catch (err) {
7167
- if (err.code !== "EEXIST")
7168
- throw cannot(err);
7169
- }
7170
- const stat = statSync(path);
7171
- if (!stat.isDirectory())
7172
- throw new AdminUsageError(`--out ${path} is not a directory; name a new folder, and the export creates it 0700.`);
7173
- if (process.platform === "win32")
7174
- return { created: false };
7175
- const mode = stat.mode & 511;
7176
- if ((mode & 63) !== 0) {
7177
- throw new AdminUsageError(`--out ${path} can be read by other accounts on this machine (mode ${mode.toString(8)}); ` + "name a new folder, which the export creates 0700, or run chmod 700 on this one yourself.");
7178
- }
7179
- const uid = process.getuid?.();
7180
- if (uid !== undefined && stat.uid !== uid) {
7181
- throw new AdminUsageError(`--out ${path} belongs to another account on this machine; name a folder of your own.`);
7182
- }
7183
- return { created: false };
7184
- }
7185
- function releaseExportDirectory(path, claimed) {
7186
- if (!claimed.created)
7187
- return;
7188
- try {
7189
- rmdirSync(path);
7190
- } catch {}
7191
- }
7192
7790
  function writePrivateFile(path, body) {
7193
7791
  const temporary = `${path}.${randomUUID()}.part`;
7194
7792
  const handle = openSync(temporary, "wx", 384);
@@ -7204,11 +7802,6 @@ function writePrivateFile(path, body) {
7204
7802
  rmSync(temporary, { force: true });
7205
7803
  }
7206
7804
  }
7207
- function jsonLines(rows) {
7208
- return rows.map((row) => JSON.stringify(row)).join(`
7209
- `) + (rows.length === 0 ? "" : `
7210
- `);
7211
- }
7212
7805
 
7213
7806
  // tools/admin/src/session.ts
7214
7807
  var WORKOS_API = "https://api.workos.com";
@@ -7465,24 +8058,6 @@ function purgeSection(label, rows) {
7465
8058
  return [`${label}: none`];
7466
8059
  return [`${label}: ${rows.length}`, ...rows.map(purgeLine)];
7467
8060
  }
7468
- function exportLines(exports, error) {
7469
- if (exports === null) {
7470
- return [
7471
- `past exports could not be read: ${error}`,
7472
- "The tier is set and the cards are deleted; only this list is missing. Run the command again to see it."
7473
- ];
7474
- }
7475
- if (exports.length === 0)
7476
- return ["past exports none: no export of this firm was ever made."];
7477
- const day = (iso) => iso.slice(0, 10);
7478
- return [
7479
- `past exports ${exports.length}`,
7480
- ...exports.map((row) => ` ${row.created_at} ${row.operator} ${day(row.from_at)} to ${day(row.to_at)} ` + `${row.rows} row${row.rows === 1 ? "" : "s"}`),
7481
- "",
7482
- "Each of those exports wrote this firm's cards and narratives, decrypted, onto the operator's own machine:",
7483
- "this command deletes the rows in the cloud and can delete no file on a laptop, so find those files and delete them by hand."
7484
- ];
7485
- }
7486
8061
  function table(headers, rows) {
7487
8062
  const widths = headers.map((head, i) => Math.max(head.length, ...rows.map((row) => row[i].length)));
7488
8063
  const line = (cells) => cells.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd();
@@ -7600,100 +8175,9 @@ function parseMatterSets(pairs) {
7600
8175
  return out;
7601
8176
  }
7602
8177
 
7603
- // tools/admin/src/unseal.ts
7604
- import { spawnSync } from "node:child_process";
7605
- import { createDecipheriv } from "node:crypto";
7606
- var KMS_ENDPOINT = "https://cloudkms.googleapis.com/v1";
7607
- var FORMAT_VERSION = 1;
7608
- var NONCE_BYTES = 12;
7609
- var TAG_BYTES = 16;
7610
- var HEADER_BYTES = 1 + NONCE_BYTES;
7611
-
7612
- class UnsealError extends Error {
7613
- constructor(message) {
7614
- super(message);
7615
- this.name = "UnsealError";
7616
- }
7617
- }
7618
- function keyVersionName(researchKeyName, version) {
7619
- return `${researchKeyName.replace(/\/cryptoKeyVersions\/\d+$/, "")}/cryptoKeyVersions/${version}`;
7620
- }
7621
- async function asymmetricDecrypt(versionName, sealedKey, accessToken, fetchImpl) {
7622
- const response = await fetchImpl(`${KMS_ENDPOINT}/${versionName}:asymmetricDecrypt`, {
7623
- method: "POST",
7624
- headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
7625
- body: JSON.stringify({ ciphertext: sealedKey.toString("base64") })
7626
- });
7627
- if (!response.ok) {
7628
- throw new UnsealError(`Cloud KMS answered ${response.status} to asymmetricDecrypt on ${versionName}. ` + "A 403 means this identity may not unseal research cards; two named people hold that role and " + "nobody else, the runtime service account included.");
7629
- }
7630
- const body = await response.json();
7631
- if (typeof body.plaintext !== "string")
7632
- throw new UnsealError("Cloud KMS answered asymmetricDecrypt without a plaintext.");
7633
- return Buffer.from(body.plaintext, "base64");
7634
- }
7635
- function openColumn(rowKey, column) {
7636
- if (rowKey.length !== 32)
7637
- throw new UnsealError(`a row key is 32 bytes, got ${rowKey.length}`);
7638
- if (column.length < HEADER_BYTES + TAG_BYTES)
7639
- throw new UnsealError("ciphertext is too short to be a sealed column");
7640
- if (column[0] !== FORMAT_VERSION)
7641
- throw new UnsealError(`ciphertext format ${column[0]} is not one this tool reads`);
7642
- const decipher = createDecipheriv("aes-256-gcm", rowKey, column.subarray(1, HEADER_BYTES), { authTagLength: TAG_BYTES });
7643
- decipher.setAAD(column.subarray(0, 1));
7644
- decipher.setAuthTag(column.subarray(column.length - TAG_BYTES));
7645
- try {
7646
- return Buffer.concat([decipher.update(column.subarray(HEADER_BYTES, column.length - TAG_BYTES)), decipher.final()]);
7647
- } catch {
7648
- throw new UnsealError("ciphertext did not authenticate under this key");
7649
- }
7650
- }
7651
- async function openCards(rows, researchKeyName, accessToken, fetchImpl = fetch) {
7652
- const opened = [];
7653
- for (const row of rows) {
7654
- const versionName = keyVersionName(researchKeyName, row.key_version);
7655
- let rowKey = null;
7656
- try {
7657
- rowKey = await asymmetricDecrypt(versionName, Buffer.from(row.sealed_key, "base64"), accessToken, fetchImpl);
7658
- const card = JSON.parse(openColumn(rowKey, Buffer.from(row.card_ct, "base64")).toString("utf8"));
7659
- const answer = openColumn(rowKey, Buffer.from(row.answer_ct, "base64")).toString("utf8");
7660
- opened.push({
7661
- id: row.id,
7662
- tenant_id: row.tenant_id,
7663
- member_id: row.member_id,
7664
- device_id: row.device_id,
7665
- purpose: row.purpose,
7666
- model: row.model,
7667
- created_at: row.created_at,
7668
- key_version: row.key_version,
7669
- card,
7670
- answer
7671
- });
7672
- } catch (err) {
7673
- throw new UnsealError(`card ${row.id} did not unseal under ${versionName}: ${err instanceof Error ? err.message : String(err)}`);
7674
- } finally {
7675
- rowKey?.fill(0);
7676
- }
7677
- }
7678
- return opened;
7679
- }
7680
- function googleAccessToken(env, platform) {
7681
- const given = env["GOOGLE_OAUTH_ACCESS_TOKEN"]?.trim();
7682
- if (given !== undefined && given !== "")
7683
- return given;
7684
- const run2 = spawnSync("gcloud", ["auth", "print-access-token"], {
7685
- encoding: "utf8",
7686
- shell: platform === "win32",
7687
- stdio: ["ignore", "pipe", "pipe"]
7688
- });
7689
- const token = typeof run2.stdout === "string" ? run2.stdout.trim() : "";
7690
- if (run2.error === undefined && run2.status === 0 && token !== "")
7691
- return token;
7692
- throw new UnsealError("no Google access token: set GOOGLE_OAUTH_ACCESS_TOKEN, or install the gcloud CLI and run " + "`gcloud auth login` as yourself. The unseal runs as YOU, never as a service account.");
7693
- }
7694
-
7695
8178
  // tools/admin/src/demo.ts
7696
- import { readFileSync as readFileSync3 } from "node:fs";
8179
+ import { readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "node:fs";
8180
+ import { join as join2 } from "node:path";
7697
8181
  function readScenario(path) {
7698
8182
  let raw;
7699
8183
  try {
@@ -7709,21 +8193,88 @@ ${parsed.error.issues.map((i) => ` ${i.path.join(".") || "(root)"}: ${i.message
7709
8193
  }
7710
8194
  return parsed.data;
7711
8195
  }
8196
+ var isDirectory = (path) => {
8197
+ try {
8198
+ return statSync2(path).isDirectory();
8199
+ } catch {
8200
+ return false;
8201
+ }
8202
+ };
8203
+ function readScenarios(path) {
8204
+ const files = isDirectory(path) ? readdirSync(path).filter((name) => name.endsWith(".json")).sort().map((name) => join2(path, name)) : [path];
8205
+ if (files.length === 0)
8206
+ throw new AdminUsageError(`${path} holds no scenario file`);
8207
+ return files.map((file) => ({ path: file, scenario: readScenario(file) }));
8208
+ }
7712
8209
  function validateScenario(argv) {
7713
8210
  const [path, ...rest] = argv;
7714
8211
  if (path === undefined || path.startsWith("--") || rest.some((arg) => arg !== "--json"))
7715
- throw new AdminUsageError("demo validate takes one scenario file, and --json");
7716
- const s = readScenario(path);
7717
- const facts = {
8212
+ throw new AdminUsageError("demo validate takes one scenario file or directory, and --json");
8213
+ const all = readScenarios(path).map(({ scenario: s }) => ({
7718
8214
  name: s.name,
8215
+ weekday: s.weekday ?? null,
7719
8216
  recordings: s.recordings.length,
7720
8217
  intervals: s.recordings.reduce((n, r) => n + r.intervals.length, 0),
7721
8218
  entries: s.entries.length,
8219
+ released: s.entries.filter((e) => e.released === true).length,
7722
8220
  replay: replayGap(s) ?? "yes"
7723
- };
8221
+ }));
7724
8222
  if (rest.includes("--json"))
7725
- return { code: 0, lines: [JSON.stringify({ valid: true, ...facts }, null, 2)] };
7726
- return { code: 0, lines: [`valid ${facts.name}`, `recordings ${facts.recordings}, ${facts.intervals} intervals, ${facts.entries} entries`, `replay ${facts.replay}`] };
8223
+ return { code: 0, lines: [JSON.stringify(all.length === 1 ? { valid: true, ...all[0] } : { valid: true, scenarios: all }, null, 2)] };
8224
+ return {
8225
+ code: 0,
8226
+ lines: all.flatMap((facts) => [
8227
+ `valid ${facts.name}${facts.weekday === null ? "" : ` weekday ${facts.weekday}`}`,
8228
+ `recordings ${facts.recordings}, ${facts.intervals} intervals, ${facts.entries} entries${facts.released === 0 ? "" : `, ${facts.released} released`}`,
8229
+ `replay ${facts.replay}`
8230
+ ])
8231
+ };
8232
+ }
8233
+ function membersOf(run2) {
8234
+ const value = run2.flags.values.get("members");
8235
+ if (value === undefined)
8236
+ return [];
8237
+ const emails = value.split(",").map((one) => one.trim()).filter((one) => one !== "");
8238
+ const bad = emails.find((one) => !/^[^@\s]+@[^@\s]+$/.test(one));
8239
+ if (bad !== undefined)
8240
+ throw new AdminUsageError(`--members is a comma-separated list of addresses, got ${JSON.stringify(bad)}`);
8241
+ return emails;
8242
+ }
8243
+ function daysOf(run2) {
8244
+ const value = run2.flags.values.get("days");
8245
+ if (value === undefined)
8246
+ return;
8247
+ const days = Number(value);
8248
+ if (!Number.isInteger(days) || days < 1 || days > DEMO_MAX_DAYS)
8249
+ throw new AdminUsageError(`--days is a whole number from 1 to ${DEMO_MAX_DAYS}, got ${JSON.stringify(value)}`);
8250
+ return days;
8251
+ }
8252
+ function seedBody(run2) {
8253
+ const path = required(run2.flags, "scenario", "the scenario file to seed, or a directory of week files with --anchor or --days");
8254
+ const members = membersOf(run2);
8255
+ const days = daysOf(run2);
8256
+ const anchor = run2.flags.values.has("anchor") ? dayOf2(run2.flags, "anchor", "the last day of the range") : undefined;
8257
+ const range = days !== undefined || anchor !== undefined;
8258
+ if (!range) {
8259
+ if (isDirectory(path))
8260
+ throw new AdminUsageError("a directory of scenarios seeds a range: give --days or --anchor, or name one file");
8261
+ const scenario = readScenario(path);
8262
+ const day2 = run2.flags.values.has("day") ? dayOf2(run2.flags, "day", "the day to seed on") : undefined;
8263
+ return { body: { scenario, ...day2 === undefined ? {} : { day: day2 }, replay: switched(run2.flags, "replay"), generate: switched(run2.flags, "generate"), members }, names: scenario.name };
8264
+ }
8265
+ if (run2.flags.values.has("day"))
8266
+ throw new AdminUsageError("--day seeds one day; --anchor and --days seed a range: give one form");
8267
+ if (switched(run2.flags, "generate"))
8268
+ throw new AdminUsageError("a range writes its days' entries itself: drop --generate");
8269
+ const read = readScenarios(path).map((one) => one.scenario);
8270
+ const week = read.filter((one) => one.weekday !== undefined);
8271
+ const scenarios = isDirectory(path) ? week : read;
8272
+ if (scenarios.length === 0)
8273
+ throw new AdminUsageError(`${path} holds no scenario that names its weekday`);
8274
+ return {
8275
+ body: { scenarios, ...anchor === undefined ? {} : { anchor }, ...days === undefined ? {} : { days }, replay: true, members },
8276
+ names: scenarios.map((one) => one.name).join(", ")
8277
+ };
7727
8278
  }
7728
8279
  async function runDemo(run2, subcommand) {
7729
8280
  if (run2.environment.name === "production")
@@ -7744,24 +8295,31 @@ async function runDemo(run2, subcommand) {
7744
8295
  ]);
7745
8296
  }
7746
8297
  if (subcommand === "seed") {
7747
- const scenario = readScenario(required(run2.flags, "scenario", "the scenario file to seed"));
7748
- const day = run2.flags.values.has("day") ? dayOf2(run2.flags, "day", "the day to seed on") : undefined;
7749
- const body = { scenario, ...day === undefined ? {} : { day }, replay: switched(run2.flags, "replay"), generate: switched(run2.flags, "generate") };
8298
+ const { body, names } = seedBody(run2);
7750
8299
  const done = AdminDemoSeedResultSchema.parse(expectOk(await call(run2.api, "POST", path("/demo/seed"), body)));
8300
+ const invited = (done.members ?? []).map((one) => `member ${one.email} ${one.outcome === "invited" ? "invited" : one.outcome === "member" ? "already a member, skipped" : "already invited, skipped"}`);
8301
+ if (done.range !== undefined) {
8302
+ return render(run2, done, [
8303
+ `seeded ${done.range.length} working days into ${done.member_id}`,
8304
+ ...done.range.map((one) => `${one.day} ${one.scenario}: ${one.entries.written} drafts written, ${one.contexts.stored} contexts stored` + `${one.replaced ? ", replaced an older seed" : ""}${one.entries.released_owed === 0 ? "" : `, ${one.entries.released_owed} to release`}`),
8305
+ ...invited
8306
+ ]);
8307
+ }
7751
8308
  return render(run2, done, [
7752
- `seeded ${scenario.name} on ${done.day}, ${done.mode}, into ${done.member_id}`,
8309
+ `seeded ${names} on ${done.day}, ${done.mode}, into ${done.member_id}`,
7753
8310
  `contexts ${done.contexts.stored} stored, ${done.contexts.repeated} already there, over ${done.recordings} recordings`,
7754
8311
  `created ${done.created.clients} clients, ${done.created.matters} matters`,
7755
- done.generate_run === null ? "Generate the day from the app, or run demo reset --generate." : `generate ${done.generate_run}`
8312
+ done.generate_run === null ? "Generate the day from the app, or run demo reset --generate." : `generate ${done.generate_run}`,
8313
+ ...invited
7756
8314
  ]);
7757
8315
  }
7758
8316
  if (subcommand === "reset" || subcommand === "remove") {
7759
- const day = dayOf2(run2.flags, "day", `the seeded day to ${subcommand}`);
8317
+ const day2 = dayOf2(run2.flags, "day", `the seeded day to ${subcommand}`);
7760
8318
  if (subcommand === "remove") {
7761
- const done2 = AdminDemoRemoveResultSchema.parse(expectOk(await call(run2.api, "POST", path("/demo/remove"), { day })));
8319
+ const done2 = AdminDemoRemoveResultSchema.parse(expectOk(await call(run2.api, "POST", path("/demo/remove"), { day: day2 })));
7762
8320
  return render(run2, done2, [`removed ${done2.day}: ${done2.recordings_deleted} recordings, ${done2.entries_deleted} entries`]);
7763
8321
  }
7764
- const done = AdminDemoResetResultSchema.parse(expectOk(await call(run2.api, "POST", path("/demo/reset"), { day, generate: switched(run2.flags, "generate") })));
8322
+ const done = AdminDemoResetResultSchema.parse(expectOk(await call(run2.api, "POST", path("/demo/reset"), { day: day2, generate: switched(run2.flags, "generate") })));
7765
8323
  return render(run2, done, [
7766
8324
  `reset ${done.day}: ${done.runs_deleted} Generate runs, ${done.drafts_deleted} drafts deleted, ${done.released_kept} released kept`,
7767
8325
  ...done.generate_run === null ? [] : [`generate ${done.generate_run}`]
@@ -7770,9 +8328,141 @@ async function runDemo(run2, subcommand) {
7770
8328
  throw new AdminUsageError(`unknown command: demo ${subcommand ?? ""}`.trim());
7771
8329
  }
7772
8330
 
8331
+ // tools/admin/src/telemetry.ts
8332
+ var POSTHOG_API = "https://eu.posthog.com";
8333
+ var ALERT_PREFIX = "sixminute alert:";
8334
+ function wantedAlerts() {
8335
+ return EVENT_KEYS.filter((key) => CATALOGUE[key].owner === "cloud" && CATALOGUE[key].expect === "daily").map((key) => ({
8336
+ event: CATALOGUE[key].name,
8337
+ name: `${ALERT_PREFIX} ${CATALOGUE[key].name}`,
8338
+ lower: 1
8339
+ }));
8340
+ }
8341
+
8342
+ class PosthogError extends Error {
8343
+ }
8344
+ function client(io, project) {
8345
+ const call2 = async (method, path, body) => {
8346
+ const response = await io.fetchImpl(`${POSTHOG_API}/api/projects/${encodeURIComponent(project)}${path}`, {
8347
+ method,
8348
+ headers: { Authorization: `Bearer ${io.posthogKey}`, "Content-Type": "application/json", Accept: "application/json" },
8349
+ ...body === undefined ? {} : { body: JSON.stringify(body) }
8350
+ });
8351
+ const text = await response.text();
8352
+ if (!response.ok)
8353
+ throw new PosthogError(`PostHog answered ${response.status} to ${method} ${path}`);
8354
+ return text === "" ? null : JSON.parse(text);
8355
+ };
8356
+ return {
8357
+ call: call2,
8358
+ async list(path) {
8359
+ const page = await call2("GET", path);
8360
+ return page?.results ?? [];
8361
+ }
8362
+ };
8363
+ }
8364
+ var insightOf = (wanted) => ({
8365
+ name: wanted.name,
8366
+ description: `Counts ${wanted.event} a day for its alert (sixminute-admin telemetry alerts).`,
8367
+ query: {
8368
+ kind: "InsightVizNode",
8369
+ source: { kind: "TrendsQuery", series: [{ kind: "EventsNode", event: wanted.event, math: "total" }], interval: "day", dateRange: { date_from: "-14d" }, filterTestAccounts: true }
8370
+ }
8371
+ });
8372
+ var alertOf = (wanted, insight) => ({
8373
+ name: wanted.name,
8374
+ insight,
8375
+ condition: { type: "absolute_value" },
8376
+ threshold: { configuration: { type: "absolute", bounds: { lower: wanted.lower } } },
8377
+ config: { type: "TrendsAlertConfig", series_index: 0 },
8378
+ calculation_interval: "daily",
8379
+ skip_weekend: true,
8380
+ enabled: true
8381
+ });
8382
+ var insightIdOf = (alert) => typeof alert.insight === "number" ? alert.insight : alert.insight.id;
8383
+ async function telemetryAlerts(io, project, yes) {
8384
+ const posthog = client(io, project);
8385
+ const insights = (await posthog.list(`/insights/?search=${encodeURIComponent(ALERT_PREFIX)}&limit=500`)).filter((one) => one.name?.startsWith(ALERT_PREFIX));
8386
+ const alerts = (await posthog.list("/alerts/?limit=500")).filter((one) => one.name.startsWith(ALERT_PREFIX));
8387
+ const plan = [];
8388
+ for (const wanted of wantedAlerts()) {
8389
+ const insight = insights.find((one) => one.name === wanted.name);
8390
+ const alert = alerts.find((one) => one.name === wanted.name);
8391
+ if (insight === undefined) {
8392
+ plan.push({
8393
+ change: `create ${wanted.event}: an insight and its alert below ${wanted.lower} a weekday`,
8394
+ write: async () => {
8395
+ const made = await posthog.call("POST", "/insights/", insightOf(wanted));
8396
+ await posthog.call("POST", "/alerts/", alertOf(wanted, made.id));
8397
+ }
8398
+ });
8399
+ } else if (alert === undefined) {
8400
+ plan.push({ change: `create ${wanted.event}: its alert below ${wanted.lower} a weekday`, write: async () => void await posthog.call("POST", "/alerts/", alertOf(wanted, insight.id)) });
8401
+ } else if (alert.threshold?.configuration?.bounds?.lower !== wanted.lower || insightIdOf(alert) !== insight.id) {
8402
+ plan.push({
8403
+ change: `update ${wanted.event}: the alert's floor to ${wanted.lower} a weekday`,
8404
+ write: async () => void await posthog.call("PATCH", `/alerts/${encodeURIComponent(alert.id)}/`, alertOf(wanted, insight.id))
8405
+ });
8406
+ }
8407
+ }
8408
+ if (yes)
8409
+ for (const step of plan)
8410
+ await step.write();
8411
+ return { changes: plan.map((step) => step.change), written: yes && plan.length > 0 };
8412
+ }
8413
+ async function telemetryCheck(io, project, days) {
8414
+ const posthog = client(io, project);
8415
+ const answer = await posthog.call("POST", "/query/", {
8416
+ query: { kind: "HogQLQuery", query: `select event, count() from events where timestamp > now() - interval ${days} day group by event` }
8417
+ });
8418
+ const received = new Set((answer?.results ?? []).map(([event]) => event));
8419
+ const names = new Set(EVENT_KEYS.map((key) => CATALOGUE[key].name));
8420
+ const own = (event) => event.startsWith("$") && !names.has(event);
8421
+ return {
8422
+ missing: [...names].filter((name) => !received.has(name)).sort(),
8423
+ unknown: [...received].filter((event) => !names.has(event) && !own(event)).sort()
8424
+ };
8425
+ }
8426
+ async function runTelemetry(subcommand, values, switches, json, io) {
8427
+ const project = values.get("project")?.trim();
8428
+ if (subcommand !== "alerts" && subcommand !== "check")
8429
+ return { code: 2, lines: ["usage: sixminute-admin telemetry alerts|check --project <id> [--yes] [--days 7] [--json]"] };
8430
+ if (project === undefined || project === "")
8431
+ return { code: 2, lines: ["--project is required: the PostHog project the alerts live in"] };
8432
+ if (io.posthogKey === null || io.posthogKey === "")
8433
+ return { code: 2, lines: ["SIXMINUTE_POSTHOG_KEY is not set: a PostHog user API key with insight and alert write access"] };
8434
+ try {
8435
+ if (subcommand === "alerts") {
8436
+ const { changes, written } = await telemetryAlerts(io, project, switches.has("yes"));
8437
+ if (json)
8438
+ return { code: 0, lines: [JSON.stringify({ project, changes, written }, null, 2)] };
8439
+ if (changes.length === 0)
8440
+ return { code: 0, lines: [`project ${project}: every daily cloud event has its alert; nothing to change`] };
8441
+ return { code: 0, lines: [`project ${project}:`, ...changes.map((line) => ` ${line}`), written ? "written." : "nothing written: run again with --yes to write these."] };
8442
+ }
8443
+ const days = Number(values.get("days") ?? "7");
8444
+ if (!Number.isInteger(days) || days < 1 || days > 90)
8445
+ return { code: 2, lines: ["--days is a whole number of days from 1 to 90"] };
8446
+ const { missing, unknown } = await telemetryCheck(io, project, days);
8447
+ if (json)
8448
+ return { code: 0, lines: [JSON.stringify({ project, days, missing, unknown }, null, 2)] };
8449
+ return {
8450
+ code: missing.length === 0 && unknown.length === 0 ? 0 : 1,
8451
+ lines: [
8452
+ `project ${project}, the last ${days} days:`,
8453
+ ` not received (${missing.length}): ${missing.join(", ") || "none"}`,
8454
+ ` not in the catalogue (${unknown.length}): ${unknown.join(", ") || "none"}`
8455
+ ]
8456
+ };
8457
+ } catch (err) {
8458
+ if (err instanceof PosthogError)
8459
+ return { code: 2, lines: [err.message] };
8460
+ throw err;
8461
+ }
8462
+ }
8463
+
7773
8464
  // tools/admin/src/run.ts
7774
8465
  var ROLE_SLUGS = FIRM_ROLE_SLUGS;
7775
- var CONFIDENTIAL_EXIT_CODE = 3;
7776
8466
  var REFUSED_EXIT_CODE = 2;
7777
8467
  var ADMIN_USAGE = `sixminute-admin, the operator's firm tool (D-73)
7778
8468
 
@@ -7820,20 +8510,13 @@ is required; without one every command answers 403.
7820
8510
 
7821
8511
  sixminute-admin tier set --env <env> --tenant tenant_... --tier <standard|confidential> [--json]
7822
8512
  Sets the firm's security tier, which no route lets a customer change.
7823
- "confidential" stores no research card for that firm and keeps its
7824
- judged copies out of every export.
7825
- DESTRUCTIVE: setting "confidential" also DELETES every research card
7826
- already stored for that firm AND the judged text of every interval
7827
- it still holds, and prints how many of each. Those rows are
8513
+ DESTRUCTIVE: setting "confidential" also DELETES the cards the firm
8514
+ still holds from the retired export, and prints how many. They are
7828
8515
  gone for good, there is no backup and nothing can bring them back.
7829
- Nothing else is deleted: the audit rows of past exports stay, and so
7830
- do the firm's own entries, their narratives and their judged copies.
7831
- Setting "standard" deletes nothing. If either purge fails after the
8516
+ The firm's entries, their narratives and their judged copies stay.
8517
+ Setting "standard" deletes nothing. If the deletion fails after the
7832
8518
  tier has changed the command says so and exits non-zero; run it
7833
- again, which purges again.
7834
- It then LISTS this firm's past research exports, which are decrypted
7835
- copies on operator machines that no command can delete: find those
7836
- files and delete them by hand.
8519
+ again, which deletes again.
7837
8520
 
7838
8521
  sixminute-admin tenant internal set --env <env> --tenant tenant_... --internal <true|false> [--json]
7839
8522
  Marks a tenant as Unison's own, or not. Only an internal tenant can be
@@ -7846,36 +8529,14 @@ is required; without one every command answers 403.
7846
8529
  Read it before a contract migration or before raising the floor
7847
8530
  (docs/plans/six-minute/release-cycle/runbook.md).
7848
8531
 
7849
- sixminute-admin raw-archive allow --env <env> --tenant tenant_... [--json]
7850
- sixminute-admin raw-archive revoke --env <env> --tenant tenant_... [--json]
8532
+ sixminute-admin raw-archive allow --env staging --tenant tenant_... [--json]
8533
+ sixminute-admin raw-archive revoke --env staging --tenant tenant_... [--json]
7851
8534
  Lets a person on the tenant switch on the raw capture archive on their
7852
8535
  own device (a development copy of a whole day, kept on that device
7853
8536
  and never uploaded), or withdraws it; a device that had it on switches
7854
8537
  itself off at its next policy read. allow refuses a tenant that is not
7855
- internal and exits 2, and the database refuses it again.
7856
-
7857
- sixminute-admin research-export allow --env <env> --tenant tenant_... [--json]
7858
- sixminute-admin research-export revoke --env <env> --tenant tenant_... [--json]
7859
- Records the firm's WRITTEN instruction to keep research data for it, or
7860
- withdraws it (policy research_export, false until set). Run allow only
7861
- on that written instruction. Without it no research card is stored for
7862
- the firm, from the next completion; revoking deletes no card already
7863
- stored, which the 30-day purge removes.
7864
-
7865
- sixminute-admin research export --env <env> --tenant tenant_... --from YYYY-MM-DD --to YYYY-MM-DD --out <dir> [--json]
7866
- Unseals one firm's research cards over a day range onto this machine
7867
- and writes two jsonl files in --out: a new folder is created 0700, and
7868
- an existing one is refused unless it is yours and 0700. The cards,
7869
- and one line per entry the engine judged in that range with the entry
7870
- as it now stands beside the frozen copy. The audit row naming the
7871
- operator, the firm and the range is written BEFORE anything is
7872
- unsealed, in the same transaction as the tier check and holding the
7873
- firm's row, so this command and "tier set confidential" cannot
7874
- interleave. Half-open at the top: --to is not included. A confidential
7875
- firm exports nothing, no file is written and the command exits
7876
- non-zero.
7877
- The unseal runs as you, with your own Google sign-in (gcloud auth
7878
- login); only the people granted the research key can open a card.
8538
+ internal and exits 2, and the database refuses it again. Staging (or
8539
+ local) only: both refuse production, which has no raw archive.
7879
8540
 
7880
8541
  sixminute-admin retention set --env <env> --tenant tenant_... --days <1-365> [--json]
7881
8542
  Sets how many days the cloud keeps the firm's interval contexts after
@@ -7896,11 +8557,15 @@ is required; without one every command answers 403.
7896
8557
  drafts under its own key, and gives intervals stored before the context
7897
8558
  store an age, a bounded pass at a time until nothing is left.
7898
8559
 
7899
- sixminute-admin research contexts --env <env> --tenant tenant_... --from YYYY-MM-DD --to YYYY-MM-DD --out <dir> [--json]
7900
- A firm's research data (R10): its interval contexts and the step ledger
7901
- over a day range, written as two jsonl files in --out (a new folder
7902
- 0700, as research export). Refused unless the firm instructed it in
7903
- writing (research-export allow); the audit row is written first.
8560
+ sixminute-admin scrub plaintext --env <env> --tenant tenant_... [--json]
8561
+ Starts the plaintext scrub of the firm's old rows, one job per active
8562
+ member, and prints how many it started: each job clears the plaintext
8563
+ twin of a sealed value (an entry's judged copy, an old interval's
8564
+ summary and label) only where the sealed value opens to the same
8565
+ content, so nothing is lost. Run backfill sealed first. Refused, exit
8566
+ 2, while a release older than the API's may still be served, because
8567
+ an older revision still reads the plaintext. Safe to run again: a
8568
+ member's job already started today is not started twice.
7904
8569
 
7905
8570
  sixminute-admin entries purge --env <env> --tenant tenant_... --author member_... (--demo | --unmatched | both) [--dry-run] [--yes] [--json]
7906
8571
  Removes entry rows from one firm. A person may only soft-delete
@@ -7908,7 +8573,8 @@ is required; without one every command answers 403.
7908
8573
  from the table: this is the operator's hand, and it
7909
8574
  runs under D-162's licence, which lasts only while the owner is the
7910
8575
  product's one user.
7911
- --demo the rows tools/demo-seed minted (id starts entry_demo-).
8576
+ --demo the rows the retired demo seeder minted (id starts entry_demo-).
8577
+ A released one is never deleted: it is listed as kept.
7912
8578
  --unmatched engine-written rows charged to no matter: not the demo
7913
8579
  prefix, a judged copy (as_judged set, so never a
7914
8580
  hand-made entry), and matter_id null. That is the residue the
@@ -7929,12 +8595,22 @@ is required; without one every command answers 403.
7929
8595
  The flag, the caps in force, and your own seeded days, live or replay.
7930
8596
  sixminute-admin demo validate tools/demo-scenarios/example-small.json [--json]
7931
8597
  Checks a scenario file offline: its shape, its times and every reference.
7932
- sixminute-admin demo seed --env staging --tenant tenant_... --scenario <file> [--day YYYY-MM-DD] [--replay] [--generate] [--json]
8598
+ sixminute-admin demo seed --env staging --tenant tenant_... --scenario <file> [--day YYYY-MM-DD] [--replay] [--generate] [--members a@x,b@y] [--json]
7933
8599
  Seeds the scenario into YOUR OWN account of the firm (accept an
7934
8600
  invitation with the same address first), on its own Scenario device.
7935
8601
  Without --day, the most recent day it lies wholly in the past on, with
7936
8602
  nothing captured. --replay makes the day's Generate answer from the
7937
8603
  scenario with no model; --generate starts that Generate.
8604
+ sixminute-admin demo seed --env staging --tenant tenant_... --scenario <dir|file> [--anchor YYYY-MM-DD] [--days N] [--members a@x,b@y] [--json]
8605
+ Seeds a range of working days instead: the last --days working days
8606
+ (5 unless named, at most 31) up to and including --anchor (today
8607
+ unless named), Saturdays and Sundays skipped. Each day takes the
8608
+ scenario of the directory whose weekday is its own, or the one file
8609
+ named. A range always replays and writes its days' entries itself, so
8610
+ --day and --generate are refused with it; seeding a day again
8611
+ replaces it.
8612
+ --members invites those addresses to the firm as administrators;
8613
+ one already a member or already invited is skipped.
7938
8614
  sixminute-admin demo reset --env staging --tenant tenant_... --day YYYY-MM-DD [--generate] [--json]
7939
8615
  Drops the seeded day's Generate runs and drafts, so Generate runs
7940
8616
  again over the same activity. Released entries stay.
@@ -8224,6 +8900,10 @@ async function runAdmin(argv, deps) {
8224
8900
  }
8225
8901
  if (group === "demo" && subcommand === "validate")
8226
8902
  return validateScenario(rest);
8903
+ if (group === "telemetry") {
8904
+ const parsed = parseFlags(rest, 2);
8905
+ return runTelemetry(subcommand, parsed.values, parsed.flags, parsed.json, { fetchImpl: deps.fetchImpl, posthogKey: deps.posthogKey ?? null });
8906
+ }
8227
8907
  const oneWord = ["login", "logout", "whoami", "doctor", "versions", "audit"].includes(group);
8228
8908
  const vocabulary2 = group === "attribute" || group === "matter" && subcommand === "sets";
8229
8909
  const threeDeep = group === "tenant" && subcommand === "internal";
@@ -8299,16 +8979,12 @@ async function runAdmin(argv, deps) {
8299
8979
  return runTierSet(run2);
8300
8980
  if (group === "raw-archive")
8301
8981
  return runRawArchive(run2, subcommand);
8302
- if (group === "research-export")
8303
- return runResearchExportFlag(run2, subcommand);
8304
- if (group === "research" && subcommand === "export")
8305
- return runResearchExport(run2);
8306
- if (group === "research" && subcommand === "contexts")
8307
- return runResearchContexts(run2);
8308
8982
  if (group === "retention" && subcommand === "set")
8309
8983
  return runRetentionSet(run2);
8310
8984
  if (group === "backfill" && subcommand === "sealed")
8311
8985
  return runBackfillSealed(run2);
8986
+ if (group === "scrub" && subcommand === "plaintext")
8987
+ return runScrubPlaintext(run2);
8312
8988
  if (group === "pipeline" && subcommand === "shadow-report")
8313
8989
  return runShadowReport(run2);
8314
8990
  if (group === "entries" && subcommand === "purge")
@@ -8431,35 +9107,16 @@ async function runTierSet(run2) {
8431
9107
  const done = AdminTierResultSchema.parse(expectOk(await call(run2.api, "PUT", tenantPath(tenantId, "/tier"), { tier })));
8432
9108
  const tenant = { id: done.tenant.id, name: done.tenant.name, security_tier: done.security_tier };
8433
9109
  if (done.security_tier !== "confidential") {
8434
- return render(run2, { tenant }, [
8435
- `tenant ${tenant.id} ${tenant.name}`,
8436
- `security_tier ${tenant.security_tier}`,
8437
- "",
8438
- "Research cards are stored for this firm for 30 days, sealed under the research key, only while it has",
8439
- "instructed it in writing (research-export allow)."
8440
- ]);
9110
+ return render(run2, { tenant }, [`tenant ${tenant.id} ${tenant.name}`, `security_tier ${tenant.security_tier}`]);
8441
9111
  }
8442
9112
  const deleted = done.cards_deleted ?? 0;
8443
- const judgedText = done.judged_text_purged ?? 0;
8444
- return render(run2, {
8445
- tenant,
8446
- research_cards_deleted: deleted,
8447
- judged_text_purged: judgedText,
8448
- ...done.past_exports === null ? { research_exports_error: done.past_exports_error } : { research_exports: done.past_exports }
8449
- }, [
9113
+ return render(run2, { tenant, research_cards_deleted: deleted }, [
8450
9114
  `tenant ${tenant.id} ${tenant.name}`,
8451
9115
  `security_tier ${tenant.security_tier}`,
8452
9116
  `cards deleted ${deleted}`,
8453
- `judged text ${judgedText}`,
8454
- "",
8455
- `The ${deleted} research cards this firm had already stored are deleted, and so is the judged text of`,
8456
- `${judgedText} of its intervals. That cannot be undone: the cards were sealed to a key nothing keeps a copy of,`,
8457
- "the text is the only copy of what a narrative was written from, and there is no backup of either.",
8458
- "The entries, their narratives and hours, the judged copies and the intervals' evidence are untouched.",
8459
- "No research card and no judged text is stored for this firm from the next completion, and its",
8460
- "judged copies stay out of every export.",
8461
9117
  "",
8462
- ...exportLines(done.past_exports, done.past_exports_error)
9118
+ `The ${deleted} cards this firm still held are deleted. That cannot be undone: there is no backup.`,
9119
+ "The entries, their narratives and hours, the judged copies and the intervals' evidence are untouched."
8463
9120
  ]);
8464
9121
  }
8465
9122
  async function runTenantInternalSet(run2) {
@@ -8478,6 +9135,9 @@ async function runRawArchive(run2, subcommand) {
8478
9135
  if (subcommand !== "allow" && subcommand !== "revoke") {
8479
9136
  throw new AdminUsageError(`unknown command: raw-archive ${subcommand ?? ""}`.trim());
8480
9137
  }
9138
+ if (run2.environment.name === "production") {
9139
+ throw new AdminUsageError("raw-archive runs on staging (or local) only: production has no raw archive");
9140
+ }
8481
9141
  const allow = subcommand === "allow";
8482
9142
  const tenantId = required(run2.flags, "tenant", "which tenant the raw archive is being set for");
8483
9143
  const { tenant } = AdminTenantFlagsSchema.parse(expectOk(await call(run2.api, "PUT", tenantPath(tenantId, "/raw-archive"), { allowed: allow })));
@@ -8488,23 +9148,9 @@ async function runRawArchive(run2, subcommand) {
8488
9148
  allow ? "A person on this tenant can now switch on the raw capture archive on their own device, in Capture settings." : "Every device on this tenant switches its raw archive off at its next policy read."
8489
9149
  ]);
8490
9150
  }
8491
- async function runResearchExportFlag(run2, subcommand) {
8492
- if (subcommand !== "allow" && subcommand !== "revoke") {
8493
- throw new AdminUsageError(`unknown command: research-export ${subcommand ?? ""}`.trim());
8494
- }
8495
- const allow = subcommand === "allow";
8496
- const tenantId = required(run2.flags, "tenant", "which firm's written instruction is being recorded");
8497
- const { tenant } = AdminResearchExportFlagSchema.parse(expectOk(await call(run2.api, "PUT", tenantPath(tenantId, "/research-export"), { research_export: allow })));
8498
- return render(run2, { tenant }, [
8499
- `tenant ${tenant.id} ${tenant.name}`,
8500
- `research_export ${tenant.research_export}`,
8501
- "",
8502
- allow ? "Research cards are stored for this firm from its next completion, for 30 days, sealed under the research key." : "No research card is stored for this firm from its next completion. Cards already stored stay until the 30-day purge."
8503
- ]);
8504
- }
8505
9151
  async function runShadowReport(run2) {
8506
- const day = required(run2.flags, "day", "which local day, YYYY-MM-DD");
8507
- const found = ShadowReportSchema.parse(expectOk(await call(run2.api, "GET", `/v1/admin/pipeline/shadow-report?day=${encodeURIComponent(day)}`)));
9152
+ const day2 = required(run2.flags, "day", "which local day, YYYY-MM-DD");
9153
+ const found = ShadowReportSchema.parse(expectOk(await call(run2.api, "GET", `/v1/admin/pipeline/shadow-report?day=${encodeURIComponent(day2)}`)));
8508
9154
  const pct = (part, whole) => whole === 0 ? "-" : `${Math.round(100 * part / whole)}%`;
8509
9155
  return render(run2, found, [
8510
9156
  `day ${found.day}`,
@@ -8541,33 +9187,13 @@ async function runBackfillSealed(run2) {
8541
9187
  `aged ${total.intervals_aged} interval(s)`
8542
9188
  ]);
8543
9189
  }
8544
- async function runResearchContexts(run2) {
8545
- const { flags } = run2;
8546
- const tenantId = required(flags, "tenant", "an export is one firm's contexts and steps");
8547
- const from = dayOf2(flags, "from", "the first day of the range");
8548
- const to = dayOf2(flags, "to", "the day the range stops before; --to is not included");
8549
- if (from >= to)
8550
- throw new AdminUsageError("--from must be before --to; the range is half-open and an empty one exports nothing.");
8551
- const out = resolve(required(flags, "out", "where to write the files; an export lives on this machine and nowhere else"));
8552
- const claimed = claimExportDirectory(out);
8553
- let done;
8554
- try {
8555
- done = ResearchContextExportSchema.parse(expectOk(await call(run2.api, "POST", tenantPath(tenantId, "/research-exports/contexts"), { from, to })));
8556
- } catch (err) {
8557
- releaseExportDirectory(out, claimed);
8558
- throw err;
8559
- }
8560
- const contextsPath = join2(out, `contexts-${tenantId}-${from}-${to}.jsonl`);
8561
- const stepsPath = join2(out, `steps-${tenantId}-${from}-${to}.jsonl`);
8562
- writePrivateFile(contextsPath, jsonLines(done.contexts));
8563
- writePrivateFile(stepsPath, jsonLines(done.steps));
8564
- return render(run2, { tenant_id: tenantId, from: done.from, to: done.to, contexts: done.contexts.length, steps: done.steps.length, files: [contextsPath, stepsPath], operator: done.operator }, [
8565
- `tenant ${tenantId} ${done.tenant_name}`,
8566
- `range ${from} to ${to} (--to not included)`,
8567
- `contexts ${done.contexts.length}`,
8568
- `steps ${done.steps.length}`,
8569
- `files ${contextsPath}`,
8570
- ` ${stepsPath}`
9190
+ var ScrubStartedSchema = exports_external.object({ tenant: exports_external.string(), started: exports_external.number().int().nonnegative() });
9191
+ async function runScrubPlaintext(run2) {
9192
+ const tenantId = required(run2.flags, "tenant", "whose old rows to scrub");
9193
+ const done = ScrubStartedSchema.parse(expectOk(await call(run2.api, "POST", tenantPath(tenantId, "/scrub-plaintext"))));
9194
+ return render(run2, done, [
9195
+ `tenant ${done.tenant}`,
9196
+ `started ${done.started} scrub job(s), one per active member`
8571
9197
  ]);
8572
9198
  }
8573
9199
  async function runVersions(run2) {
@@ -8584,7 +9210,7 @@ async function runEntriesPurge(run2) {
8584
9210
  const demo2 = switched(flags, "demo");
8585
9211
  const unmatched = switched(flags, "unmatched");
8586
9212
  if (!demo2 && !unmatched) {
8587
- throw new AdminUsageError("name what to remove: --demo (the rows tools/demo-seed minted) or --unmatched " + "(engine-written rows charged to no matter), or both. This command never guesses a selector.");
9213
+ throw new AdminUsageError("name what to remove: --demo (the rows the retired demo seeder minted) or --unmatched " + "(engine-written rows charged to no matter), or both. This command never guesses a selector.");
8588
9214
  }
8589
9215
  const author = optional(flags, "author");
8590
9216
  if (author === undefined) {
@@ -8602,10 +9228,11 @@ async function runEntriesPurge(run2) {
8602
9228
  ...demo2 ? purgeSection("demo entries (entry_demo-)", done.rows.demo) : [],
8603
9229
  ...demo2 && unmatched ? [""] : [],
8604
9230
  ...unmatched ? purgeSection("unmatched captured entries (no matter, engine-written)", done.rows.unmatched) : [],
9231
+ ...done.kept.demo.length > 0 ? ["", ...purgeSection("kept, released demo entries (a released row is never deleted)", done.kept.demo)] : [],
8605
9232
  ""
8606
9233
  ];
8607
9234
  if (dryRun) {
8608
- return render(run2, { tenant: tenantId, author, user: done.user, dry_run: true, would_delete: total, rows: done.rows }, [...listing, `--dry-run: ${total} row(s) would be deleted. Nothing was written.`]);
9235
+ return render(run2, { tenant: tenantId, author, user: done.user, dry_run: true, would_delete: total, rows: done.rows, kept: done.kept }, [...listing, `--dry-run: ${total} row(s) would be deleted. Nothing was written.`]);
8609
9236
  }
8610
9237
  if (!yes) {
8611
9238
  return {
@@ -8618,84 +9245,10 @@ async function runEntriesPurge(run2) {
8618
9245
  };
8619
9246
  }
8620
9247
  const deleted = done.deleted ?? { demo: 0, unmatched: 0, total: 0 };
8621
- return render(run2, { tenant: tenantId, author, user: done.user, deleted }, [
9248
+ return render(run2, { tenant: tenantId, author, user: done.user, deleted, kept: done.kept }, [
8622
9249
  ...listing,
8623
9250
  `deleted ${deleted.total} row(s): ${deleted.demo} demo, ${deleted.unmatched} unmatched captured.`,
8624
- "That cannot be undone. Re-seed the demo week with tools/demo-seed if the demo rows were wanted."
8625
- ]);
8626
- }
8627
- async function runResearchExport(run2) {
8628
- const { flags, deps } = run2;
8629
- const tenantId = required(flags, "tenant", "an export is one firm's cards and judged entries");
8630
- const from = dayOf2(flags, "from", "the first day of the range");
8631
- const to = dayOf2(flags, "to", "the day the range stops before; --to is not included");
8632
- if (from >= to)
8633
- throw new AdminUsageError("--from must be before --to; the range is half-open and an empty one exports nothing.");
8634
- const out = resolve(required(flags, "out", "where to write the files; an export lives on this machine and nowhere else"));
8635
- let googleToken;
8636
- try {
8637
- googleToken = deps.googleAccessToken();
8638
- } catch (err) {
8639
- throw new AdminUsageError(err instanceof Error ? err.message : String(err));
8640
- }
8641
- const claimed = claimExportDirectory(out);
8642
- let done;
8643
- try {
8644
- done = ResearchExportResultSchema.parse(expectOk(await call(run2.api, "POST", tenantPath(tenantId, "/research-exports"), { from, to })));
8645
- } catch (err) {
8646
- releaseExportDirectory(out, claimed);
8647
- throw err;
8648
- }
8649
- const range = `range ${from} to ${to} (--to not included)`;
8650
- if (done.tier === "confidential") {
8651
- releaseExportDirectory(out, claimed);
8652
- return render(run2, { tenant_id: tenantId, from: done.from, to: done.to, rows: 0, files: [], operator: done.operator, security_tier: "confidential" }, [
8653
- `tenant ${tenantId} ${done.tenant_name}`,
8654
- "security_tier confidential",
8655
- range,
8656
- "",
8657
- "Nothing was exported and no file was written. This firm stores no research card, and its judged",
8658
- "copies are kept out of every export: that is what its agreement says, and it is not a range to widen.",
8659
- `The attempt is in the audit trail, naming ${done.operator}.`
8660
- ], CONFIDENTIAL_EXIT_CODE);
8661
- }
8662
- const cardsPath = join2(out, `research-${tenantId}-${from}-${to}.jsonl`);
8663
- const pairsPath = join2(out, `judged-${tenantId}-${from}-${to}.jsonl`);
8664
- const written = [];
8665
- try {
8666
- const cards = await openCards(done.sealed_cards, done.research_key_name, googleToken, deps.fetchImpl);
8667
- writePrivateFile(cardsPath, jsonLines(cards));
8668
- written.push(cardsPath);
8669
- writePrivateFile(pairsPath, jsonLines(done.judged_pairs));
8670
- written.push(pairsPath);
8671
- } catch (err) {
8672
- for (const path of written)
8673
- rmSync3(path, { force: true });
8674
- releaseExportDirectory(out, claimed);
8675
- if (err instanceof Error && err.name === "UnsealError")
8676
- throw new AdminUsageError(err.message);
8677
- throw err;
8678
- }
8679
- return render(run2, {
8680
- tenant_id: tenantId,
8681
- from: done.from,
8682
- to: done.to,
8683
- rows: done.rows,
8684
- cards: done.cards,
8685
- pairs: done.pairs,
8686
- files: [cardsPath, pairsPath],
8687
- operator: done.operator
8688
- }, [
8689
- `tenant ${tenantId} ${done.tenant_name}`,
8690
- range,
8691
- `cards ${done.cards}`,
8692
- `judged pairs ${done.pairs}`,
8693
- `files ${cardsPath}`,
8694
- ` ${pairsPath}`,
8695
- `audit row written before the unseal, naming ${done.operator}`,
8696
- "",
8697
- "The files hold screen material of a firm's day and the narratives it billed. They live on this",
8698
- "machine, they are not pseudonymous to anyone who knows the firm, and the audit row says you read them."
9251
+ "That cannot be undone. Seed the demo again with demo seed if the demo rows were wanted."
8699
9252
  ]);
8700
9253
  }
8701
9254
  async function runAudit(run2) {
@@ -8818,7 +9371,7 @@ async function runVocabulary(run2, group, subcommand, action, positionals) {
8818
9371
  const merged = { ...await readMatterSets(firm, matterId), ...wanted };
8819
9372
  await firm("PATCH", `/matters/${enc(matterId)}`, { attribute_sets: merged });
8820
9373
  return done({ matter: matterId, attribute_sets: merged }, [
8821
- `matter ${matterId} ` + Object.entries(merged).map(([key, ids2]) => `${key}=${ids2 === null ? "inherit" : ids2.length === 0 ? "none" : ids2.join(",")}`).join(" ")
9374
+ `matter ${matterId} ` + Object.entries(merged).map(([key, ids3]) => `${key}=${ids3 === null ? "inherit" : ids3.length === 0 ? "none" : ids3.join(",")}`).join(" ")
8822
9375
  ]);
8823
9376
  }
8824
9377
  throw new AdminUsageError(`unknown command: ${group} ${subcommand ?? ""}`.trim());
@@ -8880,10 +9433,10 @@ async function runClient(run2, subcommand) {
8880
9433
  const answer = await call(run2.api, "POST", tenantPath(tenant, "/clients"), body);
8881
9434
  if (answer.status !== 201)
8882
9435
  return refused(run2, answer, [actingHead(tenant, answer)]);
8883
- const client = ClientSchema.parse(answer.body);
8884
- return render(run2, { client }, [
9436
+ const client2 = ClientSchema.parse(answer.body);
9437
+ return render(run2, { client: client2 }, [
8885
9438
  actingHead(tenant, answer),
8886
- `client ${client.id} ${client.name}${client.external_ref === null ? "" : ` ref ${client.external_ref}`}`
9439
+ `client ${client2.id} ${client2.name}${client2.external_ref === null ? "" : ` ref ${client2.external_ref}`}`
8887
9440
  ]);
8888
9441
  }
8889
9442
  if (subcommand === "list") {
@@ -8910,8 +9463,8 @@ async function runClient(run2, subcommand) {
8910
9463
  const answer = await call(run2.api, "PATCH", tenantPath(tenant, `/clients/${enc(id)}`), body);
8911
9464
  if (answer.status !== 200)
8912
9465
  return refused(run2, answer, [actingHead(tenant, answer)]);
8913
- const client = ClientSchema.parse(answer.body);
8914
- return render(run2, { client }, [actingHead(tenant, answer), clientLine(client)]);
9466
+ const client2 = ClientSchema.parse(answer.body);
9467
+ return render(run2, { client: client2 }, [actingHead(tenant, answer), clientLine(client2)]);
8915
9468
  }
8916
9469
  throw new AdminUsageError(`unknown command: client ${subcommand ?? ""}`.trim());
8917
9470
  }
@@ -8945,11 +9498,11 @@ async function runMatter(run2, subcommand) {
8945
9498
  ]);
8946
9499
  }
8947
9500
  if (subcommand === "list") {
8948
- const client = optional(flags, "client");
8949
- if (client !== undefined)
8950
- checkBody(MatterSchema.pick({ client_id: true }), { client_id: client });
9501
+ const client2 = optional(flags, "client");
9502
+ if (client2 !== undefined)
9503
+ checkBody(MatterSchema.pick({ client_id: true }), { client_id: client2 });
8951
9504
  const { rows, head } = await pullAll(run2, tenant, "/matters", MatterSchema);
8952
- const ofClient = client === undefined ? rows : rows.filter((m) => m.client_id === client);
9505
+ const ofClient = client2 === undefined ? rows : rows.filter((m) => m.client_id === client2);
8953
9506
  const matters = switched(flags, "all") ? ofClient : ofClient.filter((m) => m.active);
8954
9507
  return render(run2, { matters }, [
8955
9508
  head,
@@ -8998,7 +9551,7 @@ async function runMattersImport(run2) {
8998
9551
  return refused(run2, answer, [actingHead(tenant, answer)]);
8999
9552
  const report = ImportReportSchema.parse(answer.body);
9000
9553
  const line = (n) => `${n.created} created, ${n.updated} updated, ${n.unchanged} unchanged`;
9001
- const matters = parsed.clients.reduce((n, client) => n + client.matters.length, 0);
9554
+ const matters = parsed.clients.reduce((n, client2) => n + client2.matters.length, 0);
9002
9555
  return render(run2, { file, report }, [
9003
9556
  actingHead(tenant, answer),
9004
9557
  `file ${file} ${parsed.clients.length} client(s), ${matters} matter(s)`,
@@ -9225,12 +9778,6 @@ ${ADMIN_USAGE}`);
9225
9778
 
9226
9779
  // tools/admin/src/surface.ts
9227
9780
  import { createHash } from "node:crypto";
9228
- var MACHINE_ADMIN_PATHS = [
9229
- "/v1/admin/research/purge",
9230
- "/v1/admin/connectors/sync",
9231
- "/v1/admin/context/purge",
9232
- "/v1/admin/calendar/sync"
9233
- ];
9234
9781
  function canonicalJson(value) {
9235
9782
  if (Array.isArray(value))
9236
9783
  return `[${value.map((item) => item === undefined ? "null" : canonicalJson(item)).join(",")}]`;
@@ -9245,7 +9792,7 @@ function adminSurfaceOf(document) {
9245
9792
  const paths = document.paths ?? {};
9246
9793
  const kept = {};
9247
9794
  for (const path of Object.keys(paths)) {
9248
- if (path.startsWith("/v1/admin/") && !MACHINE_ADMIN_PATHS.includes(path))
9795
+ if (path.startsWith("/v1/admin/"))
9249
9796
  kept[path] = paths[path];
9250
9797
  }
9251
9798
  return createHash("sha256").update(canonicalJson(kept)).digest("hex").slice(0, 16);
@@ -9281,9 +9828,9 @@ var result = await runAdmin(process.argv.slice(2), {
9281
9828
  `),
9282
9829
  openBrowser,
9283
9830
  localApiUrl: process.env["SIX_MINUTE_API_URL"] ?? null,
9831
+ posthogKey: process.env["SIXMINUTE_POSTHOG_KEY"] ?? null,
9284
9832
  build: BUILT ?? sourceBuild(),
9285
- readStdin: passwordReader(process.stdin, readAllStdin),
9286
- googleAccessToken: () => googleAccessToken(process.env, process.platform)
9833
+ readStdin: passwordReader(process.stdin, readAllStdin)
9287
9834
  }).catch((err) => {
9288
9835
  if (err instanceof AdminUsageError) {
9289
9836
  process.stderr.write(`sixminute-admin: ${err.message}