@indigoai-us/hq-cli 5.226.0 → 5.227.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,54 @@
1
+ import type { LaneCompanyRef, QuestionRecord } from "./types.js";
2
+ import { type TrackerMeshRequest } from "./tracker-mesh.js";
3
+ export type TrackerMeshQuestionAudience = "owner" | "operator" | "lane";
4
+ export type TrackerMeshQuestionStatus = "pending" | "answered" | "cancelled";
5
+ export interface TrackerMeshQuestionRow {
6
+ questionId: string;
7
+ threadId: string;
8
+ companyUid: string;
9
+ projectId: string;
10
+ laneId: string;
11
+ host: {
12
+ kind: string;
13
+ id: string;
14
+ };
15
+ audience: TrackerMeshQuestionAudience;
16
+ audienceChain: string[];
17
+ routingHistory: Array<Record<string, string>>;
18
+ question: string;
19
+ context?: string;
20
+ options: string[];
21
+ recommended?: string;
22
+ askedAt: string;
23
+ status: TrackerMeshQuestionStatus;
24
+ choice?: string;
25
+ notes?: string;
26
+ answeredBy?: string;
27
+ answeredAt?: string;
28
+ deliveredAt?: string;
29
+ }
30
+ export interface TrackerMeshQuestionInput {
31
+ laneId: string;
32
+ threadId?: string;
33
+ projectId: string;
34
+ audience: TrackerMeshQuestionAudience;
35
+ audienceChain: string[];
36
+ routingHistory: Array<Record<string, string>>;
37
+ question: string;
38
+ context: string;
39
+ options: string[];
40
+ recommended?: string;
41
+ }
42
+ export interface TrackerMeshQuestionClient {
43
+ create(input: TrackerMeshQuestionInput): Promise<TrackerMeshQuestionRow>;
44
+ list(statuses?: readonly TrackerMeshQuestionStatus[]): Promise<TrackerMeshQuestionRow[]>;
45
+ answer(questionId: string, choice: string, notes?: string): Promise<TrackerMeshQuestionRow>;
46
+ delivered(questionId: string): Promise<TrackerMeshQuestionRow>;
47
+ }
48
+ /** Persist a local delivery checkpoint only after the Work Mesh stamp succeeds. */
49
+ export declare function deliverTrackerMeshQuestionOnce(question: QuestionRecord, company: LaneCompanyRef, client: Pick<TrackerMeshQuestionClient, "delivered">, persist: (deliveredAt: string) => QuestionRecord): Promise<QuestionRecord>;
50
+ export declare function createTrackerMeshQuestionClient(options: {
51
+ company: LaneCompanyRef;
52
+ request?: TrackerMeshRequest;
53
+ }): TrackerMeshQuestionClient;
54
+ //# sourceMappingURL=tracker-mesh-questions.d.ts.map
@@ -0,0 +1,138 @@
1
+ import { meshJson, requireToken } from "../mesh/client.js";
2
+ import { LanesError } from "./errors.js";
3
+ import { mapWorkMeshTrackerError } from "./tracker-mesh.js";
4
+ /** Persist a local delivery checkpoint only after the Work Mesh stamp succeeds. */
5
+ export async function deliverTrackerMeshQuestionOnce(question, company, client, persist) {
6
+ if (!question.mesh_question_id || question.status !== "answered" ||
7
+ !question.delivered_at || question.mesh_delivered_at)
8
+ return question;
9
+ const delivered = await client.delivered(question.mesh_question_id);
10
+ if (delivered.questionId !== question.mesh_question_id ||
11
+ delivered.companyUid !== company.uid ||
12
+ delivered.laneId !== question.lane_id ||
13
+ delivered.projectId !== question.project_id ||
14
+ !delivered.deliveredAt) {
15
+ throw new LanesError("tracker_mesh_response_invalid", "Work Mesh did not stamp the expected question as delivered", {
16
+ mesh_question_id: question.mesh_question_id,
17
+ question_id: question.question_id,
18
+ });
19
+ }
20
+ return persist(delivered.deliveredAt);
21
+ }
22
+ const ROOT = "/v1/work-mesh/tracker/questions";
23
+ const AUDIENCES = ["owner", "operator", "lane"];
24
+ const STATUSES = ["pending", "answered", "cancelled"];
25
+ const MAX_PAGES_PER_FILTER = 10;
26
+ function invalidResponse(field) {
27
+ throw new LanesError("tracker_mesh_response_invalid", `Work Mesh returned an invalid tracker question ${field}`, { field });
28
+ }
29
+ function parseQuestion(raw, company) {
30
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
31
+ return invalidResponse("row");
32
+ const row = raw;
33
+ if (typeof row.questionId !== "string" || !row.questionId ||
34
+ typeof row.threadId !== "string" || !row.threadId ||
35
+ row.companyUid !== company.uid ||
36
+ typeof row.projectId !== "string" || !row.projectId ||
37
+ typeof row.laneId !== "string" || !row.laneId ||
38
+ !AUDIENCES.includes(row.audience) ||
39
+ !STATUSES.includes(row.status) ||
40
+ typeof row.question !== "string" ||
41
+ !Array.isArray(row.options) || row.options.some((entry) => typeof entry !== "string") ||
42
+ !Array.isArray(row.audienceChain) || row.audienceChain.some((entry) => typeof entry !== "string") ||
43
+ !Array.isArray(row.routingHistory) ||
44
+ typeof row.askedAt !== "string" || !Number.isFinite(Date.parse(row.askedAt)))
45
+ return invalidResponse("row");
46
+ if (row.context !== undefined && typeof row.context !== "string")
47
+ return invalidResponse("context");
48
+ if (row.recommended !== undefined && typeof row.recommended !== "string")
49
+ return invalidResponse("recommended");
50
+ if (row.choice !== undefined && typeof row.choice !== "string")
51
+ return invalidResponse("choice");
52
+ if (row.notes !== undefined && typeof row.notes !== "string")
53
+ return invalidResponse("notes");
54
+ if (row.answeredBy !== undefined && typeof row.answeredBy !== "string")
55
+ return invalidResponse("answeredBy");
56
+ if (row.answeredAt !== undefined && (typeof row.answeredAt !== "string" || !Number.isFinite(Date.parse(row.answeredAt))))
57
+ return invalidResponse("answeredAt");
58
+ if (row.deliveredAt !== undefined && (typeof row.deliveredAt !== "string" || !Number.isFinite(Date.parse(row.deliveredAt))))
59
+ return invalidResponse("deliveredAt");
60
+ if (!row.host || typeof row.host !== "object" || Array.isArray(row.host) || typeof row.host.kind !== "string" || typeof row.host.id !== "string")
61
+ return invalidResponse("host");
62
+ return row;
63
+ }
64
+ export function createTrackerMeshQuestionClient(options) {
65
+ const { company } = options;
66
+ if (!company.slug.trim() || !company.uid.trim()) {
67
+ throw new LanesError("tracker_company_unresolved", "A resolved company slug and uid are required for Work Mesh tracker questions", {});
68
+ }
69
+ const request = options.request ?? (async (endpoint, init) => {
70
+ const token = await requireToken({ interactive: false });
71
+ return meshJson(token, endpoint, init);
72
+ });
73
+ const call = async (endpoint, init) => {
74
+ try {
75
+ const result = await request(endpoint, init);
76
+ if (!result || typeof result !== "object" || Array.isArray(result))
77
+ return invalidResponse("response");
78
+ return result;
79
+ }
80
+ catch (err) {
81
+ throw mapWorkMeshTrackerError(err);
82
+ }
83
+ };
84
+ return {
85
+ async create(input) {
86
+ const result = await call(ROOT, { method: "POST", body: { companyUid: company.uid, ...input } });
87
+ return parseQuestion(result.question, company);
88
+ },
89
+ async list(requestedStatuses = STATUSES) {
90
+ const uniqueStatuses = [...new Set(requestedStatuses)];
91
+ if (uniqueStatuses.some((status) => !STATUSES.includes(status))) {
92
+ throw new LanesError("tracker_mesh_query_invalid", "Unsupported Work Mesh tracker question status filter", { statuses: uniqueStatuses });
93
+ }
94
+ const rows = new Map();
95
+ for (const audience of AUDIENCES) {
96
+ for (const status of uniqueStatuses) {
97
+ let cursor;
98
+ for (let page = 0; page < MAX_PAGES_PER_FILTER; page += 1) {
99
+ const query = { companyUid: company.uid, audience, status, limit: "100", ...(cursor ? { cursor } : {}) };
100
+ const result = await call(ROOT, { method: "GET", query });
101
+ if (!Array.isArray(result.questions))
102
+ return invalidResponse("questions list");
103
+ for (const raw of result.questions) {
104
+ const row = parseQuestion(raw, company);
105
+ if (row.audience !== audience || row.status !== status)
106
+ return invalidResponse("filter scope");
107
+ rows.set(row.questionId, row);
108
+ }
109
+ if (result.nextCursor === undefined)
110
+ break;
111
+ if (typeof result.nextCursor !== "string" || !result.nextCursor || result.nextCursor === cursor)
112
+ return invalidResponse("cursor");
113
+ if (page === MAX_PAGES_PER_FILTER - 1) {
114
+ throw new LanesError("tracker_mesh_response_incomplete", "Work Mesh tracker question list exceeded its bounded page limit", { audience, status, max_pages: MAX_PAGES_PER_FILTER });
115
+ }
116
+ cursor = result.nextCursor;
117
+ }
118
+ }
119
+ }
120
+ return [...rows.values()].sort((a, b) => b.askedAt.localeCompare(a.askedAt) || a.questionId.localeCompare(b.questionId));
121
+ },
122
+ async answer(questionId, choice, notes) {
123
+ const result = await call(`${ROOT}/${encodeURIComponent(questionId)}/answer`, {
124
+ method: "POST",
125
+ body: { companyUid: company.uid, choice, ...(notes === undefined ? {} : { notes }) },
126
+ });
127
+ return parseQuestion(result.question, company);
128
+ },
129
+ async delivered(questionId) {
130
+ const result = await call(`${ROOT}/${encodeURIComponent(questionId)}/delivered`, {
131
+ method: "POST",
132
+ body: { companyUid: company.uid },
133
+ });
134
+ return parseQuestion(result.question, company);
135
+ },
136
+ };
137
+ }
138
+ //# sourceMappingURL=tracker-mesh-questions.js.map
@@ -0,0 +1,19 @@
1
+ import { LanesError } from "./errors.js";
2
+ import type { LaneCompanyRef } from "./types.js";
3
+ import type { TrackerStore } from "./tracker-store.js";
4
+ export interface TrackerMeshRequestInit {
5
+ method?: string;
6
+ body?: Record<string, unknown>;
7
+ query?: Record<string, string>;
8
+ }
9
+ export type TrackerMeshRequest = (path: string, init: TrackerMeshRequestInit) => Promise<unknown>;
10
+ export interface WorkMeshTrackerStoreOptions {
11
+ company: LaneCompanyRef;
12
+ projectId: string;
13
+ request?: TrackerMeshRequest;
14
+ now?: () => Date;
15
+ clientRecordId?: () => string;
16
+ }
17
+ export declare function mapWorkMeshTrackerError(err: unknown): LanesError;
18
+ export declare function createWorkMeshTrackerStore(options: WorkMeshTrackerStoreOptions): TrackerStore;
19
+ //# sourceMappingURL=tracker-mesh.d.ts.map
@@ -0,0 +1,228 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { meshJson, requireToken } from "../mesh/client.js";
3
+ import { LanesError } from "./errors.js";
4
+ const ROOT = "/v1/work-mesh/tracker/records";
5
+ const MAX_RECORD_PAGES = 20;
6
+ function fail(error, message, details = {}) {
7
+ throw new LanesError(error, message, details);
8
+ }
9
+ export function mapWorkMeshTrackerError(err) {
10
+ if (err instanceof LanesError)
11
+ return err;
12
+ const value = err;
13
+ const status = typeof value?.status === "number" ? value.status : Number(/^\s*(\d{3})\b/.exec(err instanceof Error ? err.message : "")?.[1]);
14
+ const code = typeof value?.code === "string" ? value.code : "";
15
+ if (status === 404 && code === "tracker_records_disabled") {
16
+ return new LanesError("tracker_records_disabled", "Work Mesh tracker records are disabled for this company", { status, code });
17
+ }
18
+ if (status === 404)
19
+ return new LanesError("tracker_record_not_found", "Work Mesh tracker record was not found", { status, code });
20
+ if (status === 409 && (code === "stale_version" || /stale.version/i.test(err instanceof Error ? err.message : ""))) {
21
+ return new LanesError("tracker_version_conflict", "Work Mesh tracker record changed before this update", { status, code });
22
+ }
23
+ if (status === 409)
24
+ return new LanesError("tracker_mesh_conflict", "Work Mesh rejected the tracker update because the record state changed", { status, code });
25
+ if (status === 401 || status === 403)
26
+ return new LanesError("tracker_mesh_auth_failed", "Work Mesh tracker access was denied for this company", { status, code });
27
+ if (status === 400 || status === 422)
28
+ return new LanesError("tracker_mesh_validation_failed", "Work Mesh rejected the tracker record data", { status, code });
29
+ return new LanesError("tracker_mesh_unavailable", `Work Mesh tracker request failed: ${err instanceof Error ? err.message : String(err)}`, { status: Number.isFinite(status) ? status : undefined });
30
+ }
31
+ function toMesh(value) {
32
+ if (Array.isArray(value))
33
+ return value.map(toMesh);
34
+ if (!value || typeof value !== "object")
35
+ return value;
36
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
37
+ key.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase()),
38
+ toMesh(item),
39
+ ]));
40
+ }
41
+ function fromMesh(value) {
42
+ if (Array.isArray(value))
43
+ return value.map(fromMesh);
44
+ if (!value || typeof value !== "object")
45
+ return value;
46
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
47
+ key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`),
48
+ fromMesh(item),
49
+ ]));
50
+ }
51
+ function recordFromMeta(raw, company, projectId, expectedKind) {
52
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
53
+ return fail("tracker_mesh_response_invalid", "Work Mesh returned a malformed tracker record", { project_id: projectId, kind: expectedKind });
54
+ }
55
+ const meta = raw;
56
+ if (meta.companyUid !== company.uid || meta.projectId !== projectId || meta.kind !== expectedKind || typeof meta.threadId !== "string" || !meta.threadId) {
57
+ return fail("tracker_mesh_response_invalid", "Work Mesh tracker record identity did not match the requested company, project, and kind", { project_id: projectId, kind: expectedKind });
58
+ }
59
+ const fields = fromMesh(meta);
60
+ const createdAt = typeof meta.createdAt === "string" ? meta.createdAt : "";
61
+ const updatedAt = typeof meta.lastActivityAt === "string" ? meta.lastActivityAt : createdAt;
62
+ const version = meta.version;
63
+ if (!Number.isSafeInteger(version) || version < 1 || !Number.isFinite(Date.parse(createdAt)) || !Number.isFinite(Date.parse(updatedAt))) {
64
+ return fail("tracker_mesh_response_invalid", "Work Mesh tracker record has invalid version or timestamps", { record_id: meta.threadId, kind: expectedKind });
65
+ }
66
+ const { company_uid: _companyUid, project_id: _projectId, thread_id: _threadId, thread_status: threadStatus, ...rest } = fields;
67
+ const record = {
68
+ ...rest,
69
+ ...(typeof threadStatus === "string" && rest.status === undefined ? { status: threadStatus } : {}),
70
+ schema_version: "hq-lanes-tracker-record.v1",
71
+ kind: expectedKind,
72
+ id: meta.threadId,
73
+ company: company.slug,
74
+ project_id: projectId,
75
+ version: version,
76
+ created_at: createdAt,
77
+ updated_at: updatedAt,
78
+ };
79
+ return record;
80
+ }
81
+ function identityMatches(record, kind, id, company, projectId) {
82
+ return record.kind === kind && record.id === id && record.company === company.slug && record.project_id === projectId;
83
+ }
84
+ function equalValue(left, right) {
85
+ return JSON.stringify(left) === JSON.stringify(right);
86
+ }
87
+ export function createWorkMeshTrackerStore(options) {
88
+ const { company, projectId } = options;
89
+ if (!company.slug.trim() || !company.uid.trim())
90
+ fail("tracker_company_unresolved", "A resolved company slug and uid are required for Work Mesh tracker storage", {});
91
+ if (!projectId.trim())
92
+ fail("tracker_project_invalid", "A project id is required for Work Mesh tracker storage", {});
93
+ const now = options.now ?? (() => new Date());
94
+ const request = options.request ?? (async (endpoint, init) => {
95
+ const token = await requireToken({ interactive: false });
96
+ return meshJson(token, endpoint, init);
97
+ });
98
+ const requestJson = async (endpoint, init) => {
99
+ try {
100
+ const result = await request(endpoint, init);
101
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
102
+ return fail("tracker_mesh_response_invalid", "Work Mesh returned an invalid tracker response", { endpoint });
103
+ }
104
+ return result;
105
+ }
106
+ catch (err) {
107
+ throw mapWorkMeshTrackerError(err);
108
+ }
109
+ };
110
+ const list = async (kind) => {
111
+ const records = [];
112
+ let cursor;
113
+ for (let page = 0; page < MAX_RECORD_PAGES; page += 1) {
114
+ const query = { companyUid: company.uid, projectId, kind, limit: "100", ...(cursor ? { cursor } : {}) };
115
+ const result = await requestJson(ROOT, { method: "GET", query });
116
+ if (!Array.isArray(result.threads))
117
+ return fail("tracker_mesh_response_invalid", "Work Mesh tracker list did not include a threads array", { kind, project_id: projectId });
118
+ for (const row of result.threads)
119
+ records.push(recordFromMeta(row, company, projectId, kind));
120
+ if (result.nextCursor === undefined)
121
+ return records.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || a.id.localeCompare(b.id));
122
+ if (typeof result.nextCursor !== "string" || !result.nextCursor || result.nextCursor === cursor) {
123
+ return fail("tracker_mesh_response_invalid", "Work Mesh tracker list returned an invalid pagination cursor", { kind, project_id: projectId });
124
+ }
125
+ cursor = result.nextCursor;
126
+ }
127
+ return fail("tracker_mesh_response_incomplete", "Work Mesh tracker list exceeded its bounded page limit", { kind, project_id: projectId, max_pages: MAX_RECORD_PAGES });
128
+ };
129
+ const read = async (kind, id) => {
130
+ const record = (await list(kind)).find((candidate) => candidate.id === id);
131
+ if (record && !identityMatches(record, kind, id, company, projectId)) {
132
+ return fail("tracker_company_mismatch", "Work Mesh tracker record does not belong to the selected company and project", { kind, id, project_id: projectId });
133
+ }
134
+ return record ?? null;
135
+ };
136
+ return {
137
+ read,
138
+ list,
139
+ async create(input) {
140
+ if (input.company !== company.slug || input.project_id !== projectId) {
141
+ return fail("tracker_company_mismatch", "Tracker record company and project must match the resolved Work Mesh scope", { company: input.company, project_id: input.project_id });
142
+ }
143
+ const clientRecordId = input.id ?? (options.clientRecordId ?? randomUUID)();
144
+ const { id: _id, company: _company, project_id: _projectId, kind, ...fields } = input;
145
+ const result = await requestJson(ROOT, {
146
+ method: "POST",
147
+ body: { companyUid: company.uid, kind, projectId, clientRecordId, ...toMesh(fields) },
148
+ });
149
+ if (typeof result.threadId !== "string" || !result.threadId || !Number.isSafeInteger(result.version) || result.version < 1 || typeof result.createdAt !== "string" || !Number.isFinite(Date.parse(result.createdAt))) {
150
+ return fail("tracker_mesh_response_invalid", "Work Mesh did not return valid tracker creation metadata", { kind, project_id: projectId });
151
+ }
152
+ const createdAt = result.createdAt;
153
+ return {
154
+ ...input,
155
+ schema_version: "hq-lanes-tracker-record.v1",
156
+ kind,
157
+ id: result.threadId,
158
+ company: company.slug,
159
+ project_id: projectId,
160
+ version: result.version,
161
+ created_at: createdAt,
162
+ updated_at: typeof result.lastActivityAt === "string" ? result.lastActivityAt : createdAt,
163
+ };
164
+ },
165
+ async update(kind, id, expectedVersion, change, metadata) {
166
+ const current = await read(kind, id);
167
+ if (!current)
168
+ return fail("tracker_record_not_found", `Tracker ${kind} record ${id} was not found`, { kind, id, project_id: projectId });
169
+ if (current.version !== expectedVersion) {
170
+ return fail("tracker_version_conflict", `Tracker record version conflict: expected ${expectedVersion}, found ${current.version}`, { kind, id, expected: expectedVersion, actual: current.version });
171
+ }
172
+ const changed = change(current);
173
+ if (changed.id !== current.id || changed.kind !== current.kind || changed.company !== current.company || changed.project_id !== current.project_id || changed.created_at !== current.created_at) {
174
+ return fail("tracker_update_invalid", "Tracker update cannot change record identity", { kind, id });
175
+ }
176
+ const patch = Object.fromEntries(Object.entries(changed).filter(([key, value]) => !["schema_version", "kind", "id", "company", "project_id", "version", "created_at", "updated_at"].includes(key) && !equalValue(current[key], value)));
177
+ if (Object.keys(patch).length === 0)
178
+ return current;
179
+ const currentValues = Array.isArray(current.values) ? current.values : [];
180
+ const changedValues = Array.isArray(changed.values) ? changed.values : [];
181
+ const isKpiAppend = kind === "kpi" && changedValues.length === currentValues.length + 1 && currentValues.every((value, index) => equalValue(value, changedValues[index]));
182
+ const currentAcks = Array.isArray(current.acknowledgements) ? current.acknowledgements : [];
183
+ const changedAcks = Array.isArray(changed.acknowledgements) ? changed.acknowledgements : [];
184
+ const isAckAppend = kind === "directive" && changedAcks.length === currentAcks.length + 1 && currentAcks.every((value, index) => equalValue(value, changedAcks[index]));
185
+ let version;
186
+ let updatedAt = now().toISOString();
187
+ if (isKpiAppend) {
188
+ const value = changedValues.at(-1);
189
+ const response = await requestJson(`${ROOT}/${encodeURIComponent(id)}/events`, {
190
+ method: "POST",
191
+ body: { companyUid: company.uid, eventKind: "kpi_value", payload: toMesh(value) },
192
+ });
193
+ version = Number(response.version ?? current.version + 1);
194
+ if (typeof response.createdAt === "string")
195
+ updatedAt = response.createdAt;
196
+ }
197
+ else if (isAckAppend) {
198
+ const ack = changedAcks.at(-1);
199
+ const response = await requestJson(`${ROOT}/${encodeURIComponent(id)}/events`, {
200
+ method: "POST",
201
+ body: { companyUid: company.uid, eventKind: "ack", payload: toMesh(ack) },
202
+ });
203
+ version = Number(response.version ?? current.version + 1);
204
+ if (typeof response.createdAt === "string")
205
+ updatedAt = response.createdAt;
206
+ }
207
+ else {
208
+ const action = kind === "blocker" && current.resolved_at == null && changed.resolved_at != null
209
+ ? "resolve"
210
+ : ((kind === "kpi" || kind === "okr") && changed.status === "approved" && current.status !== "approved")
211
+ ? "approve"
212
+ : "update";
213
+ const result = await requestJson(`${ROOT}/${encodeURIComponent(id)}`, {
214
+ method: "PATCH",
215
+ body: { companyUid: company.uid, action, expectedVersion, ...(action === "approve" && metadata?.approvalQuestionId ? { approvalQuestionId: metadata.approvalQuestionId } : {}), ...toMesh(patch) },
216
+ });
217
+ version = Number(result.version);
218
+ if (typeof result.lastActivityAt === "string")
219
+ updatedAt = result.lastActivityAt;
220
+ }
221
+ if (!Number.isSafeInteger(version) || version <= current.version) {
222
+ return fail("tracker_mesh_response_invalid", "Work Mesh tracker update returned an invalid version", { kind, id });
223
+ }
224
+ return { ...changed, schema_version: "hq-lanes-tracker-record.v1", version, created_at: current.created_at, updated_at: updatedAt };
225
+ },
226
+ };
227
+ }
228
+ //# sourceMappingURL=tracker-mesh.js.map
@@ -17,11 +17,22 @@ export interface TrackerRecordInput {
17
17
  project_id: string;
18
18
  [key: string]: unknown;
19
19
  }
20
+ export type StoreValue<T> = T | Promise<T>;
20
21
  export interface TrackerStore {
22
+ read(kind: TrackerKind, id: string): StoreValue<TrackerRecord | null>;
23
+ list(kind: TrackerKind): StoreValue<TrackerRecord[]>;
24
+ create(record: TrackerRecordInput): StoreValue<TrackerRecord>;
25
+ update(kind: TrackerKind, id: string, expectedVersion: number, change: (current: TrackerRecord) => TrackerRecord, metadata?: {
26
+ approvalQuestionId?: string;
27
+ }): StoreValue<TrackerRecord>;
28
+ }
29
+ export interface LocalTrackerStore extends TrackerStore {
21
30
  read(kind: TrackerKind, id: string): TrackerRecord | null;
22
31
  list(kind: TrackerKind): TrackerRecord[];
23
32
  create(record: TrackerRecordInput): TrackerRecord;
24
- update(kind: TrackerKind, id: string, expectedVersion: number, change: (current: TrackerRecord) => TrackerRecord): TrackerRecord;
33
+ update(kind: TrackerKind, id: string, expectedVersion: number, change: (current: TrackerRecord) => TrackerRecord, metadata?: {
34
+ approvalQuestionId?: string;
35
+ }): TrackerRecord;
25
36
  }
26
- export declare function createLocalTrackerStore(projectDirectory: string, now?: () => Date): TrackerStore;
37
+ export declare function createLocalTrackerStore(projectDirectory: string, now?: () => Date): LocalTrackerStore;
27
38
  //# sourceMappingURL=tracker-store.d.ts.map
@@ -238,7 +238,7 @@ export function createLocalTrackerStore(projectDirectory, now = () => new Date()
238
238
  writeExclusive(path.join(directory, `${id}.json`), `${JSON.stringify(record, null, 2)}\n`);
239
239
  return record;
240
240
  },
241
- update(kind, id, expectedVersion, change) {
241
+ update(kind, id, expectedVersion, change, _metadata) {
242
242
  const file = recordPath(root, kind, id);
243
243
  const directory = ensureKindDirectory(root, kind);
244
244
  return withRecordLock(file, () => {
@@ -638,6 +638,10 @@ export interface QuestionRecord {
638
638
  version: number;
639
639
  /** Null until the owner starts a resume carrying this answer; absent on old records. */
640
640
  delivered_at?: string | null;
641
+ /** Work Mesh's server-issued id; absent for pre-TRK-9 local questions. */
642
+ mesh_question_id?: string;
643
+ /** Local checkpoint that the server accepted this question's delivery stamp. */
644
+ mesh_delivered_at?: string | null;
641
645
  body: QuestionBodyRef;
642
646
  routing_history: RoutingHistoryEntry[];
643
647
  answer?: QuestionAnswer;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Return the largest measured cell length without passing an unbounded number
3
+ * of values as function arguments. Large vault tables can contain hundreds of
4
+ * thousands of rows, beyond the argument limit of Math.max(...values).
5
+ */
6
+ export declare function maxLength<T>(values: Iterable<T>, measure: (value: T) => number, floor: number): number;
7
+ //# sourceMappingURL=table-width.d.ts.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Return the largest measured cell length without passing an unbounded number
3
+ * of values as function arguments. Large vault tables can contain hundreds of
4
+ * thousands of rows, beyond the argument limit of Math.max(...values).
5
+ */
6
+ export function maxLength(values, measure, floor) {
7
+ let maximum = floor;
8
+ for (const value of values) {
9
+ const length = measure(value);
10
+ if (length > maximum)
11
+ maximum = length;
12
+ }
13
+ return maximum;
14
+ }
15
+ //# sourceMappingURL=table-width.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.226.0",
3
+ "version": "5.227.1",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {