@stofone/dsh-agent-roster 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +60 -1
  2. package/README.md +27 -2
  3. package/README.zh.md +25 -2
  4. package/client.js +350 -3
  5. package/examples/dist/core-review-1.0.0.json +9 -0
  6. package/examples/dist/research-0.1.0.json +7 -0
  7. package/examples-orchestration/dist/core-review-1.0.0.json +9 -0
  8. package/index.js +120 -0
  9. package/lib/compile2.js +13 -4
  10. package/lib/search.js +155 -0
  11. package/locale/en.json +19 -2
  12. package/locale/zh.json +19 -2
  13. package/orgs/agent-roster-collective/dist/api-design-1.0.0.json +10 -0
  14. package/orgs/agent-roster-collective/dist/content-ops-1.0.0.json +9 -0
  15. package/orgs/agent-roster-collective/dist/contract-review-1.0.0.json +9 -0
  16. package/orgs/agent-roster-collective/dist/data-quality-1.0.0.json +10 -0
  17. package/orgs/agent-roster-collective/dist/db-migration-1.0.0.json +10 -0
  18. package/orgs/agent-roster-collective/dist/docs-writer-1.0.0.json +9 -0
  19. package/orgs/agent-roster-collective/dist/finance-recon-1.0.0.json +9 -0
  20. package/orgs/agent-roster-collective/dist/frontend-ui-1.0.0.json +10 -0
  21. package/orgs/agent-roster-collective/dist/i18n-l10n-1.0.0.json +10 -0
  22. package/orgs/agent-roster-collective/dist/incident-response-1.0.0.json +10 -0
  23. package/orgs/agent-roster-collective/dist/literature-review-1.0.0.json +9 -0
  24. package/orgs/agent-roster-collective/dist/market-research-1.0.0.json +9 -0
  25. package/orgs/agent-roster-collective/dist/ml-eval-1.0.0.json +9 -0
  26. package/orgs/agent-roster-collective/dist/performance-1.0.0.json +9 -0
  27. package/orgs/agent-roster-collective/dist/product-discovery-1.0.0.json +9 -0
  28. package/orgs/agent-roster-collective/dist/qa-regression-1.0.0.json +10 -0
  29. package/orgs/agent-roster-collective/dist/recruiting-1.0.0.json +9 -0
  30. package/orgs/agent-roster-collective/dist/release-manager-1.0.0.json +9 -0
  31. package/orgs/agent-roster-collective/dist/security-audit-1.0.0.json +10 -0
  32. package/orgs/agent-roster-collective/dist/sre-oncall-1.0.0.json +10 -0
  33. package/orgs/agent-roster-collective/dist/support-triage-1.0.0.json +9 -0
  34. package/package.json +1 -1
  35. package/schema/compiled.v1.schema.json +30 -0
package/index.js CHANGED
@@ -26,6 +26,7 @@ import { validateRoster2 } from "./lib/roster2.js";
26
26
  import { sendError, sendJson, createAuthGate, readJsonBody, requireMethod } from "./lib/http.js";
27
27
  import { buildPanelSnapshot, installCollection, loadRemotePanel, resolveRoots } from "./lib/service.js";
28
28
  import { createTtlCache, fetchCollection } from "./lib/remote.js";
29
+ import { searchRosters } from "./lib/search.js";
29
30
  import { registerRosterTools } from "./lib/tools.js";
30
31
  import { deleteRosterDocument, readRosterDocument, writeRosterDocument } from "./lib/write.js";
31
32
 
@@ -173,6 +174,125 @@ export function apply(ctx, config) {
173
174
  "collection route",
174
175
  );
175
176
 
