@istuen/pt 0.1.1 → 0.1.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/dist/asset-pack/manifest.d.ts +6 -1
- package/dist/asset-pack/manifest.js +12 -5
- package/dist/asset-pack/md-file-pack.js +17 -3
- package/dist/builtin/assets/domains/pack-management.md +170 -0
- package/dist/builtin/assets/profiles/guide.profile.md +1 -1
- package/dist/builtin/assets/pt-asset-pack.yaml +22 -0
- package/dist/index.js +29 -3
- package/dist/manual-session.d.ts +5 -2
- package/dist/manual-session.js +7 -4
- package/dist/manual-track.d.ts +39 -7
- package/dist/manual-track.js +82 -11
- package/dist/manual-writeback.d.ts +37 -0
- package/dist/manual-writeback.js +195 -0
- package/dist/parse/index.d.ts +0 -3
- package/dist/parse/index.js +19 -2
- package/package.json +2 -2
- package/src/asset-pack/manifest.ts +19 -5
- package/src/asset-pack/md-file-pack.ts +22 -6
- package/src/builtin/assets/domains/pack-management.md +170 -0
- package/src/builtin/assets/profiles/guide.profile.md +1 -1
- package/src/builtin/assets/pt-asset-pack.yaml +22 -0
- package/src/index.ts +28 -3
- package/src/manual-session.ts +7 -3
- package/src/manual-track.ts +100 -12
- package/src/manual-writeback.ts +227 -0
- package/src/parse/index.ts +21 -2
- package/dist/builtin/assets/domains/pack-repair.md +0 -47
- package/src/builtin/assets/domains/pack-repair.md +0 -47
package/src/manual-session.ts
CHANGED
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
ExtensionUIContext,
|
|
29
29
|
} from "@earendil-works/pi-coding-agent";
|
|
30
30
|
import {
|
|
31
|
+
checkManualCompletion,
|
|
31
32
|
isManualActive,
|
|
32
33
|
parseManualProgress,
|
|
33
34
|
renderManualFooterSuffix,
|
|
@@ -124,9 +125,12 @@ function refreshInjectionFooter(ui: ExtensionUIContext, session: SessionState):
|
|
|
124
125
|
|
|
125
126
|
/** 刷新 widget(aboveEditor)。根据 session.activeManual 决定显示/撤掉。
|
|
126
127
|
* - 无 activeManual → 撤 widget
|
|
127
|
-
* - 文件不存在 /
|
|
128
|
+
* - 文件不存在 / 真完成(status=completed + stepDone===stepTotal + 无 —/INCONCLUSIVE)→ 清 activeManual + 撤 widget
|
|
129
|
+
* - 伪完成(status=completed 但 step 未全勾或有 —/INCONCLUSIVE)→ 仍渲染 widget,加 ⚠ fake done 提示
|
|
128
130
|
* - in-progress → 渲染 3 行 widget + 更新 cachedManualProgress(footer 同步读)
|
|
129
|
-
* v12.x:state 全部从 session 参数读,不再读写 module-level 单例。
|
|
131
|
+
* v12.x:state 全部从 session 参数读,不再读写 module-level 单例。
|
|
132
|
+
* v15.x(issue pt-manual-completion-check-too-loose):用 checkManualCompletion 替代 `p.status === "completed"`,
|
|
133
|
+
* 让伪完成的 manual 仍 active(widget 重新挂载,提示用户步骤未全完成)。 */
|
|
130
134
|
async function refreshManualWidget(ui: ExtensionUIContext, session: SessionState): Promise<void> {
|
|
131
135
|
const m = session.activeManual;
|
|
132
136
|
if (!m) {
|
|
@@ -135,7 +139,7 @@ async function refreshManualWidget(ui: ExtensionUIContext, session: SessionState
|
|
|
135
139
|
return;
|
|
136
140
|
}
|
|
137
141
|
const p = await parseManualProgress(m.filePath);
|
|
138
|
-
if (!p || p
|
|
142
|
+
if (!p || !checkManualCompletion(p)) {
|
|
139
143
|
session.activeManual = null;
|
|
140
144
|
session.cachedManualProgress = null;
|
|
141
145
|
ui.setWidget("pt-manual", undefined);
|
package/src/manual-track.ts
CHANGED
|
@@ -13,17 +13,37 @@
|
|
|
13
13
|
|
|
14
14
|
import { access, readFile } from "node:fs/promises";
|
|
15
15
|
|
|
16
|
+
/** ## 执行状态 表的 Outcome 列规范化为 4 个值之一(其他字符串如 TODO 视为 INCONCLUSIVE)。
|
|
17
|
+
* - "—" :未填写(buildManualDoc 表头初始值)
|
|
18
|
+
* - "COMPLETED":执行符合预期
|
|
19
|
+
* - "DEVIATED" :偏离预期(不是失败,见 src/schema.ts ProbeOutcomeKind 注释)
|
|
20
|
+
* - "INCONCLUSIVE":无法判定(人工评估 / probe 缺参数等) */
|
|
21
|
+
export type OutcomeKind = "—" | "COMPLETED" | "DEVIATED" | "INCONCLUSIVE";
|
|
22
|
+
|
|
23
|
+
/** Manual 实例文件 ## 执行状态 表的某一行(stepIndex + outcome)。
|
|
24
|
+
* stepIndex 1-indexed(与表行 | N | ... 对齐)。 */
|
|
25
|
+
export interface StepOutcome {
|
|
26
|
+
stepIndex: number;
|
|
27
|
+
outcome: OutcomeKind;
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
/** Manual 实例文件 parse 结果。
|
|
17
31
|
* - procedure:从 frontmatter.procedure 取
|
|
18
32
|
* - stepDone / stepTotal:实时正则数 "- [x]" / "- [ ]"
|
|
19
33
|
* - status:frontmatter.status(in-progress / completed)
|
|
20
|
-
* - nextStep:第一个 "- [ ]" 行去前缀的文本(下一步该做的),无则 null
|
|
34
|
+
* - nextStep:第一个 "- [ ]" 行去前缀的文本(下一步该做的),无则 null
|
|
35
|
+
* - stepOutcomes:从 ## 执行状态 表抽的每步 outcome(长度 ≤ stepTotal,缺行视为未完成)
|
|
36
|
+
* - hasUnfinishedOutcome:任一 outcome 是 "—" 或 "INCONCLUSIVE"(伪完成征兆之一)
|
|
37
|
+
* - pseudoComplete:status=completed 但有伪完成征兆(stepDone < stepTotal 或 hasUnfinishedOutcome) */
|
|
21
38
|
export interface ManualProgress {
|
|
22
39
|
procedure: string;
|
|
23
40
|
stepDone: number;
|
|
24
41
|
stepTotal: number;
|
|
25
42
|
status: string;
|
|
26
43
|
nextStep: string | null;
|
|
44
|
+
stepOutcomes: StepOutcome[];
|
|
45
|
+
hasUnfinishedOutcome: boolean;
|
|
46
|
+
pseudoComplete: boolean;
|
|
27
47
|
}
|
|
28
48
|
|
|
29
49
|
/** 从 manual 实例文件 parse 进度。失败返回 null(容错,不抛)。 */
|
|
@@ -37,12 +57,25 @@ export async function parseManualProgress(filePath: string): Promise<ManualProgr
|
|
|
37
57
|
}
|
|
38
58
|
}
|
|
39
59
|
|
|
60
|
+
/** 规范化 Outcome 列字符串到 OutcomeKind。未识别值归 INCONCLUSIVE(保守按未完成处理)。 */
|
|
61
|
+
function normalizeOutcome(raw: string): OutcomeKind {
|
|
62
|
+
const trimmed = raw.trim();
|
|
63
|
+
if (trimmed === "—" || trimmed === "-" || trimmed === "") return "—";
|
|
64
|
+
if (trimmed === "COMPLETED") return "COMPLETED";
|
|
65
|
+
if (trimmed === "DEVIATED") return "DEVIATED";
|
|
66
|
+
if (trimmed === "INCONCLUSIVE") return "INCONCLUSIVE";
|
|
67
|
+
return "INCONCLUSIVE";
|
|
68
|
+
}
|
|
69
|
+
|
|
40
70
|
/** 内部:从字符串内容 parse(便于单测 & 未来内联输入)。
|
|
41
71
|
* 算法:
|
|
42
72
|
* 1. frontmatter:取首段 `---` ... `---` 之间的 YAML
|
|
43
73
|
* 2. procedure / status:从 frontmatter 行解析(`key: value` 形式,宽松)
|
|
44
74
|
* 3. checklist:全文正则 `/^- \[([ x])\]\s+(.+)$/gm` 数 done / total
|
|
45
|
-
* 4. nextStep:第一个未完成项的文本(去前缀)
|
|
75
|
+
* 4. nextStep:第一个未完成项的文本(去前缀)
|
|
76
|
+
* 5. stepOutcomes:从 ## 执行状态 段抽 `| N | <outcome> | <message> |` 行
|
|
77
|
+
* 6. hasUnfinishedOutcome:任一 outcome 是 "—" 或 "INCONCLUSIVE"
|
|
78
|
+
* 7. pseudoComplete:status=completed 但 stepDone<stepTotal 或 hasUnfinishedOutcome */
|
|
46
79
|
export function parseManualProgressFromContent(content: string): ManualProgress | null {
|
|
47
80
|
const fm = extractFrontmatter(content);
|
|
48
81
|
const procedure = fm?.procedure ?? "(unknown)";
|
|
@@ -64,7 +97,37 @@ export function parseManualProgressFromContent(content: string): ManualProgress
|
|
|
64
97
|
}
|
|
65
98
|
}
|
|
66
99
|
|
|
67
|
-
|
|
100
|
+
// 5. 抽 ## 执行状态 表的每行 outcome
|
|
101
|
+
const stepOutcomes: StepOutcome[] = [];
|
|
102
|
+
// 表格行格式 `| N | <outcome> | <message> |`,message 列可空(`\s*` 兼容末列前的 0+ 空格)
|
|
103
|
+
const tableRe = /^\| (\d+) \| (.+?) \| [^\|]*\s*\|$/gm;
|
|
104
|
+
for (const m of content.matchAll(tableRe)) {
|
|
105
|
+
const idxStr = m[1] ?? "";
|
|
106
|
+
const rawOutcome = m[2] ?? "";
|
|
107
|
+
const idx = Number.parseInt(idxStr, 10);
|
|
108
|
+
if (Number.isFinite(idx) && idx > 0) {
|
|
109
|
+
stepOutcomes.push({
|
|
110
|
+
stepIndex: idx,
|
|
111
|
+
outcome: normalizeOutcome(rawOutcome),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const hasUnfinishedOutcome = stepOutcomes.some(
|
|
117
|
+
(o) => o.outcome === "—" || o.outcome === "INCONCLUSIVE"
|
|
118
|
+
);
|
|
119
|
+
const pseudoComplete = status === "completed" && (stepDone < stepTotal || hasUnfinishedOutcome);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
procedure,
|
|
123
|
+
stepDone,
|
|
124
|
+
stepTotal,
|
|
125
|
+
status,
|
|
126
|
+
nextStep,
|
|
127
|
+
stepOutcomes,
|
|
128
|
+
hasUnfinishedOutcome,
|
|
129
|
+
pseudoComplete,
|
|
130
|
+
};
|
|
68
131
|
}
|
|
69
132
|
|
|
70
133
|
/** frontmatter 抽取(极简 YAML:只解 key: value)。 */
|
|
@@ -88,15 +151,22 @@ function extractFrontmatter(content: string): SimpleFrontmatter | null {
|
|
|
88
151
|
}
|
|
89
152
|
|
|
90
153
|
/** widget 渲染:3 行 string[],给 `ui.setWidget("pt-manual", lines, { placement: "aboveEditor" })` 用。
|
|
91
|
-
* - 第 1 行:`pt ▶ <procedure> step <done>/<total> (<status>)`(completed 加 ` ✓ done`)
|
|
154
|
+
* - 第 1 行:`pt ▶ <procedure> step <done>/<total> (<status>)`(completed 加 ` ✓ done`;pseudoComplete 加 `⚠ fake done`)
|
|
92
155
|
* - 第 2 行:` next: <nextStep 文本>`(无 nextStep 时该行省略)
|
|
93
156
|
* - 第 3 行:` file: <filePath>`
|
|
94
|
-
* status === completed 时仍显示,但 footer 后缀改 done;widget 撤掉由 caller 决定(见 index.ts refreshManualWidget)。
|
|
157
|
+
* status === completed 时仍显示,但 footer 后缀改 done;widget 撤掉由 caller 决定(见 index.ts refreshManualWidget)。
|
|
158
|
+
* v15.x(issue pt-manual-completion-check-too-loose):pseudoComplete 时加 ⚠ 提示用户
|
|
159
|
+
* (status=completed 但 stepDone<stepTotal 或 hasUnfinishedOutcome → 伪完成)。 */
|
|
95
160
|
export function renderManualWidgetLines(filePath: string, p: ManualProgress): string[] {
|
|
96
|
-
|
|
161
|
+
let statusText: string;
|
|
162
|
+
if (p.status === "completed") {
|
|
163
|
+
statusText = p.pseudoComplete ? "⚠ fake done" : "✓ done";
|
|
164
|
+
} else {
|
|
165
|
+
statusText = `(${p.status})`;
|
|
166
|
+
}
|
|
97
167
|
const lines: string[] = [];
|
|
98
168
|
lines.push(`pt ▶ ${p.procedure} step ${p.stepDone}/${p.stepTotal} ${statusText}`);
|
|
99
|
-
if (p.nextStep && p.status !== "completed") {
|
|
169
|
+
if (p.nextStep && !p.pseudoComplete && p.status !== "completed") {
|
|
100
170
|
lines.push(` next: ${p.nextStep}`);
|
|
101
171
|
}
|
|
102
172
|
lines.push(` file: ${filePath}`);
|
|
@@ -105,17 +175,34 @@ export function renderManualWidgetLines(filePath: string, p: ManualProgress): st
|
|
|
105
175
|
|
|
106
176
|
/** footer 后缀:有 activeManual 时追加。
|
|
107
177
|
* - in-progress → `· manual: <procedure> <done>/<total>`
|
|
108
|
-
* - completed → `· manual: <procedure> done`
|
|
178
|
+
* - completed → `· manual: <procedure> done`
|
|
179
|
+
* - pseudoComplete(status=completed 但 stepDone<stepTotal 或 hasUnfinishedOutcome)→ `· manual: <procedure> ⚠ fake done` */
|
|
109
180
|
export function renderManualFooterSuffix(p: ManualProgress): string {
|
|
110
181
|
if (p.status === "completed") {
|
|
111
|
-
return
|
|
182
|
+
return p.pseudoComplete
|
|
183
|
+
? `· manual: ${p.procedure} ⚠ fake done`
|
|
184
|
+
: `· manual: ${p.procedure} done`;
|
|
112
185
|
}
|
|
113
186
|
return `· manual: ${p.procedure} ${p.stepDone}/${p.stepTotal}`;
|
|
114
187
|
}
|
|
115
188
|
|
|
116
|
-
/**
|
|
189
|
+
/** 检查 manual 实例是否应视为 active(widget 继续显示 + footer 显示)。
|
|
190
|
+
* v15.x(issue pt-manual-completion-check-too-loose):硬校验 status=completed 也需 step 全勾 + outcome 非 —/INCONCLUSIVE。
|
|
191
|
+
* - status !== "completed" → true(active)
|
|
192
|
+
* - status="completed" 但 stepDone < stepTotal → true(伪完成)
|
|
193
|
+
* - status="completed" 但 hasUnfinishedOutcome → true(伪完成)
|
|
194
|
+
* - status="completed" 且 stepDone === stepTotal 且无 —/INCONCLUSIVE → false(真完成) */
|
|
195
|
+
export function checkManualCompletion(p: ManualProgress): boolean {
|
|
196
|
+
if (p.status !== "completed") return true;
|
|
197
|
+
if (p.stepDone < p.stepTotal) return true;
|
|
198
|
+
if (p.hasUnfinishedOutcome) return true;
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 文件存在 + checkManualCompletion(p) === true。配合 widget/footer 决定要不要展示。
|
|
117
203
|
* - 用 fs.access 探测存在(轻量,不读全文)
|
|
118
|
-
* - 真的 parse status 用 parseManualProgress(一次 readFile 取齐)
|
|
204
|
+
* - 真的 parse status 用 parseManualProgress(一次 readFile 取齐)
|
|
205
|
+
* v15.x:status=completed 时不再直接判 active,需 checkManualCompletion 硬校验 */
|
|
119
206
|
export async function isManualActive(filePath: string): Promise<boolean> {
|
|
120
207
|
try {
|
|
121
208
|
await access(filePath);
|
|
@@ -123,5 +210,6 @@ export async function isManualActive(filePath: string): Promise<boolean> {
|
|
|
123
210
|
return false;
|
|
124
211
|
}
|
|
125
212
|
const p = await parseManualProgress(filePath);
|
|
126
|
-
|
|
213
|
+
if (!p) return false;
|
|
214
|
+
return checkManualCompletion(p);
|
|
127
215
|
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// src/manual-writeback.ts — pt_verify 结果自动写回 manual 实例(纯函数内核)
|
|
2
|
+
//
|
|
3
|
+
// v15.x(issue pt-verify-result-not-written-back-to-manual):
|
|
4
|
+
// pt_verify tool 跑完 probe 后,自动把结果写回 activeManual 文件的 ## 执行状态 表。
|
|
5
|
+
// 闭环不靠 LLM 自觉——与 pt-collab.md 的 acceptance 原则一致。
|
|
6
|
+
//
|
|
7
|
+
// 设计要点:
|
|
8
|
+
// - probe ↔ step 映射:按 FlowStep.observe 列表匹配 probe 名(首个匹配)
|
|
9
|
+
// - 多 observe 同步:frontmatter 注释维护已完成 probe 列表;该 step 全部 observe 都
|
|
10
|
+
// 完成时 checklist `- [ ]` → `- [x]`
|
|
11
|
+
// - 写回幂等:同一 probe 跑两次 → 用最新 outcome/message 覆盖原行
|
|
12
|
+
// - 纯函数:writeProbeResult 只改字符串,不碰 IO;IO 由 caller 包 withFileMutationQueue
|
|
13
|
+
//
|
|
14
|
+
// 边界纪律:
|
|
15
|
+
// - 不动 parseManualProgress / renderManualWidgetLines / renderManualFooterSuffix
|
|
16
|
+
// (manual-track.ts 保持纯解析语义,写回是上层职责)
|
|
17
|
+
// - 不依赖 ExtensionAPI,可单测
|
|
18
|
+
// - manual 实例文件不是 Pt 资产(不是 Domain/Blueprint/Profile)→ 不进 parse/ 层
|
|
19
|
+
|
|
20
|
+
import type { ProbeOutcomeKind } from "./schema.js";
|
|
21
|
+
|
|
22
|
+
/** findStepForProbe 返回结构(probe 命中的 step 信息) */
|
|
23
|
+
export interface StepMatch {
|
|
24
|
+
/** step 的 1-indexed 编号(对应 manual 文件 ## 执行状态 表的行号) */
|
|
25
|
+
stepIndex: number;
|
|
26
|
+
/** step 的 desc 文本(去 "- [ ] " 前缀) */
|
|
27
|
+
desc: string;
|
|
28
|
+
/** step 的 observe 列表(来自 ` - 验证参照:xxx, yyy` 行) */
|
|
29
|
+
observeNames: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** writeProbeResult 返回结构 */
|
|
33
|
+
export interface WritebackResult {
|
|
34
|
+
/** 匹配上的 step 数(0 = probe 名不在任何 step 的 observe 中) */
|
|
35
|
+
matchCount: number;
|
|
36
|
+
/** 写回的 step 索引(1-indexed) */
|
|
37
|
+
stepIndexes: number[];
|
|
38
|
+
/** 因全部 observe 完成而勾选 checklist 的 step 索引(1-indexed) */
|
|
39
|
+
checkedSteps: number[];
|
|
40
|
+
/** 改后的文件内容(changed=false 时返回原 content) */
|
|
41
|
+
content: string;
|
|
42
|
+
/** 是否真的改了文件 */
|
|
43
|
+
changed: boolean;
|
|
44
|
+
/** 改后的 completed-probes 列表(含历史 + 本次) */
|
|
45
|
+
completedProbes: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 从 manual 内容里扫 step + observe,建立 stepIndex → StepMatch 索引。
|
|
49
|
+
* 算法:
|
|
50
|
+
* 1. 逐行扫描:step 行 `^- \[([ x])\] (.+)$` 记录 stepIndex(1-indexed 累计)——兼容已勾选 `- [x]`;
|
|
51
|
+
* 验证参照行 `^ - 验证参照:(.+)$` 关联到上一个 step。
|
|
52
|
+
* 2. step 行格式兼容:`^- \[ \] \d+\. xxx`(旧版 buildManualDoc)也支持(取 \d+ 后内容)。
|
|
53
|
+
* 失败返回空 Map(不含任何 step)。 */
|
|
54
|
+
function indexSteps(content: string): Map<number, StepMatch> {
|
|
55
|
+
const out = new Map<number, StepMatch>();
|
|
56
|
+
let stepIndex = 0;
|
|
57
|
+
let lastStep: StepMatch | null = null;
|
|
58
|
+
const lines = content.split("\n");
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
// step 行:兼容 "- [ ] desc" / "- [x] desc" / "- [ ] N. desc" / "- [x] N. desc"
|
|
61
|
+
// 多轮 writeback 场景下,已勾选 step 必须算入索引(避免后续 step 索引错乱)
|
|
62
|
+
const stepMatch = line.match(/^- \[[ x]\]\s+(?:\d+\.\s+)?(.+)$/);
|
|
63
|
+
if (stepMatch) {
|
|
64
|
+
stepIndex++;
|
|
65
|
+
lastStep = {
|
|
66
|
+
stepIndex,
|
|
67
|
+
desc: (stepMatch[1] ?? "").trim(),
|
|
68
|
+
observeNames: [],
|
|
69
|
+
};
|
|
70
|
+
out.set(stepIndex, lastStep);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// observe 行(紧跟 step 的子项)
|
|
74
|
+
const observeMatch = line.match(/^ {2}- 验证参照:(.+)$/);
|
|
75
|
+
if (observeMatch && lastStep) {
|
|
76
|
+
const names = (observeMatch[1] ?? "")
|
|
77
|
+
.split(",")
|
|
78
|
+
.map((s) => s.trim())
|
|
79
|
+
.filter((s) => s.length > 0);
|
|
80
|
+
lastStep.observeNames = names;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 从 manual 内容里按 probe 名找匹配的 step。多个 step 有同一 observe 时取首个。
|
|
87
|
+
* 返回 null = probe 名不在任何 step 的 observe 中。 */
|
|
88
|
+
export function findStepForProbe(content: string, probeName: string): StepMatch | null {
|
|
89
|
+
if (!probeName) return null;
|
|
90
|
+
const steps = indexSteps(content);
|
|
91
|
+
for (const step of steps.values()) {
|
|
92
|
+
if (step.observeNames.includes(probeName)) {
|
|
93
|
+
return step;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 解析 frontmatter 注释里的 completed probes 列表(pt-verify-completed 行)。
|
|
100
|
+
* 格式:<!-- pt-verify-completed: probe1, probe2, probe3 -->
|
|
101
|
+
* 缺省返空 Set。 */
|
|
102
|
+
function parseCompletedProbes(content: string): Set<string> {
|
|
103
|
+
const out = new Set<string>();
|
|
104
|
+
const m = content.match(/<!--\s*pt-verify-completed:\s*([\s\S]*?)\s*-->/);
|
|
105
|
+
if (!m?.[1]) return out;
|
|
106
|
+
for (const name of m[1].split(",")) {
|
|
107
|
+
const trimmed = name.trim();
|
|
108
|
+
if (trimmed.length > 0) out.add(trimmed);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 把 completed probes Set 序列化成 frontmatter 注释行(含前后空格)。
|
|
114
|
+
* 若 Set 为空,返回空字符串(删除注释行时不主动清,由 caller 决定)。 */
|
|
115
|
+
function renderCompletedProbes(probes: Set<string>): string {
|
|
116
|
+
if (probes.size === 0) return "";
|
|
117
|
+
const list = [...probes].sort().join(", ");
|
|
118
|
+
return `<!-- pt-verify-completed: ${list} -->`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 写回 probe 结果到 manual 文件内容(同步,不读文件)。
|
|
122
|
+
* - 写执行状态表 `| N | — | |` → `| N | <outcome> | <message> |`
|
|
123
|
+
* - 改 frontmatter 注释维护 completed probe 列表
|
|
124
|
+
* - 多 observe 同步:若 step 的全部 observe 都 in 列表 → checklist `- [ ]` → `- [x]`
|
|
125
|
+
* - 幂等:同一 probe 跑两次 → 用最新覆盖原 message
|
|
126
|
+
* - 边界:
|
|
127
|
+
* - 找不到匹配 step → matchCount=0, changed=false(让 caller 走 warn 分支)
|
|
128
|
+
* - 找不到执行状态表对应行 → 不改(保守:manual 损坏不擅自补) */
|
|
129
|
+
export function writeProbeResult(
|
|
130
|
+
content: string,
|
|
131
|
+
probeName: string,
|
|
132
|
+
outcome: ProbeOutcomeKind,
|
|
133
|
+
message: string
|
|
134
|
+
): WritebackResult {
|
|
135
|
+
const baseResult: WritebackResult = {
|
|
136
|
+
matchCount: 0,
|
|
137
|
+
stepIndexes: [],
|
|
138
|
+
checkedSteps: [],
|
|
139
|
+
content,
|
|
140
|
+
changed: false,
|
|
141
|
+
completedProbes: [],
|
|
142
|
+
};
|
|
143
|
+
if (!probeName) return baseResult;
|
|
144
|
+
|
|
145
|
+
const steps = indexSteps(content);
|
|
146
|
+
if (steps.size === 0) return baseResult;
|
|
147
|
+
|
|
148
|
+
const match = findStepForProbe(content, probeName);
|
|
149
|
+
if (!match) return baseResult;
|
|
150
|
+
baseResult.matchCount = 1;
|
|
151
|
+
baseResult.stepIndexes.push(match.stepIndex);
|
|
152
|
+
|
|
153
|
+
// 1. 改执行状态表对应行(幂等覆盖:找 `| N | ... |` 任意中间值)
|
|
154
|
+
// `.+` 贪婪匹配到最后一个 ` |$`,覆盖已填行(COMPLETED|DEVIATED|INCONCLUSIVE)。
|
|
155
|
+
let out = content;
|
|
156
|
+
const statusLineRe = new RegExp(`^\\| ${match.stepIndex} \\| .+ \\|$`, "m");
|
|
157
|
+
if (statusLineRe.test(out)) {
|
|
158
|
+
// 转义 message 里的 | 字符(避免破坏 markdown 表格)+ 反斜杠(避免后续正则误判)
|
|
159
|
+
const safeMsg = message.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
|
|
160
|
+
out = out.replace(statusLineRe, `| ${match.stepIndex} | ${outcome} | ${safeMsg} |`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 2. 维护 frontmatter 注释里的 completed probes 列表
|
|
164
|
+
const completed = parseCompletedProbes(out);
|
|
165
|
+
completed.add(probeName);
|
|
166
|
+
baseResult.completedProbes = [...completed];
|
|
167
|
+
|
|
168
|
+
// 写回或追加 frontmatter 注释
|
|
169
|
+
const commentRe = /<!--\s*pt-verify-completed:\s*[\s\S]*?-->/;
|
|
170
|
+
const newComment = renderCompletedProbes(completed);
|
|
171
|
+
if (commentRe.test(out)) {
|
|
172
|
+
out = out.replace(commentRe, newComment);
|
|
173
|
+
} else if (newComment) {
|
|
174
|
+
// 追加到 frontmatter 后空行(frontmatter 后第一个空行)
|
|
175
|
+
const fmEndRe = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/m;
|
|
176
|
+
const fmMatch = out.match(fmEndRe);
|
|
177
|
+
if (fmMatch?.index !== undefined) {
|
|
178
|
+
const insertAt = fmMatch.index + fmMatch[0].length;
|
|
179
|
+
out = out.slice(0, insertAt) + `\n${newComment}\n` + out.slice(insertAt);
|
|
180
|
+
} else {
|
|
181
|
+
// 无 frontmatter(不应该,但兼容)→ 追加到文件头
|
|
182
|
+
out = `${newComment}\n${out}`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 3. 多 observe 同步:若 step 的全部 observe 都 in completed → checklist 打勾
|
|
187
|
+
const checkedSteps: number[] = [];
|
|
188
|
+
const checklistLines: string[] = []; // [{lineIdx, newLine}] 延迟到循环外统一 replace
|
|
189
|
+
const lines = out.split("\n");
|
|
190
|
+
// 复用 indexSteps 但要在已更新的 out 里重新解析(completed 变了不影响 step 索引)
|
|
191
|
+
const freshSteps = indexSteps(out);
|
|
192
|
+
for (const [idx, step] of freshSteps) {
|
|
193
|
+
if (step.observeNames.length === 0) continue;
|
|
194
|
+
const allCompleted = step.observeNames.every((n) => completed.has(n));
|
|
195
|
+
if (!allCompleted) continue;
|
|
196
|
+
// 找该 step 对应的 checklist 行(兼容已勾选 - [x])
|
|
197
|
+
let stepLineIdx = -1;
|
|
198
|
+
let lineNum = 0;
|
|
199
|
+
let sIdx = 0;
|
|
200
|
+
for (const line of lines) {
|
|
201
|
+
if (line.match(/^- \[[ x]\]\s+(?:\d+\.\s+)?.+$/)) {
|
|
202
|
+
sIdx++;
|
|
203
|
+
if (sIdx === idx) {
|
|
204
|
+
stepLineIdx = lineNum;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
lineNum++;
|
|
209
|
+
}
|
|
210
|
+
if (stepLineIdx === -1) continue;
|
|
211
|
+
const oldLine = lines[stepLineIdx];
|
|
212
|
+
if (oldLine === undefined) continue;
|
|
213
|
+
const newLine = oldLine.replace(/^- \[ \]/, "- [x]");
|
|
214
|
+
if (newLine !== oldLine) {
|
|
215
|
+
lines[stepLineIdx] = newLine;
|
|
216
|
+
checkedSteps.push(idx);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (checkedSteps.length > 0) {
|
|
220
|
+
out = lines.join("\n");
|
|
221
|
+
baseResult.checkedSteps = checkedSteps;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
baseResult.content = out;
|
|
225
|
+
baseResult.changed = out !== content;
|
|
226
|
+
return baseResult;
|
|
227
|
+
}
|
package/src/parse/index.ts
CHANGED
|
@@ -37,6 +37,17 @@ import { parseProfile } from "./profile.js";
|
|
|
37
37
|
/** MD adapter:按目录位置分发到 domain/blueprint/profile adapter,组装 SchemaBundle。
|
|
38
38
|
* v9 命名约定:适配的是 MD 文件格式(不再叫 OXN——OXN 是历史名)。
|
|
39
39
|
* v15.x PR1(§3.1):内部构造 4 类 AssetPack → loadXxx → N 元 dedupByNameN。 */
|
|
40
|
+
|
|
41
|
+
/** v15.x §2.4.4:位置 alias 短名 → source(位置指针独立于 pack.name)。
|
|
42
|
+
* 用于 findActiveProfile 查位置 alias(kind="location")时按 source 查 pack,
|
|
43
|
+
* 与 pack.name 解耦——builtin pack 加 manifest.name="pt-builtin" 后 @pt/... 仍能寻址。
|
|
44
|
+
* key 是 parseRef 输出的位置 alias 短名(prj/gbl/pt),value 是 AssetPack.source。
|
|
45
|
+
* 与 LOC_ALIAS(source → alias)是反向表,互不重复定义。 */
|
|
46
|
+
const ALIAS_TO_SOURCE: ReadonlyMap<string, string> = new Map([
|
|
47
|
+
["prj", "project"],
|
|
48
|
+
["gbl", "global"],
|
|
49
|
+
["pt", "builtin"],
|
|
50
|
+
]);
|
|
40
51
|
export const mdAdapter: SourceAdapter = {
|
|
41
52
|
name: "md",
|
|
42
53
|
|
|
@@ -182,8 +193,16 @@ function findActiveProfile(
|
|
|
182
193
|
profileRef: string
|
|
183
194
|
): Profile {
|
|
184
195
|
if (profileRef.startsWith("@")) {
|
|
185
|
-
const { pack, name } = parseRef(profileRef, ""); // 限定 ref 不需要 selfPack
|
|
186
|
-
|
|
196
|
+
const { kind, pack, name } = parseRef(profileRef, ""); // 限定 ref 不需要 selfPack
|
|
197
|
+
// v15.x §2.4.4:位置 alias(@prj/@gbl/@pt,kind="location")按 source 查(固定 3 slot 物理位置指针),
|
|
198
|
+
// 与 pack.name 解耦——builtin pack 加 manifest.name 后,pack.name 变化不影响 @pt/... 寻址。
|
|
199
|
+
// 身份 alias(kind="identity")按 pack.name 查(manifest.name 身份指针)。
|
|
200
|
+
const packIdx =
|
|
201
|
+
kind === "location"
|
|
202
|
+
? ALIAS_TO_SOURCE.has(pack)
|
|
203
|
+
? packs.findIndex((p) => p.source === ALIAS_TO_SOURCE.get(pack))
|
|
204
|
+
: -1
|
|
205
|
+
: packs.findIndex((p) => p.name === pack);
|
|
187
206
|
if (packIdx < 0) {
|
|
188
207
|
throw new Error(`active profile "@${pack}/${name}" references unknown pack "${pack}"`);
|
|
189
208
|
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pack-repair
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# pack-repair
|
|
6
|
-
|
|
7
|
-
v15.x PR1(§6.7.4):project pack 校验失败时的修复引导手册。builtin profile `guide`
|
|
8
|
-
会引用本 domain——用户 `/manual:pack-repair` 触发 FlowTemplate,按步骤修复。
|
|
9
|
-
|
|
10
|
-
## Scene
|
|
11
|
-
|
|
12
|
-
### pack-structure
|
|
13
|
-
- desc: Pt pack 标准目录结构——必须含 domains/ + blueprints/ + profiles/ 三个子目录之一;可选 pt-asset-pack.yaml manifest(PR2 启用)
|
|
14
|
-
|
|
15
|
-
### pack-sources
|
|
16
|
-
- desc: v15.x Pack 有 4 类来源——project(`<cwd>/.pt/assets/`)/ settings(`.pi/settings.json` 的 `pt.asset-packs[]`,PR4 启用)/ global(`~/.pt/assets/`)/ builtin(`src/builtin/assets/`,随 npm 包发布)
|
|
17
|
-
|
|
18
|
-
### validation-codes
|
|
19
|
-
- desc: validatePack 返回的错误码——`dir-not-found`(路径不存在)/ `no-asset-subdir`(无 asset 子目录)/ `load-failed`(加载抛异常)
|
|
20
|
-
|
|
21
|
-
## Flows
|
|
22
|
-
|
|
23
|
-
### pack-repair
|
|
24
|
-
- argument-hint: (无)
|
|
25
|
-
- intent: project pack 校验失败时引导修复——按错误码分类处置后重启 session 验证
|
|
26
|
-
- vars: []
|
|
27
|
-
- step: /pt status 查看 pack 健康状态(§6.7.6),定位失效 pack + 错误码
|
|
28
|
-
- step: 按 errors[].code 分类处置:
|
|
29
|
-
- dir-not-found:创建 pack 目录(project: mkdir -p .pt/assets/{domains,blueprints,profiles};global: mkdir -p ~/.pt/assets/{domains,blueprints,profiles})
|
|
30
|
-
- no-asset-subdir:至少创建一个 asset 子目录(domains/blueprints/profiles 任选其一)
|
|
31
|
-
- load-failed:检查资产文件格式(.md frontmatter 合法 / .blueprint.yaml 语法正确)
|
|
32
|
-
- step: 修复后重启 pi session(projectPackDegraded 在 session_start 重新校验时清零)
|
|
33
|
-
- step: /pt status 确认 pack 健康(✅,无 DEGRADED 行)
|
|
34
|
-
|
|
35
|
-
## Rules
|
|
36
|
-
|
|
37
|
-
### validate-pack-never-throws
|
|
38
|
-
- check: validatePack 失败时返 ValidationResult 对象(ok=false + errors[]),不抛异常——保证加载链不阻断(§6.7.7)
|
|
39
|
-
|
|
40
|
-
### project-pack-degradation
|
|
41
|
-
- check: project pack 失效时强制激活 builtin guide profile(projectPackDegraded=true),覆盖用户配置的 pt.default-profile——保证 pi 可用让用户修复(§6.7.3)
|
|
42
|
-
|
|
43
|
-
### restart-after-repair
|
|
44
|
-
- check: 修复 project pack 后必须重启 pi session——projectPackDegraded 标记在 session_start 重新校验时清零,不重启则继续降级
|
|
45
|
-
|
|
46
|
-
### global-pack-guide-non-interactive
|
|
47
|
-
- check: 全局 Pack 初始化引导仅在 TTY + 非 CI + 无 PT_NO_GUIDE 环境触发(§7.5.1)——避免阻塞 CI / 后台进程
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pack-repair
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# pack-repair
|
|
6
|
-
|
|
7
|
-
v15.x PR1(§6.7.4):project pack 校验失败时的修复引导手册。builtin profile `guide`
|
|
8
|
-
会引用本 domain——用户 `/manual:pack-repair` 触发 FlowTemplate,按步骤修复。
|
|
9
|
-
|
|
10
|
-
## Scene
|
|
11
|
-
|
|
12
|
-
### pack-structure
|
|
13
|
-
- desc: Pt pack 标准目录结构——必须含 domains/ + blueprints/ + profiles/ 三个子目录之一;可选 pt-asset-pack.yaml manifest(PR2 启用)
|
|
14
|
-
|
|
15
|
-
### pack-sources
|
|
16
|
-
- desc: v15.x Pack 有 4 类来源——project(`<cwd>/.pt/assets/`)/ settings(`.pi/settings.json` 的 `pt.asset-packs[]`,PR4 启用)/ global(`~/.pt/assets/`)/ builtin(`src/builtin/assets/`,随 npm 包发布)
|
|
17
|
-
|
|
18
|
-
### validation-codes
|
|
19
|
-
- desc: validatePack 返回的错误码——`dir-not-found`(路径不存在)/ `no-asset-subdir`(无 asset 子目录)/ `load-failed`(加载抛异常)
|
|
20
|
-
|
|
21
|
-
## Flows
|
|
22
|
-
|
|
23
|
-
### pack-repair
|
|
24
|
-
- argument-hint: (无)
|
|
25
|
-
- intent: project pack 校验失败时引导修复——按错误码分类处置后重启 session 验证
|
|
26
|
-
- vars: []
|
|
27
|
-
- step: /pt status 查看 pack 健康状态(§6.7.6),定位失效 pack + 错误码
|
|
28
|
-
- step: 按 errors[].code 分类处置:
|
|
29
|
-
- dir-not-found:创建 pack 目录(project: mkdir -p .pt/assets/{domains,blueprints,profiles};global: mkdir -p ~/.pt/assets/{domains,blueprints,profiles})
|
|
30
|
-
- no-asset-subdir:至少创建一个 asset 子目录(domains/blueprints/profiles 任选其一)
|
|
31
|
-
- load-failed:检查资产文件格式(.md frontmatter 合法 / .blueprint.yaml 语法正确)
|
|
32
|
-
- step: 修复后重启 pi session(projectPackDegraded 在 session_start 重新校验时清零)
|
|
33
|
-
- step: /pt status 确认 pack 健康(✅,无 DEGRADED 行)
|
|
34
|
-
|
|
35
|
-
## Rules
|
|
36
|
-
|
|
37
|
-
### validate-pack-never-throws
|
|
38
|
-
- check: validatePack 失败时返 ValidationResult 对象(ok=false + errors[]),不抛异常——保证加载链不阻断(§6.7.7)
|
|
39
|
-
|
|
40
|
-
### project-pack-degradation
|
|
41
|
-
- check: project pack 失效时强制激活 builtin guide profile(projectPackDegraded=true),覆盖用户配置的 pt.default-profile——保证 pi 可用让用户修复(§6.7.3)
|
|
42
|
-
|
|
43
|
-
### restart-after-repair
|
|
44
|
-
- check: 修复 project pack 后必须重启 pi session——projectPackDegraded 标记在 session_start 重新校验时清零,不重启则继续降级
|
|
45
|
-
|
|
46
|
-
### global-pack-guide-non-interactive
|
|
47
|
-
- check: 全局 Pack 初始化引导仅在 TTY + 非 CI + 无 PT_NO_GUIDE 环境触发(§7.5.1)——避免阻塞 CI / 后台进程
|