@weiping/wingline-core 0.1.0 → 0.2.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.
- package/dist/audit-table.d.ts +11 -0
- package/dist/audit-table.js +85 -0
- package/dist/audit.js +1 -1
- package/dist/commands/about.d.ts +13 -0
- package/dist/commands/about.js +12 -1
- package/dist/commands/autostart/sys.d.ts +3 -0
- package/dist/commands/autostart/sys.js +23 -0
- package/dist/commands/deps.d.ts +2 -0
- package/dist/commands/deps.js +2 -0
- package/dist/commands/dispatcher/attention.d.ts +2 -0
- package/dist/commands/dispatcher/attention.js +10 -9
- package/dist/commands/dispatcher/controls.d.ts +1 -1
- package/dist/commands/dispatcher/controls.js +5 -3
- package/dist/commands/dispatcher/lifecycle.d.ts +4 -0
- package/dist/commands/dispatcher/lifecycle.js +7 -1
- package/dist/commands/dispatcher/loop.js +2 -0
- package/dist/commands/down.js +3 -0
- package/dist/commands/init.d.ts +17 -0
- package/dist/commands/init.js +106 -0
- package/dist/commands/launch.d.ts +10 -2
- package/dist/commands/launch.js +14 -3
- package/dist/commands/restore/index.js +6 -0
- package/dist/commands/restore/posts.d.ts +1 -1
- package/dist/commands/restore/posts.js +17 -17
- package/dist/commands/restore/resume-command.d.ts +0 -1
- package/dist/commands/restore/resume-command.js +0 -3
- package/dist/commands/send.js +1 -2
- package/dist/commands/spec.d.ts +8 -6
- package/dist/commands/spec.js +66 -9
- package/dist/commands/task/gate-exec.d.ts +5 -1
- package/dist/commands/task/gate-exec.js +29 -5
- package/dist/commands/task/notify.d.ts +1 -1
- package/dist/commands/task/notify.js +1 -1
- package/dist/commands/task/plan.d.ts +1 -0
- package/dist/commands/task/plan.js +1 -1
- package/dist/commands/task/query.d.ts +2 -0
- package/dist/commands/task/query.js +11 -2
- package/dist/commands/templates.d.ts +32 -0
- package/dist/commands/templates.js +120 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/orca/client.js +27 -9
- package/dist/orca/types.d.ts +6 -0
- package/dist/registry/registry.d.ts +20 -0
- package/dist/registry/registry.js +35 -2
- package/dist/server/api-read.js +55 -37
- package/dist/server/api-spec.js +43 -7
- package/dist/server/api-write.d.ts +86 -2
- package/dist/server/api-write.js +285 -12
- package/dist/server/index.js +14 -6
- package/dist/server/sse.js +22 -10
- package/dist/spec/doc.d.ts +13 -0
- package/dist/spec/doc.js +62 -0
- package/dist/spec/draft.d.ts +16 -0
- package/dist/spec/draft.js +15 -8
- package/dist/spec/edit.d.ts +21 -0
- package/dist/spec/edit.js +136 -0
- package/dist/spec/facade.d.ts +41 -0
- package/dist/spec/facade.js +68 -0
- package/dist/spec/load.d.ts +1 -0
- package/dist/spec/load.js +7 -6
- package/dist/spec/plan-change.js +4 -2
- package/package.json +1 -1
package/dist/spec/edit.d.ts
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
type Path = Array<string | number>;
|
|
2
2
|
/** 把表单的一处修改写回 YAML:`value === null` 删除该字段。 */
|
|
3
3
|
export declare function setField(text: string, path: Path, value: unknown): string;
|
|
4
|
+
type SpecPost = {
|
|
5
|
+
id: string;
|
|
6
|
+
agent: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
launch?: string;
|
|
9
|
+
worktree?: string;
|
|
10
|
+
brief?: string[];
|
|
11
|
+
};
|
|
12
|
+
export type SpecEditOp = {
|
|
13
|
+
op?: 'set';
|
|
14
|
+
path: Path;
|
|
15
|
+
value: unknown;
|
|
16
|
+
} | {
|
|
17
|
+
op: 'add_post';
|
|
18
|
+
post: SpecPost;
|
|
19
|
+
} | {
|
|
20
|
+
op: 'remove_post';
|
|
21
|
+
index: number;
|
|
22
|
+
};
|
|
23
|
+
/** 把团队编辑页的一处规格修改写回 YAML:op 缺省是 set(等价 setField),add_post / remove_post 增删岗位。 */
|
|
24
|
+
export declare function applySpecOp(text: string, op: SpecEditOp): string;
|
|
4
25
|
export {};
|
package/dist/spec/edit.js
CHANGED
|
@@ -198,3 +198,139 @@ function fillEmptySections(doc, path) {
|
|
|
198
198
|
doc.setIn(path.slice(0, i), empty);
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
|
+
/** 解析规格文本;语法错误按输入错误报告(与 setField 同一口径)。 */
|
|
202
|
+
function specDoc(text) {
|
|
203
|
+
const doc = parseDocument(text);
|
|
204
|
+
const err = doc.errors[0];
|
|
205
|
+
if (err)
|
|
206
|
+
throw new WinglineError('input', 'spec_invalid', `YAML 有语法错误:${err.message}`);
|
|
207
|
+
return doc;
|
|
208
|
+
}
|
|
209
|
+
/** 岗位增删都靠按行定位:posts 必须是块序列(不能是 flow 或缺失)。 */
|
|
210
|
+
function postsSeq(doc) {
|
|
211
|
+
const posts = doc.get('posts', true);
|
|
212
|
+
if (!isSeq(posts) || posts.flow)
|
|
213
|
+
throw new WinglineError('input', 'spec_invalid', 'posts 必须是块序列');
|
|
214
|
+
return posts;
|
|
215
|
+
}
|
|
216
|
+
/** 新岗位的节点:brief 保持模板的 flow 写法,undefined 与空数组字段不写入。 */
|
|
217
|
+
function postNode(doc, post) {
|
|
218
|
+
const filled = Object.fromEntries(Object.entries(post).filter(([, v]) => v !== undefined && !(Array.isArray(v) && v.length === 0)));
|
|
219
|
+
const node = doc.createNode(filled);
|
|
220
|
+
const brief = isMap(node) ? node.get('brief', true) : null;
|
|
221
|
+
if (isSeq(brief))
|
|
222
|
+
brief.flow = true;
|
|
223
|
+
return node;
|
|
224
|
+
}
|
|
225
|
+
/** 在 posts 末尾按最后一项的既有缩进追加新岗位;插入点之后一个字节不动。定位不了缩进时返回 null。 */
|
|
226
|
+
function appendPost(text, posts, node) {
|
|
227
|
+
const map = blockMap(posts.items[posts.items.length - 1]);
|
|
228
|
+
const keyStart = map ? map.items[0].key?.range?.[0] : undefined;
|
|
229
|
+
if (!map || keyStart === undefined)
|
|
230
|
+
return null;
|
|
231
|
+
const lineBegin = lineStart(text, keyStart);
|
|
232
|
+
const indent = /^(\s*)- /.exec(text.slice(lineBegin, keyStart));
|
|
233
|
+
if (!indent)
|
|
234
|
+
return null;
|
|
235
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
236
|
+
const pad = ' '.repeat(indent[1].length);
|
|
237
|
+
const body = new Document(node).toString(RENDER).trimEnd().split('\n')
|
|
238
|
+
.map((l, i) => (i === 0 ? `${pad}- ${l}` : `${pad} ${l}`)).join(eol);
|
|
239
|
+
// 插在最后一项(含它的行尾注释)那行结束之后,原有的空行与注释留在新岗位后面
|
|
240
|
+
const at = pairEnd(text, map.items[map.items.length - 1]);
|
|
241
|
+
return text.slice(0, at) + body + eol + text.slice(at);
|
|
242
|
+
}
|
|
243
|
+
function addPost(text, post) {
|
|
244
|
+
const doc = specDoc(text);
|
|
245
|
+
const posts = postsSeq(doc);
|
|
246
|
+
if (posts.items.some((it) => isMap(it) && it.get('id') === post.id)) {
|
|
247
|
+
throw new WinglineError('input', 'spec_invalid', `岗位 id 重复:${post.id}`);
|
|
248
|
+
}
|
|
249
|
+
const node = postNode(doc, post);
|
|
250
|
+
const out = appendPost(text, posts, node);
|
|
251
|
+
if (out !== null)
|
|
252
|
+
return out;
|
|
253
|
+
posts.add(node); // 最后一项不是块映射、算不出缩进:整篇交给 Document API,宁可重排不写坏
|
|
254
|
+
return doc.toString();
|
|
255
|
+
}
|
|
256
|
+
/** 整段删除 posts 的第 index 个元素:从它首键所在行的行首删到内容结束(含换行)。 */
|
|
257
|
+
function deleteSeqItem(text, posts, index) {
|
|
258
|
+
const map = blockMap(posts.items[index]);
|
|
259
|
+
const keyStart = map ? map.items[0].key?.range?.[0] : undefined;
|
|
260
|
+
if (!map || keyStart === undefined)
|
|
261
|
+
return null;
|
|
262
|
+
const lineBegin = lineStart(text, keyStart);
|
|
263
|
+
if (!/^\s*-\s+$/.test(text.slice(lineBegin, keyStart)))
|
|
264
|
+
return null;
|
|
265
|
+
return text.slice(0, lineBegin) + text.slice(pairEnd(text, map.items[map.items.length - 1]));
|
|
266
|
+
}
|
|
267
|
+
/** 删除一个键;父映射会因此变空时连父键一起删,一次删到能按行手术的层级,少触发 Document API 整篇重排。 */
|
|
268
|
+
function pruneExisting(text, path) {
|
|
269
|
+
const doc = parseDocument(text);
|
|
270
|
+
if (!doc.hasIn(path))
|
|
271
|
+
return text;
|
|
272
|
+
let cut = path;
|
|
273
|
+
for (let i = path.length - 1; i > 0; i--) {
|
|
274
|
+
const parent = doc.getIn(path.slice(0, i), true);
|
|
275
|
+
if (!isMap(parent) || parent.items.length !== 1)
|
|
276
|
+
break;
|
|
277
|
+
cut = path.slice(0, i);
|
|
278
|
+
}
|
|
279
|
+
return setField(text, cut, null);
|
|
280
|
+
}
|
|
281
|
+
/** 从其余岗位的 links.*.(reviewed_by|hands_to|consults) 数组里剔除被删 id;剔空的数组连键清掉。 */
|
|
282
|
+
function stripPostRefs(text, doc, removedId) {
|
|
283
|
+
const links = doc.get('links', true);
|
|
284
|
+
let out = text;
|
|
285
|
+
if (!isMap(links) || links.flow)
|
|
286
|
+
return out;
|
|
287
|
+
for (const pair of links.items) {
|
|
288
|
+
const postId = keyText(pair);
|
|
289
|
+
const rels = isMap(pair.value) && !pair.value.flow ? pair.value : null;
|
|
290
|
+
if (!postId || postId === removedId || !rels)
|
|
291
|
+
continue;
|
|
292
|
+
for (const rel of rels.items) {
|
|
293
|
+
const name = keyText(rel);
|
|
294
|
+
if (!name || !['reviewed_by', 'hands_to', 'consults'].includes(name) || !isSeq(rel.value))
|
|
295
|
+
continue;
|
|
296
|
+
const kept = rel.value.items.filter((n) => !(isScalar(n) && n.value === removedId)).map((n) => n.value);
|
|
297
|
+
if (kept.length === rel.value.items.length)
|
|
298
|
+
continue;
|
|
299
|
+
out = kept.length > 0 ? setField(out, ['links', postId, name], kept) : pruneExisting(out, ['links', postId, name]);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
function removePost(text, index) {
|
|
305
|
+
const doc = specDoc(text);
|
|
306
|
+
const posts = postsSeq(doc);
|
|
307
|
+
if (posts.items.length <= 1)
|
|
308
|
+
throw new WinglineError('input', 'spec_invalid', '团队至少保留一个岗位');
|
|
309
|
+
if (!Number.isInteger(index) || !posts.items[index])
|
|
310
|
+
throw new WinglineError('input', 'spec_invalid', `岗位下标不合法:${index}`);
|
|
311
|
+
const removedId = isMap(posts.items[index]) ? posts.items[index].get('id') : undefined;
|
|
312
|
+
const firstLeft = posts.items[index === 0 ? 1 : 0];
|
|
313
|
+
const nextLead = isMap(firstLeft) ? firstLeft.get('id') : undefined;
|
|
314
|
+
const cut = deleteSeqItem(text, posts, index);
|
|
315
|
+
let out = cut;
|
|
316
|
+
if (out === null) {
|
|
317
|
+
doc.deleteIn(['posts', index]); // 被删项不是块映射、行定位不了:整篇交给 Document API,宁可重排不写坏
|
|
318
|
+
out = doc.toString();
|
|
319
|
+
}
|
|
320
|
+
if (typeof removedId === 'string') {
|
|
321
|
+
out = pruneExisting(out, ['links', removedId]);
|
|
322
|
+
out = pruneExisting(out, ['gates', removedId]);
|
|
323
|
+
out = stripPostRefs(out, doc, removedId);
|
|
324
|
+
}
|
|
325
|
+
if (typeof nextLead === 'string' && doc.get('lead') === removedId)
|
|
326
|
+
out = setField(out, ['lead'], nextLead);
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
/** 把团队编辑页的一处规格修改写回 YAML:op 缺省是 set(等价 setField),add_post / remove_post 增删岗位。 */
|
|
330
|
+
export function applySpecOp(text, op) {
|
|
331
|
+
if (op.op === 'add_post')
|
|
332
|
+
return addPost(text, op.post);
|
|
333
|
+
if (op.op === 'remove_post')
|
|
334
|
+
return removePost(text, op.index);
|
|
335
|
+
return setField(text, op.path, op.value);
|
|
336
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type SaveSpecArgs, type SpecEditResult, type SpecForEdit } from '../commands/spec.js';
|
|
2
|
+
import type { CommandDeps } from '../commands/deps.js';
|
|
3
|
+
import type { SpecEditOp } from '../spec/edit.js';
|
|
4
|
+
import type { SpecPlan } from '../spec/plan-change.js';
|
|
5
|
+
export type { SpecEditResult, SpecForEdit, SpecEditOp, SpecPlan, SaveSpecArgs };
|
|
6
|
+
/** 一次 validate 的入参(不含 team,由调用方传入团队名)。 */
|
|
7
|
+
export interface SpecValidateInput {
|
|
8
|
+
yaml: string;
|
|
9
|
+
set?: SpecEditOp;
|
|
10
|
+
files?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
/** 一次 save 的入参(不含 team);审计走 audited save。 */
|
|
13
|
+
export interface SpecSaveInput extends Omit<SaveSpecArgs, 'team'> {
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 团队规格编辑门面:读盘走现有 command,写操作走审计包装。
|
|
17
|
+
* 由插件 worker、测试与后续 Phase 3 面板共用。
|
|
18
|
+
*/
|
|
19
|
+
export declare function createSpecFacade(deps: CommandDeps): {
|
|
20
|
+
/** 加载编辑器:YAML、草稿、哈希、引用文档正文。 */
|
|
21
|
+
readForEdit(team: string): SpecForEdit;
|
|
22
|
+
/** 校验并预演;不写任何文件。 */
|
|
23
|
+
validate(team: string, input: SpecValidateInput): SpecEditResult;
|
|
24
|
+
/** 保存规格(及文档);写入审计(command: spec save)。 */
|
|
25
|
+
save(team: string, input: SpecSaveInput): Promise<{
|
|
26
|
+
wrote: "draft" | "spec";
|
|
27
|
+
path: string;
|
|
28
|
+
hash: string;
|
|
29
|
+
docsUpdated: string[];
|
|
30
|
+
}>;
|
|
31
|
+
/** 丢弃草稿。 */
|
|
32
|
+
discardDraft(team: string, retryRequest?: string): Promise<{
|
|
33
|
+
discarded: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
/** 规格目录下某引用文档的绝对路径(越界路径会被 core 拒绝)。 */
|
|
36
|
+
resolveDoc(team: string, rel: string): string | null;
|
|
37
|
+
/** 读取 NORMS / brief 等引用文档正文。 */
|
|
38
|
+
readDoc(team: string, rel: string): string | null;
|
|
39
|
+
/** 写入引用文档(原子写);一般由 save 一并处理。 */
|
|
40
|
+
writeDoc(team: string, rel: string, content: string): void;
|
|
41
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { auditedCommands } from '../audit.js';
|
|
3
|
+
import { coreAuditTable } from '../audit-table.js';
|
|
4
|
+
import { discardDraftCmd, readSpecForEdit, saveSpecCmd, validateSpecEdit, } from '../commands/spec.js';
|
|
5
|
+
import { readDocText, resolveDocPath, writeDocAtomic } from '../spec/doc.js';
|
|
6
|
+
const facaded = (deps) => auditedCommands({ saveSpecCmd, discardDraftCmd }, coreAuditTable(), { actor: 'human' });
|
|
7
|
+
/**
|
|
8
|
+
* 团队规格编辑门面:读盘走现有 command,写操作走审计包装。
|
|
9
|
+
* 由插件 worker、测试与后续 Phase 3 面板共用。
|
|
10
|
+
*/
|
|
11
|
+
export function createSpecFacade(deps) {
|
|
12
|
+
const audited = facaded(deps);
|
|
13
|
+
function specDir(team) {
|
|
14
|
+
const row = deps.registry.getTeam(team);
|
|
15
|
+
if (!row)
|
|
16
|
+
return null;
|
|
17
|
+
return path.dirname(row.specPath);
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
/** 加载编辑器:YAML、草稿、哈希、引用文档正文。 */
|
|
21
|
+
readForEdit(team) {
|
|
22
|
+
return readSpecForEdit(deps, team);
|
|
23
|
+
},
|
|
24
|
+
/** 校验并预演;不写任何文件。 */
|
|
25
|
+
validate(team, input) {
|
|
26
|
+
return validateSpecEdit(deps, { team, ...input });
|
|
27
|
+
},
|
|
28
|
+
/** 保存规格(及文档);写入审计(command: spec save)。 */
|
|
29
|
+
async save(team, input) {
|
|
30
|
+
return audited.saveSpecCmd(deps, { team, ...input });
|
|
31
|
+
},
|
|
32
|
+
/** 丢弃草稿。 */
|
|
33
|
+
async discardDraft(team, retryRequest) {
|
|
34
|
+
return audited.discardDraftCmd(deps, { team, retryRequest });
|
|
35
|
+
},
|
|
36
|
+
/** 规格目录下某引用文档的绝对路径(越界路径会被 core 拒绝)。 */
|
|
37
|
+
resolveDoc(team, rel) {
|
|
38
|
+
const dir = specDir(team);
|
|
39
|
+
if (!dir)
|
|
40
|
+
return null;
|
|
41
|
+
try {
|
|
42
|
+
return resolveDocPath(dir, rel);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
/** 读取 NORMS / brief 等引用文档正文。 */
|
|
49
|
+
readDoc(team, rel) {
|
|
50
|
+
const dir = specDir(team);
|
|
51
|
+
if (!dir)
|
|
52
|
+
return null;
|
|
53
|
+
try {
|
|
54
|
+
return readDocText(resolveDocPath(dir, rel));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
/** 写入引用文档(原子写);一般由 save 一并处理。 */
|
|
61
|
+
writeDoc(team, rel, content) {
|
|
62
|
+
const dir = specDir(team);
|
|
63
|
+
if (!dir)
|
|
64
|
+
throw new Error(`团队 ${team} 未登记`);
|
|
65
|
+
writeDocAtomic(resolveDocPath(dir, rel), content);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
package/dist/spec/load.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ export type SpecError = {
|
|
|
66
66
|
/** 从字符串校验团队规格;文件引用相对 specPath 所在目录解析。错误至多一条(遇到第一处即停)。 */
|
|
67
67
|
export declare function validateSpecText(text: string, specPath: string, o?: {
|
|
68
68
|
platform?: NodeJS.Platform;
|
|
69
|
+
virtualFiles?: ReadonlySet<string>;
|
|
69
70
|
}): {
|
|
70
71
|
spec: TeamSpec | null;
|
|
71
72
|
parsed: unknown | null;
|
package/dist/spec/load.js
CHANGED
|
@@ -175,9 +175,10 @@ function launchOf(p, i, at) {
|
|
|
175
175
|
return `${p.agent} --model ${p.model}`;
|
|
176
176
|
return at.fail('launch_required', ['posts', i, 'model'], `agent ${p.agent} 指定了 model,请用 launch 指定完整启动命令`);
|
|
177
177
|
}
|
|
178
|
-
function resolveFile(dir, rel, at, p) {
|
|
178
|
+
function resolveFile(dir, rel, at, p, virtualFiles) {
|
|
179
179
|
const abs = path.resolve(dir, toPosix(rel));
|
|
180
|
-
|
|
180
|
+
// 编辑器里新引用的文档还没落盘:以虚拟文件表的存在性为准(rel 按 YAML 原样归一到 / 后比较)
|
|
181
|
+
if (!fs.existsSync(abs) && !virtualFiles?.has(toPosix(rel)))
|
|
181
182
|
at.fail('file_missing', p, `找不到文件 ${rel}(解析为 ${abs})`);
|
|
182
183
|
return abs;
|
|
183
184
|
}
|
|
@@ -198,7 +199,7 @@ export function validateSpecText(text, specPath, o = {}) {
|
|
|
198
199
|
return { spec: null, parsed: null, errors: [toError(e)] };
|
|
199
200
|
}
|
|
200
201
|
try {
|
|
201
|
-
return { spec: specFromRaw(parsed.raw, parsed.at, abs, o.platform ?? process.platform), parsed: parsed.raw, errors: [] };
|
|
202
|
+
return { spec: specFromRaw(parsed.raw, parsed.at, abs, o.platform ?? process.platform, o.virtualFiles), parsed: parsed.raw, errors: [] };
|
|
202
203
|
}
|
|
203
204
|
catch (e) {
|
|
204
205
|
return { spec: null, parsed: parsed.raw, errors: [toError(e)] };
|
|
@@ -216,7 +217,7 @@ export function loadTeamSpec(specPath, o = {}) {
|
|
|
216
217
|
const { raw, at } = parseYamlText(source, abs);
|
|
217
218
|
return specFromRaw(raw, at, abs, o.platform ?? process.platform);
|
|
218
219
|
}
|
|
219
|
-
function specFromRaw(raw, at, abs, platform) {
|
|
220
|
+
function specFromRaw(raw, at, abs, platform, virtualFiles) {
|
|
220
221
|
const dir = path.dirname(abs);
|
|
221
222
|
const team = validate(raw, at);
|
|
222
223
|
crossCheck(team, at);
|
|
@@ -224,7 +225,7 @@ function specFromRaw(raw, at, abs, platform) {
|
|
|
224
225
|
version: 1,
|
|
225
226
|
name: team.name,
|
|
226
227
|
repo: team.repo,
|
|
227
|
-
normsPath: resolveFile(dir, team.norms, at, ['norms']),
|
|
228
|
+
normsPath: resolveFile(dir, team.norms, at, ['norms'], virtualFiles),
|
|
228
229
|
lead: team.lead,
|
|
229
230
|
posts: team.posts.map((p, i) => ({
|
|
230
231
|
id: p.id,
|
|
@@ -233,7 +234,7 @@ function specFromRaw(raw, at, abs, platform) {
|
|
|
233
234
|
launch: launchOf(p, i, at),
|
|
234
235
|
...environmentOf(p, i, at),
|
|
235
236
|
worktree: p.worktree,
|
|
236
|
-
briefPaths: p.brief.map((b, j) => resolveFile(dir, b, at, ['posts', i, 'brief', j])),
|
|
237
|
+
briefPaths: p.brief.map((b, j) => resolveFile(dir, b, at, ['posts', i, 'brief', j], virtualFiles)),
|
|
237
238
|
selfClaim: p.self_claim,
|
|
238
239
|
tags: p.tags,
|
|
239
240
|
planFirst: p.plan_first,
|
package/dist/spec/plan-change.js
CHANGED
|
@@ -18,10 +18,11 @@ function snakeKeys(v) {
|
|
|
18
18
|
return v;
|
|
19
19
|
return Object.fromEntries(Object.entries(v).map(([k, x]) => [snake(k), snakeKeys(x)]));
|
|
20
20
|
}
|
|
21
|
-
/** 把 TeamSpec 还原成 YAML
|
|
21
|
+
/** 把 TeamSpec 还原成 YAML 里的字段形态,供按字段比较。岗位级字段(plan_first / self_claim / tags)也进哈希,预演必须同样列出。 */
|
|
22
22
|
function fieldView(s) {
|
|
23
23
|
const links = {};
|
|
24
24
|
const gates = {};
|
|
25
|
+
const posts = {};
|
|
25
26
|
for (const p of s.posts) {
|
|
26
27
|
const l = { reviewed_by: p.reviewedBy, hands_to: p.handsTo, consults: p.consults ?? [] };
|
|
27
28
|
const filled = Object.fromEntries(Object.entries(l).filter(([, v]) => v.length > 0));
|
|
@@ -29,8 +30,9 @@ function fieldView(s) {
|
|
|
29
30
|
links[p.id] = filled;
|
|
30
31
|
if (p.gate)
|
|
31
32
|
gates[p.id] = p.gate;
|
|
33
|
+
posts[p.id] = { plan_first: p.planFirst, self_claim: p.selfClaim, tags: p.tags };
|
|
32
34
|
}
|
|
33
|
-
return { lead: s.lead, links, gates, budgets: snakeKeys(s.budgets), patrol: snakeKeys(s.patrol) };
|
|
35
|
+
return { lead: s.lead, links, gates, budgets: snakeKeys(s.budgets), patrol: snakeKeys(s.patrol), posts };
|
|
34
36
|
}
|
|
35
37
|
function flatten(v, prefix, out) {
|
|
36
38
|
if (v && typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length > 0) {
|