@istuen/pt 0.1.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/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/agent/api-bridge.d.ts +8 -0
- package/dist/agent/api-bridge.js +83 -0
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/index.js +3 -0
- package/dist/agent/pi-adapter.d.ts +35 -0
- package/dist/agent/pi-adapter.js +251 -0
- package/dist/agent/registry.d.ts +4 -0
- package/dist/agent/registry.js +40 -0
- package/dist/asset-health.d.ts +50 -0
- package/dist/asset-health.js +267 -0
- package/dist/asset-pack/loader.d.ts +47 -0
- package/dist/asset-pack/loader.js +121 -0
- package/dist/asset-pack/manifest.d.ts +23 -0
- package/dist/asset-pack/manifest.js +96 -0
- package/dist/asset-pack/md-file-pack.d.ts +41 -0
- package/dist/asset-pack/md-file-pack.js +153 -0
- package/dist/asset-pack/validate.d.ts +55 -0
- package/dist/asset-pack/validate.js +131 -0
- package/dist/builtin/assets/blueprints/dev-knowledge.blueprint.yaml +13 -0
- package/dist/builtin/assets/domains/agent-info.md +15 -0
- package/dist/builtin/assets/domains/authoring.md +201 -0
- package/dist/builtin/assets/domains/pack-repair.md +47 -0
- package/dist/builtin/assets/domains/project-analysis.md +52 -0
- package/dist/builtin/assets/domains/usage.md +81 -0
- package/dist/builtin/assets/domains/user-info.md +15 -0
- package/dist/builtin/assets/profiles/guide.profile.md +28 -0
- package/dist/commands.d.ts +69 -0
- package/dist/commands.js +344 -0
- package/dist/compile/agent-context.d.ts +48 -0
- package/dist/compile/agent-context.js +423 -0
- package/dist/compile/index.d.ts +1 -0
- package/dist/compile/index.js +5 -0
- package/dist/compile/resolve-use.d.ts +36 -0
- package/dist/compile/resolve-use.js +171 -0
- package/dist/compile/type-guards.d.ts +29 -0
- package/dist/compile/type-guards.js +107 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.js +133 -0
- package/dist/constants.d.ts +52 -0
- package/dist/constants.js +81 -0
- package/dist/diagnostics.d.ts +14 -0
- package/dist/diagnostics.js +29 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +838 -0
- package/dist/injection-status.d.ts +45 -0
- package/dist/injection-status.js +134 -0
- package/dist/log.d.ts +64 -0
- package/dist/log.js +139 -0
- package/dist/manual-session.d.ts +24 -0
- package/dist/manual-session.js +152 -0
- package/dist/manual-track.d.ts +35 -0
- package/dist/manual-track.js +107 -0
- package/dist/parse/blueprint.d.ts +5 -0
- package/dist/parse/blueprint.js +59 -0
- package/dist/parse/domain-renderers.d.ts +8 -0
- package/dist/parse/domain-renderers.js +115 -0
- package/dist/parse/domain.d.ts +7 -0
- package/dist/parse/domain.js +69 -0
- package/dist/parse/index.d.ts +9 -0
- package/dist/parse/index.js +223 -0
- package/dist/parse/profile.d.ts +18 -0
- package/dist/parse/profile.js +117 -0
- package/dist/parse/ref-resolver.d.ts +63 -0
- package/dist/parse/ref-resolver.js +174 -0
- package/dist/parse/shared.d.ts +76 -0
- package/dist/parse/shared.js +273 -0
- package/dist/profile-persist.d.ts +22 -0
- package/dist/profile-persist.js +53 -0
- package/dist/render/cache.d.ts +15 -0
- package/dist/render/cache.js +135 -0
- package/dist/render/index.d.ts +3 -0
- package/dist/render/index.js +14 -0
- package/dist/render/session-inject.d.ts +5 -0
- package/dist/render/session-inject.js +24 -0
- package/dist/render/turn-inject.d.ts +36 -0
- package/dist/render/turn-inject.js +248 -0
- package/dist/schema.d.ts +448 -0
- package/dist/schema.js +45 -0
- package/dist/session.d.ts +82 -0
- package/dist/session.js +102 -0
- package/dist/slog.d.ts +3 -0
- package/dist/slog.js +27 -0
- package/dist/transpile.d.ts +34 -0
- package/dist/transpile.js +220 -0
- package/dist/verify/file-hash.d.ts +2 -0
- package/dist/verify/file-hash.js +30 -0
- package/dist/verify/fs-content-match.d.ts +2 -0
- package/dist/verify/fs-content-match.js +26 -0
- package/dist/verify/fs-exists.d.ts +2 -0
- package/dist/verify/fs-exists.js +13 -0
- package/dist/verify/fs-not-exists.d.ts +2 -0
- package/dist/verify/fs-not-exists.js +13 -0
- package/dist/verify/git-status-clean.d.ts +2 -0
- package/dist/verify/git-status-clean.js +19 -0
- package/dist/verify/index.d.ts +7 -0
- package/dist/verify/index.js +46 -0
- package/dist/verify/lint-check.d.ts +2 -0
- package/dist/verify/lint-check.js +17 -0
- package/dist/verify/read-stderr.d.ts +5 -0
- package/dist/verify/read-stderr.js +20 -0
- package/dist/verify/ref-check.d.ts +13 -0
- package/dist/verify/ref-check.js +88 -0
- package/dist/verify/test-pass.d.ts +2 -0
- package/dist/verify/test-pass.js +17 -0
- package/dist/verify/ts-compiles.d.ts +2 -0
- package/dist/verify/ts-compiles.js +17 -0
- package/package.json +62 -0
- package/src/agent/api-bridge.ts +105 -0
- package/src/agent/index.ts +4 -0
- package/src/agent/pi-adapter.ts +326 -0
- package/src/agent/registry.ts +44 -0
- package/src/asset-health.ts +327 -0
- package/src/asset-pack/loader.ts +141 -0
- package/src/asset-pack/manifest.ts +118 -0
- package/src/asset-pack/md-file-pack.ts +202 -0
- package/src/asset-pack/validate.ts +186 -0
- package/src/builtin/assets/blueprints/dev-knowledge.blueprint.yaml +13 -0
- package/src/builtin/assets/domains/agent-info.md +15 -0
- package/src/builtin/assets/domains/authoring.md +201 -0
- package/src/builtin/assets/domains/pack-repair.md +47 -0
- package/src/builtin/assets/domains/project-analysis.md +52 -0
- package/src/builtin/assets/domains/usage.md +81 -0
- package/src/builtin/assets/domains/user-info.md +15 -0
- package/src/builtin/assets/profiles/guide.profile.md +28 -0
- package/src/commands.ts +405 -0
- package/src/compile/agent-context.ts +487 -0
- package/src/compile/index.ts +5 -0
- package/src/compile/resolve-use.ts +208 -0
- package/src/compile/type-guards.ts +132 -0
- package/src/config.ts +154 -0
- package/src/constants.ts +104 -0
- package/src/diagnostics.ts +36 -0
- package/src/index.ts +1028 -0
- package/src/injection-status.ts +155 -0
- package/src/log.ts +173 -0
- package/src/manual-session.ts +190 -0
- package/src/manual-track.ts +127 -0
- package/src/parse/blueprint.ts +82 -0
- package/src/parse/domain-renderers.ts +120 -0
- package/src/parse/domain.ts +78 -0
- package/src/parse/index.ts +266 -0
- package/src/parse/profile.ts +139 -0
- package/src/parse/ref-resolver.ts +198 -0
- package/src/parse/shared.ts +331 -0
- package/src/profile-persist.ts +60 -0
- package/src/render/cache.ts +150 -0
- package/src/render/index.ts +14 -0
- package/src/render/session-inject.ts +26 -0
- package/src/render/turn-inject.ts +278 -0
- package/src/schema.ts +542 -0
- package/src/session.ts +190 -0
- package/src/slog.ts +32 -0
- package/src/transpile.ts +302 -0
- package/src/verify/file-hash.ts +29 -0
- package/src/verify/fs-content-match.ts +28 -0
- package/src/verify/fs-exists.ts +14 -0
- package/src/verify/fs-not-exists.ts +17 -0
- package/src/verify/git-status-clean.ts +23 -0
- package/src/verify/index.ts +57 -0
- package/src/verify/lint-check.ts +21 -0
- package/src/verify/read-stderr.ts +24 -0
- package/src/verify/ref-check.ts +118 -0
- package/src/verify/test-pass.ts +18 -0
- package/src/verify/ts-compiles.ts +21 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// src/parse/profile.ts — profiles/*.md → Profile IR
|
|
2
|
+
//
|
|
3
|
+
// Phase 9.3:v9 新增 — Profile(业务端实例)= 引用 Blueprint + 选 Domains(YAML 全局 + 聚合组追加)。
|
|
4
|
+
// - YAML frontmatter:
|
|
5
|
+
// - blueprint: <blueprint-name>
|
|
6
|
+
// - domains: [d1, d2, ...] ← 全局 Domain 列表(自动分发到所有聚合组)
|
|
7
|
+
// - ## <聚合组名> : 聚合组实例化(与 Blueprint 同名)
|
|
8
|
+
// - ### Domains : 追加到本聚合组的 Domain 名列表
|
|
9
|
+
//
|
|
10
|
+
// Profile asset 格式(v9 + v9.1 modules-to-profile-complete):
|
|
11
|
+
// ---
|
|
12
|
+
// name: <profile-name>
|
|
13
|
+
// blueprint: <blueprint-name>
|
|
14
|
+
// domains: [d1, d2, ...]
|
|
15
|
+
// ---
|
|
16
|
+
//
|
|
17
|
+
// ## 会话背景
|
|
18
|
+
// ### Modules
|
|
19
|
+
// - Scene ← 段名(跨所有引用域该段)
|
|
20
|
+
// - User.user-profile ← 段.项(跨所有引用域该段下 H3 项)
|
|
21
|
+
// - Agent.senior-developer
|
|
22
|
+
// ### Domains
|
|
23
|
+
// - d3
|
|
24
|
+
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { reportWarn } from "../diagnostics.js";
|
|
27
|
+
import { SUFFIX_PROFILE } from "../constants.js";
|
|
28
|
+
import type { ModName, Profile, ProfileGroup } from "../schema.js";
|
|
29
|
+
import type { SourceAdapterContext } from "../schema.js";
|
|
30
|
+
import { extractDomainsList, extractModulesList, readAsset, sArr } from "./shared.js";
|
|
31
|
+
|
|
32
|
+
/** 已知 H2 段名(段名 = 命名空间)。modName 解析时左段必须是段名。
|
|
33
|
+
* 扩展新段名 = 加这一行 + registerModuleRenderer 加一行 + 一致性测试。
|
|
34
|
+
* v9.1+(modules-to-profile-complete):加 "User" / "Agent" 专用段(user-info / agent-info 重命名)。
|
|
35
|
+
* 公开导出(src/asset-health.ts 复用做 unknown-modname 检测——避免重复常量)。 */
|
|
36
|
+
export const KNOWN_SECTION_NAMES: ReadonlySet<string> = new Set([
|
|
37
|
+
"Scene",
|
|
38
|
+
"Participant", // 保留兼容——之前 user-info / agent-info 用的段名
|
|
39
|
+
"Trigger",
|
|
40
|
+
"Rules",
|
|
41
|
+
"Flows",
|
|
42
|
+
"Checklists",
|
|
43
|
+
"User", // user-info 重命名后专用段
|
|
44
|
+
"Agent", // agent-info 重命名后专用段
|
|
45
|
+
"Term", // fallback term 形态
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/** 解析 modName 字符串 → { section, item? }。
|
|
49
|
+
* 形态 1: "Scene" → { section: "Scene" }(整段)
|
|
50
|
+
* 形态 2: "User.user-profile" → { section: "User", item: "user-profile" }(单 H3 项)
|
|
51
|
+
* 解析失败(不含 '.' 但段名未知 / 含 '.' 但段名未知 / 段名已知但项名为空)→ 返 null。
|
|
52
|
+
* 不跨段匹配 item("User.user-profile" 不匹配 "Agent" 段下同名 H3)—— 跨段同名由段名命名空间避免。 */
|
|
53
|
+
export function parseModName(raw: string): ModName | null {
|
|
54
|
+
const modName = raw.trim();
|
|
55
|
+
if (!modName) return null;
|
|
56
|
+
if (modName.includes(".")) {
|
|
57
|
+
const dotIdx = modName.indexOf(".");
|
|
58
|
+
const section = modName.slice(0, dotIdx);
|
|
59
|
+
const item = modName.slice(dotIdx + 1);
|
|
60
|
+
if (KNOWN_SECTION_NAMES.has(section) && item) {
|
|
61
|
+
return { section, item };
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (KNOWN_SECTION_NAMES.has(modName)) {
|
|
66
|
+
return { section: modName };
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 读 profiles/<fileName>.md → Profile { name, blueprint, domains, groups }
|
|
72
|
+
* v10.x:assetDir 让 fixtures 可指向 tests/fixtures/assets/(默认 .pt/assets)。
|
|
73
|
+
* v9.1(modules-to-profile 迁移):每个 H2 段加读 `### Modules` 填 ProfileGroup.modules。
|
|
74
|
+
* v9.1+(modules-to-profile-complete):modName 字符串解析为 ModName 对象(2 形态)。 */
|
|
75
|
+
export async function parseProfile(
|
|
76
|
+
absDir: string,
|
|
77
|
+
fileName: string,
|
|
78
|
+
adapterCtx?: SourceAdapterContext
|
|
79
|
+
): Promise<Profile> {
|
|
80
|
+
const asset = await readAsset(join(absDir, fileName));
|
|
81
|
+
|
|
82
|
+
const blueprint =
|
|
83
|
+
typeof asset.frontmatter.blueprint === "string" ? asset.frontmatter.blueprint : "";
|
|
84
|
+
|
|
85
|
+
const domains = sArr(asset.frontmatter.domains); // YAML 全局 domains
|
|
86
|
+
|
|
87
|
+
// v14.x:tagline 选填,从 frontmatter 读(string 校验,非字符串静默忽略)
|
|
88
|
+
// 不做长度硬截——展示层(footer / 选择器)按需 truncate;资产可写长描述,
|
|
89
|
+
// UI 表现由使用方决定(UI 是显示抽象,资产是数据抽象)。
|
|
90
|
+
const taglineRaw = asset.frontmatter.tagline;
|
|
91
|
+
const tagline =
|
|
92
|
+
typeof taglineRaw === "string" && taglineRaw.trim().length > 0 ? taglineRaw.trim() : undefined;
|
|
93
|
+
|
|
94
|
+
// v15.x PR5(§5.1):use 单继承字段——选填 string,解析阶段只读不展开
|
|
95
|
+
// 引用解析在 expandProfile(src/compile/resolve-use.ts)做——分离关注点
|
|
96
|
+
const useRaw = asset.frontmatter.use;
|
|
97
|
+
const use = typeof useRaw === "string" && useRaw.trim().length > 0 ? useRaw.trim() : undefined;
|
|
98
|
+
|
|
99
|
+
// groups:每个 H2 = 聚合组实例化
|
|
100
|
+
// - ### Domains → 追加到本聚合组的 Domain 名列表(v9 既有)
|
|
101
|
+
// - ### Modules → 本插槽填的聚合模块列表(v9.1+),modName 解析为 ModName 对象
|
|
102
|
+
const groups: ProfileGroup[] = [];
|
|
103
|
+
for (const [h2Name, section] of Object.entries(asset.sections)) {
|
|
104
|
+
const appendDomains = extractDomainsList(section);
|
|
105
|
+
const rawMods = extractModulesList(section);
|
|
106
|
+
const modules: ModName[] = [];
|
|
107
|
+
for (const raw of rawMods) {
|
|
108
|
+
const parsed = parseModName(raw);
|
|
109
|
+
if (parsed) {
|
|
110
|
+
modules.push(parsed);
|
|
111
|
+
} else {
|
|
112
|
+
// 解析失败——log warn,skip(与设计文档 §5 "缺填" 一致:可能故意 / 笔误)
|
|
113
|
+
reportWarn(
|
|
114
|
+
adapterCtx,
|
|
115
|
+
`Profile「${asset.name}」聚合组「${h2Name}」的 modName 解析失败:${raw}`,
|
|
116
|
+
{ profile: asset.name, group: h2Name, modName: raw }
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
groups.push({ name: h2Name, domains: appendDomains, modules });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
name:
|
|
125
|
+
typeof asset.frontmatter.name === "string"
|
|
126
|
+
? asset.frontmatter.name
|
|
127
|
+
: stripProfileSuffix(asset.name),
|
|
128
|
+
blueprint,
|
|
129
|
+
domains,
|
|
130
|
+
groups,
|
|
131
|
+
tagline,
|
|
132
|
+
use,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function stripProfileSuffix(fileBase: string): string {
|
|
137
|
+
// v9 命名约定:<name>.profile.md → 去 .profile 后缀
|
|
138
|
+
return fileBase.replace(new RegExp(`${SUFFIX_PROFILE}$`), "");
|
|
139
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// src/parse/ref-resolver.ts — @pack/name 限定语法工具(v15.x PR3)
|
|
2
|
+
//
|
|
3
|
+
// 设计源:.pt/docs/designs/pt-asset-pack.md §4(@pack/name 限定语法)+ §4.4.2(fingerprint + dedup)
|
|
4
|
+
//
|
|
5
|
+
// 职责:
|
|
6
|
+
// - parseRef:统一解析限定 + 不限定 ref → {pack, name}
|
|
7
|
+
// - fingerprint:sha256(rootDir + content) 截 16 字符
|
|
8
|
+
// - resolveAndDedupRefs:解析 refs + fp dedup(后者覆盖前者,Map.set 语义)
|
|
9
|
+
//
|
|
10
|
+
// 关键纪律:
|
|
11
|
+
// - parseRef 抛异常(不像 parseManifest 容错)—— 畸形 ref 是配置错误
|
|
12
|
+
// - 别名归一:@project/foo → @prj/foo(兼容旧文档,§4.1)
|
|
13
|
+
// - 解析阶段展开为统一形态——IR 不升级(profile.domains 仍 string[])
|
|
14
|
+
// - fingerprint 用 rootDir + content:保证"同 pack 同内容同 fp" + "不同 pack 偶然同内容不同 fp"
|
|
15
|
+
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import type { AssetPack, Blueprint, Profile, WorkingSet } from "../schema.js";
|
|
18
|
+
|
|
19
|
+
// ==================== §4.2:parseRef ====================
|
|
20
|
+
|
|
21
|
+
/** reserved pack 名 + 别名(§2.4.1 + §4.1 + §2.4.4 双层语义)。
|
|
22
|
+
* v15.x §2.4.4:这些是"位置 alias"——固定指向 3 个物理位置 slot,与 manifest.name 身份层独立。
|
|
23
|
+
* 精确匹配:别名归一为短名(project → prj 等),非位置 alias 原样保留。 */
|
|
24
|
+
const LOCATION_ALIASES: ReadonlyMap<string, string> = new Map([
|
|
25
|
+
["prj", "prj"],
|
|
26
|
+
["project", "prj"],
|
|
27
|
+
["gbl", "gbl"],
|
|
28
|
+
["global", "gbl"],
|
|
29
|
+
["pt", "pt"],
|
|
30
|
+
["builtin", "pt"],
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/** 解析 ref → {kind, pack, name}。双层语义(§2.4.4):
|
|
34
|
+
* - kind="location":位置 alias(prj/gbl/pt + project/global/builtin)→ 按 pack name 物理位置查
|
|
35
|
+
* - kind="identity":身份 alias(manifest.name)→ 按 pack.name 身份查
|
|
36
|
+
* 不限定 ref 自动绑定 selfPack(kind="identity")。
|
|
37
|
+
* 畸形输入抛异常(§4.5.1 运行时校验)。 */
|
|
38
|
+
export function parseRef(
|
|
39
|
+
raw: string,
|
|
40
|
+
selfPack: string
|
|
41
|
+
): { kind: "location" | "identity"; pack: string; name: string } {
|
|
42
|
+
if (raw.startsWith("@")) {
|
|
43
|
+
const rest = raw.slice(1);
|
|
44
|
+
const slashIdx = rest.indexOf("/");
|
|
45
|
+
if (slashIdx <= 0) {
|
|
46
|
+
throw new Error(`malformed @pack/name: "${raw}"(缺 / 或 pack 名为空)`);
|
|
47
|
+
}
|
|
48
|
+
const rawPack = rest.slice(0, slashIdx);
|
|
49
|
+
const name = rest.slice(slashIdx + 1);
|
|
50
|
+
if (!name) {
|
|
51
|
+
throw new Error(`malformed @pack/name: "${raw}"(asset 名为空)`);
|
|
52
|
+
}
|
|
53
|
+
const locAlias = LOCATION_ALIASES.get(rawPack);
|
|
54
|
+
if (locAlias) {
|
|
55
|
+
// 位置 alias:归一为短名(project → prj),kind=location
|
|
56
|
+
return { kind: "location", pack: locAlias, name };
|
|
57
|
+
}
|
|
58
|
+
// 身份 alias:原值(manifest.name 原样保留),kind=identity
|
|
59
|
+
return { kind: "identity", pack: rawPack, name };
|
|
60
|
+
}
|
|
61
|
+
// 不限定 ref:自动绑定 self.sourcePack——selfPack 是 manifest.name(身份),kind=identity
|
|
62
|
+
if (!selfPack) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`unqualified ref "${raw}" but profile has no sourcePack (load context missing)`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return { kind: "identity", pack: selfPack, name: raw };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 判断 raw 是否限定 ref(以 @ 开头)。诊断用。 */
|
|
71
|
+
export function isQualifiedRef(raw: string): boolean {
|
|
72
|
+
return raw.startsWith("@");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 位置 alias 归一(project → prj 等)。非位置 alias 原样返回。
|
|
76
|
+
* 与 parseRef 的 kind="location" 分支语义一致——独立暴露便于诊断。 */
|
|
77
|
+
export function normalizePackName(raw: string): string {
|
|
78
|
+
return LOCATION_ALIASES.get(raw) ?? raw;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ==================== §4.6:resolveBlueprint ====================
|
|
82
|
+
|
|
83
|
+
/** v15.x PR6(fix pt-parse-blueprint-warn-misleading):跨 pack Blueprint 解析。
|
|
84
|
+
* v15.x §4.4.2:workingSet 改为双索引 WorkingSet<Blueprint>——按 parseRef 的 kind 分发查询:
|
|
85
|
+
* - kind="location":查 location 索引(位置 alias 短名)
|
|
86
|
+
* - kind="identity":查 identity 索引(manifest.name)
|
|
87
|
+
* - 限定 ref(@pack/name):只在目标 pack 查;查不到返 undefined(精确语义)
|
|
88
|
+
* - 不限定 ref(foo):先在 self.sourcePack 查;查不到按 packNames 顺序 fallback
|
|
89
|
+
* - 与 transpile 阶段(§4.6 back-compat)行为等价——parse 阶段不再单独报
|
|
90
|
+
* "unknown Blueprint" warn(false positive),统一交给 compile 阶段 throw。
|
|
91
|
+
*
|
|
92
|
+
* 设计:parse 和 compile 共用同一份 fallback 逻辑,避免行为漂移。
|
|
93
|
+
* packNames 顺序 = [project, ...settings.reverse(), global, builtin],前者赢。
|
|
94
|
+
* fallback 时跳过 selfPack(已查过),按声明顺序查后续 pack(查 identity 索引,因 packNames 都是 pack.name = manifest.name)。 */
|
|
95
|
+
export function resolveBlueprint(
|
|
96
|
+
profile: { blueprint: string; sourcePack?: string },
|
|
97
|
+
blueprintWS: WorkingSet<Blueprint>,
|
|
98
|
+
packNames: readonly string[]
|
|
99
|
+
): { pack: AssetPack; asset: Blueprint } | undefined {
|
|
100
|
+
if (!profile.blueprint) return undefined;
|
|
101
|
+
const { kind, pack, name } = parseRef(profile.blueprint, profile.sourcePack ?? "");
|
|
102
|
+
const ws = kind === "location" ? blueprintWS.location : blueprintWS.identity;
|
|
103
|
+
const direct = ws.get(`${pack}/${name}`);
|
|
104
|
+
if (direct) return direct;
|
|
105
|
+
// 不限定 ref + direct miss → fallback 查找其他 pack(按 packNames 顺序查 identity 索引)
|
|
106
|
+
if (!profile.blueprint.startsWith("@")) {
|
|
107
|
+
for (const fb of packNames) {
|
|
108
|
+
if (fb === pack) continue;
|
|
109
|
+
const entry = blueprintWS.identity.get(`${fb}/${name}`);
|
|
110
|
+
if (entry) return entry;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ==================== §4.4.2:fingerprint ====================
|
|
117
|
+
|
|
118
|
+
/** v15.x PR3(§4.4.2):fingerprint = sha256(rootDir + content)。
|
|
119
|
+
* - rootDir 不同 → fp 不同(避免不同 pack 偶然同内容被合并)
|
|
120
|
+
* - content 不同 → fp 不同
|
|
121
|
+
* 用于 resolveAndDedupRefs 的 dedup by fp(后者覆盖前者)。 */
|
|
122
|
+
export function fingerprint<T>(pack: AssetPack, asset: T): string {
|
|
123
|
+
const input = `${pack.rootDir}\n${stableStringify(asset)}`;
|
|
124
|
+
return createHash("sha256").update(input).digest("hex").slice(0, 16);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 与 compile/agent-context.ts 同逻辑(PR3 不强制抽公共——YAGNI)。 */
|
|
128
|
+
function stableStringify(obj: unknown): string {
|
|
129
|
+
if (obj === null || typeof obj !== "object") return JSON.stringify(obj);
|
|
130
|
+
if (Array.isArray(obj)) return `[${obj.map(stableStringify).join(",")}]`;
|
|
131
|
+
if (!isRecord(obj)) return JSON.stringify(obj);
|
|
132
|
+
const keys = Object.keys(obj).sort();
|
|
133
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isRecord(x: unknown): x is Record<string, unknown> {
|
|
137
|
+
return !!x && typeof x === "object" && !Array.isArray(x);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ==================== §4.4.2:resolveAndDedupRefs ====================
|
|
141
|
+
|
|
142
|
+
/** v15.x §4.4.2(双层语义):解析 refs + fingerprint dedup(后者覆盖前者)。
|
|
143
|
+
* workingSet 改为 WorkingSet<T> 双索引——按 parseRef 的 kind 分发查询:
|
|
144
|
+
* - kind="location":查 location 索引(位置 alias 短名)
|
|
145
|
+
* - kind="identity":查 identity 索引(manifest.name)
|
|
146
|
+
* fallback 查其他 pack 时查 identity 索引(packNames 是 manifest.name 列表)。
|
|
147
|
+
*
|
|
148
|
+
* 阶段 1:所有 ref 解析为 {pack, asset, fp, rawRef}(保留原顺序)
|
|
149
|
+
* 阶段 2:dedup by fp——Map.set 同 fp 后写覆盖前写 = 后者赢
|
|
150
|
+
*
|
|
151
|
+
* 返回值按首次插入顺序,值=最后写入的 ref(同 fp 后者赢)。
|
|
152
|
+
* 场景 A/B/C(同 fp)→ dedup 后 1 份;场景 D/E/F(不同 fp)→ 保留多份。
|
|
153
|
+
*
|
|
154
|
+
* 错误信息含 loadedPackNames 帮用户定位(§4.5.1)。 */
|
|
155
|
+
export function resolveAndDedupRefs<T extends { name: string }>(
|
|
156
|
+
refs: string[],
|
|
157
|
+
self: Profile,
|
|
158
|
+
workingSet: WorkingSet<T>,
|
|
159
|
+
loadedPackNames: string[],
|
|
160
|
+
options: { skipOnMissing?: boolean } = {}
|
|
161
|
+
): Array<{ pack: AssetPack; asset: T; fp: string; rawRef: string }> {
|
|
162
|
+
const resolved: Array<{ pack: AssetPack; asset: T; fp: string; rawRef: string }> = [];
|
|
163
|
+
for (const raw of refs) {
|
|
164
|
+
const { kind, pack: packName, name } = parseRef(raw, self.sourcePack ?? "");
|
|
165
|
+
const ws = kind === "location" ? workingSet.location : workingSet.identity;
|
|
166
|
+
let entry = ws.get(`${packName}/${name}`);
|
|
167
|
+
// v15.x PR3(§4.6 back-compat):显式 @pack/name 查不到 → 抛错(§4.5.1)。
|
|
168
|
+
// 不限定 ref 在 selfPack 查不到时(§4.6 等价今天 dedupByNameN 语义):
|
|
169
|
+
// 按 packs 优先级顺序逐个 fallback 查询(查 identity 索引,packNames 是 manifest.name 列表)。
|
|
170
|
+
// 例:profile 写在 project pack,不限定 `foo` → identity "pt-internal/foo" 不命中 → fallback "gbl/foo" → "pt/foo"。
|
|
171
|
+
if (!entry && !raw.startsWith("@")) {
|
|
172
|
+
for (const fallbackPack of loadedPackNames) {
|
|
173
|
+
if (fallbackPack === packName) continue;
|
|
174
|
+
const fallbackEntry = workingSet.identity.get(`${fallbackPack}/${name}`);
|
|
175
|
+
if (fallbackEntry) {
|
|
176
|
+
entry = fallbackEntry;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!entry) {
|
|
182
|
+
// skipOnMissing:scan 场景用——静默跳过不存在的 ref(与 dedupByNameN 旧行为一致)
|
|
183
|
+
if (options.skipOnMissing) continue;
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Profile "${self.name}" references "@${packName}/${name}" but pack "${packName}" has no asset "${name}". ` +
|
|
186
|
+
`Loaded packs: [${loadedPackNames.join(", ")}]`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const fp = fingerprint(entry.pack, entry.asset);
|
|
190
|
+
resolved.push({ pack: entry.pack, asset: entry.asset, fp, rawRef: raw });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const byFp = new Map<string, (typeof resolved)[0]>();
|
|
194
|
+
for (const r of resolved) {
|
|
195
|
+
byFp.set(r.fp, r); // 同 fp 后写覆盖前写 = 后者赢
|
|
196
|
+
}
|
|
197
|
+
return [...byFp.values()]; // 保留首次插入顺序
|
|
198
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// src/parse/shared.ts — 通用 MD 词法+语法(domain/blueprint/profile 共享)
|
|
2
|
+
//
|
|
3
|
+
// Phase 7.3:从原 src/parse/{parser,types}.ts 抽公共部分。
|
|
4
|
+
// - Domain/Blueprint/Profile adapter 都按 H2 段解析,H3 子项 → Item。
|
|
5
|
+
// - 共享 frontmatter 解析、H2 段切分、H3 项解析、scalar 值解析。
|
|
6
|
+
// - 共享 Asset/Section/Item 类型。
|
|
7
|
+
//
|
|
8
|
+
// Phase 9.3:v9 适配 — Channel 删除(Channel 留作未来 Connector),inferKind 用 frontmatter 字段。
|
|
9
|
+
// AssetKind 去除 "channel"——但保留 type 值以防历史资源解析时遗留错误。
|
|
10
|
+
|
|
11
|
+
import { readFile } from "node:fs/promises";
|
|
12
|
+
|
|
13
|
+
// ==================== 类型(OXN 中间表示,Pt 核心不见) ====================
|
|
14
|
+
|
|
15
|
+
/** MD 资产内部 kind(v9 适配)。
|
|
16
|
+
* - "domain" : Content Domain(v7/v9 内容层)
|
|
17
|
+
* - "blueprint" : Blueprint(v7 配置层 / v9 结构层)
|
|
18
|
+
* - "profile" : Profile(v9 配置层,新增)
|
|
19
|
+
* - "term" / "workflow" / "glossary" : Domain type 标签(frontmatter.type,P9 删 stack 死类型后残留的历史枚举值)
|
|
20
|
+
* - "scene" / "manual" : v6 Struct kind 兼容(v7 资产迁移期残留)
|
|
21
|
+
*
|
|
22
|
+
* v9 删除了 "channel"(Channel 留作未来 Connector,本版本不实现)——不再出现在 kind 联合中。
|
|
23
|
+
* 如解析到历史资源带 channel 段,frontmatter.kind 可 指 配 "channel"(保留为字符串兜底)。 */
|
|
24
|
+
export type AssetKind =
|
|
25
|
+
| "domain"
|
|
26
|
+
| "blueprint"
|
|
27
|
+
| "profile"
|
|
28
|
+
| "term"
|
|
29
|
+
| "workflow"
|
|
30
|
+
| "glossary"
|
|
31
|
+
| "scene"
|
|
32
|
+
| "manual"
|
|
33
|
+
// 兜底:v8 channel 历史资源可能含 frontmatter.kind: "channel"
|
|
34
|
+
| "channel";
|
|
35
|
+
|
|
36
|
+
export interface Item {
|
|
37
|
+
/** H3 标题(如 "文章"、"select-topic") */
|
|
38
|
+
name: string;
|
|
39
|
+
/** "- key: value" / "- key: [a, b]" / "- key: { ... }" 字段。
|
|
40
|
+
* 值可以是 string / string[] / 嵌套对象(YAML 子集);consumer 用 typeof 收窄。 */
|
|
41
|
+
fields: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface Section {
|
|
45
|
+
/** "## Terms" 原文 */
|
|
46
|
+
heading: string;
|
|
47
|
+
/** 段内 markdown(含 H3 子项) */
|
|
48
|
+
raw: string;
|
|
49
|
+
/** H3 子项解析结果 */
|
|
50
|
+
items: Item[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface Asset {
|
|
54
|
+
kind: AssetKind;
|
|
55
|
+
/** 文件名去后缀(v7/v8 例:"project-dev.channel" / "pt-blueprint") */
|
|
56
|
+
name: string;
|
|
57
|
+
frontmatter: Record<string, unknown>;
|
|
58
|
+
/** frontmatter 之后的 markdown */
|
|
59
|
+
body: string;
|
|
60
|
+
/** H2 段名(首字母大写)→ 段内容 */
|
|
61
|
+
sections: Record<string, Section>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ==================== 词法/语法(共享) ====================
|
|
65
|
+
|
|
66
|
+
/** 读 asset 文件并解析 frontmatter + H2 段。 */
|
|
67
|
+
export async function readAsset(path: string): Promise<Asset> {
|
|
68
|
+
const raw = await readFile(path, "utf8");
|
|
69
|
+
const { fm, body } = parseFrontmatter(raw);
|
|
70
|
+
|
|
71
|
+
// 从文件路径末尾推 name(无后缀)
|
|
72
|
+
const fileName = path.split("/").pop() ?? "";
|
|
73
|
+
const name = fileName.replace(/\.[^.]+$/, "");
|
|
74
|
+
|
|
75
|
+
// Phase term-P9.3:type 字段已删——kind 推断逻辑仅保留为兼容(无 caller 实际使用)。
|
|
76
|
+
// 资产加载走目录路由(src/parse/index.ts loadAllDomains / loadAllBlueprints / loadAllProfiles),
|
|
77
|
+
// 不依赖 readAsset 的 kind 字段。
|
|
78
|
+
const kind: AssetKind = "domain"; // 默认值——保留 AssetKind 联合类型以备未来扩展
|
|
79
|
+
|
|
80
|
+
const sectionsArr = splitSections(body);
|
|
81
|
+
const sections: Record<string, Section> = {};
|
|
82
|
+
for (const sec of sectionsArr) {
|
|
83
|
+
const key = sec.heading.replace(/^#+\s*/, "").trim();
|
|
84
|
+
sections[key] = { heading: sec.heading, raw: sec.raw, items: parseItems(sec.raw) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { kind, name, frontmatter: fm, body, sections };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 解析 frontmatter --- ... --- 块(简易:key: value / key: [a, b] / key: { ... }) */
|
|
91
|
+
export function parseFrontmatter(text: string): {
|
|
92
|
+
fm: Record<string, unknown>;
|
|
93
|
+
body: string;
|
|
94
|
+
} {
|
|
95
|
+
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
96
|
+
if (!m) return { fm: {}, body: text };
|
|
97
|
+
const fm: Record<string, unknown> = {};
|
|
98
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
99
|
+
const kv = line.match(/^([a-zA-Z_][\w-]*)\s*:\s*(.*)$/);
|
|
100
|
+
if (!kv) continue;
|
|
101
|
+
const key = kv[1];
|
|
102
|
+
const val = kv[2].trim();
|
|
103
|
+
fm[key] = parseScalar(val);
|
|
104
|
+
}
|
|
105
|
+
return { fm, body: m[2] };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 解析 scalar 值:string / string[] / 嵌套对象(YAML 子集)。
|
|
109
|
+
* 嵌套值通过 parseScalar 递归解析,支持任意深度。 */
|
|
110
|
+
function parseScalar(val: string): unknown {
|
|
111
|
+
if (!val) return "";
|
|
112
|
+
// [a, b]
|
|
113
|
+
const arrMatch = val.match(/^\[(.*)\]$/);
|
|
114
|
+
if (arrMatch) {
|
|
115
|
+
return arrMatch[1]
|
|
116
|
+
.split(",")
|
|
117
|
+
.map((s) => unquote(s.trim()))
|
|
118
|
+
.filter((s) => s !== "");
|
|
119
|
+
}
|
|
120
|
+
// { key: value, ... } → 嵌套对象
|
|
121
|
+
if (/^\{.*\}$/.test(val.trim())) {
|
|
122
|
+
return parseInlineObject(val.trim());
|
|
123
|
+
}
|
|
124
|
+
return unquote(val);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseInlineObject(s: string): Record<string, unknown> {
|
|
128
|
+
const inner = s.slice(1, -1).trim();
|
|
129
|
+
if (!inner) return {};
|
|
130
|
+
const out: Record<string, unknown> = {};
|
|
131
|
+
for (const part of splitTopLevel(inner, ",")) {
|
|
132
|
+
const kv = part.match(/^\s*([a-zA-Z_][\w-]*)\s*:\s*(.+)$/);
|
|
133
|
+
if (kv) out[kv[1]] = parseScalar(kv[2].trim());
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function splitTopLevel(s: string, sep: string): string[] {
|
|
139
|
+
const out: string[] = [];
|
|
140
|
+
let depth = 0;
|
|
141
|
+
let buf = "";
|
|
142
|
+
for (const ch of s) {
|
|
143
|
+
if (ch === "{" || ch === "[") depth++;
|
|
144
|
+
else if (ch === "}" || ch === "]") depth--;
|
|
145
|
+
if (ch === sep && depth === 0) {
|
|
146
|
+
out.push(buf);
|
|
147
|
+
buf = "";
|
|
148
|
+
} else {
|
|
149
|
+
buf += ch;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (buf.trim()) out.push(buf);
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function unquote(s: string): string {
|
|
157
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
158
|
+
return s.slice(1, -1);
|
|
159
|
+
}
|
|
160
|
+
return s;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** 按 `## ` 切 H2 段。H1 `# ` 不在此处理(asset 一般只有 1 个 H1 当标题) */
|
|
164
|
+
export function splitSections(body: string): Array<{ heading: string; raw: string }> {
|
|
165
|
+
const lines = body.split(/\r?\n/);
|
|
166
|
+
const sections: Array<{ heading: string; raw: string }> = [];
|
|
167
|
+
let cur: { heading: string; lines: string[] } | null = null;
|
|
168
|
+
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
const h2 = line.match(/^##\s+(.+)$/);
|
|
171
|
+
if (h2) {
|
|
172
|
+
if (cur) sections.push({ heading: cur.heading, raw: cur.lines.join("\n") });
|
|
173
|
+
cur = { heading: line.trim(), lines: [] };
|
|
174
|
+
} else if (cur) {
|
|
175
|
+
cur.lines.push(line);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (cur) sections.push({ heading: cur.heading, raw: cur.lines.join("\n") });
|
|
179
|
+
return sections;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** 把段内 markdown 解析成 Item[]:
|
|
183
|
+
* - 有 H3 (`### name`) → 每个 H3 是一个 item,下属 `- key: value` 是 fields
|
|
184
|
+
* - 无 H3,整段就是 list → 顶层 `- key: value` / `- name` 直接作为 items
|
|
185
|
+
*
|
|
186
|
+
* 顶层 list 支持两种行形态:
|
|
187
|
+
* - `- key: value` → { name: "key", fields: { key: value } }
|
|
188
|
+
* - `- name` → { name: "name", fields: {} }(裸名列表,例 ## Modules 段)
|
|
189
|
+
*/
|
|
190
|
+
export function parseItems(sectionRaw: string): Item[] {
|
|
191
|
+
const lines = sectionRaw.split(/\r?\n/);
|
|
192
|
+
const hasH3 = lines.some((l) => /^###\s+/.test(l));
|
|
193
|
+
|
|
194
|
+
if (hasH3) {
|
|
195
|
+
const items: Item[] = [];
|
|
196
|
+
let cur: Item | null = null;
|
|
197
|
+
for (const line of lines) {
|
|
198
|
+
const h3 = line.match(/^###\s+(.+)$/);
|
|
199
|
+
if (h3) {
|
|
200
|
+
if (cur) items.push(cur);
|
|
201
|
+
cur = { name: h3[1].trim(), fields: {} };
|
|
202
|
+
} else if (cur) {
|
|
203
|
+
const fv = line.match(/^\s*-\s+([a-zA-Z_][\w-]*)\s*:\s*(.+)$/);
|
|
204
|
+
if (fv) cur.fields[fv[1]] = parseScalar(fv[2].trim());
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (cur) items.push(cur);
|
|
208
|
+
return items;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 无 H3:顶层 list 直接当 items(支持 `- key: value` 和 `- name` 两种行)
|
|
212
|
+
const items: Item[] = [];
|
|
213
|
+
for (const line of lines) {
|
|
214
|
+
const fv = line.match(/^\s*-\s+([a-zA-Z_][\w-]*)\s*:\s*(.+)$/);
|
|
215
|
+
if (fv) {
|
|
216
|
+
items.push({
|
|
217
|
+
name: fv[1],
|
|
218
|
+
fields: { [fv[1]]: parseScalar(fv[2].trim()) },
|
|
219
|
+
});
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const bare = line.match(/^\s*-\s+(.+?)\s*$/);
|
|
223
|
+
if (bare) {
|
|
224
|
+
items.push({ name: bare[1].trim(), fields: {} });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return items;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ==================== 字段取值辅助(共享) ====================
|
|
231
|
+
|
|
232
|
+
/** 把 unknown 收窄为 string(数组则 join)。无值 → ""。 */
|
|
233
|
+
export function s(v: unknown): string {
|
|
234
|
+
if (typeof v === "string") return v;
|
|
235
|
+
if (Array.isArray(v)) return v.filter((x): x is string => typeof x === "string").join(", ");
|
|
236
|
+
return "";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** 把 unknown 收窄为 string[](单值包成 1-数组)。无值 → []。 */
|
|
240
|
+
export function sArr(v: unknown): string[] {
|
|
241
|
+
if (Array.isArray(v)) return v.filter((x): x is string => typeof x === "string");
|
|
242
|
+
if (typeof v === "string" && v) return [v];
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** 从一个 H2 段里取某个字段的标量值(兼容裸值 / 顶层 list `- key: value` / H3 项 fields.key)。
|
|
247
|
+
* 用途:Blueprint 聚合组 H2 下读 inject/mode,Profile 聚合组 H2 下读 ### Domains 追加列表。 */
|
|
248
|
+
export function extractFieldValue(section: Section | undefined, key: string): string {
|
|
249
|
+
if (!section) return "";
|
|
250
|
+
|
|
251
|
+
// 1. 顶层 list 行:`- inject: session`
|
|
252
|
+
for (const item of section.items) {
|
|
253
|
+
const v = item.fields[key];
|
|
254
|
+
if (v !== undefined) {
|
|
255
|
+
const sv = s(v);
|
|
256
|
+
if (sv) return sv;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 2. 裸值段(## H2 后第一行非空文本)—— 取第一个匹配 `key: value` 的行
|
|
261
|
+
for (const line of section.raw.split(/\r?\n/)) {
|
|
262
|
+
const m = line.match(new RegExp(`^\\s*${key}\\s*:\\s*(.+?)\\s*$`));
|
|
263
|
+
if (m) {
|
|
264
|
+
return s(parseScalar(m[1].trim()));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return "";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** 同 extractBareListUnderH3(section, "Modules") 的别名(Channel 用)。 */
|
|
272
|
+
export function extractModulesList(section: Section | undefined): string[] {
|
|
273
|
+
return extractBareListUnderH3(section, "Modules");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** 同 extractBareListUnderH3(section, "Domains") 的别名(Blueprint 用)。 */
|
|
277
|
+
export function extractDomainsList(section: Section | undefined): string[] {
|
|
278
|
+
return extractBareListUnderH3(section, "Domains");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** 从一个 H2 段下取指定 H3 名下的所有裸名列表项(`- name`)。
|
|
282
|
+
* 用途:Blueprint `## 会话背景` → `### Modules` 列 Domain Schema Name;
|
|
283
|
+
* Profile `## 会话背景` → `### Domains` 列参与本聚合组的 Domain 名。
|
|
284
|
+
*
|
|
285
|
+
* 行为:扫描 section.raw,找到 `### <h3Name>` 行后收集紧随其后的 `- name` 行
|
|
286
|
+
* (无 `key: value`),遇下一个 H3 或段尾终止。
|
|
287
|
+
*
|
|
288
|
+
* 兼容:若 H3 项的 fields.modules / fields.refs 是数组(旧写法),也支持。 */
|
|
289
|
+
export function extractBareListUnderH3(section: Section | undefined, h3Name: string): string[] {
|
|
290
|
+
if (!section) return [];
|
|
291
|
+
|
|
292
|
+
// 兼容路径:若 fields 里有数组字段(旧写法 `### Modules` 下用 `- modules: [Scene]` 等)
|
|
293
|
+
const item = section.items.find((it) => it.name === h3Name);
|
|
294
|
+
if (item) {
|
|
295
|
+
const arr = item.fields.modules ?? item.fields.refs;
|
|
296
|
+
if (Array.isArray(arr)) {
|
|
297
|
+
const strs = arr.filter((x): x is string => typeof x === "string");
|
|
298
|
+
if (strs.length > 0) return strs;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 主路径:扫 raw text 找 H3 + 后随的 `- name` 行
|
|
303
|
+
const lines = section.raw.split(/\r?\n/);
|
|
304
|
+
const out: string[] = [];
|
|
305
|
+
let inTarget = false;
|
|
306
|
+
for (const line of lines) {
|
|
307
|
+
const h3 = line.match(/^###\s+(.+)$/);
|
|
308
|
+
if (h3) {
|
|
309
|
+
const name = h3[1].trim();
|
|
310
|
+
inTarget = name === h3Name;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (!inTarget) continue;
|
|
314
|
+
// 裸名行:`- Scene` / `- pt-architecture`(无冒号或冒号后无值)
|
|
315
|
+
const bare = line.match(/^\s*-\s+([^\s:]+)\s*$/);
|
|
316
|
+
if (bare) {
|
|
317
|
+
out.push(bare[1].trim());
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
// H4 段也终止(子嵌套不展开)
|
|
321
|
+
if (/^#+\s/.test(line)) break;
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Phase term-P9.5:删除 inferKind / inferKindFromFrontmatter 死代码。
|
|
327
|
+
// Type 字段删除(P9.3)+ Blueprint 转 YAML(P4.5)后,asset kind 推断链路(frontmatter
|
|
328
|
+
// type/agent + H2 Slots/Tools/Compilation 识别)全部失去作用——资产加载按目录路由
|
|
329
|
+
// (src/parse/index.ts loadAllDomains/Blueprints/Profiles),不依赖 readAsset 的 kind 字段。
|
|
330
|
+
// AssetKind 联合类型保留(供未来扩展),具体推断函数清理。
|
|
331
|
+
// Channel 已删除,Domain 走通用 term 形态。 */
|