@stofone/dsh-agent-roster 0.10.1 → 0.10.2

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/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.10.2] - 2026-11-03
8
+
9
+ ### Fixed
10
+
11
+ - **Every v2 collection was unfetchable (HTTP 404).** The index assumed the v1 flat layout
12
+ (`<path>/<id>/roster.yaml`), which stopped existing the moment rosters moved to `rosters/<id>/`.
13
+ A collection may now pin one relative path per roster in an optional `files` map;
14
+ `registry sync` fills it in and points v2 rosters at the self-contained `dist/<id>-<ver>.json`
15
+ (an assembly alone cannot be pulled — its members are role references).
16
+ - Pulling a compiled artifact flattens it into an inline v1 document (personas are the assembled
17
+ prompt, which is what would actually be sent); run `migrate` afterwards if you want the role split
18
+ on your machine. A bare `roster@2` is reported as `collection-roster-needs-roles` instead of being
19
+ fetched as if it were one file.
20
+ - `flattenCompiledToRoster` no longer emits empty `title`/`description` strings — v1 rejects those,
21
+ so a flattened artifact used to fail its own validation.
22
+
7
23
  ## [0.10.1] - 2026-11-03
8
24
 
9
25
  ### Docs
@@ -456,7 +472,10 @@
456
472
  - 工具返回值含 `undefined` 键,运行时按 `value is not lossless JSON` 整次拒绝;缺失值统一写 `null`,
457
473
  且所有返回值先过 `lib/json.js#lossless`。
458
474
 
459
- [Unreleased]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.9.2...main
475
+ [Unreleased]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.10.2...main
476
+ [0.10.2]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.10.1...v0.10.2
477
+ [0.10.1]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.10.0...v0.10.1
478
+ [0.10.0]: https://github.com/dsh-agent-roster/dsh-agent-roster/releases/tag/v0.10.0
460
479
  [0.9.2]: https://github.com/dsh-agent-roster/dsh-agent-roster/releases/tag/v0.9.2
461
480
  [0.9.1]: https://github.com/dsh-agent-roster/dsh-agent-roster/releases/tag/v0.9.1
462
481
  [0.9.0]: https://github.com/dsh-agent-roster/dsh-agent-roster/releases/tag/v0.9.0
package/README.md CHANGED
@@ -383,6 +383,10 @@ repository's `index/registry.json`, and anyone who subscribes can see them in th
383
383
  - **Pulling**: writes are idempotent and report one of `created`, `updated` (explicit overwrite), `unchanged`
384
384
  (same content, nothing written) or `conflict` (the local copy differs and is **not** overwritten by default).
385
385
  Conflicts are yours to resolve: merge in the editor, or pull again with `overwrite` in the request body.
386
+ - **Where each roster lives**: a collection's `rosters[]` is just the list of ids; the optional
387
+ `files` map says where each one is (`{"core-review": "dist/core-review-1.0.0.json"}`). It is optional —
388
+ without it the fetcher assumes the v1 layout `<id>/roster.yaml` — but a v2 library needs it, because an
389
+ assembly only references roles and cannot be pulled on its own. `registry sync` writes this map for you.
386
390
  - **The index has a schema**: [`schema/registry.v1.schema.json`](./schema/registry.v1.schema.json) mirrors the
387
391
  runtime validator, so an editor flags a typo before CI does.