177
+ // 「档案库」与「搜索」共用同一次摊平动作:本机 + 所有订阅来源。
178
+ // 两条路由各写一遍,缓存口径、并发上限与诊断文案迟早会分叉。
179
+ const gatherEntries = async ({ refresh = false } = {}) => {
180
+ const library = collectLibrary(ctx, settings, { name: pkg.name, version: pkg.version });
181
+ const entries = [];
182
+ let localCount = 0;
183
+ for (const workspace of library.workspaces) {
184
+ for (const roster of workspace.rosters) {
185
+ entries.push({ scope: "local", roster, libraryId: workspace.rootDir ?? null, file: roster.file ?? null });
186
+ localCount += 1;
187
+ }
188
+ }
189
+
190
+ const diagnostics = [];
191
+ const collections = [];
192
+ try {
193
+ const remote = await loadRemotePanel(settings, { refresh });
194
+ for (const item of remote.diagnostics ?? []) if (item.severity !== "info") diagnostics.push(item);
195
+ for (const collection of remote.collections ?? []) collections.push(collection);
196
+ } catch (error) {
197
+ diagnostics.push({
198
+ severity: "warn",
199
+ code: "index-unavailable",
200
+ message: `订阅索引拉不到,这次只显示本机档案:${error instanceof Error ? error.message : String(error)}`,
201
+ });
202
+ }
203
+
204
+ // 来源两两并发:一个慢集合不该把所有来源都拖住,也不该一次打三十个请求。
205
+ const sourceConcurrency = 2;
206
+ let cursor = 0;
207
+ const fetchNext = async () => {
208
+ while (cursor < collections.length) {
209
+ const collection = collections[cursor];
210
+ cursor += 1;
211
+ const key = `${collection.source ?? ""}::${collection.id}`;
212
+ try {
213
+ const cached = collectionCache.get(key);
214
+ const fetched = cached ?? collectionCache.set(key, await fetchCollection(collection));
215
+ for (const roster of fetched.rosters ?? []) {
216
+ entries.push({
217
+ scope: "remote",
218
+ roster,
219
+ collectionId: collection.id,
220
+ collectionTitle: collection.title ?? null,
221
+ file: roster.file ?? null,
222
+ });
223
+ }
224
+ for (const item of fetched.diagnostics ?? []) if (item.severity === "error") diagnostics.push(item);
225
+ } catch (error) {
226
+ diagnostics.push({
227
+ severity: "warn",
228
+ code: "source-failed",
229
+ message: `${collection.id} 拉取失败:${error instanceof Error ? error.message : String(error)}`,
230
+ });
231
+ }
232
+ }
233
+ };
234
+ await Promise.all(Array.from({ length: Math.min(sourceConcurrency, collections.length) }, fetchNext));
235
+
236
+ return { entries, diagnostics: diagnostics.slice(0, 20), counts: { local: localCount, remote: entries.length - localCount } };
237
+ };
238
+
239
+ const catalogEntry = (entry) => {
240
+ const roster = entry.roster;
241
+ return {
242
+ scope: entry.scope,
243
+ rosterId: typeof roster.id === "string" ? roster.id : "",
244
+ title: typeof roster.title === "string" ? roster.title : "",
245
+ description: typeof roster.description === "string" ? roster.description : "",
246
+ tags: Array.isArray(roster.tags) ? roster.tags : [],
247
+ members: Array.isArray(roster.members) ? roster.members.length : 0,
248
+ memberRoles: (Array.isArray(roster.members) ? roster.members : [])
249
+ .map((member) => (typeof member?.role === "string" ? member.role : ""))
250
+ .filter((role) => role !== "")
251
+ .slice(0, 4),
252
+ libraryId: entry.libraryId ?? null,
253
+ collectionId: entry.collectionId ?? null,
254
+ collectionTitle: entry.collectionTitle ?? null,
255
+ file: entry.file ?? null,
256
+ };
257
+ };
258
+
259
+ // 档案库:像插件市场那样「全部摊开」,供浏览、按标签筛选、按来源聚合。
260
+ register(
261
+ "/plugins/dsh-agent-roster/catalog",
262
+ async (req, res) => {
263
+ if (guard(req, res)) return;
264
+ if (!requireMethod(req, res, "GET")) return;
265
+ const query = new URL(req.url ?? "/", "http://localhost").searchParams;
266
+ const gathered = await gatherEntries({ refresh: query.get("refresh") === "1" });
267
+ sendJson(res, 200, {
268
+ ok: true,
269
+ entries: gathered.entries.map(catalogEntry),
270
+ counts: gathered.counts,
271
+ diagnostics: gathered.diagnostics,
272
+ });
273
+ },
274
+ "catalog route",
275
+ );
276
+
277
+ // 搜索:同一个「摊平」结果上做排序,回车即可打开。空查询不做无谓的摊平。
278
+ register(
279
+ "/plugins/dsh-agent-roster/search",
280
+ async (req, res) => {
281
+ if (guard(req, res)) return;
282
+ if (!requireMethod(req, res, "GET")) return;
283
+ const query = new URL(req.url ?? "/", "http://localhost").searchParams;
284
+ const q = (query.get("q") ?? "").trim();
285
+ if (q === "") {
286
+ sendJson(res, 200, { ok: true, q: "", tokens: [], total: 0, matches: [], counts: { local: 0, remote: 0 }, diagnostics: [] });
287
+ return;
288
+ }
289
+ const gathered = await gatherEntries();
290
+ const result = searchRosters(q, gathered.entries);
291
+ sendJson(res, 200, { ...result, counts: gathered.counts, diagnostics: gathered.diagnostics });
292
+ },
293
+ "search route",
294
+ );
295
+
176
296
  register(
177
297
  "/plugins/dsh-agent-roster/locale",
178
298
  async (req, res) => {
package/lib/compile2.js CHANGED
@@ -231,7 +231,13 @@ export function compileRoster(options) {
231
231
  source: {
232
232
  roster: roster.id,
233
233
  version: roster.version,
234
+ // 产物是唯一被分发的形态:发现它所需的一切(标题、标签、许可、维护者)必须自带,
235
+ // 否则"从远处找一份档案"就只剩一个 id。
234
236
  title: roster.title ?? null,
237
+ description: roster.description ?? null,
238
+ tags: Array.isArray(roster.tags) ? roster.tags : [],
239
+ license: roster.license ?? null,
240
+ maintainers: Array.isArray(roster.maintainers) ? roster.maintainers : [],
235
241
  repo: options.source?.repo ?? null,
236
242
  ref: options.source?.ref ?? null,
237
243
  lock: options.source?.lock ?? null,
@@ -510,10 +516,10 @@ export function rosterViewOf(compiled, roster2, options = {}) {
510
516
  version: compiled.source.version,
511
517
  file: compiled.source.file ?? null,
512
518
  title: roster2?.title ?? compiled.source.title ?? "",
513
- description: roster2?.description ?? "",
514
- license: roster2?.license ?? "",
515
- maintainers: roster2?.maintainers ?? [],
516
- tags: roster2?.tags ?? [],
519
+ description: roster2?.description ?? compiled.source.description ?? "",
520
+ license: roster2?.license ?? compiled.source.license ?? "",
521
+ maintainers: roster2?.maintainers ?? compiled.source.maintainers ?? [],
522
+ tags: roster2?.tags ?? compiled.source.tags ?? [],
517
523
  notes: roster2?.notes ?? null,
518
524
  members,
519
525
  captain,
@@ -618,6 +624,9 @@ export function flattenCompiledToRoster(compiled) {
618
624
  };
619
625
  if (source.title) roster.title = source.title;
620
626
  if (source.description) roster.description = source.description;
627
+ if (source.license) roster.license = source.license;
628
+ if (Array.isArray(source.maintainers) && source.maintainers.length > 0) roster.maintainers = source.maintainers;
629
+ if (Array.isArray(source.tags) && source.tags.length > 0) roster.tags = source.tags;
621
630
  if (compiled.captain) {
622
631
  const captain = { name: String(compiled.captain.role ?? "").replace(/@[^@]*$/, "") };
623
632
  if (compiled.captain.provider) captain.provider = compiled.captain.provider;
package/lib/search.js ADDED
@@ -0,0 +1,155 @@
1
+ /**
2
+ * 找一份档案,而不是浏览一堆合集。
3
+ *
4
+ * 面板原来的组织单位是「合集」:先展开一个来源,再在里面翻。但真实任务几乎总是反向的——
5
+ * 「我需要一个能盯数据质量的队伍」,先知道自己要什么,再去找谁有。所以这里把搜索做成
6
+ * 一等能力:一个问题,横跨本机与所有订阅来源,命中的每一行都要说清**为什么命中**。
7
+ *
8
+ * 两条刻意的规则:
9
+ * 1. **多词是「且」**:`data quality` 要求每个词都落在某个字段里,任一落空即不入选。
10
+ * 否则「评分」会把一堆只沾了「分」的档案捞上来,搜索结果比列表更难读。
11
+ * 2. **命中要有出处**:每行返回 `hits[{field,label,text}]`,界面直接显示「命中 标签:数据质量」。
12
+ * 只给分数是没法解释的——用户凭什么信这一条排在前面。
13
+ *
14
+ * 纯函数:不读文件、不联网,输入就是已经装配好的档案视图。
15
+ */
16
+
17
+ export const SEARCH_LIMIT = 20;
18
+
19
+ /** 字段权重:id 是名字,title 是自我描述,tags 是别人怎么找它。 */
20
+ export const SEARCH_FIELDS = [
21
+ { field: "id", weight: 100, label: "档案 id" },
22
+ { field: "title", weight: 56, label: "标题" },
23
+ { field: "tags", weight: 48, label: "标签" },
24
+ { field: "skills", weight: 40, label: "技能要求" },
25
+ { field: "requires", weight: 36, label: "能力要求" },
26
+ { field: "memberRole", weight: 34, label: "成员角色" },
27
+ { field: "memberName", weight: 26, label: "成员名" },
28
+ { field: "memberTitle", weight: 22, label: "成员标题" },
29
+ { field: "description", weight: 18, label: "描述" },
30
+ { field: "notes", weight: 12, label: "备注" },
31
+ ];
32
+
33
+ const WEIGHT = new Map(SEARCH_FIELDS.map((item) => [item.field, item]));
34
+
35
+ /** 把一个词切成可比较的片段:空白与中文标点都算分隔。 */
36
+ export function tokenize(query) {
37
+ if (typeof query !== "string") return [];
38
+ return query
39
+ .toLowerCase()
40
+ .split(/[\s,,、;;|/]+/)
41
+ .map((token) => token.trim())
42
+ .filter((token) => token !== "");
43
+ }
44
+
45
+ function push(list, field, text) {
46
+ if (typeof text !== "string") return;
47
+ const value = text.trim();
48
+ if (value === "") return;
49
+ const meta = WEIGHT.get(field);
50
+ if (meta === undefined) return;
51
+ list.push({ field, label: meta.label, text: value, weight: meta.weight });
52
+ }
53
+
54
+ /**
55
+ * 把一份档案摊成「可被搜到的字段」。同一字段里的多个值各自成条,
56
+ * 这样「标签:数据质量」能精确指出是哪一条标签命中,而不是含糊地说"标签命中"。
57
+ */
58
+ export function searchableFields(roster) {
59
+ const fields = [];
60
+ if (roster === null || typeof roster !== "object") return fields;
61
+ push(fields, "id", roster.id);
62
+ push(fields, "title", roster.title);
63
+ push(fields, "description", roster.description);
64
+ push(fields, "notes", roster.notes);
65
+ for (const tag of Array.isArray(roster.tags) ? roster.tags : []) push(fields, "tags", tag);
66
+ for (const member of Array.isArray(roster.members) ? roster.members : []) {
67
+ push(fields, "memberName", member?.name);
68
+ push(fields, "memberRole", member?.role);
69
+ push(fields, "memberTitle", member?.title);
70
+ for (const skill of Array.isArray(member?.skills) ? member.skills : []) push(fields, "skills", skill);
71
+ }
72
+ const requires = roster.requires !== null && typeof roster.requires === "object" ? roster.requires : null;
73
+ if (requires !== null) {
74
+ for (const skill of Array.isArray(requires.skills) ? requires.skills : []) push(fields, "skills", skill);
75
+ for (const policy of Array.isArray(requires.toolPolicies) ? requires.toolPolicies : []) push(fields, "requires", policy);
76
+ }
77
+ return fields;
78
+ }
79
+
80
+ /**
81
+ * 一条 token 落在哪个字段上最值钱。
82
+ * 前缀命中与完全命中各加一点:搜 `data` 时 `data-quality` 应该排在 `reconcile-metadata` 前面。
83
+ */
84
+ function bestMatch(token, fields) {
85
+ let best = null;
86
+ for (const item of fields) {
87
+ const haystack = item.text.toLowerCase();
88
+ const index = haystack.indexOf(token);
89
+ if (index === -1) continue;
90
+ let score = item.weight;
91
+ if (index === 0) score += item.weight * 0.4;
92
+ if (haystack === token) score += item.weight * 0.8;
93
+ if (best === null || score > best.score) best = { score, item };
94
+ }
95
+ return best;
96
+ }
97
+
98
+ /**
99
+ * @param {string} query
100
+ * @param {{scope: "local" | "remote", roster: object, libraryId?: string|null,
101
+ * collectionId?: string|null, collectionTitle?: string|null, file?: string|null}[]} entries
102
+ * @returns {{ok: true, q: string, tokens: string[], total: number,
103
+ * matches: {scope: string, rosterId: string, title: string, description: string,
104
+ * tags: string[], members: number, libraryId: string|null,
105
+ * collectionId: string|null, collectionTitle: string|null, file: string|null,
106
+ * score: number, hits: {field: string, label: string, text: string}[]}[]}}
107
+ */
108
+ export function searchRosters(query, entries, { limit = SEARCH_LIMIT } = {}) {
109
+ const tokens = tokenize(query);
110
+ if (tokens.length === 0) return { ok: true, q: "", tokens: [], total: 0, matches: [] };
111
+
112
+ const matches = [];
113
+ for (const entry of Array.isArray(entries) ? entries : []) {
114
+ const roster = entry?.roster;
115
+ if (roster === null || typeof roster !== "object") continue;
116
+ const fields = searchableFields(roster);
117
+ let score = 0;
118
+ const hits = [];
119
+ const seen = new Set();
120
+ let complete = true;
121
+ for (const token of tokens) {
122
+ const found = bestMatch(token, fields);
123
+ if (found === null) {
124
+ complete = false;
125
+ break;
126
+ }
127
+ score += found.score;
128
+ if (!seen.has(found.item.field)) {
129
+ seen.add(found.item.field);
130
+ hits.push({ field: found.item.field, label: found.item.label, text: found.item.text });
131
+ }
132
+ }
133
+ if (!complete) continue;
134
+ const id = typeof roster.id === "string" ? roster.id : "";
135
+ // 同名匹配下,短 id 更可能是"那一个":`data-quality` 先于 `data-quality-and-observability-review`。
136
+ score += Math.max(0, 24 - id.length);
137
+ matches.push({
138
+ scope: entry.scope === "remote" ? "remote" : "local",
139
+ rosterId: id,
140
+ title: typeof roster.title === "string" ? roster.title : "",
141
+ description: typeof roster.description === "string" ? roster.description : "",
142
+ tags: Array.isArray(roster.tags) ? roster.tags.slice(0, 6) : [],
143
+ members: Array.isArray(roster.members) ? roster.members.length : 0,
144
+ libraryId: entry.libraryId ?? null,
145
+ collectionId: entry.collectionId ?? null,
146
+ collectionTitle: entry.collectionTitle ?? null,
147
+ file: entry.file ?? null,
148
+ score: Math.round(score),
149
+ hits,
150
+ });
151
+ }
152
+
153
+ matches.sort((left, right) => right.score - left.score || left.rosterId.localeCompare(right.rosterId));
154
+ return { ok: true, q: query.trim(), tokens, total: matches.length, matches: matches.slice(0, limit) };
155
+ }
package/locale/en.json CHANGED
@@ -64,7 +64,8 @@
64
64
  "metaRosters": "rosters",
65
65
  "metaMembers": "members",
66
66
  "metaTeams": "teams",
67
- "metaOrganization": "Organization {name}"
67
+ "metaOrganization": "Organization {name}",
68
+ "tabsCatalog": "Library"
68
69
  },
69
70
  "roster": {
70
71
  "members": "{count} members",
@@ -301,5 +302,21 @@
301
302
  },
302
303
  "source.filter": "Filter this collection",
303
304
  "source.filterPlaceholder": "Filter by id, title or member",
304
- "source.filterCount": "{shown} of {total}"
305
+ "source.filterCount": "{shown} of {total}",
306
+ "catalog": {
307
+ "placeholder": "What should it do? e.g. data quality, release, review",
308
+ "searching": "Searching…",
309
+ "counts": "{shown} / {total} · {local} local · {remote} remote",
310
+ "empty": "Nothing matches. Try another wording, or check Sources for a subscription.",
311
+ "failed": "The library did not load: the host did not answer.",
312
+ "members": "{count} members",
313
+ "local": "local",
314
+ "remoteFrom": "from {collection}",
315
+ "open": "Open",
316
+ "pull": "Pull",
317
+ "mode": "Layout",
318
+ "modeSource": "By source",
319
+ "modeFlat": "Flat",
320
+ "refresh": "Refresh"
321
+ }
305
322
  }
package/locale/zh.json CHANGED
@@ -64,7 +64,8 @@
64
64
  "metaRosters": "份档案",
65
65
  "metaMembers": "名成员",
66
66
  "metaTeams": "支团队",
67
- "metaOrganization": "组织 {name}"
67
+ "metaOrganization": "组织 {name}",
68
+ "tabsCatalog": "档案库"
68
69
  },
69
70
  "roster": {
70
71
  "members": "{count} 名成员",
@@ -301,5 +302,21 @@
301
302
  },
302
303
  "source.filter": "在这份集合里查找",
303
304
  "source.filterPlaceholder": "按 id、标题或成员名过滤",
304
- "source.filterCount": "{shown} / {total}"
305
+ "source.filterCount": "{shown} / {total}",
306
+ "catalog": {
307
+ "placeholder": "要找什么能力?例:数据质量、发版、代码评审",
308
+ "searching": "正在搜索…",
309
+ "counts": "{shown} / {total} 份 · 本机 {local} · 远端 {remote}",
310
+ "empty": "没有符合条件的档案。换个词,或去「档案源」看看订阅。",
311
+ "failed": "档案库加载失败:宿主没有响应。",
312
+ "members": "{count} 名成员",
313
+ "local": "本机",
314
+ "remoteFrom": "来自 {collection}",
315
+ "open": "打开",
316
+ "pull": "拉取",
317
+ "mode": "显示方式",
318
+ "modeSource": "按来源",
319
+ "modeFlat": "平铺",
320
+ "refresh": "刷新"
321
+ }
305
322
  }
@@ -4,6 +4,16 @@
4
4
  "roster": "api-design",
5
5
  "version": "1.0.0",
6
6
  "title": "API 设计评审组",
7
+ "description": "在写第一行实现之前,把接口的形状、演进与失败语义定下来。",
8
+ "tags": [
9
+ "api",
10
+ "design",
11
+ "review"
12
+ ],
13
+ "license": "MIT",
14
+ "maintainers": [
15
+ "STofone"
16
+ ],
7
17
  "repo": "dsh-agent-roster/dsh-agent-roster",
8
18
  "ref": "main",
9
19
  "lock": null,
@@ -4,6 +4,15 @@
4
4
  "roster": "content-ops",
5
5
  "version": "1.0.0",
6
6
  "title": "内容运营组",
7
+ "description": "一次发布:选题、成稿、分发,三件事各有验收。",
8
+ "tags": [
9
+ "content",
10
+ "publishing"
11
+ ],
12
+ "license": "MIT",
13
+ "maintainers": [
14
+ "STofone"
15
+ ],
7
16
  "repo": "dsh-agent-roster/dsh-agent-roster",
8
17
  "ref": "main",
9
18
  "lock": null,
@@ -4,6 +4,15 @@
4
4
  "roster": "contract-review",
5
5
  "version": "1.0.0",
6
6
  "title": "合同审阅组",
7
+ "description": "找到会让你赔钱、被锁定或无法退出的条款。",
8
+ "tags": [
9
+ "legal",
10
+ "contract"
11
+ ],
12
+ "license": "MIT",
13
+ "maintainers": [
14
+ "STofone"
15
+ ],
7
16
  "repo": "dsh-agent-roster/dsh-agent-roster",
8
17
  "ref": "main",
9
18
  "lock": null,
@@ -4,6 +4,16 @@
4
4
  "roster": "data-quality",
5
5
  "version": "1.0.0",
6
6
  "title": "数据质量组",
7
+ "description": "为一份数据表定“什么算合格”,并把不合格变成可复现的判据。",
8
+ "tags": [
9
+ "data",
10
+ "quality",
11
+ "checks"
12
+ ],
13
+ "license": "MIT",
14
+ "maintainers": [
15
+ "STofone"
16
+ ],
7
17
  "repo": "dsh-agent-roster/dsh-agent-roster",
8
18
  "ref": "main",
9
19
  "lock": null,
@@ -4,6 +4,16 @@
4
4
  "roster": "db-migration",
5
5
  "version": "1.0.0",
6
6
  "title": "数据库迁移组",
7
+ "description": "把一次 schema 变更做到可回滚:先证明能回,再动手。",
8
+ "tags": [
9
+ "database",
10
+ "migration",
11
+ "reliability"
12
+ ],
13
+ "license": "MIT",
14
+ "maintainers": [
15
+ "STofone"
16
+ ],
7
17
  "repo": "dsh-agent-roster/dsh-agent-roster",
8
18
  "ref": "main",
9
19
  "lock": null,
@@ -4,6 +4,15 @@
4
4
  "roster": "docs-writer",
5
5
  "version": "1.0.0",
6
6
  "title": "文档组",
7
+ "description": "让文档里的每条命令都真的能跑,每个读数都带时间。",
8
+ "tags": [
9
+ "docs",
10
+ "writing"
11
+ ],
12
+ "license": "MIT",
13
+ "maintainers": [
14
+ "STofone"
15
+ ],
7
16
  "repo": "dsh-agent-roster/dsh-agent-roster",
8
17
  "ref": "main",
9
18
  "lock": null,
@@ -4,6 +4,15 @@
4
4
  "roster": "finance-recon",
5
5
  "version": "1.0.0",
6
6
  "title": "财务对账组",
7
+ "description": "让两套账的差异落到具体行,而不是“大概对得上”。",
8
+ "tags": [
9
+ "finance",
10
+ "reconciliation"
11
+ ],
12
+ "license": "MIT",
13
+ "maintainers": [
14
+ "STofone"
15
+ ],
7
16
  "repo": "dsh-agent-roster/dsh-agent-roster",
8
17
  "ref": "main",
9
18
  "lock": null,
@@ -4,6 +4,16 @@
4
4
  "roster": "frontend-ui",
5
5
  "version": "1.0.0",
6
6
  "title": "前端实现组",
7
+ "description": "把设计做成能用、能读、能被键盘与读屏器操作的东西。",
8
+ "tags": [
9
+ "frontend",
10
+ "ui",
11
+ "a11y"
12
+ ],
13
+ "license": "MIT",
14
+ "maintainers": [
15
+ "STofone"
16
+ ],
7
17
  "repo": "dsh-agent-roster/dsh-agent-roster",
8
18
  "ref": "main",
9
19
  "lock": null,
@@ -4,6 +4,16 @@
4
4
  "roster": "i18n-l10n",
5
5
  "version": "1.0.0",
6
6
  "title": "本地化组",
7
+ "description": "把一份文案做成两种语言,且两种语言说同一件事。",
8
+ "tags": [
9
+ "i18n",
10
+ "docs",
11
+ "review"
12
+ ],
13
+ "license": "MIT",
14
+ "maintainers": [
15
+ "STofone"
16
+ ],
7
17
  "repo": "dsh-agent-roster/dsh-agent-roster",
8
18
  "ref": "main",
9
19
  "lock": null,
@@ -4,6 +4,16 @@
4
4
  "roster": "incident-response",
5
5
  "version": "1.0.0",
6
6
  "title": "事故响应组",
7
+ "description": "线上出事时的三个人:指挥、定位、记录;先止损再找根因。",
8
+ "tags": [
9
+ "incident",
10
+ "oncall",
11
+ "reliability"
12
+ ],
13
+ "license": "MIT",
14
+ "maintainers": [
15
+ "STofone"
16
+ ],
7
17
  "repo": "dsh-agent-roster/dsh-agent-roster",
8
18
  "ref": "main",
9
19
  "lock": null,
@@ -4,6 +4,15 @@
4
4
  "roster": "literature-review",
5
5
  "version": "1.0.0",
6
6
  "title": "文献综述组",
7
+ "description": "把一堆论文读成一张能追到出处的证据表。",
8
+ "tags": [
9
+ "research",
10
+ "literature"
11
+ ],
12
+ "license": "MIT",
13
+ "maintainers": [
14
+ "STofone"
15
+ ],
7
16
  "repo": "dsh-agent-roster/dsh-agent-roster",
8
17
  "ref": "main",
9
18
  "lock": null,
@@ -4,6 +4,15 @@
4
4
  "roster": "market-research",
5
5
  "version": "1.0.0",
6
6
  "title": "市场研究组",
7
+ "description": "把“听说”变成“有出处的数”,并说清它什么时候会失效。",
8
+ "tags": [
9
+ "research",
10
+ "market"
11
+ ],
12
+ "license": "MIT",
13
+ "maintainers": [
14
+ "STofone"
15
+ ],
7
16
  "repo": "dsh-agent-roster/dsh-agent-roster",
8
17
  "ref": "main",
9
18
  "lock": null,
@@ -4,6 +4,15 @@
4
4
  "roster": "ml-eval",
5
5
  "version": "1.0.0",
6
6
  "title": "模型评测组",
7
+ "description": "跑分之前先问:这套题能不能区分好坏。",
8
+ "tags": [
9
+ "ml",
10
+ "evaluation"
11
+ ],
12
+ "license": "MIT",
13
+ "maintainers": [
14
+ "STofone"
15
+ ],
7
16
  "repo": "dsh-agent-roster/dsh-agent-roster",
8
17
  "ref": "main",
9
18
  "lock": null,
@@ -4,6 +4,15 @@
4
4
  "roster": "performance",
5
5
  "version": "1.0.0",
6
6
  "title": "性能优化组",
7
+ "description": "先量再改:没有基线就没有优化。",
8
+ "tags": [
9
+ "performance",
10
+ "profiling"
11
+ ],
12
+ "license": "MIT",
13
+ "maintainers": [
14
+ "STofone"
15
+ ],
7
16
  "repo": "dsh-agent-roster/dsh-agent-roster",
8
17
  "ref": "main",
9
18
  "lock": null,
@@ -4,6 +4,15 @@
4
4
  "roster": "product-discovery",
5
5
  "version": "1.0.0",
6
6
  "title": "产品发现组",
7
+ "description": "在写需求之前,先证明这个需求真的存在。",
8
+ "tags": [
9
+ "product",
10
+ "discovery"
11
+ ],
12
+ "license": "MIT",
13
+ "maintainers": [
14
+ "STofone"
15
+ ],
7
16
  "repo": "dsh-agent-roster/dsh-agent-roster",
8
17
  "ref": "main",
9
18
  "lock": null,