@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.
Files changed (166) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/dist/agent/api-bridge.d.ts +8 -0
  4. package/dist/agent/api-bridge.js +83 -0
  5. package/dist/agent/index.d.ts +2 -0
  6. package/dist/agent/index.js +3 -0
  7. package/dist/agent/pi-adapter.d.ts +35 -0
  8. package/dist/agent/pi-adapter.js +251 -0
  9. package/dist/agent/registry.d.ts +4 -0
  10. package/dist/agent/registry.js +40 -0
  11. package/dist/asset-health.d.ts +50 -0
  12. package/dist/asset-health.js +267 -0
  13. package/dist/asset-pack/loader.d.ts +47 -0
  14. package/dist/asset-pack/loader.js +121 -0
  15. package/dist/asset-pack/manifest.d.ts +23 -0
  16. package/dist/asset-pack/manifest.js +96 -0
  17. package/dist/asset-pack/md-file-pack.d.ts +41 -0
  18. package/dist/asset-pack/md-file-pack.js +153 -0
  19. package/dist/asset-pack/validate.d.ts +55 -0
  20. package/dist/asset-pack/validate.js +131 -0
  21. package/dist/builtin/assets/blueprints/dev-knowledge.blueprint.yaml +13 -0
  22. package/dist/builtin/assets/domains/agent-info.md +15 -0
  23. package/dist/builtin/assets/domains/authoring.md +201 -0
  24. package/dist/builtin/assets/domains/pack-repair.md +47 -0
  25. package/dist/builtin/assets/domains/project-analysis.md +52 -0
  26. package/dist/builtin/assets/domains/usage.md +81 -0
  27. package/dist/builtin/assets/domains/user-info.md +15 -0
  28. package/dist/builtin/assets/profiles/guide.profile.md +28 -0
  29. package/dist/commands.d.ts +69 -0
  30. package/dist/commands.js +344 -0
  31. package/dist/compile/agent-context.d.ts +48 -0
  32. package/dist/compile/agent-context.js +423 -0
  33. package/dist/compile/index.d.ts +1 -0
  34. package/dist/compile/index.js +5 -0
  35. package/dist/compile/resolve-use.d.ts +36 -0
  36. package/dist/compile/resolve-use.js +171 -0
  37. package/dist/compile/type-guards.d.ts +29 -0
  38. package/dist/compile/type-guards.js +107 -0
  39. package/dist/config.d.ts +31 -0
  40. package/dist/config.js +133 -0
  41. package/dist/constants.d.ts +52 -0
  42. package/dist/constants.js +81 -0
  43. package/dist/diagnostics.d.ts +14 -0
  44. package/dist/diagnostics.js +29 -0
  45. package/dist/index.d.ts +2 -0
  46. package/dist/index.js +838 -0
  47. package/dist/injection-status.d.ts +45 -0
  48. package/dist/injection-status.js +134 -0
  49. package/dist/log.d.ts +64 -0
  50. package/dist/log.js +139 -0
  51. package/dist/manual-session.d.ts +24 -0
  52. package/dist/manual-session.js +152 -0
  53. package/dist/manual-track.d.ts +35 -0
  54. package/dist/manual-track.js +107 -0
  55. package/dist/parse/blueprint.d.ts +5 -0
  56. package/dist/parse/blueprint.js +59 -0
  57. package/dist/parse/domain-renderers.d.ts +8 -0
  58. package/dist/parse/domain-renderers.js +115 -0
  59. package/dist/parse/domain.d.ts +7 -0
  60. package/dist/parse/domain.js +69 -0
  61. package/dist/parse/index.d.ts +9 -0
  62. package/dist/parse/index.js +223 -0
  63. package/dist/parse/profile.d.ts +18 -0
  64. package/dist/parse/profile.js +117 -0
  65. package/dist/parse/ref-resolver.d.ts +63 -0
  66. package/dist/parse/ref-resolver.js +174 -0
  67. package/dist/parse/shared.d.ts +76 -0
  68. package/dist/parse/shared.js +273 -0
  69. package/dist/profile-persist.d.ts +22 -0
  70. package/dist/profile-persist.js +53 -0
  71. package/dist/render/cache.d.ts +15 -0
  72. package/dist/render/cache.js +135 -0
  73. package/dist/render/index.d.ts +3 -0
  74. package/dist/render/index.js +14 -0
  75. package/dist/render/session-inject.d.ts +5 -0
  76. package/dist/render/session-inject.js +24 -0
  77. package/dist/render/turn-inject.d.ts +36 -0
  78. package/dist/render/turn-inject.js +248 -0
  79. package/dist/schema.d.ts +448 -0
  80. package/dist/schema.js +45 -0
  81. package/dist/session.d.ts +82 -0
  82. package/dist/session.js +102 -0
  83. package/dist/slog.d.ts +3 -0
  84. package/dist/slog.js +27 -0
  85. package/dist/transpile.d.ts +34 -0
  86. package/dist/transpile.js +220 -0
  87. package/dist/verify/file-hash.d.ts +2 -0
  88. package/dist/verify/file-hash.js +30 -0
  89. package/dist/verify/fs-content-match.d.ts +2 -0
  90. package/dist/verify/fs-content-match.js +26 -0
  91. package/dist/verify/fs-exists.d.ts +2 -0
  92. package/dist/verify/fs-exists.js +13 -0
  93. package/dist/verify/fs-not-exists.d.ts +2 -0
  94. package/dist/verify/fs-not-exists.js +13 -0
  95. package/dist/verify/git-status-clean.d.ts +2 -0
  96. package/dist/verify/git-status-clean.js +19 -0
  97. package/dist/verify/index.d.ts +7 -0
  98. package/dist/verify/index.js +46 -0
  99. package/dist/verify/lint-check.d.ts +2 -0
  100. package/dist/verify/lint-check.js +17 -0
  101. package/dist/verify/read-stderr.d.ts +5 -0
  102. package/dist/verify/read-stderr.js +20 -0
  103. package/dist/verify/ref-check.d.ts +13 -0
  104. package/dist/verify/ref-check.js +88 -0
  105. package/dist/verify/test-pass.d.ts +2 -0
  106. package/dist/verify/test-pass.js +17 -0
  107. package/dist/verify/ts-compiles.d.ts +2 -0
  108. package/dist/verify/ts-compiles.js +17 -0
  109. package/package.json +62 -0
  110. package/src/agent/api-bridge.ts +105 -0
  111. package/src/agent/index.ts +4 -0
  112. package/src/agent/pi-adapter.ts +326 -0
  113. package/src/agent/registry.ts +44 -0
  114. package/src/asset-health.ts +327 -0
  115. package/src/asset-pack/loader.ts +141 -0
  116. package/src/asset-pack/manifest.ts +118 -0
  117. package/src/asset-pack/md-file-pack.ts +202 -0
  118. package/src/asset-pack/validate.ts +186 -0
  119. package/src/builtin/assets/blueprints/dev-knowledge.blueprint.yaml +13 -0
  120. package/src/builtin/assets/domains/agent-info.md +15 -0
  121. package/src/builtin/assets/domains/authoring.md +201 -0
  122. package/src/builtin/assets/domains/pack-repair.md +47 -0
  123. package/src/builtin/assets/domains/project-analysis.md +52 -0
  124. package/src/builtin/assets/domains/usage.md +81 -0
  125. package/src/builtin/assets/domains/user-info.md +15 -0
  126. package/src/builtin/assets/profiles/guide.profile.md +28 -0
  127. package/src/commands.ts +405 -0
  128. package/src/compile/agent-context.ts +487 -0
  129. package/src/compile/index.ts +5 -0
  130. package/src/compile/resolve-use.ts +208 -0
  131. package/src/compile/type-guards.ts +132 -0
  132. package/src/config.ts +154 -0
  133. package/src/constants.ts +104 -0
  134. package/src/diagnostics.ts +36 -0
  135. package/src/index.ts +1028 -0
  136. package/src/injection-status.ts +155 -0
  137. package/src/log.ts +173 -0
  138. package/src/manual-session.ts +190 -0
  139. package/src/manual-track.ts +127 -0
  140. package/src/parse/blueprint.ts +82 -0
  141. package/src/parse/domain-renderers.ts +120 -0
  142. package/src/parse/domain.ts +78 -0
  143. package/src/parse/index.ts +266 -0
  144. package/src/parse/profile.ts +139 -0
  145. package/src/parse/ref-resolver.ts +198 -0
  146. package/src/parse/shared.ts +331 -0
  147. package/src/profile-persist.ts +60 -0
  148. package/src/render/cache.ts +150 -0
  149. package/src/render/index.ts +14 -0
  150. package/src/render/session-inject.ts +26 -0
  151. package/src/render/turn-inject.ts +278 -0
  152. package/src/schema.ts +542 -0
  153. package/src/session.ts +190 -0
  154. package/src/slog.ts +32 -0
  155. package/src/transpile.ts +302 -0
  156. package/src/verify/file-hash.ts +29 -0
  157. package/src/verify/fs-content-match.ts +28 -0
  158. package/src/verify/fs-exists.ts +14 -0
  159. package/src/verify/fs-not-exists.ts +17 -0
  160. package/src/verify/git-status-clean.ts +23 -0
  161. package/src/verify/index.ts +57 -0
  162. package/src/verify/lint-check.ts +21 -0
  163. package/src/verify/read-stderr.ts +24 -0
  164. package/src/verify/ref-check.ts +118 -0
  165. package/src/verify/test-pass.ts +18 -0
  166. package/src/verify/ts-compiles.ts +21 -0