388
392
  - **Publishing your own**: add an entry to `collections` in `index/registry.json` (`id`, `repo: github:owner/repo`,
package/README.zh.md CHANGED
@@ -364,6 +364,9 @@ node bin/cli.mjs compile --roster examples-orchestration/rosters/core-review \
364
364
  冲突要你自己决定:要么在编辑器里合并,要么再点一次「拉取到本机」并在请求里带 `overwrite`。
365
365
  - **索引本身也有 schema**:[`schema/registry.v1.schema.json`](./schema/registry.v1.schema.json) 与运行时校验器对齐,
366
366
  拼错字段在编辑器里就会提示,不必等 CI。
367
+ - **每份档案住在哪**:collection 的 `rosters[]` 只是 id 名单,实际位置由可选的 `files` 映射给出
368
+ (如 `{"core-review": "dist/core-review-1.0.0.json"}`)。不写就按 v1 布局 `<id>/roster.yaml` 猜——v2 的库必须写,
369
+ 因为组装本身只引用角色,单独拉一个文件拉不到角色目录。`registry sync` 会替你填。
367
370
  - **登记自己的档案**:在 `index/registry.json` 的 `collections` 里加一条(`id`、`repo: github:owner/repo`、
368
371
  `ref`、`path`、`rosters[]`),提交 PR 即可;`.github/workflows/registry.yml` 会校验索引与仓库真实文件是否对得上。
369
372
 
package/bin/cli.mjs CHANGED
@@ -26,6 +26,7 @@ import { compileRosterAt } from "../lib/compile2.js";
26
26
  import { migrateLibrary } from "../lib/migrate.js";
27
27
  import { compilePlan } from "../lib/plan.js";
28
28
  import { mergeRunIntoPlan, validateRun } from "../lib/run.js";
29
+ import { DIST_DIR } from "../lib/migrate.js";
29
30
 
30
31
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
31
32
 
@@ -442,6 +443,17 @@ async function runRegistry(options) {
442
443
  entry.rosters = ids;
443
444
  changed = true;
444
445
  }
446
+ const files = {};
447
+ for (const roster of scanned.rosters.slice().sort((a, b) => a.id.localeCompare(b.id))) {
448
+ files[roster.id] =
449
+ roster.format === "v2"
450
+ ? `${DIST_DIR}/${roster.id}-${roster.version}.json`
451
+ : `${roster.id}/${roster.file ?? "roster.yaml"}`;
452
+ }
453
+ if (JSON.stringify(files) !== JSON.stringify(entry.files ?? {})) {
454
+ entry.files = files;
455
+ changed = true;
456
+ }
445
457
  }
