@stofone/dsh-agent-roster 0.13.1 → 1.0.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.
@@ -1,331 +0,0 @@
1
- /**
2
- * 组织档案(organization):一座档案库的"目录页"。
3
- *
4
- * 为什么需要它:档案库只能回答"这一份档案写了什么",回答不了"这些档案是什么关系、
5
- * 哪份是生产、哪份是归档、谁维护"。当一座库里放二十份来自不同来源的档案时,
6
- * 缺了这层就只能靠目录名猜。
7
- *
8
- * 边界(与 capabilities.yaml 同一条纪律):
9
- * - 组织只做**声明与核对**:声明库里有哪几份档案、各自什么状态;
10
- * 它不创建团队、不改档案、不进 `agent_teams_add_member`。
11
- * - 它**能被机械核对**:声明了但目录不在 = error,目录在却没声明 = warn,
12
- * 已归档却仍在用 = info。核对不了的字段一律不收(未知键即错误)。
13
- * - 位置即身份:`organization` 必须等于所在目录名,和 `roster` 的规则一致。
14
- *
15
- * @module dsh-agent-roster/organization
16
- */
17
-
18
- /** 组织文件的 `kind`。与 roster / registry / capabilities 并列,互不冒充。 */
19
- export const ORGANIZATION_KIND = "dsh-agent-roster-organization/v1";
20
-
21
- /** 组织文件允许的文件名,按优先级排列。 */
22
- export const ORGANIZATION_FILE_NAMES = ["organization.yaml", "organization.yml", "organization.json"];
23
-
24
- /** 档案在组织里的状态。刻意只有两档:归档不等于删除,删除就是没有这个目录。 */
25
- export const ORGANIZATION_STATUSES = ["active", "archived"];
26
-
27
- /** 长度上限:与档案的 LIMITS 同一套口径,超了就报错而不是截断。 */
28
- export const LIMITS = {
29
- rosters: 64,
30
- titleChars: 120,
31
- descriptionChars: 500,
32
- licenseChars: 120,
33
- homepageChars: 200,
34
- maintainers: 16,
35
- maintainerChars: 120,
36
- noteChars: 300,
37
- };
38
-
39
- /** 组织 id 与档案 id 同形:小写、连字符、首字符必须是字母或数字。 */
40
- export const ID_PATTERN = /^[a-z0-9][a-z0-9-]{1,63}$/;
41
-
42
- const ROOT_FIELDS = new Set([
43
- "kind",
44
- "organization",
45
- "title",
46
- "description",
47
- "license",
48
- "homepage",
49
- "maintainers",
50
- "rosters",
51
- ]);
52
-
53
- const ROSTER_FIELDS = new Set(["id", "title", "status", "origin", "note"]);
54
-
55
- /** 顶层字段清单(供 schema 对齐测试反向钉住,避免两处各写一份)。 */
56
- export const ORGANIZATION_FIELDS = Object.freeze([...ROOT_FIELDS]);
57
-
58
- /** 每条登记项的字段清单。 */
59
- export const ORGANIZATION_ROSTER_FIELDS = Object.freeze([...ROSTER_FIELDS]);
60
-
61
- /**
62
- * 一条诊断。形状与 `lib/validate.js` 保持一致,好让 render / panel / CLI 用同一套渲染。
63
- *
64
- * @param {"error"|"warn"|"info"} severity
65
- * @param {string} code
66
- * @param {string} file
67
- * @param {string} reason
68
- * @returns {{severity: string, code: string, path: string, file: string, reason: string}}
69
- */
70
- function diagnose(severity, code, file, reason) {
71
- return { severity, code, path: "", file, reason };
72
- }
73
-
74
- /**
75
- * 校验一份组织档案。
76
- *
77
- * 与 roster 的校验同一条纪律:**能核对的才收**。`homepage` 只校验是不是 http(s),
78
- * 不联网;`maintainers` 只校验形态,不校验这个人存在不存在。
79
- *
80
- * @param {unknown} value 解析后的 JSON/YAML 值
81
- * @param {{file?: string, expectedId?: string|null}} [options]
82
- * @returns {{ok: boolean, organization: object|null, diagnostics: object[]}}
83
- */
84
- export function validateOrganization(value, options = {}) {
85
- const file = options.file ?? ORGANIZATION_FILE_NAMES[0];
86
- const expectedId = options.expectedId ?? null;
87
- /** @type {object[]} */
88
- const diagnostics = [];
89
-
90
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
91
- diagnostics.push(diagnose("error", "organization-not-object", file, "组织文件必须是一个对象(键值对)。"));
92
- return { ok: false, organization: null, diagnostics };
93
- }
94
-
95
- for (const key of Object.keys(value)) {
96
- if (!ROOT_FIELDS.has(key)) {
97
- diagnostics.push(
98
- diagnose(
99
- "error",
100
- "unknown-field",
101
- file,
102
- `未知字段 "${key}"。组织文件只收 ${[...ROOT_FIELDS].join(" / ")}:` +
103
- "核对不了的字段不收——写了也不会被任何人验证,只会被误当成事实。",
104
- ),
105
- );
106
- }
107
- }
108
-
109
- if (value.kind !== ORGANIZATION_KIND) {
110
- diagnostics.push(
111
- diagnose("error", "organization-kind", file, `kind 必须是 "${ORGANIZATION_KIND}",实际是 ${JSON.stringify(value.kind)}。`),
112
- );
113
- }
114
-
115
- const id = value.organization;
116
- if (typeof id !== "string" || !ID_PATTERN.test(id)) {
117
- diagnostics.push(
118
- diagnose("error", "organization-id", file, "organization 必须是 2–64 位小写字母/数字/连字符,且以字母或数字开头。"),
119
- );
120
- } else if (expectedId !== null && id !== expectedId) {
121
- diagnostics.push(
122
- diagnose(
123
- "error",
124
- "organization-id-mismatch",
125
- file,
126
- `organization 是 "${id}",但所在目录叫 "${expectedId}"。位置即身份:组织 id 必须等于目录名。`,
127
- ),
128
- );
129
- }
130
-
131
- const title = optionalString(value, "title", LIMITS.titleChars, diagnostics, file, "organization-title");
132
- optionalString(value, "description", LIMITS.descriptionChars, diagnostics, file, "organization-description");
133
- optionalString(value, "license", LIMITS.licenseChars, diagnostics, file, "organization-license");
134
-
135
- if (value.homepage !== undefined) {
136
- const homepage = optionalString(value, "homepage", LIMITS.homepageChars, diagnostics, file, "organization-homepage");
137
- if (typeof homepage === "string" && !/^https?:\/\/\S+$/i.test(homepage)) {
138
- diagnostics.push(
139
- diagnose("error", "organization-homepage", file, "homepage 只收 http(s) 地址:别的写法既打不开也核对不了。"),
140
- );
141
- }
142
- }
143
-
144
- if (value.maintainers !== undefined) {
145
- if (!Array.isArray(value.maintainers)) {
146
- diagnostics.push(diagnose("error", "organization-maintainers", file, "maintainers 必须是字符串数组。"));
147
- } else if (value.maintainers.length > LIMITS.maintainers) {
148
- diagnostics.push(
149
- diagnose("error", "organization-maintainers", file, `maintainers 最多 ${LIMITS.maintainers} 项。`),
150
- );
151
- } else {
152
- for (const who of value.maintainers) {
153
- if (typeof who !== "string" || who.length === 0 || who.length > LIMITS.maintainerChars) {
154
- diagnostics.push(
155
- diagnose("error", "organization-maintainer", file, `维护者名字必须是 1–${LIMITS.maintainerChars} 个字符的字符串。`),
156
- );
157
- break;
158
- }
159
- }
160
- }
161
- }
162
-
163
- const rosters = [];
164
- const rawRosters = value.rosters;
165
- if (!Array.isArray(rawRosters) || rawRosters.length === 0) {
166
- diagnostics.push(diagnose("error", "organization-rosters", file, "rosters 必须是非空数组:组织的意义就是登记这些档案。"));
167
- } else if (rawRosters.length > LIMITS.rosters) {
168
- diagnostics.push(diagnose("error", "organization-rosters", file, `rosters 最多 ${LIMITS.rosters} 项。`));
169
- } else {
170
- const seen = new Set();
171
- rawRosters.forEach((item, index) => {
172
- const where = `rosters[${index}]`;
173
- if (item === null || typeof item !== "object" || Array.isArray(item)) {
174
- diagnostics.push(diagnose("error", "organization-roster-not-object", file, `${where} 必须是对象。`));
175
- return;
176
- }
177
- for (const key of Object.keys(item)) {
178
- if (!ROSTER_FIELDS.has(key)) {
179
- diagnostics.push(
180
- diagnose("error", "unknown-field", file, `${where} 的未知字段 "${key}"(只收 ${[...ROSTER_FIELDS].join(" / ")})。`),
181
- );
182
- }
183
- }
184
- if (typeof item.id !== "string" || !ID_PATTERN.test(item.id)) {
185
- diagnostics.push(diagnose("error", "organization-roster-id", file, `${where}.id 必须是合法档案 id。`));
186
- return;
187
- }
188
- if (seen.has(item.id)) {
189
- diagnostics.push(
190
- diagnose("error", "organization-duplicate-roster", file, `档案 "${item.id}" 在组织里登记了两次。`),
191
- );
192
- return;
193
- }
194
- seen.add(item.id);
195
- const status = item.status ?? "active";
196
- if (!ORGANIZATION_STATUSES.includes(status)) {
197
- diagnostics.push(
198
- diagnose("error", "organization-roster-status", file, `${where}.status 只能是 ${ORGANIZATION_STATUSES.join(" / ")}。`),
199
- );
200
- }
201
- const entryTitle = optionalString(item, "title", LIMITS.titleChars, diagnostics, file, "organization-roster-title", where);
202
- const origin = optionalString(item, "origin", LIMITS.titleChars, diagnostics, file, "organization-roster-origin", where);
203
- const note = optionalString(item, "note", LIMITS.noteChars, diagnostics, file, "organization-roster-note", where);
204
- rosters.push({
205
- id: item.id,
206
- title: entryTitle ?? "",
207
- status,
208
- origin: origin ?? "",
209
- note: note ?? "",
210
- });
211
- });
212
- }
213
-
214
- if (diagnostics.some((item) => item.severity === "error")) {
215
- return { ok: false, organization: null, diagnostics };
216
- }
217
-
218
- return {
219
- ok: true,
220
- organization: {
221
- kind: ORGANIZATION_KIND,
222
- id,
223
- title: title ?? "",
224
- description: value.description ?? "",
225
- license: value.license ?? "",
226
- homepage: value.homepage ?? "",
227
- maintainers: Array.isArray(value.maintainers) ? [...value.maintainers] : [],
228
- rosters,
229
- },
230
- diagnostics,
231
- };
232
- }
233
-
234
- /**
235
- * 读一个可选字符串字段并钉住上限。
236
- *
237
- * @param {object} source
238
- * @param {string} key
239
- * @param {number} max
240
- * @param {object[]} diagnostics
241
- * @param {string} file
242
- * @param {string} code
243
- * @param {string} [where]
244
- * @returns {string|undefined}
245
- */
246
- function optionalString(source, key, max, diagnostics, file, code, where = "") {
247
- const value = source[key];
248
- if (value === undefined) return undefined;
249
- const label = where === "" ? key : `${where}.${key}`;
250
- if (typeof value !== "string" || value.length > max) {
251
- diagnostics.push(diagnose("error", code, file, `${label} 必须是长度不超过 ${max} 的字符串。`));
252
- return undefined;
253
- }
254
- return value;
255
- }
256
-
257
- /**
258
- * 组织声明与磁盘实际情况的交叉核对。
259
- *
260
- * 三件事分开说,因为它们的严重度不同:
261
- * - **缺**(声明了却找不到目录):error。组织页在撒谎,比没有组织页更糟。
262
- * - **多**(目录在却没登记):warn。不是错误,但意味着有人绕过组织加了档案。
263
- * - **归档仍在**:info。归档是声明,不是删除;提醒一句即可。
264
- *
265
- * @param {object|null} organization
266
- * @param {string[]} rosterIds 磁盘上真正通过校验的档案 id
267
- * @returns {{declared: number, present: number, missing: string[], undeclared: string[], archived: string[], status: string}}
268
- */
269
- export function checkOrganization(organization, rosterIds) {
270
- const onDisk = new Set(rosterIds);
271
- const declared = new Set(organization === null ? [] : organization.rosters.map((item) => item.id));
272
- const missing = [...declared].filter((id) => !onDisk.has(id)).sort();
273
- const undeclared = [...onDisk].filter((id) => !declared.has(id)).sort();
274
- const archived = organization === null
275
- ? []
276
- : organization.rosters.filter((item) => item.status === "archived" && onDisk.has(item.id)).map((item) => item.id).sort();
277
- return {
278
- declared: declared.size,
279
- present: onDisk.size,
280
- missing,
281
- undeclared,
282
- archived,
283
- status: organization === null ? "absent" : missing.length > 0 ? "incomplete" : "ok",
284
- };
285
- }
286
-
287
- /**
288
- * 把交叉核对结果折成诊断。
289
- *
290
- * @param {object|null} organization
291
- * @param {object} check {@link checkOrganization} 的返回值
292
- * @param {string} file 组织文件相对路径
293
- * @returns {object[]}
294
- */
295
- export function organizationDiagnostics(organization, check, file) {
296
- const diagnostics = [];
297
- if (organization === null) return diagnostics;
298
- if (check.missing.length > 0) {
299
- diagnostics.push(
300
- diagnose(
301
- "error",
302
- "organization-roster-missing",
303
- file,
304
- `组织声明了 ${check.missing.length} 份档案但库里找不到:${check.missing.join(", ")}。` +
305
- "要么把目录加回来,要么把它们从 organization 里删掉——组织页不能比磁盘更乐观。",
306
- ),
307
- );
308
- }
309
- if (check.undeclared.length > 0) {
310
- diagnostics.push(
311
- diagnose(
312
- "warn",
313
- "organization-roster-undeclared",
314
- file,
315
- `库里有 ${check.undeclared.length} 份档案没在组织里登记:${check.undeclared.join(", ")}。` +
316
- "它们仍会被加载,但组织页看不到它们——补一行登记,或确认它们不该在这里。",
317
- ),
318
- );
319
- }
320
- if (check.archived.length > 0) {
321
- diagnostics.push(
322
- diagnose(
323
- "info",
324
- "organization-roster-archived",
325
- file,
326
- `已归档但仍在库里的档案:${check.archived.join(", ")}。归档只是声明,不影响加载。`,
327
- ),
328
- );
329
- }
330
- return diagnostics;
331
- }
@@ -1,118 +0,0 @@
1
- # 组织档案:一座档案库的目录页。
2
- #
3
- # 它只做声明与核对:声明库里有哪几份档案、各自什么状态;
4
- # 不创建团队、不改档案、不进 agent_teams_add_member 参数。
5
- # 声明了却找不到目录 = error;目录在却没登记 = warn;已归档仍在 = info。
6
- kind: dsh-agent-roster-organization/v1
7
- organization: agent-roster-collective
8
- title: 团队档案合集
9
- description: 二十份可直接建队的档案:从代码审查到事故响应,覆盖常见的协作分工。
10
- license: MIT
11
- maintainers:
12
- - STofone
13
- rosters:
14
- - id: api-design
15
- title: API 设计评审组
16
- status: active
17
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
18
- note: 3 名成员:接口负责人、结构与演进评审、失败语义评审
19
- - id: incident-response
20
- title: 事故响应组
21
- status: active
22
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
23
- note: 3 名成员:事故指挥、定位、记录
24
- - id: data-quality
25
- title: 数据质量组
26
- status: active
27
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
28
- note: 3 名成员:数据负责人、画像、判据编写
29
- - id: db-migration
30
- title: 数据库迁移组
31
- status: active
32
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
33
- note: 3 名成员:迁移负责人、迁移实现、数据验证
34
- - id: i18n-l10n
35
- title: 本地化组
36
- status: active
37
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
38
- note: 3 名成员:本地化负责人、翻译、双语审校
39
- - id: security-audit
40
- title: 安全审计组
41
- status: active
42
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
43
- note: 3 名成员:审计负责人、代码审计、威胁建模
44
- - id: performance
45
- title: 性能优化组
46
- status: active
47
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
48
- note: 3 名成员:性能负责人、热点定位、压测与回归
49
- - id: frontend-ui
50
- title: 前端实现组
51
- status: active
52
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
53
- note: 3 名成员:前端负责人、实现、可访问性审查
54
- - id: docs-writer
55
- title: 文档组
56
- status: active
57
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
58
- note: 3 名成员:文档负责人、正文撰写、示例验证
59
- - id: release-manager
60
- title: 发布管理组
61
- status: active
62
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
63
- note: 3 名成员:发布负责人、变更说明、回滚预案
64
- - id: support-triage
65
- title: 客服分诊组
66
- status: active
67
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
68
- note: 3 名成员:分诊负责人、来信分诊、回复撰写
69
- - id: recruiting
70
- title: 招聘筛选组
71
- status: active
72
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
73
- note: 3 名成员:招聘负责人、简历筛选、面试安排
74
- - id: market-research
75
- title: 市场研究组
76
- status: active
77
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
78
- note: 3 名成员:研究负责人、资料检索、综合研判
79
- - id: finance-recon
80
- title: 财务对账组
81
- status: active
82
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
83
- note: 3 名成员:对账负责人、逐笔匹配、差异归因
84
- - id: contract-review
85
- title: 合同审阅组
86
- status: active
87
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
88
- note: 3 名成员:审阅负责人、条款识别、风险提示
89
- - id: content-ops
90
- title: 内容运营组
91
- status: active
92
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
93
- note: 3 名成员:内容负责人、成稿编辑、分发与回读
94
- - id: qa-regression
95
- title: 回归测试组
96
- status: active
97
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
98
- note: 3 名成员:测试负责人、用例编写、不稳定排查
99
- - id: ml-eval
100
- title: 模型评测组
101
- status: active
102
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
103
- note: 3 名成员:评测负责人、数据集构建、指标与统计
104
- - id: sre-oncall
105
- title: SRE 值守组
106
- status: active
107
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
108
- note: 3 名成员:值守负责人、手册编写、容量与成本
109
- - id: literature-review
110
- title: 文献综述组
111
- status: active
112
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
113
- note: 3 名成员:综述负责人、信息提取、质量评价
114
- - id: product-discovery
115
- title: 产品发现组
116
- status: active
117
- origin: 本项目自撰(结构参考公开的多 Agent 团队实践)
118
- note: 3 名成员:发现负责人、用户访谈、反方
@@ -1,50 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://github.com/dsh-agent-roster/dsh-agent-roster/blob/main/schema/organization.v1.schema.json",
4
- "title": "DSH agent roster organization document",
5
- "description": "一座档案库的目录页:声明库里有哪几份档案、各自什么状态。校验的权威实现是 lib/organization.js;本文件供编辑器与外部工具提示,两者由 test/organization.test.js 对齐。",
6
- "type": "object",
7
- "additionalProperties": false,
8
- "required": ["kind", "organization", "rosters"],
9
- "properties": {
10
- "$schema": {
11
- "type": "string",
12
- "description": "编辑器用的 schema 地址,插件不读它。"
13
- },
14
- "kind": { "const": "dsh-agent-roster-organization/v1" },
15
- "organization": {
16
- "$ref": "#/$defs/id",
17
- "description": "组织 id,同时是档案库根目录名。位置即身份。"
18
- },
19
- "title": { "type": "string", "maxLength": 120 },
20
- "description": { "type": "string", "maxLength": 500 },
21
- "license": { "type": "string", "maxLength": 120 },
22
- "homepage": { "type": "string", "maxLength": 200, "pattern": "^https?://\\S+$" },
23
- "maintainers": {
24
- "type": "array",
25
- "maxItems": 16,
26
- "items": { "type": "string", "minLength": 1, "maxLength": 120 }
27
- },
28
- "rosters": {
29
- "type": "array",
30
- "minItems": 1,
31
- "maxItems": 64,
32
- "description": "登记在案的档案。声明了却找不到目录会报错;目录在却没登记会告警。",
33
- "items": {
34
- "type": "object",
35
- "additionalProperties": false,
36
- "required": ["id"],
37
- "properties": {
38
- "id": { "$ref": "#/$defs/id" },
39
- "title": { "type": "string", "maxLength": 120 },
40
- "status": { "enum": ["active", "archived"] },
41
- "origin": { "type": "string", "maxLength": 120 },
42
- "note": { "type": "string", "maxLength": 300 }
43
- }
44
- }
45
- }
46
- },
47
- "$defs": {
48
- "id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{1,63}$" }
49
- }
50
- }