@tomradex/domain 0.2.0 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attachments.d.ts +19 -0
- package/dist/attachments.js +13 -0
- package/dist/delivery.d.ts +3 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +56 -0
- package/dist/fields.d.ts +4 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/language.d.ts +49 -0
- package/dist/language.js +117 -0
- package/dist/query.d.ts +8 -0
- package/dist/query.js +39 -0
- package/dist/schema.d.ts +9 -8
- package/dist/schema.js +7 -5
- package/dist/types.d.ts +12 -3
- package/dist/words.d.ts +39 -0
- package/dist/words.js +45 -0
- package/package.json +1 -1
- package/dist/view-kinds.d.ts +0 -13
- package/dist/view-kinds.js +0 -12
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a ticket body points at a file of the project's Files store: by id, so a
|
|
3
|
+
* rename or a move in Files never breaks the link and never rewrites the body.
|
|
4
|
+
*/
|
|
5
|
+
export declare const FILE_LINK = "tomrade-file://";
|
|
6
|
+
/** Each file id the text links to, once, in order of first mention. */
|
|
7
|
+
export declare function linkedFileIds(text: string | null | undefined): string[];
|
|
8
|
+
/** A linked file as an agent can use it: where it is now and what it is. */
|
|
9
|
+
export interface FileDescription {
|
|
10
|
+
fileId: string;
|
|
11
|
+
/** Relative to the Files store root. */
|
|
12
|
+
path: string;
|
|
13
|
+
name: string;
|
|
14
|
+
mime: string;
|
|
15
|
+
size: number;
|
|
16
|
+
/** On this machine, for engines that read files themselves; null when the store has no folder here. */
|
|
17
|
+
localPath: string | null;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=attachments.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a ticket body points at a file of the project's Files store: by id, so a
|
|
3
|
+
* rename or a move in Files never breaks the link and never rewrites the body.
|
|
4
|
+
*/
|
|
5
|
+
export const FILE_LINK = "tomrade-file://";
|
|
6
|
+
const LINKED = /tomrade-file:\/\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/gi;
|
|
7
|
+
/** Each file id the text links to, once, in order of first mention. */
|
|
8
|
+
export function linkedFileIds(text) {
|
|
9
|
+
if (!text)
|
|
10
|
+
return [];
|
|
11
|
+
return [...new Set([...text.matchAll(LINKED)].map((m) => m[1].toLowerCase()))];
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=attachments.js.map
|
package/dist/delivery.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import type { FileDescription } from "./attachments.js";
|
|
2
3
|
import type { Author } from "./types.js";
|
|
3
4
|
/** Metadata only. File bytes and immutable versions belong to the Files plugin. */
|
|
4
5
|
export interface FileReference {
|
|
@@ -66,5 +67,7 @@ export declare const reviewDeliverySchema: z.ZodObject<{
|
|
|
66
67
|
/** The future Files plugin supplies this resolver; no fallback file store exists. */
|
|
67
68
|
export interface FilesResolver {
|
|
68
69
|
getVersion(projectId: string, reference: FileReference): Promise<FileReference | null>;
|
|
70
|
+
/** The files behind these ids as they are now; an id with no file here is left out. */
|
|
71
|
+
describe(projectId: string, fileIds: string[]): Promise<FileDescription[]>;
|
|
69
72
|
}
|
|
70
73
|
//# sourceMappingURL=delivery.d.ts.map
|
package/dist/errors.d.ts
CHANGED
|
@@ -50,4 +50,26 @@ export declare class FieldDefinitionError extends Error {
|
|
|
50
50
|
readonly reason: string;
|
|
51
51
|
constructor(fieldId: string, reason: string);
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* What a failed model call means for the person — each wants a different fix: the key, the address, a wait.
|
|
55
|
+
* `slow`: the model was reached but gave no answer in time; only a check that waits for one says it.
|
|
56
|
+
*/
|
|
57
|
+
export type ModelErrorKind = "auth" | "quota" | "rate" | "model" | "server" | "network" | "slow" | "other";
|
|
58
|
+
/** Why a model turned a working key down: the wrong API for it, not open to this key's tier or account, or not known. */
|
|
59
|
+
export type ModelRefusal = "method" | "tier" | "unknown";
|
|
60
|
+
export declare function modelRefusal(message: string): ModelRefusal | null;
|
|
61
|
+
/** Only a key the provider calls wrong or missing is `auth`; a model it will not serve this key is `model`, even on a 403. */
|
|
62
|
+
export declare function modelErrorKind(message: string): ModelErrorKind;
|
|
63
|
+
/** The provider's own words in a failed call's detail (its JSON `message`), or null; cut short where the detail was. */
|
|
64
|
+
export declare function providerMessage(detail: string): string | null;
|
|
65
|
+
/**
|
|
66
|
+
* An undo aimed at one write — a toast's — refused because the thing changed
|
|
67
|
+
* again since. The marker survives IPC's message wrapping, so the renderer can
|
|
68
|
+
* say it in the person's language.
|
|
69
|
+
*/
|
|
70
|
+
export declare const STALE_UNDO = "[stale-undo]";
|
|
71
|
+
export declare class StaleUndoError extends Error {
|
|
72
|
+
readonly entityId: string;
|
|
73
|
+
constructor(entityId: string);
|
|
74
|
+
}
|
|
53
75
|
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.js
CHANGED
|
@@ -96,4 +96,60 @@ export class FieldDefinitionError extends Error {
|
|
|
96
96
|
this.name = "FieldDefinitionError";
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
+
const BAD_KEY = /\b401\b|authenticat|unauthori[sz]ed|invalid[ _-]?(api[ _-]?)?key|incorrect api key|api[ _-]?key[ _-]?(not valid|invalid|expired|missing|required)|(missing|no) api[ _-]?key/i;
|
|
100
|
+
export function modelRefusal(message) {
|
|
101
|
+
if (/not supported for (generateContent|this (api|method|endpoint))|not a chat model|only supported (in|on|by) (the )?(v1\/)?responses|not supported (in|on|by) (the )?(v1\/)?chat\/completions|does not support (chat|generateContent)/i.test(message))
|
|
102
|
+
return "method";
|
|
103
|
+
if (/model.{0,40}(not found|does not exist|not exist|unknown)|no such model|unknown model|model_not_found/i.test(message) && !/not have access to model/i.test(message))
|
|
104
|
+
return "unknown";
|
|
105
|
+
if (/free[ _-]?tier\w*.{0,80}\blimit: ?0\b|not available (on|for|in) (the |your |this )?(free |current )?(tier|plan)|not have access to (the |this )?model|not have permission to (use|access) (the |this )?model|not allow-?listed|model.{0,60}(not available|unavailable) (to|for) (you|this)/i.test(message))
|
|
106
|
+
return "tier";
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
/** Only a key the provider calls wrong or missing is `auth`; a model it will not serve this key is `model`, even on a 403. */
|
|
110
|
+
export function modelErrorKind(message) {
|
|
111
|
+
if (BAD_KEY.test(message))
|
|
112
|
+
return "auth";
|
|
113
|
+
if (modelRefusal(message))
|
|
114
|
+
return "model";
|
|
115
|
+
if (/\b403\b|api key|permission denied|forbidden/i.test(message))
|
|
116
|
+
return "auth";
|
|
117
|
+
if (/\b402\b|insufficient|balance|quota|billing|credit/i.test(message))
|
|
118
|
+
return "quota";
|
|
119
|
+
if (/\b429\b|rate.?limit|too many requests/i.test(message))
|
|
120
|
+
return "rate";
|
|
121
|
+
if (/\b404\b|model.{0,40}(not found|does not exist|not exist|unknown|invalid)|no such model|unknown model/i.test(message))
|
|
122
|
+
return "model";
|
|
123
|
+
if (/\b5\d\d\b|overloaded|bad gateway|service unavailable/i.test(message))
|
|
124
|
+
return "server";
|
|
125
|
+
if (/fetch failed|ECONNREFUSED|ECONNRESET|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|network|timed? ?out|socket hang up|certificate/i.test(message))
|
|
126
|
+
return "network";
|
|
127
|
+
return "other";
|
|
128
|
+
}
|
|
129
|
+
/** The provider's own words in a failed call's detail (its JSON `message`), or null; cut short where the detail was. */
|
|
130
|
+
export function providerMessage(detail) {
|
|
131
|
+
const found = /"message"\s*:\s*"((?:[^"\\]|\\.)*)("|$)/.exec(detail);
|
|
132
|
+
if (!found?.[1].trim())
|
|
133
|
+
return null;
|
|
134
|
+
let said = found[1];
|
|
135
|
+
try {
|
|
136
|
+
said = JSON.parse(`"${said.replace(/\\$/, "")}"`);
|
|
137
|
+
}
|
|
138
|
+
catch { }
|
|
139
|
+
return found[2] ? said : `${said}…`;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* An undo aimed at one write — a toast's — refused because the thing changed
|
|
143
|
+
* again since. The marker survives IPC's message wrapping, so the renderer can
|
|
144
|
+
* say it in the person's language.
|
|
145
|
+
*/
|
|
146
|
+
export const STALE_UNDO = "[stale-undo]";
|
|
147
|
+
export class StaleUndoError extends Error {
|
|
148
|
+
entityId;
|
|
149
|
+
constructor(entityId) {
|
|
150
|
+
super(`${STALE_UNDO} ${entityId} changed again after that edit; undo it from History instead`);
|
|
151
|
+
this.entityId = entityId;
|
|
152
|
+
this.name = "StaleUndoError";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
99
155
|
//# sourceMappingURL=errors.js.map
|
package/dist/fields.d.ts
CHANGED
|
@@ -34,6 +34,10 @@ export interface FieldDef {
|
|
|
34
34
|
group: boolean;
|
|
35
35
|
};
|
|
36
36
|
rank: number;
|
|
37
|
+
/** Computed by a plugin from what it keeps (a ticket's spend, say); its value rides on `item.fields` when items are read. */
|
|
38
|
+
derived?: boolean;
|
|
39
|
+
/** How a number reads: `usd` is money. */
|
|
40
|
+
unit?: "usd";
|
|
37
41
|
}
|
|
38
42
|
export type BuiltinFieldId = "title" | "status" | "workType" | "priority" | "epic" | "labels" | "board" | "updated" | "key" | "provenance";
|
|
39
43
|
/** The fields every project has, in display order. Ids are reserved: a custom field can never claim one. */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export * from "./types.js";
|
|
2
2
|
export * from "./delivery.js";
|
|
3
|
+
export * from "./attachments.js";
|
|
3
4
|
export * from "./errors.js";
|
|
4
5
|
export * from "./schema.js";
|
|
5
6
|
export * from "./derive.js";
|
|
6
7
|
export * from "./fields.js";
|
|
7
8
|
export * from "./query.js";
|
|
9
|
+
export * from "./words.js";
|
|
10
|
+
export * from "./language.js";
|
|
8
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export * from "./types.js";
|
|
2
2
|
export * from "./delivery.js";
|
|
3
|
+
export * from "./attachments.js";
|
|
3
4
|
export * from "./errors.js";
|
|
4
5
|
export * from "./schema.js";
|
|
5
6
|
export * from "./derive.js";
|
|
6
7
|
export * from "./fields.js";
|
|
7
8
|
export * from "./query.js";
|
|
9
|
+
export * from "./words.js";
|
|
10
|
+
export * from "./language.js";
|
|
8
11
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Languages as data. English is the source every dictionary translates: a
|
|
3
|
+
* translation is a set of `namespace → key → template` entries, and an entry
|
|
4
|
+
* counts only where English has the key and the template asks for the same
|
|
5
|
+
* values. Everything here is pure, so the main process and the renderer
|
|
6
|
+
* merge, check and pick languages the same way.
|
|
7
|
+
*/
|
|
8
|
+
/** A BCP 47 language tag: "en", "zh-Hant", "pt-BR". */
|
|
9
|
+
export type LanguageTag = string;
|
|
10
|
+
/** Templates by namespace, then key. */
|
|
11
|
+
export type Dictionaries = Record<string, Record<string, string>>;
|
|
12
|
+
/** The tag in its canonical spelling ("pt-br" → "pt-BR"), or null when it is not a language tag. */
|
|
13
|
+
export declare function canonicalTag(tag: unknown): LanguageTag | null;
|
|
14
|
+
export declare const isLanguageTag: (tag: unknown) => tag is LanguageTag;
|
|
15
|
+
/** Any language a person may be reading in; English for anything that is not a tag ("system", nothing at all). */
|
|
16
|
+
export declare const languageTag: (locale: unknown) => LanguageTag;
|
|
17
|
+
/** The language's English name, for a prompt's "reply in …" line: "Brazilian Portuguese". */
|
|
18
|
+
export declare function languageName(locale: unknown): string;
|
|
19
|
+
/** What a template fills in or keys off: its `{{slot}}`s and the `[[cmd:…]]` chords the interface writes. */
|
|
20
|
+
export declare const slotsOf: (template: string) => string[];
|
|
21
|
+
export declare const sameSlots: (a: string, b: string) => boolean;
|
|
22
|
+
/** The entries of a translation that can stand in for English: keys English has, asking for the values its template does. */
|
|
23
|
+
export declare function usableWords(english: Dictionaries, translated: Dictionaries): Dictionaries;
|
|
24
|
+
export interface LanguageCoverage {
|
|
25
|
+
/** Entries English has. */
|
|
26
|
+
total: number;
|
|
27
|
+
/** Entries translated with the slots English asks for. */
|
|
28
|
+
translated: number;
|
|
29
|
+
/** `namespace.key` English has and the translation does not; these read in English. */
|
|
30
|
+
missing: string[];
|
|
31
|
+
/** `namespace.key` the translation has and English no longer does; nothing reads them. */
|
|
32
|
+
stale: string[];
|
|
33
|
+
/** Entries whose template asks for other values than English's; these read in English. */
|
|
34
|
+
slotMismatches: {
|
|
35
|
+
key: string;
|
|
36
|
+
english: string[];
|
|
37
|
+
translated: string[];
|
|
38
|
+
}[];
|
|
39
|
+
}
|
|
40
|
+
/** How much of English a translation covers, and what in it nothing can use. */
|
|
41
|
+
export declare function languageCoverage(english: Dictionaries, translated: Dictionaries): LanguageCoverage;
|
|
42
|
+
/**
|
|
43
|
+
* The language to show for a system that prefers `system`, among those the
|
|
44
|
+
* app has: the same tag, else the same language written in the same script —
|
|
45
|
+
* the system's region first, then the language's plain tag. zh-TW finds
|
|
46
|
+
* zh-Hant and never zh; pt-PT finds pt-BR when that is all there is.
|
|
47
|
+
*/
|
|
48
|
+
export declare function pickLanguage(system: string | readonly string[] | undefined | null, available: readonly LanguageTag[]): LanguageTag | null;
|
|
49
|
+
//# sourceMappingURL=language.d.ts.map
|
package/dist/language.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Languages as data. English is the source every dictionary translates: a
|
|
3
|
+
* translation is a set of `namespace → key → template` entries, and an entry
|
|
4
|
+
* counts only where English has the key and the template asks for the same
|
|
5
|
+
* values. Everything here is pure, so the main process and the renderer
|
|
6
|
+
* merge, check and pick languages the same way.
|
|
7
|
+
*/
|
|
8
|
+
const canonical = new Map();
|
|
9
|
+
/** The tag in its canonical spelling ("pt-br" → "pt-BR"), or null when it is not a language tag. */
|
|
10
|
+
export function canonicalTag(tag) {
|
|
11
|
+
if (typeof tag !== "string" || !/^[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*$/.test(tag))
|
|
12
|
+
return null;
|
|
13
|
+
let known = canonical.get(tag);
|
|
14
|
+
if (known === undefined) {
|
|
15
|
+
try {
|
|
16
|
+
known = Intl.getCanonicalLocales(tag)[0] ?? null;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
known = null;
|
|
20
|
+
}
|
|
21
|
+
canonical.set(tag, known);
|
|
22
|
+
}
|
|
23
|
+
return known;
|
|
24
|
+
}
|
|
25
|
+
export const isLanguageTag = (tag) => canonicalTag(tag) !== null;
|
|
26
|
+
/** Any language a person may be reading in; English for anything that is not a tag ("system", nothing at all). */
|
|
27
|
+
export const languageTag = (locale) => canonicalTag(locale) ?? "en";
|
|
28
|
+
// Intl names zh "Chinese"; the app's zh is the simplified script.
|
|
29
|
+
const NAMES = { zh: "Simplified Chinese", "zh-Hant": "Traditional Chinese" };
|
|
30
|
+
/** The language's English name, for a prompt's "reply in …" line: "Brazilian Portuguese". */
|
|
31
|
+
export function languageName(locale) {
|
|
32
|
+
const tag = languageTag(locale);
|
|
33
|
+
if (NAMES[tag])
|
|
34
|
+
return NAMES[tag];
|
|
35
|
+
try {
|
|
36
|
+
return new Intl.DisplayNames(["en"], { type: "language" }).of(tag) ?? tag;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return tag;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** What a template fills in or keys off: its `{{slot}}`s and the `[[cmd:…]]` chords the interface writes. */
|
|
43
|
+
export const slotsOf = (template) => [...new Set((template.match(/\{\{\s*\w+\s*\}\}|\[\[cmd:\w+\]\]/g) ?? []).map((s) => s.replace(/\s/g, "")))].sort();
|
|
44
|
+
export const sameSlots = (a, b) => slotsOf(a).join("\0") === slotsOf(b).join("\0");
|
|
45
|
+
/** The entries of a translation that can stand in for English: keys English has, asking for the values its template does. */
|
|
46
|
+
export function usableWords(english, translated) {
|
|
47
|
+
const out = {};
|
|
48
|
+
for (const [ns, entries] of Object.entries(translated)) {
|
|
49
|
+
const source = english[ns];
|
|
50
|
+
if (!source || typeof entries !== "object" || entries === null)
|
|
51
|
+
continue;
|
|
52
|
+
for (const [key, text] of Object.entries(entries)) {
|
|
53
|
+
if (typeof text !== "string" || typeof source[key] !== "string" || !sameSlots(source[key], text))
|
|
54
|
+
continue;
|
|
55
|
+
(out[ns] ??= {})[key] = text;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/** How much of English a translation covers, and what in it nothing can use. */
|
|
61
|
+
export function languageCoverage(english, translated) {
|
|
62
|
+
const out = { total: 0, translated: 0, missing: [], stale: [], slotMismatches: [] };
|
|
63
|
+
for (const [ns, entries] of Object.entries(english)) {
|
|
64
|
+
for (const [key, source] of Object.entries(entries)) {
|
|
65
|
+
out.total += 1;
|
|
66
|
+
const text = translated[ns]?.[key];
|
|
67
|
+
if (typeof text !== "string")
|
|
68
|
+
out.missing.push(`${ns}.${key}`);
|
|
69
|
+
else if (!sameSlots(source, text))
|
|
70
|
+
out.slotMismatches.push({ key: `${ns}.${key}`, english: slotsOf(source), translated: slotsOf(text) });
|
|
71
|
+
else
|
|
72
|
+
out.translated += 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const [ns, entries] of Object.entries(translated)) {
|
|
76
|
+
for (const key of Object.keys(entries ?? {}))
|
|
77
|
+
if (typeof english[ns]?.[key] !== "string")
|
|
78
|
+
out.stale.push(`${ns}.${key}`);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
const parts = (tag) => {
|
|
83
|
+
try {
|
|
84
|
+
const l = new Intl.Locale(tag).maximize();
|
|
85
|
+
return { language: l.language, script: l.script, region: l.region };
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* The language to show for a system that prefers `system`, among those the
|
|
93
|
+
* app has: the same tag, else the same language written in the same script —
|
|
94
|
+
* the system's region first, then the language's plain tag. zh-TW finds
|
|
95
|
+
* zh-Hant and never zh; pt-PT finds pt-BR when that is all there is.
|
|
96
|
+
*/
|
|
97
|
+
export function pickLanguage(system, available) {
|
|
98
|
+
const wanted = (typeof system === "string" ? [system] : (system ?? [])).map((s) => canonicalTag(s.replace(/_/g, "-").split(".")[0])).filter((s) => !!s);
|
|
99
|
+
const options = available.flatMap((tag) => {
|
|
100
|
+
const p = parts(tag);
|
|
101
|
+
return p ? [{ tag, ...p }] : [];
|
|
102
|
+
});
|
|
103
|
+
for (const tag of wanted) {
|
|
104
|
+
const exact = available.find((a) => a.toLowerCase() === tag.toLowerCase());
|
|
105
|
+
if (exact)
|
|
106
|
+
return exact;
|
|
107
|
+
const want = parts(tag);
|
|
108
|
+
if (!want)
|
|
109
|
+
continue;
|
|
110
|
+
const same = options.filter((o) => o.language === want.language && o.script === want.script);
|
|
111
|
+
const pick = same.find((o) => o.region === want.region && o.tag.includes("-")) ?? same.find((o) => o.tag === want.language) ?? same[0];
|
|
112
|
+
if (pick)
|
|
113
|
+
return pick.tag;
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=language.js.map
|
package/dist/query.d.ts
CHANGED
|
@@ -54,6 +54,14 @@ export interface QueryContext {
|
|
|
54
54
|
export declare function dayBound(value: FieldValue | null | undefined, op: "gte" | "lte"): number | null;
|
|
55
55
|
export declare function matchesClause(item: Item, clause: FilterClause): boolean;
|
|
56
56
|
export declare const matchesFilter: (item: Item, filter: FilterClause[]) => boolean;
|
|
57
|
+
/**
|
|
58
|
+
* The terms of a search: whitespace-separated words, except that a run in
|
|
59
|
+
* double quotes ("login timeout", “登录 超时”) is one term matched as a whole,
|
|
60
|
+
* spaces included. A quote with no partner runs to the end of the string.
|
|
61
|
+
*/
|
|
62
|
+
export declare function searchTerms(search: string): string[];
|
|
63
|
+
/** Every term appears, case aside, in the ticket's key, title or body — what the items index matches, plus the key. */
|
|
64
|
+
export declare function matchesSearch(item: Item, terms: string[]): boolean;
|
|
57
65
|
/** A comparable for one sort key: numbers for numeric fields, strings otherwise, null when empty. */
|
|
58
66
|
export declare function sortKeyOf(item: Item, fieldId: string, ctx: QueryContext): number | string | null;
|
|
59
67
|
/** Empty values sort last whichever direction; ties fall through to manual rank, then recency. */
|
package/dist/query.js
CHANGED
|
@@ -53,6 +53,45 @@ export function matchesClause(item, clause) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
export const matchesFilter = (item, filter) => filter.every((c) => matchesClause(item, c));
|
|
56
|
+
/**
|
|
57
|
+
* The terms of a search: whitespace-separated words, except that a run in
|
|
58
|
+
* double quotes ("login timeout", “登录 超时”) is one term matched as a whole,
|
|
59
|
+
* spaces included. A quote with no partner runs to the end of the string.
|
|
60
|
+
*/
|
|
61
|
+
export function searchTerms(search) {
|
|
62
|
+
const out = [];
|
|
63
|
+
const closer = { '"': '"', "\u201c": "\u201d" };
|
|
64
|
+
let i = 0;
|
|
65
|
+
while (i < search.length) {
|
|
66
|
+
const c = search[i];
|
|
67
|
+
if (/\s/.test(c)) {
|
|
68
|
+
i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const close = closer[c];
|
|
72
|
+
if (close) {
|
|
73
|
+
const end = search.indexOf(close, i + 1);
|
|
74
|
+
const phrase = (end === -1 ? search.slice(i + 1) : search.slice(i + 1, end)).trim();
|
|
75
|
+
if (phrase)
|
|
76
|
+
out.push(phrase);
|
|
77
|
+
i = end === -1 ? search.length : end + 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
let j = i;
|
|
81
|
+
while (j < search.length && !/\s/.test(search[j]))
|
|
82
|
+
j++;
|
|
83
|
+
out.push(search.slice(i, j));
|
|
84
|
+
i = j;
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/** Every term appears, case aside, in the ticket's key, title or body — what the items index matches, plus the key. */
|
|
89
|
+
export function matchesSearch(item, terms) {
|
|
90
|
+
if (!terms.length)
|
|
91
|
+
return true;
|
|
92
|
+
const text = `${item.key} ${item.title} ${item.body ?? ""}`.toLowerCase();
|
|
93
|
+
return terms.every((term) => text.includes(term.toLowerCase()));
|
|
94
|
+
}
|
|
56
95
|
/** A comparable for one sort key: numbers for numeric fields, strings otherwise, null when empty. */
|
|
57
96
|
export function sortKeyOf(item, fieldId, ctx) {
|
|
58
97
|
const raw = fieldValueOf(item, fieldId);
|
package/dist/schema.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const itemKindSchema: z.ZodEnum<{
|
|
3
|
-
ticket: "ticket";
|
|
4
3
|
epic: "epic";
|
|
4
|
+
ticket: "ticket";
|
|
5
5
|
}>;
|
|
6
6
|
export declare const workTypeSchema: z.ZodEnum<{
|
|
7
7
|
coding: "coding";
|
|
@@ -15,7 +15,7 @@ export declare const itemStatusSchema: z.ZodEnum<{
|
|
|
15
15
|
done: "done";
|
|
16
16
|
cancelled: "cancelled";
|
|
17
17
|
}>;
|
|
18
|
-
export declare const prioritySchema: z.
|
|
18
|
+
export declare const prioritySchema: z.ZodLiteral<0 | 1 | 2 | 3 | 4>;
|
|
19
19
|
export declare const listItemsFilterSchema: z.ZodObject<{
|
|
20
20
|
projectId: z.ZodOptional<z.ZodString>;
|
|
21
21
|
boardId: z.ZodOptional<z.ZodString>;
|
|
@@ -28,14 +28,14 @@ export declare const listItemsFilterSchema: z.ZodObject<{
|
|
|
28
28
|
cancelled: "cancelled";
|
|
29
29
|
}>>;
|
|
30
30
|
kind: z.ZodOptional<z.ZodEnum<{
|
|
31
|
-
ticket: "ticket";
|
|
32
31
|
epic: "epic";
|
|
32
|
+
ticket: "ticket";
|
|
33
33
|
}>>;
|
|
34
34
|
workType: z.ZodOptional<z.ZodEnum<{
|
|
35
35
|
coding: "coding";
|
|
36
36
|
general: "general";
|
|
37
37
|
}>>;
|
|
38
|
-
priority: z.ZodOptional<z.
|
|
38
|
+
priority: z.ZodOptional<z.ZodLiteral<0 | 1 | 2 | 3 | 4>>;
|
|
39
39
|
label: z.ZodOptional<z.ZodString>;
|
|
40
40
|
parentId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
41
41
|
search: z.ZodOptional<z.ZodString>;
|
|
@@ -66,8 +66,8 @@ export declare const createItemSchema: z.ZodObject<{
|
|
|
66
66
|
title: z.ZodString;
|
|
67
67
|
body: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
68
68
|
kind: z.ZodOptional<z.ZodEnum<{
|
|
69
|
-
ticket: "ticket";
|
|
70
69
|
epic: "epic";
|
|
70
|
+
ticket: "ticket";
|
|
71
71
|
}>>;
|
|
72
72
|
workType: z.ZodOptional<z.ZodEnum<{
|
|
73
73
|
coding: "coding";
|
|
@@ -81,7 +81,7 @@ export declare const createItemSchema: z.ZodObject<{
|
|
|
81
81
|
done: "done";
|
|
82
82
|
cancelled: "cancelled";
|
|
83
83
|
}>>;
|
|
84
|
-
priority: z.ZodOptional<z.
|
|
84
|
+
priority: z.ZodOptional<z.ZodLiteral<0 | 1 | 2 | 3 | 4>>;
|
|
85
85
|
labels: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
86
86
|
parentId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
87
87
|
projectId: z.ZodOptional<z.ZodString>;
|
|
@@ -93,8 +93,8 @@ export declare const patchSchema: z.ZodObject<{
|
|
|
93
93
|
title: z.ZodOptional<z.ZodString>;
|
|
94
94
|
body: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
95
95
|
kind: z.ZodOptional<z.ZodEnum<{
|
|
96
|
-
ticket: "ticket";
|
|
97
96
|
epic: "epic";
|
|
97
|
+
ticket: "ticket";
|
|
98
98
|
}>>;
|
|
99
99
|
workType: z.ZodOptional<z.ZodEnum<{
|
|
100
100
|
coding: "coding";
|
|
@@ -108,11 +108,12 @@ export declare const patchSchema: z.ZodObject<{
|
|
|
108
108
|
done: "done";
|
|
109
109
|
cancelled: "cancelled";
|
|
110
110
|
}>>;
|
|
111
|
-
priority: z.ZodOptional<z.
|
|
111
|
+
priority: z.ZodOptional<z.ZodLiteral<0 | 1 | 2 | 3 | 4>>;
|
|
112
112
|
labels: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
113
113
|
blocks: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
114
114
|
parentId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
115
115
|
rank: z.ZodOptional<z.ZodNumber>;
|
|
116
|
+
boardId: z.ZodOptional<z.ZodString>;
|
|
116
117
|
fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>>;
|
|
117
118
|
}, z.core.$strip>;
|
|
118
119
|
//# sourceMappingURL=schema.d.ts.map
|
package/dist/schema.js
CHANGED
|
@@ -2,7 +2,8 @@ import { z } from "zod";
|
|
|
2
2
|
export const itemKindSchema = z.enum(["ticket", "epic"]);
|
|
3
3
|
export const workTypeSchema = z.enum(["coding", "general"]);
|
|
4
4
|
export const itemStatusSchema = z.enum(["backlog", "todo", "doing", "verifying", "done", "cancelled"]);
|
|
5
|
-
|
|
5
|
+
// Agents read a bare 0–4 as "1 is most urgent" unless told otherwise.
|
|
6
|
+
export const prioritySchema = z.literal([0, 1, 2, 3, 4]).describe("0 none, 1 low, 2 medium, 3 high, 4 urgent — higher is more urgent");
|
|
6
7
|
export const listItemsFilterSchema = z.object({
|
|
7
8
|
projectId: z.string().optional(),
|
|
8
9
|
boardId: z.string().optional(),
|
|
@@ -11,7 +12,7 @@ export const listItemsFilterSchema = z.object({
|
|
|
11
12
|
workType: workTypeSchema.optional(),
|
|
12
13
|
priority: prioritySchema.optional(),
|
|
13
14
|
label: z.string().optional(),
|
|
14
|
-
parentId: z.string().nullable().optional(),
|
|
15
|
+
parentId: z.string().nullable().optional().describe("The parent item's id or key, e.g. TOM-12; null for none"),
|
|
15
16
|
search: z.string().optional()
|
|
16
17
|
});
|
|
17
18
|
export const fieldValueSchema = z.union([z.string(), z.number()]);
|
|
@@ -32,7 +33,7 @@ export const createItemSchema = z.object({
|
|
|
32
33
|
status: itemStatusSchema.optional(),
|
|
33
34
|
priority: prioritySchema.optional(),
|
|
34
35
|
labels: z.array(z.string()).optional(),
|
|
35
|
-
parentId: z.string().nullable().optional(),
|
|
36
|
+
parentId: z.string().nullable().optional().describe("The parent item's id or key, e.g. TOM-12; null for none"),
|
|
36
37
|
projectId: z.string().optional(),
|
|
37
38
|
boardId: z.string().optional(),
|
|
38
39
|
fields: fieldsPatchSchema.optional()
|
|
@@ -46,9 +47,10 @@ export const patchSchema = z.object({
|
|
|
46
47
|
status: itemStatusSchema.optional(),
|
|
47
48
|
priority: prioritySchema.optional(),
|
|
48
49
|
labels: z.array(z.string()).optional(),
|
|
49
|
-
blocks: z.array(z.string()).optional(),
|
|
50
|
-
parentId: z.string().nullable().optional(),
|
|
50
|
+
blocks: z.array(z.string()).optional().describe("Ids or keys of the items this one blocks"),
|
|
51
|
+
parentId: z.string().nullable().optional().describe("The parent item's id or key, e.g. TOM-12; null for none"),
|
|
51
52
|
rank: z.number().optional(),
|
|
53
|
+
boardId: z.string().optional().describe("Move the item to this workstream (list_workstreams)"),
|
|
52
54
|
fields: fieldsPatchSchema.optional()
|
|
53
55
|
});
|
|
54
56
|
//# sourceMappingURL=schema.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -91,8 +91,9 @@ export interface UndoResult {
|
|
|
91
91
|
/** Result of reverting one agent run. `kept` names writes left in place because someone edited the entity afterwards. */
|
|
92
92
|
export interface UndoRunResult {
|
|
93
93
|
reverted: number;
|
|
94
|
+
/** `entity` is an oplog entity, or what another undo provider calls its own (a file, a setting). */
|
|
94
95
|
kept: {
|
|
95
|
-
entity:
|
|
96
|
+
entity: string;
|
|
96
97
|
entityId: string;
|
|
97
98
|
label: string;
|
|
98
99
|
reason: string;
|
|
@@ -175,6 +176,8 @@ export interface Project {
|
|
|
175
176
|
/** Prefix of the human issue key, e.g. 'TOM' in TOM-142. */
|
|
176
177
|
keyPrefix: string;
|
|
177
178
|
archived: boolean;
|
|
179
|
+
/** The tour a first run opens with: this machine's own, never in the account's catalog. */
|
|
180
|
+
sample?: boolean;
|
|
178
181
|
createdAt: number;
|
|
179
182
|
updatedAt: number;
|
|
180
183
|
}
|
|
@@ -182,6 +185,8 @@ export interface CreateProjectInput {
|
|
|
182
185
|
name: string;
|
|
183
186
|
description?: string | null;
|
|
184
187
|
keyPrefix?: string;
|
|
188
|
+
/** The language the seeded workstream and views are named in: "en" (the default) or "zh". */
|
|
189
|
+
locale?: string;
|
|
185
190
|
}
|
|
186
191
|
export type ProjectPatch = Partial<Pick<Project, "name" | "description" | "archived">>;
|
|
187
192
|
/**
|
|
@@ -285,8 +290,12 @@ export interface ThemeAutomation {
|
|
|
285
290
|
schedule: ThemeScheduleRule[];
|
|
286
291
|
shuffleMinutes: number;
|
|
287
292
|
}
|
|
288
|
-
/**
|
|
289
|
-
|
|
293
|
+
/**
|
|
294
|
+
* 'system' follows the OS locale, falling back to English when no language
|
|
295
|
+
* the app has matches it; otherwise a language tag — one the app ships
|
|
296
|
+
* ("en", "zh", "zh-Hant", "ja", "es", "de", "fr", "ru") or one a language pack adds.
|
|
297
|
+
*/
|
|
298
|
+
export type LanguagePreference = "system" | (string & {});
|
|
290
299
|
export interface LlmSettings {
|
|
291
300
|
apiKey: string;
|
|
292
301
|
baseURL: string;
|
package/dist/words.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type Dictionaries, type LanguageTag } from "./language.js";
|
|
2
|
+
/**
|
|
3
|
+
* What the backend says to a person, kept as one book of templates per
|
|
4
|
+
* package. A template names its values as {{slot}}; the code picks which
|
|
5
|
+
* template to use and fills it, so a translation is data and nothing more.
|
|
6
|
+
* Prompts written for an agent are not words: they stay in code, in English.
|
|
7
|
+
*/
|
|
8
|
+
/** The languages Tomrade's own code writes in, each complete. Any other language reads English, or what an installed language pack brings. */
|
|
9
|
+
export type WordsLocale = "en" | "zh";
|
|
10
|
+
export declare const wordsLocale: (locale: unknown) => WordsLocale;
|
|
11
|
+
/** The `{{slot}}` names a template asks for. */
|
|
12
|
+
export type Slots<S extends string> = S extends `${string}{{${infer V}}}${infer R}` ? V | Slots<R> : never;
|
|
13
|
+
type Fill<S extends string> = [Slots<S>] extends [never] ? [] : [vars: Record<Slots<S>, string | number>];
|
|
14
|
+
type SameSlots<A extends string, B extends string> = [Slots<A>] extends [Slots<B>] ? ([Slots<B>] extends [Slots<A>] ? true : false) : false;
|
|
15
|
+
/** Each translation is complete and names exactly the slots its English does. */
|
|
16
|
+
type Translation<E extends Record<string, string>, Z> = {
|
|
17
|
+
[K in keyof E]: K extends keyof Z ? (Z[K] extends string ? (SameSlots<E[K], Z[K]> extends true ? Z[K] : `slots differ from en: ${E[K]}`) : string) : string;
|
|
18
|
+
};
|
|
19
|
+
export interface Speak<E extends Record<string, string>> {
|
|
20
|
+
<K extends keyof E & string>(key: K, ...vars: Fill<E[K]>): string;
|
|
21
|
+
/** The language asked for; a word it lacks reads English. */
|
|
22
|
+
readonly locale: LanguageTag;
|
|
23
|
+
}
|
|
24
|
+
export type WordBook<E extends Record<string, string>> = (locale: unknown) => Speak<E>;
|
|
25
|
+
export declare const fillSlots: (template: string, vars?: Record<string, string | number>) => string;
|
|
26
|
+
export declare function defineWords<const E extends Record<string, string>, const Z extends Record<string, string>>(id: string, book: {
|
|
27
|
+
en: E;
|
|
28
|
+
zh: Z & Translation<E, Z>;
|
|
29
|
+
}): WordBook<E>;
|
|
30
|
+
/** The English of every book defined so far, keyed by book id. */
|
|
31
|
+
export declare const englishWords: () => Dictionaries;
|
|
32
|
+
/**
|
|
33
|
+
* A language pack's words for the backend's books, spoken from now on. An
|
|
34
|
+
* entry English lacks, or one asking for other slots, reads English. The
|
|
35
|
+
* pack installed last speaks first. Returns the undo.
|
|
36
|
+
*/
|
|
37
|
+
export declare function installWords(locale: LanguageTag, books: Dictionaries): () => void;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=words.d.ts.map
|
package/dist/words.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { languageTag, sameSlots } from "./language.js";
|
|
2
|
+
export const wordsLocale = (locale) => (locale === "zh" ? "zh" : "en");
|
|
3
|
+
export const fillSlots = (template, vars) => vars ? template.replace(/\{\{(\w+)\}\}/g, (_, name) => String(vars[name] ?? "")) : template;
|
|
4
|
+
/** Every book's English, by id: what a language pack translates. */
|
|
5
|
+
const english = new Map();
|
|
6
|
+
/** Language → book → key: what installed language packs bring, each layer kept apart so one can be taken back. */
|
|
7
|
+
const packs = new Map();
|
|
8
|
+
export function defineWords(id, book) {
|
|
9
|
+
english.set(id, book.en);
|
|
10
|
+
const zh = book.zh;
|
|
11
|
+
return (locale) => {
|
|
12
|
+
const lang = languageTag(locale);
|
|
13
|
+
const speak = (key, vars) => {
|
|
14
|
+
const source = book.en[key];
|
|
15
|
+
let text;
|
|
16
|
+
// A pack's entry stands in only for a key English has, asking for the same values.
|
|
17
|
+
for (const layer of packs.get(lang) ?? []) {
|
|
18
|
+
const said = layer[id]?.[key];
|
|
19
|
+
if (text === undefined && source !== undefined && typeof said === "string" && sameSlots(source, said))
|
|
20
|
+
text = said;
|
|
21
|
+
}
|
|
22
|
+
return fillSlots(text ?? (lang === "zh" ? zh[key] : undefined) ?? source ?? key, vars);
|
|
23
|
+
};
|
|
24
|
+
return Object.assign(speak, { locale: lang });
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** The English of every book defined so far, keyed by book id. */
|
|
28
|
+
export const englishWords = () => Object.fromEntries(english);
|
|
29
|
+
/**
|
|
30
|
+
* A language pack's words for the backend's books, spoken from now on. An
|
|
31
|
+
* entry English lacks, or one asking for other slots, reads English. The
|
|
32
|
+
* pack installed last speaks first. Returns the undo.
|
|
33
|
+
*/
|
|
34
|
+
export function installWords(locale, books) {
|
|
35
|
+
const tag = languageTag(locale);
|
|
36
|
+
packs.set(tag, [books, ...(packs.get(tag) ?? [])]);
|
|
37
|
+
return () => {
|
|
38
|
+
const rest = (packs.get(tag) ?? []).filter((l) => l !== books);
|
|
39
|
+
if (rest.length)
|
|
40
|
+
packs.set(tag, rest);
|
|
41
|
+
else
|
|
42
|
+
packs.delete(tag);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=words.js.map
|
package/package.json
CHANGED
package/dist/view-kinds.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { GroupBy, ViewKind } from "./types.js";
|
|
2
|
-
/**
|
|
3
|
-
* The `views` table still carries legacy `name`/`icon`/`group_by` columns
|
|
4
|
-
* (kept for schema stability — see packages/db). These are the values written
|
|
5
|
-
* into them for each built-in kind; the app itself never reads them back.
|
|
6
|
-
*/
|
|
7
|
-
export declare const VIEW_KIND_META: Record<ViewKind, {
|
|
8
|
-
name: string;
|
|
9
|
-
icon: string;
|
|
10
|
-
groupBy: GroupBy;
|
|
11
|
-
rank: number;
|
|
12
|
-
}>;
|
|
13
|
-
//# sourceMappingURL=view-kinds.d.ts.map
|
package/dist/view-kinds.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `views` table still carries legacy `name`/`icon`/`group_by` columns
|
|
3
|
-
* (kept for schema stability — see packages/db). These are the values written
|
|
4
|
-
* into them for each built-in kind; the app itself never reads them back.
|
|
5
|
-
*/
|
|
6
|
-
export const VIEW_KIND_META = {
|
|
7
|
-
status: { name: "Status", icon: "s-doing", groupBy: "status", rank: 0 },
|
|
8
|
-
priority: { name: "Priority", icon: "i-bars", groupBy: "priority", rank: 1 },
|
|
9
|
-
epic: { name: "Epic", icon: "i-stack", groupBy: "epic", rank: 2 },
|
|
10
|
-
dependencies: { name: "Dependencies", icon: "i-graph", groupBy: "status", rank: 3 }
|
|
11
|
-
};
|
|
12
|
-
//# sourceMappingURL=view-kinds.js.map
|