446
458
  if (options.write) {
447
459
  writeFileSync(file, `${JSON.stringify(document, null, 2)}\n`);
package/docs/roster-v2.md CHANGED
@@ -212,9 +212,14 @@ gates:
212
212
 
213
213
  三者正交:`requires` 说"必须有",`capabilities.yaml` 说"这份组装必须有",`organization.yaml` 说"这座库有哪些"。
214
214
 
215
+ 远端的 registry 索引也是同理:登记的 `rosters[]` 只是 id 名单,实际位置由 `files` 映射给出。
216
+ v1 的库可以省略(回退成 `<id>/roster.yaml`);v2 的库必须写,**指向 `dist/` 的编译产物**——
217
+ 组装文件本身只引用角色,单独拉一个 `roster.yaml` 拉不到角色目录,那不是"没实现",是"拉下来也不完整"。
218
+ 拉到编译产物后,客户端把它摊成一份内联 v1 文档(人设就是拼装结果),要角色分层就在本地跑 `migrate`。
219
+
215
220
  ## 10. 未决与下一步
216
221
 
217
222
  1. **施工图 ↔ 团队真实卡 id 的映射**:`run@1` 已经能读、能合并(`compile --run` 会把上一轮失败写进重试卡),但「施工图内部 id ↔ 团队真实 task id」的映射表还没落成文件——目前靠执行者按 id 命名建卡。
218
- 2. `dist` 的发布与 `registry` 只登记产物(现在 registry 登记的是档案路径)。
223
+ 2. `dist` 的发布流程(索引已能逐份指向 `dist/<id>-<ver>.json`,但"从 dist 反推角色版本、做跨库一致性检查"还没做)。
219
224
  3. `profile/agent-preset` 目标:把队长与成员的 preset 片段生成出来,仍然只产参不写入。
220
225
  4. 编辑器支持:三个 schema 已就位,LSP 级校验待做。
@@ -2,7 +2,7 @@
2
2
  "$schema": "../schema/registry.v1.schema.json",
3
3
  "kind": "dsh-agent-roster-registry/v1",
4
4
  "version": 1,
5
- "generatedAt": "2026-11-02T17:00:43.713Z",
5
+ "generatedAt": "2026-11-03T05:50:01.540Z",
6
6
  "generator": "dsh-agent-roster/dsh-agent-roster (hand-maintained; verified by .github/workflows/registry.yml)",
7
7
  "source": "https://github.com/dsh-agent-roster/dsh-agent-roster",
8
8
  "collections": [
@@ -26,7 +26,11 @@
26
26
  "maintainers": [
27
27
  "STofone"
28
28
  ],
29
- "homepage": "https://github.com/dsh-agent-roster/dsh-agent-roster#examples"
29
+ "homepage": "https://github.com/dsh-agent-roster/dsh-agent-roster#examples",
30
+ "files": {
31
+ "core-review": "dist/core-review-1.0.0.json",
32
+ "research": "dist/research-0.1.0.json"
33
+ }
30
34
  },
31
35
  {
32
36
  "id": "agent-roster-collective",
@@ -70,7 +74,30 @@
70
74
  "maintainers": [
71
75
  "STofone"
72
76
  ],
73
- "homepage": "https://github.com/dsh-agent-roster/dsh-agent-roster/tree/main/orgs/agent-roster-collective"
77
+ "homepage": "https://github.com/dsh-agent-roster/dsh-agent-roster/tree/main/orgs/agent-roster-collective",
78
+ "files": {
79
+ "api-design": "dist/api-design-1.0.0.json",
80
+ "content-ops": "dist/content-ops-1.0.0.json",
81
+ "contract-review": "dist/contract-review-1.0.0.json",
82
+ "data-quality": "dist/data-quality-1.0.0.json",
83
+ "db-migration": "dist/db-migration-1.0.0.json",
84
+ "docs-writer": "dist/docs-writer-1.0.0.json",
85
+ "finance-recon": "dist/finance-recon-1.0.0.json",
86
+ "frontend-ui": "dist/frontend-ui-1.0.0.json",
87
+ "i18n-l10n": "dist/i18n-l10n-1.0.0.json",
88
+ "incident-response": "dist/incident-response-1.0.0.json",
89
+ "literature-review": "dist/literature-review-1.0.0.json",
90
+ "market-research": "dist/market-research-1.0.0.json",
91
+ "ml-eval": "dist/ml-eval-1.0.0.json",
92
+ "performance": "dist/performance-1.0.0.json",
93
+ "product-discovery": "dist/product-discovery-1.0.0.json",
94
+ "qa-regression": "dist/qa-regression-1.0.0.json",
95
+ "recruiting": "dist/recruiting-1.0.0.json",
96
+ "release-manager": "dist/release-manager-1.0.0.json",
97
+ "security-audit": "dist/security-audit-1.0.0.json",
98
+ "sre-oncall": "dist/sre-oncall-1.0.0.json",
99
+ "support-triage": "dist/support-triage-1.0.0.json"
100
+ }
74
101
  },
75
102
  {
76
103
  "id": "agent-teams",
package/lib/compile2.js CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  ROLE_PARTS_DIR,
22
22
  ROSTER2_FILE_NAMES,
23
23
  } from "./format-v2.js";
24
+ import { ROSTER_KIND } from "./format.js";
24
25
  import { diagnostic, parseDocumentText } from "./parse.js";
25
26
  import { loadRoleAt, safeResolve } from "./role.js";
26
27
  import { validateRoster2 } from "./roster2.js";
@@ -520,3 +521,110 @@ export function rosterViewOf(compiled, roster2, options = {}) {
520
521
  orchestration: compiled.orchestration ?? null,
521
522
  };
522
523
  }