@@ -0,0 +1,40 @@
1
+ // src/agent/registry.ts — AgentAdapter 注册表
2
+ //
3
+ // Phase 9.6:v9 新增 — 按 Agent 名取 Adapter 实例。
4
+ // 加新 Agent(如 Codex / OpenCode)只在这里加一行注册 + 实现 Adapter。
5
+ //
6
+ // v12.x(issue pt-session-singleton-pi-web-pollution 修复):
7
+ // - 原设计 `agentAdapters: Record<string, AgentAdapter>` 是 module-level 单例
8
+ // → 所有 session 共享同一个 PiAdapter 实例 → 单例 `this.segment / this.ctx /
9
+ // this.blueprint / this.domains / this.injectedApi` 跨 session 被覆盖
10
+ // → tab B 调 setContext 覆盖 this.segment → tab A 的 before_agent_start handler
11
+ // 读到 tab B 的 segment → LLM 拿到错业务上下文(业务上下文级污染)
12
+ // - 改造为 `WeakMap<ExtensionAPI, Record<string, AgentAdapter>>`:
13
+ // - 每 session 一份 `Record<name, Adapter>` 缓存
14
+ // - pi 实例销毁时(pi-web session 退出)WeakMap 自动 GC
15
+ // - TUI 模式只有一个 pi 实例,行为等同 module-level 单例
16
+ // - getAgentAdapter 签名改成 `(pi, name)`:调用方传 pi 作 key
17
+ // - `src/index.ts:105` `transpileActive` 改 `getAgentAdapter(pi, ...)`
18
+ // - `src/index.ts:70-77` `registerInjectionIfReady` 调 `adapter.registerInject(...)`
19
+ // 时拿到的 adapter 已是 per-pi 实例,handler 闭包持 this(per-pi),this.segment
20
+ // 不被其他 session 覆盖
21
+ import { PiAdapter } from "./pi-adapter.js";
22
+ /** v12.x:per-pi Adapter 缓存(弱引用,pi 实例销毁后自动 GC)。 */
23
+ const adapterCache = new WeakMap();
24
+ /** 取指定 pi + name 的 AgentAdapter。未找到返 pi 的 per-pi 实例(fallback)。 */
25
+ export function getAgentAdapter(pi, name) {
26
+ let cache = adapterCache.get(pi);
27
+ if (!cache) {
28
+ cache = {};
29
+ adapterCache.set(pi, cache);
30
+ }
31
+ let adapter = cache[name];
32
+ if (!adapter) {
33
+ // 当前只支持 pi;未来 codex/opencode 在这里 switch name
34
+ adapter = new PiAdapter();
35
+ cache[name] = adapter;
36
+ if (!cache.pi)
37
+ cache.pi = adapter; // 把 fallback 也指到同一实例
38
+ }
39
+ return adapter;
40
+ }
@@ -0,0 +1,50 @@
1
+ import type { AssetPack } from "./schema.js";
2
+ import type { Blueprint, Domain, Profile, SourceAdapterContext } from "./schema.js";
3
+ /** 问题严重程度。error 必须修;warning 可延后。 */
4
+ export type IssueSeverity = "error" | "warning";
5
+ /** 问题归属:profile / blueprint / domain。本 issue v1 范围只产 profile 类。 */
6
+ export type IssueScope = "profile" | "blueprint" | "domain";
7
+ /** 单条问题。issue-doc-structure 类似——结构化便于测试断言 + UI 格式化 + 自动 fix。 */
8
+ export interface AssetHealthIssue {
9
+ severity: IssueSeverity;
10
+ scope: IssueScope;
11
+ /** 问题所在资产名(profile.name / blueprint.name / domain.name)。 */
12
+ name: string;
13
+ /** 可选字段路径(如 "groups.会话背景.modules"),便于精确指向。 */
14
+ field?: string;
15
+ /** 人类可读问题描述。 */
16
+ msg: string;
17
+ /** 修复建议(hint)。 */
18
+ hint?: string;
19
+ /** 自动 fix 命令(v2 `--fix` 用,本 issue v1 不实现执行)。 */
20
+ fix?: string;
21
+ }
22
+ /** 规则 id。issue §Layer 2 5 条规则 → 5 个 id。 */
23
+ export type HealthRuleId = "missing-modules" | "dangling-blueprint-ref" | "orphan-h2" | "empty-segment" | "unknown-modname";
24
+ /** 资产健康扫描结果(按 profile 聚合)。 */
25
+ export interface AssetHealthReport {
26
+ issues: AssetHealthIssue[];
27
+ errors: number;
28
+ warnings: number;
29
+ }
30
+ /** 扫描整个项目的资产配置(issue §Layer 2 主入口)。
31
+ *
32
+ * 检测 5 类反模式:
33
+ * 1. missing-modules ProfileGroup.modules 为空
34
+ * 2. dangling-blueprint-ref profile.blueprint 不存在
35
+ * 3. orphan-h2 ProfileGroup.name 不在 Blueprint.groups
36
+ * 4. empty-segment compileAgentContext 产出为空段
37
+ * 5. unknown-modname `### Modules` 项不在 KNOWN_SECTION_NAMES(需重读文件)
38
+ *
39
+ * 参数:
40
+ * - cwd:项目根目录(unknown-modname 重读 profile 文件需要)
41
+ * - profiles / blueprints / domains:从 SchemaBundle 取的全集
42
+ * - adapterCtx:可选。log writer 用于 trace(warn 不走 notify——会刷屏)
43
+ *
44
+ * 调用方:session_start / /pt check / pt_check tool。
45
+ *
46
+ * 不抛错——任何内层异常降级为 log warn + 不阻止结果。体检失败不该阻塞 session 启动。
47
+ */
48
+ export declare function scanProjectHealth(cwd: string, profiles: Profile[], blueprints: Blueprint[], domains: Domain[], packs: AssetPack[], // v15.x PR2:compileAgentContext 需要 packs 进 sourceHash
49
+ profilePack: string, // v15.x PR2:scan 时用 default "prj"——不真正读 pack 信息
50
+ adapterCtx?: SourceAdapterContext): Promise<AssetHealthReport>;
@@ -0,0 +1,267 @@
1
+ // src/asset-health.ts — 资产配置体检(issue pt-asset-migration-visibility Layer 2)
2
+ //
3
+ // 设计动机:存量项目升级 Pt 后无迁移可见性。
4
+ // - v9.1 modules-to-profile 迁移后,旧 profile 缺 `### Modules` → 空 segment
5
+ // - 单 profile 运行时检测(issue pt-status-no-injection-state 修复 2/3)只在切换时触发
6
+ // - session_start 批量体检:主动告知"项目有 N 个配置问题",不踩坑
7
+ //
8
+ // 5 条规则(issue §Layer 2 表):
9
+ // 1. missing-modules (error) ProfileGroup.modules 为空
10
+ // 2. dangling-blueprint-ref (error) profile.blueprint 不存在
11
+ // 3. orphan-h2 (warning) ProfileGroup.name 不在 Blueprint.groups
12
+ // 4. empty-segment (error) compileAgentContext 产出为空段
13
+ // 5. unknown-modname (warning) `### Modules` 项不在 KNOWN_SECTION_NAMES
14
+ //
15
+ // 边界纪律:
16
+ // - 不替代运行时检测(transpile 内的 reportWarn)——两层互补:运行时按 profile 切,scan 按全集
17
+ // - 不写资产——只检测 + 报告;修复走 `/pt check --fix`(v2 范围,本 issue 不实现)
18
+ // - 不引入新依赖——只用 node:fs/promises + 已有 compile 层
19
+ import { readFile } from "node:fs/promises";
20
+ import { join } from "node:path";
21
+ import { compileAgentContext } from "./compile/agent-context.js";
22
+ import { PROFILES_DIR } from "./constants.js";
23
+ import { KNOWN_SECTION_NAMES } from "./parse/profile.js";
24
+ // ==================== 主入口 ====================
25
+ /** 扫描整个项目的资产配置(issue §Layer 2 主入口)。
26
+ *
27
+ * 检测 5 类反模式:
28
+ * 1. missing-modules ProfileGroup.modules 为空
29
+ * 2. dangling-blueprint-ref profile.blueprint 不存在
30
+ * 3. orphan-h2 ProfileGroup.name 不在 Blueprint.groups
31
+ * 4. empty-segment compileAgentContext 产出为空段
32
+ * 5. unknown-modname `### Modules` 项不在 KNOWN_SECTION_NAMES(需重读文件)
33
+ *
34
+ * 参数:
35
+ * - cwd:项目根目录(unknown-modname 重读 profile 文件需要)
36
+ * - profiles / blueprints / domains:从 SchemaBundle 取的全集
37
+ * - adapterCtx:可选。log writer 用于 trace(warn 不走 notify——会刷屏)
38
+ *
39
+ * 调用方:session_start / /pt check / pt_check tool。
40
+ *
41
+ * 不抛错——任何内层异常降级为 log warn + 不阻止结果。体检失败不该阻塞 session 启动。
42
+ */
43
+ export async function scanProjectHealth(cwd, profiles, blueprints, domains, packs, // v15.x PR2:compileAgentContext 需要 packs 进 sourceHash
44
+ profilePack, // v15.x PR2:scan 时用 default "prj"——不真正读 pack 信息
45
+ adapterCtx) {
46
+ const issues = [];
47
+ const blueprintNames = new Set(blueprints.map((b) => b.name));
48
+ const _domainByName = new Map(domains.map((d) => [d.name, d]));
49
+ // ===== IR-only 规则(1-3)=====
50
+ for (const profile of profiles) {
51
+ // 2. dangling-blueprint-ref
52
+ if (!blueprintNames.has(profile.blueprint)) {
53
+ issues.push({
54
+ severity: "error",
55
+ scope: "profile",
56
+ name: profile.name,
57
+ field: "blueprint",
58
+ msg: `Profile「${profile.name}」引用 Blueprint「${profile.blueprint}」不存在`,
59
+ hint: `检查拼写 / 项目 .pt/assets/blueprints/ 是否漏文件 / 备选内建 blueprint`,
60
+ });
61
+ continue; // 无 Blueprint 引用,下面的 group 检查无意义
62
+ }
63
+ const blueprint = blueprints.find((b) => b.name === profile.blueprint);
64
+ if (!blueprint)
65
+ continue; // 上一步已 guard(type guard 收窄需要)
66
+ const bpGroupNames = new Set(blueprint.groups.map((g) => g.name));
67
+ for (const group of profile.groups) {
68
+ // 3. orphan-h2
69
+ if (!bpGroupNames.has(group.name)) {
70
+ issues.push({
71
+ severity: "warning",
72
+ scope: "profile",
73
+ name: profile.name,
74
+ field: `groups.${group.name}`,
75
+ msg: `Profile「${profile.name}」的 H2「${group.name}」不在 Blueprint「${blueprint.name}」聚合组中`,
76
+ hint: `删除该 H2,或加到 Blueprint ${blueprint.name} 的 groups 项`,
77
+ });
78
+ }
79
+ // 1. missing-modules
80
+ if (group.modules.length === 0) {
81
+ issues.push({
82
+ severity: "error",
83
+ scope: "profile",
84
+ name: profile.name,
85
+ field: `groups.${group.name}.modules`,
86
+ msg: `Profile「${profile.name}」聚合组「${group.name}」缺 ### Modules`,
87
+ hint: `在 H2 段下加 \`### Modules\` 列出 modName(段名 Scene/Trigger/Rules/Flows/Checklists/User/Agent 等)`,
88
+ fix: `/pt check --fix ${profile.name}`,
89
+ });
90
+ }
91
+ }
92
+ }
93
+ // 4. empty-segment:对每个 profile 跑 compileAgentContext 检查产物
94
+ // - compile 失败已通过 agent-context.ts 的 reportWarn 报告;scan 跳过避免重复
95
+ // - 缺 Blueprint 引用已在 dangling-blueprint-ref 报过,跳过
96
+ for (const profile of profiles) {
97
+ const blueprint = blueprints.find((b) => b.name === profile.blueprint);
98
+ if (!blueprint)
99
+ continue;
100
+ try {
101
+ // v15.x PR3:scanProjectHealth 内部构造 working set(从 profiles/blueprints/domains + packs)
102
+ // scan 默认把 domain/blueprint 归到 prj pack,模拟项目 pack 加载行为。
103
+ // packs 为空时(如测试场景)用 profilePack 作 fallback key,配 mock AssetPack
104
+ const effectivePackName = profilePack || "prj";
105
+ const targetPack = packs.find((p) => p.source === "project") ??
106
+ packs[0] ?? {
107
+ name: effectivePackName,
108
+ version: "0.0.0",
109
+ rootDir: "/test",
110
+ source: "project",
111
+ loadDomains: () => Promise.resolve([]),
112
+ loadBlueprints: () => Promise.resolve([]),
113
+ loadProfiles: () => Promise.resolve([]),
114
+ };
115
+ // v15.x §4.4.2:scan 临时构造双索引 workingSet——location(按位置 alias)+ identity(按 pack.name)
116
+ // scan 场景 targetPack 是 project source → locAlias = "prj"
117
+ const domainLocWS = new Map();
118
+ const domainIdWS = new Map();
119
+ for (const d of domains) {
120
+ domainIdWS.set(`${targetPack.name}/${d.name}`, { pack: targetPack, asset: d });
121
+ domainLocWS.set(`prj/${d.name}`, { pack: targetPack, asset: d });
122
+ }
123
+ const blueprintLocWS = new Map();
124
+ const blueprintIdWS = new Map();
125
+ for (const b of blueprints) {
126
+ blueprintIdWS.set(`${targetPack.name}/${b.name}`, { pack: targetPack, asset: b });
127
+ blueprintLocWS.set(`prj/${b.name}`, { pack: targetPack, asset: b });
128
+ }
129
+ // scan 临时给 profile 打 sourcePack(如未设)——不修改原 profile(仅本调用范围)
130
+ const profileForCompile = profile.sourcePack
131
+ ? profile
132
+ : { ...profile, sourcePack: effectivePackName };
133
+ const ctx = compileAgentContext(profileForCompile, blueprint, domains, packs, profilePack, {
134
+ domains: { location: domainLocWS, identity: domainIdWS },
135
+ blueprints: { location: blueprintLocWS, identity: blueprintIdWS },
136
+ profiles: { location: new Map(), identity: new Map() },
137
+ });
138
+ const totalLen = Object.values(ctx.modules).reduce((acc, s) => acc + s.length, 0);
139
+ if (totalLen === 0) {
140
+ issues.push({
141
+ severity: "error",
142
+ scope: "profile",
143
+ name: profile.name,
144
+ msg: `Profile「${profile.name}」编译产出全聚合组空字符串`,
145
+ hint: `检查 Profile 的 \`### Modules\` 配置、Blueprint groups 名匹配、Domain H2 段名`,
146
+ });
147
+ }
148
+ }
149
+ catch (e) {
150
+ adapterCtx?.log?.debug("scanProjectHealth:compile failed", {
151
+ profile: profile.name,
152
+ err: e instanceof Error ? e.message : String(e),
153
+ });
154
+ }
155
+ }
156
+ // 5. unknown-modname:重读 profile 文件的 `### Modules` 原始行 vs KNOWN_SECTION_NAMES
157
+ // 原因:parseProfile 已过滤失败 modname(reportWarn 一次)。scan 重读可检测存量项目
158
+ // 未切过的 profile,避免"切一个 warn 一个"的渐进发现。
159
+ for (const profile of profiles) {
160
+ try {
161
+ const rawMods = await readRawModNames(cwd, profile);
162
+ for (const raw of rawMods) {
163
+ if (!isKnownModName(raw)) {
164
+ issues.push({
165
+ severity: "warning",
166
+ scope: "profile",
167
+ name: profile.name,
168
+ field: "groups.*.modules",
169
+ msg: `Profile「${profile.name}」的 modName「${raw}」段名不在已知集合`,
170
+ hint: `检查段名拼写 / KNOWN_SECTION_NAMES:Scene/Trigger/Rules/Flows/Checklists/User/Agent/Participant/Term`,
171
+ });
172
+ }
173
+ }
174
+ }
175
+ catch (e) {
176
+ adapterCtx?.log?.debug("scanProjectHealth:readRawModNames failed", {
177
+ profile: profile.name,
178
+ err: e instanceof Error ? e.message : String(e),
179
+ });
180
+ }
181
+ }
182
+ const errors = issues.filter((i) => i.severity === "error").length;
183
+ const warnings = issues.filter((i) => i.severity === "warning").length;
184
+ adapterCtx?.log?.info("scanProjectHealth:done", {
185
+ profileCount: profiles.length,
186
+ issueCount: issues.length,
187
+ errors,
188
+ warnings,
189
+ });
190
+ return { issues, errors, warnings };
191
+ }
192
+ // ==================== unknown-modname 规则辅助 ====================
193
+ /** 重读 profile 文件的 `### Modules` 段原始 modname 列表(unknown-modname 规则专用)。
194
+ * builtin profile / 文件不存在 → 返空数组(不报错)。 */
195
+ async function readRawModNames(cwd, profile) {
196
+ const file = join(cwd, PROFILES_DIR, `${profile.name}.profile.md`);
197
+ let raw;
198
+ try {
199
+ raw = await readFile(file, "utf8");
200
+ }
201
+ catch {
202
+ return []; // 文件不存在(builtin profile)跳过
203
+ }
204
+ // 简易 section 切分(不复用 parse/shared 的 splitSections——避免 import 链路拖长)
205
+ const sections = splitSectionsLight(raw);
206
+ const out = [];
207
+ for (const sec of sections) {
208
+ out.push(...extractModulesListFromRaw(sec));
209
+ }
210
+ return out;
211
+ }
212
+ /** 按 `## ` 切 H2 段(不计 H1)。空段过滤。 */
213
+ function splitSectionsLight(body) {
214
+ const lines = body.split(/\r?\n/);
215
+ const out = [];
216
+ let cur = null;
217
+ for (const line of lines) {
218
+ const h2 = line.match(/^##\s+(.+)$/);
219
+ if (h2) {
220
+ if (cur)
221
+ out.push(cur.join("\n"));
222
+ cur = [];
223
+ }
224
+ if (cur)
225
+ cur.push(line);
226
+ }
227
+ if (cur)
228
+ out.push(cur.join("\n"));
229
+ return out;
230
+ }
231
+ /** 从段文本取 `### Modules` 下的 `- xxx` 行(裸名,不含 "key: value")。 */
232
+ function extractModulesListFromRaw(sectionRaw) {
233
+ const lines = sectionRaw.split(/\r?\n/);
234
+ const out = [];
235
+ let inModules = false;
236
+ for (const line of lines) {
237
+ const h3 = line.match(/^###\s+(.+)$/);
238
+ if (h3) {
239
+ inModules = h3[1].trim() === "Modules";
240
+ continue;
241
+ }
242
+ if (!inModules)
243
+ continue;
244
+ const bare = line.match(/^\s*-\s+([^\s:]+)\s*$/);
245
+ if (bare) {
246
+ out.push(bare[1].trim());
247
+ continue;
248
+ }
249
+ // H4 段也终止(子嵌套不展开)
250
+ if (/^#+\s/.test(line))
251
+ break;
252
+ }
253
+ return out;
254
+ }
255
+ /** 判断 modName 字符串是否合法(KNOWN_SECTION_NAMES 段名 + 可选 段.项)。 */
256
+ function isKnownModName(raw) {
257
+ const trimmed = raw.trim();
258
+ if (!trimmed)
259
+ return false;
260
+ if (trimmed.includes(".")) {
261
+ const dotIdx = trimmed.indexOf(".");
262
+ const section = trimmed.slice(0, dotIdx);
263
+ const item = trimmed.slice(dotIdx + 1);
264
+ return KNOWN_SECTION_NAMES.has(section) && item.length > 0;
265
+ }
266
+ return KNOWN_SECTION_NAMES.has(trimmed);
267
+ }
@@ -0,0 +1,47 @@
1
+ import type { AssetPack, PackSource, SourceAdapterContext } from "../schema.js";
2
+ /** v15.x PR4(§6.2):路径解析——支持 ~ / 绝对 / 相对 cwd。
3
+ * ~ 开头 → home dir(join 后自然处理 ~user/foo / ~/foo / ~foo);绝对路径原样;相对路径 resolve(cwd, raw)。 */
4
+ export declare function resolvePackPath(raw: string, cwd: string): string;
5
+ /** 全局 Pack 默认路径(§6.4)。优先级:adapterCtx.globalPackDir > env > 默认 ~/.pt/assets。
6
+ * 返回值永远是绝对路径或可被 resolve 的相对路径。 */
7
+ export declare function getGlobalPackDir(adapterCtx?: SourceAdapterContext): string;
8
+ /**
9
+ * 尝试加载 pack:目录不存在返空 Pack(不报错,§6.4)。
10
+ * v15.x §2.4.2 双层语义:所有 pack 都走 MdFilePack.create 读 manifest,
11
+ * reserved pack 无 manifest 时 name 退化到位置别名(back-compat)。
12
+ *
13
+ * 不预加载——目录不存在时 loadDomains() 等返空数组,与"空 Pack"等价。
14
+ * 这样调用方可以无差别调 loadXxx,不用关心目录是否存在。
15
+ *
16
+ * PR2:构造从 sync 改 async——走 MdFilePack.create 读 manifest。
17
+ * adapterCtx 透传给 MdFilePack:parse 失败时调 reportError 上抛(PR1 补丁 S2)。
18
+ */
19
+ export declare function tryLoadPack(rootDir: string, source: PackSource, adapterCtx?: SourceAdapterContext): Promise<AssetPack>;
20
+ /** 构造 project pack(§6.5:路径可配,默认 .pt/assets)。
21
+ * v15.x PR4:优先读 settings 的 pt.project-pack-dir,fallback adapterCtx.assetDir / ASSETS_DIR。
22
+ * project pack 身份由 manifest.name 决定(无 manifest 时退化到 "prj",back-compat)。
23
+ * 指向项目外路径合法(../shared / 绝对),文档提示慎用。 */
24
+ export declare function loadProjectPack(cwd: string, adapterCtx?: SourceAdapterContext): Promise<AssetPack>;
25
+ /** 构造 global pack(§6.4):无 manifest 时退化到 "gbl"(back-compat)。 */
26
+ export declare function loadGlobalPack(adapterCtx?: SourceAdapterContext): Promise<AssetPack>;
27
+ /** 构造 builtin pack(src/builtin/assets/,随 npm 包发布):无 manifest 时退化到 "pt"。 */
28
+ export declare function loadBuiltinPack(adapterCtx?: SourceAdapterContext): Promise<AssetPack>;
29
+ /** v15.x PR4(§6.1 + §6.3):settings pack 加载。
30
+ * 读 .pi/settings.json 的 pt.asset-packs[](只 path 字段,§6.2 单一事实源),构造 AssetPack[]。
31
+ * - pack name 从 manifest 读(MdFilePack.create 内部走 §2.4.2 逻辑)
32
+ * - 路径解析 resolvePackPath(~ / 绝对 / 相对 cwd)
33
+ * - path 无效 / 目录不存在 / parse 失败 → 跳过该 pack(§6.7.5 不阻断)
34
+ * - 返回顺序 = settings 声明顺序(mdAdapter.load 负责 .reverse() 实现后者赢)
35
+ * - settings pack 无 manifest 时 name=basename 兜底(非 reserved,走 §2.4.2 路径 3) */
36
+ export declare function loadSettingsPacks(cwd: string): Promise<AssetPack[]>;
37
+ /**
38
+ * 调试 / 测试用:判断给定路径是否"看起来像 Pack"——是目录 + 至少含一个 asset 子目录。
39
+ * PR1 阶段主要用于测试断言,prod 路径不调。
40
+ */
41
+ export declare function looksLikePackDir(rootDir: string): boolean;
42
+ /**
43
+ * v15.x PR1(§6.7.3):project pack 降级时强制覆盖 profileName 为 "guide"。
44
+ * 抽为 pure helper 便于单元测试——transpileActive 调一下,逻辑集中。
45
+ * back-compat:degraded=false 或 requested="guide" 时返 requested 原值。
46
+ */
47
+ export declare function applyProjectPackDegrade(projectPackDegraded: boolean, requested: string): string;
@@ -0,0 +1,121 @@
1
+ // src/asset-pack/loader.ts — Pack 构造工厂(v15.x PR1)
2
+ //
3
+ // 设计源:.pt/docs/designs/pt-asset-pack.md §3.1(加载顺序)/ §6.4(全局 Pack)/ §6.5(project-pack-dir)
4
+ //
5
+ // 职责:把"目录路径 + 来源类型"封装成 AssetPack 实例(PR1 走 MdFilePack)。
6
+ // mdAdapter.load 调用这些函数构造 4 类 pack 数组。
7
+ //
8
+ // PR1 范围:
9
+ // - reserved pack(project/global/builtin)name 固定("prj"/"gbl"/"pt")——跳过 basename
10
+ // - project pack 路径走 adapterCtx.assetDir / 默认 ASSETS_DIR(PR4 接通 pt.project-pack-dir 后改读 settings)
11
+ // - global pack 路径走 adapterCtx.globalPackDir / env / 默认 ~/.pt/assets
12
+ // - settings pack 加载 stub:loadSettingsPacks() 返空数组(PR4 接通 .pi/settings.json pt.asset-packs)
13
+ // - directory 不存在不报错——tryLoadPack 内部不预加载,loadXxx 返空数组("空 Pack" 等价)
14
+ //
15
+ // 不做的:manifest 解析(PR2)、@pack/name 限定语法(PR3)、settings 加载(PR4)。
16
+ import { existsSync } from "node:fs";
17
+ import { homedir } from "node:os";
18
+ import { isAbsolute, join, resolve } from "node:path";
19
+ import { ASSETS_DIR, BUILTIN_ASSETS_DIR } from "../constants.js";
20
+ import { readProjectSetting } from "../config.js";
21
+ import { MdFilePack } from "./md-file-pack.js";
22
+ /** v15.x PR4(§6.2):路径解析——支持 ~ / 绝对 / 相对 cwd。
23
+ * ~ 开头 → home dir(join 后自然处理 ~user/foo / ~/foo / ~foo);绝对路径原样;相对路径 resolve(cwd, raw)。 */
24
+ export function resolvePackPath(raw, cwd) {
25
+ if (raw.startsWith("~")) {
26
+ return join(homedir(), raw.slice(1));
27
+ }
28
+ if (isAbsolute(raw)) {
29
+ return raw;
30
+ }
31
+ return resolve(cwd, raw);
32
+ }
33
+ /** 全局 Pack 默认路径(§6.4)。优先级:adapterCtx.globalPackDir > env > 默认 ~/.pt/assets。
34
+ * 返回值永远是绝对路径或可被 resolve 的相对路径。 */
35
+ export function getGlobalPackDir(adapterCtx) {
36
+ return (adapterCtx?.globalPackDir ?? process.env.PT_GLOBAL_PACK_DIR ?? join(homedir(), ".pt", "assets"));
37
+ }
38
+ /**
39
+ * 尝试加载 pack:目录不存在返空 Pack(不报错,§6.4)。
40
+ * v15.x §2.4.2 双层语义:所有 pack 都走 MdFilePack.create 读 manifest,
41
+ * reserved pack 无 manifest 时 name 退化到位置别名(back-compat)。
42
+ *
43
+ * 不预加载——目录不存在时 loadDomains() 等返空数组,与"空 Pack"等价。
44
+ * 这样调用方可以无差别调 loadXxx,不用关心目录是否存在。
45
+ *
46
+ * PR2:构造从 sync 改 async——走 MdFilePack.create 读 manifest。
47
+ * adapterCtx 透传给 MdFilePack:parse 失败时调 reportError 上抛(PR1 补丁 S2)。
48
+ */
49
+ export async function tryLoadPack(rootDir, source, adapterCtx) {
50
+ return MdFilePack.create({ rootDir, source, adapterCtx });
51
+ }
52
+ /** 构造 project pack(§6.5:路径可配,默认 .pt/assets)。
53
+ * v15.x PR4:优先读 settings 的 pt.project-pack-dir,fallback adapterCtx.assetDir / ASSETS_DIR。
54
+ * project pack 身份由 manifest.name 决定(无 manifest 时退化到 "prj",back-compat)。
55
+ * 指向项目外路径合法(../shared / 绝对),文档提示慎用。 */
56
+ export async function loadProjectPack(cwd, adapterCtx) {
57
+ // 优先级:pt.project-pack-dir > adapterCtx.assetDir > ASSETS_DIR(默认 .pt/assets)
58
+ const configured = await readProjectSetting(cwd, "pt.project-pack-dir");
59
+ const rawDir = configured ?? adapterCtx?.assetDir ?? ASSETS_DIR;
60
+ const dir = resolvePackPath(rawDir, cwd);
61
+ return tryLoadPack(dir, "project", adapterCtx);
62
+ }
63
+ /** 构造 global pack(§6.4):无 manifest 时退化到 "gbl"(back-compat)。 */
64
+ export async function loadGlobalPack(adapterCtx) {
65
+ return tryLoadPack(getGlobalPackDir(adapterCtx), "global", adapterCtx);
66
+ }
67
+ /** 构造 builtin pack(src/builtin/assets/,随 npm 包发布):无 manifest 时退化到 "pt"。 */
68
+ export async function loadBuiltinPack(adapterCtx) {
69
+ return tryLoadPack(BUILTIN_ASSETS_DIR, "builtin", adapterCtx);
70
+ }
71
+ /** v15.x PR4(§6.1 + §6.3):settings pack 加载。
72
+ * 读 .pi/settings.json 的 pt.asset-packs[](只 path 字段,§6.2 单一事实源),构造 AssetPack[]。
73
+ * - pack name 从 manifest 读(MdFilePack.create 内部走 §2.4.2 逻辑)
74
+ * - 路径解析 resolvePackPath(~ / 绝对 / 相对 cwd)
75
+ * - path 无效 / 目录不存在 / parse 失败 → 跳过该 pack(§6.7.5 不阻断)
76
+ * - 返回顺序 = settings 声明顺序(mdAdapter.load 负责 .reverse() 实现后者赢)
77
+ * - settings pack 无 manifest 时 name=basename 兜底(非 reserved,走 §2.4.2 路径 3) */
78
+ export async function loadSettingsPacks(cwd) {
79
+ const entries = await readProjectSetting(cwd, "pt.asset-packs");
80
+ if (!Array.isArray(entries) || entries.length === 0) {
81
+ return [];
82
+ }
83
+ const packs = [];
84
+ for (const entry of entries) {
85
+ // 只 path 字段——name 从 manifest 读(§6.2 单一事实源)
86
+ if (!entry || typeof entry.path !== "string" || !entry.path.trim()) {
87
+ // 无效条目跳过(loader 不调 ui,预警由 session_start 的 validatePack 兜底)
88
+ continue;
89
+ }
90
+ const rootDir = resolvePackPath(entry.path.trim(), cwd);
91
+ try {
92
+ const pack = await tryLoadPack(rootDir, "settings");
93
+ packs.push(pack);
94
+ }
95
+ catch {
96
+ // MdFilePack.create 内部已容错,catch 仅防御意外抛错——不阻断其他 pack
97
+ }
98
+ }
99
+ return packs;
100
+ }
101
+ /**
102
+ * 调试 / 测试用:判断给定路径是否"看起来像 Pack"——是目录 + 至少含一个 asset 子目录。
103
+ * PR1 阶段主要用于测试断言,prod 路径不调。
104
+ */
105
+ export function looksLikePackDir(rootDir) {
106
+ if (!existsSync(rootDir))
107
+ return false;
108
+ return (existsSync(join(rootDir, "domains")) ||
109
+ existsSync(join(rootDir, "blueprints")) ||
110
+ existsSync(join(rootDir, "profiles")));
111
+ }
112
+ /**
113
+ * v15.x PR1(§6.7.3):project pack 降级时强制覆盖 profileName 为 "guide"。
114
+ * 抽为 pure helper 便于单元测试——transpileActive 调一下,逻辑集中。
115
+ * back-compat:degraded=false 或 requested="guide" 时返 requested 原值。
116
+ */
117
+ export function applyProjectPackDegrade(projectPackDegraded, requested) {
118
+ if (projectPackDegraded && requested !== "guide")
119
+ return "guide";
120
+ return requested;
121
+ }
@@ -0,0 +1,23 @@
1
+ /** manifest 解析结果。ok=false 表示文件不存在/解析失败/校验不过——调用方走默认值。 */
2
+ export interface ParsedManifest {
3
+ /** 文件存在且 YAML 解析成功(name/version/description 校验失败仍 ok=true,错误进 warnings)。 */
4
+ ok: boolean;
5
+ /** 合法 kebab-case name(ok=true 且 manifest 有 name 且校验通过时) */
6
+ name?: string;
7
+ /** 合法 semver(ok=true 且 manifest 有 version 且校验通过时) */
8
+ version?: string;
9
+ /** 原样保留(仅诊断用) */
10
+ description?: string;
11
+ /** 非致命问题(name 非 kebab / version 非 semver / 保留名冲突 / top-level 不是 mapping) */
12
+ warnings: string[];
13
+ }
14
+ /**
15
+ * 解析 <rootDir>/pt-asset-pack.yaml(可选)。
16
+ * - 文件不存在 → { ok: false, warnings: [] }(隐式 pack,走 basename 兜底)
17
+ * - 文件存在但解析失败 → { ok: false, warnings: ["manifest parse failed: ..."] }
18
+ * - name 非 kebab-case / 保留名冲突 → warnings + name 丢弃(走 basename)
19
+ * - version 非 semver → warnings + version 丢弃(走 "0.0.0")
20
+ *
21
+ * 永远不抛异常(§2.2 校验规则:解析失败当无 manifest 处理,不阻断加载)。
22
+ */
23
+ export declare function parseManifest(rootDir: string): Promise<ParsedManifest>;
@@ -0,0 +1,96 @@
1
+ // src/asset-pack/manifest.ts — pt-asset-pack.yaml manifest 解析(v15.x PR2)
2
+ //
3
+ // 设计源:.pt/docs/designs/pt-asset-pack.md §2.2(manifest 格式)+ §2.4.1(命名约束)
4
+ //
5
+ // 职责:解析 <rootDir>/pt-asset-pack.yaml(可选),提取 name/version/description。
6
+ // 所有问题收集到 warnings——不抛异常,调用方走默认值(§2.2 校验规则)。
7
+ //
8
+ // PR2 阶段保留名 reserved pack(source=project/global/builtin)跳过 manifest,
9
+ // 用固定名 prj/gbl/pt——见 md-file-pack.ts create()。
10
+ //
11
+ // semver 正则按设计文档 §2.2 固定(不引 semver 包)。
12
+ import { readFile } from "node:fs/promises";
13
+ import { join } from "node:path";
14
+ import { parse as parseYaml } from "yaml";
15
+ import { errMsg } from "../diagnostics.js";
16
+ /** reserved pack 名(§2.4.1 精确匹配禁用)+ 别名兼容。
17
+ * 精确匹配,不是前缀禁用——`pt-internal` 合法;只有完全等于集合内值才触发冲突。 */
18
+ const RESERVED_NAMES = new Set([
19
+ "gbl",
20
+ "prj",
21
+ "pt",
22
+ "project",
23
+ "global",
24
+ "builtin",
25
+ ]);
26
+ /** semver 正则(§2.2 实现备注)。
27
+ * 格式:<major>.<minor>.<patch>[-<pre-release>][+<build>]
28
+ * pre-release / build 标识符字符集:[0-9A-Za-z.-] */
29
+ const SEMVER_RE = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
30
+ /** kebab-case 正则(§2.4.1:[a-z0-9-]+,1-64 字符)。 */
31
+ const KEBAB_RE = /^[a-z0-9-]{1,64}$/;
32
+ /**
33
+ * 解析 <rootDir>/pt-asset-pack.yaml(可选)。
34
+ * - 文件不存在 → { ok: false, warnings: [] }(隐式 pack,走 basename 兜底)
35
+ * - 文件存在但解析失败 → { ok: false, warnings: ["manifest parse failed: ..."] }
36
+ * - name 非 kebab-case / 保留名冲突 → warnings + name 丢弃(走 basename)
37
+ * - version 非 semver → warnings + version 丢弃(走 "0.0.0")
38
+ *
39
+ * 永远不抛异常(§2.2 校验规则:解析失败当无 manifest 处理,不阻断加载)。
40
+ */
41
+ export async function parseManifest(rootDir) {
42
+ const file = join(rootDir, "pt-asset-pack.yaml");
43
+ let raw;
44
+ try {
45
+ raw = await readFile(file, "utf8");
46
+ }
47
+ catch {
48
+ // 文件不存在 → 隐式 pack
49
+ return { ok: false, warnings: [] };
50
+ }
51
+ let data;
52
+ try {
53
+ data = parseYaml(raw);
54
+ }
55
+ catch (e) {
56
+ return { ok: false, warnings: [`manifest parse failed: ${errMsg(e)}`] };
57
+ }
58
+ if (!isRecord(data)) {
59
+ return { ok: false, warnings: ["manifest top-level not a mapping"] };
60
+ }
61
+ const warnings = [];
62
+ let name;
63
+ let version;
64
+ let description;
65
+ // name 校验
66
+ if (typeof data.name === "string" && data.name.trim()) {
67
+ const n = data.name.trim();
68
+ if (!KEBAB_RE.test(n)) {
69
+ warnings.push(`manifest.name "${n}" not kebab-case, falling back to basename`);
70
+ }
71
+ else if (RESERVED_NAMES.has(n)) {
72
+ warnings.push(`manifest.name "${n}" is reserved, falling back to basename`);
73
+ }
74
+ else {
75
+ name = n;
76
+ }
77
+ }
78
+ // version 校验
79
+ if (typeof data.version === "string" && data.version.trim()) {
80
+ const v = data.version.trim();
81
+ if (!SEMVER_RE.test(v)) {
82
+ warnings.push(`manifest.version "${v}" not semver, treating as "0.0.0"`);
83
+ }
84
+ else {
85
+ version = v;
86
+ }
87
+ }
88
+ // description 原样保留(仅诊断用,无校验)
89
+ if (typeof data.description === "string" && data.description.trim()) {
90
+ description = data.description.trim();
91
+ }
92
+ return { ok: true, name, version, description, warnings };
93
+ }
94
+ function isRecord(x) {
95
+ return !!x && typeof x === "object" && !Array.isArray(x);
96
+ }