524
+
525
+ /**
526
+ * 校验一份编译产物。
527
+ *
528
+ * 这是「消费者只看产物」的入口:远端订阅拿到的就是它,所以这里只认产物该有的形状,
529
+ * 不回头读角色文件——拉一份档案不该变成拉一个仓库。
530
+ *
531
+ * @param {unknown} value
532
+ * @param {{ file?: string | null }} [options]
533
+ * @returns {{ ok: boolean, compiled: object | null, diagnostics: object[] }}
534
+ */
535
+ export function validateCompiled(value, { file = null } = {}) {
536
+ const diagnostics = [];
537
+ const at = (relative) => (file === null ? relative : `${file} · ${relative}`);
538
+ const fail = () => ({ ok: false, compiled: null, diagnostics });
539
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
540
+ diagnostics.push({ severity: "error", code: "compiled-not-object", reason: "编译产物必须是一个对象", file });
541
+ return fail();
542
+ }
543
+ if (value.kind !== COMPILED_KIND) {
544
+ diagnostics.push({
545
+ severity: "error",
546
+ code: "compiled-kind",
547
+ reason: `判别键必须是 ${COMPILED_KIND},实际是 ${JSON.stringify(value.kind ?? null)}`,
548
+ file,
549
+ });
550
+ }
551
+ const source = value.source;
552
+ if (source === null || typeof source !== "object") {
553
+ diagnostics.push({ severity: "error", code: "compiled-source", reason: "缺少 source", file });
554
+ } else {
555
+ for (const key of ["roster", "version"]) {
556
+ if (typeof source[key] !== "string" || source[key] === "") {
557
+ diagnostics.push({
558
+ severity: "error",
559
+ code: "compiled-source",
560
+ reason: `source.${key} 必须是非空字符串`,
561
+ file: at("source"),
562
+ });
563
+ }
564
+ }
565
+ }
566
+ const members = value.members;
567
+ if (!Array.isArray(members) || members.length === 0) {
568
+ diagnostics.push({ severity: "error", code: "compiled-members", reason: "members 必须是非空数组", file });
569
+ } else {
570
+ for (const [index, member] of members.entries()) {
571
+ const where = at(`members[${String(index)}]`);
572
+ if (member === null || typeof member !== "object") {
573
+ diagnostics.push({ severity: "error", code: "compiled-member", reason: "成员必须是对象", file: where });
574
+ continue;
575
+ }
576
+ if (typeof member.name !== "string" || member.name === "") {
577
+ diagnostics.push({ severity: "error", code: "compiled-member", reason: "成员缺少 name", file: where });
578
+ }
579
+ if (typeof member.executionPrompt !== "string" || member.executionPrompt.trim() === "") {
580
+ diagnostics.push({ severity: "error", code: "compiled-member", reason: "成员缺少 executionPrompt", file: where });
581
+ }
582
+ }
583
+ }
584
+ const ok = diagnostics.every((item) => item.severity !== "error");
585
+ return { ok, compiled: ok ? value : null, diagnostics };
586
+ }
587
+
588
+ /**
589
+ * 把编译产物摊回一份 v1 档案文档。
590
+ *
591
+ * 远端订阅只拿得到产物:产物里的人设已经是最终拼装结果,所以导入本地时写成 v1 内联档案,
592
+ * 不假装自己带着角色目录。想要角色分层,导入后跑一次 `migrate` 重写即可。
593
+ *
594
+ * @param {object} compiled
595
+ * @returns {object}
596
+ */
597
+ export function flattenCompiledToRoster(compiled) {
598
+ const source = compiled.source ?? {};
599
+ const members = (compiled.members ?? []).map((member) => {
600
+ const flat = {
601
+ name: member.name,
602
+ role: member.role ?? "",
603
+ systemPrompt: member.executionPrompt,
604
+ };
605
+ if (member.title) flat.title = member.title;
606
+ if (member.description) flat.description = member.description;
607
+ if (member.provider) flat.provider = member.provider;
608
+ if (member.model) flat.model = member.model;
609
+ if (member.reasoningEffort) flat.reasoningEffort = member.reasoningEffort;
610
+ if (member.toolPolicy) flat.toolPolicy = member.toolPolicy;
611
+ return flat;
612
+ });
613
+ const roster = {
614
+ kind: ROSTER_KIND,
615
+ roster: source.roster,
616
+ version: source.version,
617
+ members,
618
+ };
619
+ if (source.title) roster.title = source.title;
620
+ if (source.description) roster.description = source.description;
621
+ if (compiled.captain) {
622
+ const captain = { name: String(compiled.captain.role ?? "").replace(/@[^@]*$/, "") };
623
+ if (compiled.captain.provider) captain.provider = compiled.captain.provider;
624
+ if (compiled.captain.model) captain.model = compiled.captain.model;
625
+ if (compiled.captain.reasoningEffort) captain.reasoningEffort = compiled.captain.reasoningEffort;
626
+ if (compiled.captain.persona) captain.personaPrefix = compiled.captain.persona;
627
+ roster.captain = captain;
628
+ }
629
+ return roster;
630
+ }
package/lib/format.js CHANGED
@@ -199,6 +199,7 @@ export const COLLECTION_FIELDS = [
199
199
  "ref",
200
200
  "path",
201
201
  "rosters",
202
+ "files",
202
203
  "tags",
203
204
  "license",
204
205
  "maintainers",
package/lib/remote.js CHANGED
@@ -23,6 +23,8 @@ import fs from "node:fs";
23
23
  import nodePath from "node:path";
24
24
  import { gunzipSync } from "node:zlib";
25
25
 
26
+ import { flattenCompiledToRoster, validateCompiled } from "./compile2.js";
27
+ import { COMPILED_KIND, ROSTER2_KIND } from "./format-v2.js";
26
28
  import {
27
29
  COLLECTION_FIELDS,
28
30
  LIMITS,
@@ -486,6 +488,25 @@ export function validateRegistry(value, meta = {}) {
486
488
  }
487
489
  if (bad) return;
488
490
 
491
+ const files = {};
492
+ if (entry.files !== undefined) {
493
+ if (entry.files === null || typeof entry.files !== "object" || Array.isArray(entry.files)) {
494
+ diagnostics.push(diagnostic("warn", "collection-files", `${where}.files 必须是「档案 id → 仓库内相对路径」的对象`, file));
495
+ } else {
496
+ for (const [key, value] of Object.entries(entry.files)) {
497
+ if (!rosters.includes(key)) {
498
+ diagnostics.push(diagnostic("warn", "collection-files-unknown", `${where}.files 里的 ${key} 不在 rosters 名单里,已忽略`, file));
499
+ continue;
500
+ }
501
+ if (typeof value !== "string" || value.trim() === "" || !PATTERNS.relPath.test(value) || value.split("/").includes("..")) {
502
+ diagnostics.push(diagnostic("warn", "collection-files-path", `${where}.files.${key} 必须是仓库内相对路径`, file));
503
+ continue;
504
+ }
505
+ files[key] = value.trim();
506
+ }
507
+ }
508
+ }
509
+
489
510
  seen.add(id);
490
511
  collections.push({
491
512
  id,
@@ -495,6 +516,7 @@ export function validateRegistry(value, meta = {}) {
495
516
  ref,
496
517
  path: path.replace(/^\/+|\/+$/g, ""),
497
518
  rosters,
519
+ files,
498
520
  tags: Array.isArray(entry.tags) ? entry.tags.filter((tag) => typeof tag === "string") : [],
499
521
  license: typeof entry.license === "string" ? entry.license : "",
500
522
  maintainers: Array.isArray(entry.maintainers) ? entry.maintainers.filter((item) => typeof item === "string") : [],
@@ -526,8 +548,10 @@ export async function fetchCollection(collection, options = {}) {
526
548
  for (const rosterId of wanted) {
527
549
  let loaded = null;
528
550
  let lastError = "";
529
- for (const fileName of ["roster.yaml", "roster.yml", "roster.json"]) {
530
- const url = collectionFileUrl(collection, rosterId, fileName);
551
+ const listedPath = collection.files?.[rosterId] ?? null;
552
+ const candidateNames = listedPath === null ? ["roster.yaml", "roster.yml", "roster.json"] : [null];
553
+ for (const fileName of candidateNames) {
554
+ const url = listedPath === null ? collectionFileUrl(collection, rosterId, fileName) : collectionFileUrl(collection, rosterId, "");
531
555
  try {
532
556
  const response = await fetchImpl(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
533
557
  if (!response.ok) {
@@ -545,15 +569,37 @@ export async function fetchCollection(collection, options = {}) {
545
569
  continue;
546
570
  }
547
571
  const parsed = parseDocumentText(loaded.text, { file: loaded.url });
548
- const checked = parsed.ok
549
- ? validateRoster(parsed.value, { file: loaded.url, expectedId: rosterId })
550
- : { ok: false, diagnostics: parsed.diagnostics };
572
+ if (parsed.ok && parsed.value !== null && typeof parsed.value === "object" && parsed.value.kind === ROSTER2_KIND) {
573
+ diagnostics.push(
574
+ diagnostic(
575
+ "warn",
576
+ "collection-roster-needs-roles",
577
+ `${collection.id}/${rosterId} 是 v2 组装,成员是角色引用,单个文件拉不到角色目录;索引应指向 dist/ 里的编译产物`,
578
+ ),
579
+ );
580
+ continue;
581
+ }
582
+ let checked;
583
+ if (parsed.ok && parsed.value !== null && typeof parsed.value === "object" && parsed.value.kind === COMPILED_KIND) {
584
+ const compiled = validateCompiled(parsed.value, { file: loaded.url });
585
+ if (!compiled.ok) {
586
+ diagnostics.push(...compiled.diagnostics);
587
+ continue;
588
+ }
589
+ checked = validateRoster(flattenCompiledToRoster(compiled.compiled), { file: loaded.url, expectedId: rosterId });
590
+ checked = { ...checked, format: "compiled" };
591
+ } else {
592
+ checked = parsed.ok
593
+ ? validateRoster(parsed.value, { file: loaded.url, expectedId: rosterId })
594
+ : { ok: false, diagnostics: parsed.diagnostics };
595
+ }
551
596
  if (!checked.ok) {
552
597
  diagnostics.push(...checked.diagnostics);
553
598
  continue;
554
599
  }
555
600
  rosters.push({
556
601
  ...checked.roster,
602
+ ...(checked.format === undefined ? {} : { format: checked.format }),
557
603
  origin: {
558
604
  kind: "remote",
559
605
  collection: collection.id,
@@ -569,10 +615,12 @@ export async function fetchCollection(collection, options = {}) {
569
615
  return { collection, rosters, diagnostics };
570
616
  }
571
617
 
572
- /** 拼一份档案的 raw 地址。 */
618
+ /** 拼一份档案的 raw 地址(relativePath 由索引给出,或回退到 v1 的 `<id>/roster.<ext>`)。 */
573
619
  export function collectionFileUrl(collection, rosterId, fileName) {
574
620
  const repo = collection.repo.replace(/^github:/, "");
575
- return `${GITHUB_RAW}/${repo}/${encodeURIComponent(collection.ref)}/${collection.path}/${rosterId}/${fileName}`;
621
+ const relative = collection.files?.[rosterId] ?? `${rosterId}/${fileName}`;
622
+ const base = collection.path === "" ? "" : `${collection.path}/`;
623
+ return `${GITHUB_RAW}/${repo}/${encodeURIComponent(collection.ref)}/${base}${relative}`;
576
624
  }
577
625
 
578
626
  /** 一次读多个源(串行:GitHub 未认证配额经不起并发烧)。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stofone/dsh-agent-roster",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "多 Agent 团队的静态成员档案定义与校验层:档案库在面板里看得见、改得动,团队成员的人设、模型与工具边界固化成可校验、可版本化的文件,并在 dsh-agent-teams 创建成员时把档案桥接给运行时。",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -5,32 +5,57 @@
5
5
  "description": "公共档案索引:登记若干 collection(仓库 + 目录 + 确有哪几份档案),供订阅与一键安装。权威校验实现是 lib/remote.js#validateRegistry;本文件供编辑器与外部工具提示,两者由 test/registry-schema.test.js 对齐。注意:运行时对未知字段只告警不判错,这里写成 additionalProperties:false 是为了让编辑器早点指出拼写错误。",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
- "required": ["kind", "collections"],
8
+ "required": [
9
+ "kind",
10
+ "collections"
11
+ ],
9
12
  "properties": {
10
13
  "$schema": {
11
14
  "type": "string",
12
15
  "description": "编辑器用的 schema 地址,插件不读它。"
13
16
  },
14
- "kind": { "const": "dsh-agent-roster-registry/v1" },
15
- "version": { "type": "integer", "minimum": 1 },
17
+ "kind": {
18
+ "const": "dsh-agent-roster-registry/v1"
19
+ },
20
+ "version": {
21
+ "type": "integer",
22
+ "minimum": 1
23
+ },
16
24
  "generatedAt": {
17
25
  "type": "string",
18
26
  "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?Z$",
19
27
  "description": "上一次校验通过的时间,由 CI 或维护者盖章。"
20
28
  },
21
- "generator": { "type": "string", "maxLength": 200 },
22
- "source": { "type": "string", "maxLength": 200 },
29
+ "generator": {
30
+ "type": "string",
31
+ "maxLength": 200
32
+ },
33
+ "source": {
34
+ "type": "string",
35
+ "maxLength": 200
36
+ },
23
37
  "collections": {
24
38
  "type": "array",
25
39
  "maxItems": 500,
26
40
  "items": {
27
41
  "type": "object",
28
42
  "additionalProperties": false,
29
- "required": ["id", "repo"],
43
+ "required": [
44
+ "id",
45
+ "repo"
46
+ ],
30
47
  "properties": {
31
- "id": { "$ref": "#/$defs/id" },
32
- "title": { "type": "string", "maxLength": 120 },
33
- "description": { "type": "string", "maxLength": 500 },
48
+ "id": {
49
+ "$ref": "#/$defs/id"
50
+ },
51
+ "title": {
52
+ "type": "string",
53
+ "maxLength": 120
54
+ },
55
+ "description": {
56
+ "type": "string",
57
+ "maxLength": 500
58
+ },
34
59
  "repo": {
35
60
  "type": "string",
36
61
  "pattern": "^(?:github:)?[A-Za-z0-9][A-Za-z0-9_.-]{0,38}/[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$",
@@ -52,7 +77,9 @@
52
77
  "type": "array",
53
78
  "maxItems": 200,
54
79
  "description": "目录里确实存在的档案 id。索引不撒谎:不存在的 id 会被 registry sync 剔除。",
55
- "items": { "$ref": "#/$defs/id" }
80
+ "items": {
81
+ "$ref": "#/$defs/id"
82
+ }
56
83
  },
57
84
  "tags": {
58
85
  "type": "array",
@@ -62,13 +89,34 @@
62
89
  "pattern": "^[a-z0-9][a-z0-9-]{0,31}$"
63
90
  }
64
91
  },
65
- "license": { "type": "string", "maxLength": 120 },
92
+ "license": {
93
+ "type": "string",
94
+ "maxLength": 120
95
+ },
66
96
  "maintainers": {
67
97
  "type": "array",
68
98
  "maxItems": 16,
69
- "items": { "type": "string", "minLength": 1, "maxLength": 120 }
99
+ "items": {
100
+ "type": "string",
101
+ "minLength": 1,
102
+ "maxLength": 120
103
+ }
70
104
  },
71
- "homepage": { "type": "string", "maxLength": 200, "pattern": "^https?://\\S+$" }
105
+ "homepage": {
106
+ "type": "string",
107
+ "maxLength": 200,
108
+ "pattern": "^https?://\\S+$"
109
+ },
110
+ "files": {
111
+ "type": "object",
112
+ "description": "档案 id → 仓库内相对路径。v2 档案指向 dist/ 的编译产物(自包含),v1 可省略。",
113
+ "propertyNames": {
114
+ "$ref": "#/$defs/id"
115
+ },
116
+ "additionalProperties": {
117
+ "$ref": "#/$defs/relPath"
118
+ }
119
+ }
72
120
  }
73
121
  }
74
122
  }
@@ -78,6 +126,13 @@
78
126
  "type": "string",
79
127
  "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
80
128
  "description": "kebab-case 标识,与档案目录名同规则。"
129
+ },
130
+ "relPath": {
131
+ "type": "string",
132
+ "minLength": 1,
133
+ "maxLength": 200,
134
+ "pattern": "^(?:[A-Za-z0-9._-]+/)*[A-Za-z0-9._-]+$",
135
+ "description": "仓库内相对路径,不含 ..、不以 / 开头。"
81
136
  }
82
137
  }
83
138
  }