@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,57 @@
|
|
|
1
|
+
// src/verify/index.ts — verify 注册表 + runVerify 入口
|
|
2
|
+
//
|
|
3
|
+
// P1:observe 字段的实现库。纯函数 (cwd, params) => ProbeOutcome,不依赖 OXN 的 kernel/verdict 链。
|
|
4
|
+
// 加新 probe = 注册表加一行 + 实现文件一个。
|
|
5
|
+
|
|
6
|
+
import type { ProbeOutcome } from "../schema.js";
|
|
7
|
+
import { fsContentMatch } from "./fs-content-match.js";
|
|
8
|
+
import { fsExists } from "./fs-exists.js";
|
|
9
|
+
import { fsNotExists } from "./fs-not-exists.js";
|
|
10
|
+
import { lintCheck } from "./lint-check.js";
|
|
11
|
+
import { tsCompiles } from "./ts-compiles.js";
|
|
12
|
+
import { testPass } from "./test-pass.js";
|
|
13
|
+
import { gitStatusClean } from "./git-status-clean.js";
|
|
14
|
+
import { fileHash } from "./file-hash.js";
|
|
15
|
+
|
|
16
|
+
/** verify 函数签名:纯函数,接收 cwd + 参数,返回 ProbeOutcome。 */
|
|
17
|
+
export type VerifyFunction = (cwd: string, params: Record<string, string>) => Promise<ProbeOutcome>;
|
|
18
|
+
|
|
19
|
+
/** 注册表:probe 名 → verify 函数。 */
|
|
20
|
+
const registry: Record<string, VerifyFunction> = {
|
|
21
|
+
"fs-content-match": fsContentMatch,
|
|
22
|
+
"fs-exists": fsExists,
|
|
23
|
+
"fs-not-exists": fsNotExists,
|
|
24
|
+
"lint-check": lintCheck,
|
|
25
|
+
"ts-compiles": tsCompiles,
|
|
26
|
+
"test-pass": testPass,
|
|
27
|
+
"git-status-clean": gitStatusClean,
|
|
28
|
+
"file-hash": fileHash,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** 列出所有已注册的 probe 名(pt_verify tool 的错误提示用)。 */
|
|
32
|
+
export function listProbes(): string[] {
|
|
33
|
+
return Object.keys(registry);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 按名执行 verify。未注册的 probe 名返回 INCONCLUSIVE。 */
|
|
37
|
+
export async function runVerify(
|
|
38
|
+
cwd: string,
|
|
39
|
+
name: string,
|
|
40
|
+
params: Record<string, string>
|
|
41
|
+
): Promise<ProbeOutcome> {
|
|
42
|
+
const fn = registry[name];
|
|
43
|
+
if (!fn) {
|
|
44
|
+
return {
|
|
45
|
+
outcome: "INCONCLUSIVE",
|
|
46
|
+
message: `未知 probe: ${name}(可用: ${listProbes().join(", ")})`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
return await fn(cwd, params);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
return {
|
|
53
|
+
outcome: "INCONCLUSIVE",
|
|
54
|
+
message: `probe ${name} 执行异常: ${e instanceof Error ? e.message : String(e)}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// src/verify/lint-check.ts — 验证 lint 通过
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import type { ProbeOutcome } from "../schema.js";
|
|
4
|
+
import { readStderr } from "./read-stderr.js";
|
|
5
|
+
|
|
6
|
+
export async function lintCheck(
|
|
7
|
+
cwd: string,
|
|
8
|
+
params: Record<string, string>
|
|
9
|
+
): Promise<ProbeOutcome> {
|
|
10
|
+
const cmd = params.cmd ?? "npx biome check";
|
|
11
|
+
try {
|
|
12
|
+
execSync(cmd, { cwd, stdio: "pipe", timeout: 30000 });
|
|
13
|
+
return { outcome: "COMPLETED", message: `lint 通过: ${cmd}` };
|
|
14
|
+
} catch (e) {
|
|
15
|
+
return {
|
|
16
|
+
outcome: "DEVIATED",
|
|
17
|
+
message: `lint 失败: ${cmd}`,
|
|
18
|
+
actual: readStderr(e).slice(0, 500),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/verify/read-stderr.ts — execSync 异常 stderr 提取公共辅助(P3.5 抽出)
|
|
2
|
+
//
|
|
3
|
+
// execSync 抛异常时 stderr 是 Uint8Array,需要 Buffer.from 转换。
|
|
4
|
+
// lint-check.ts + test-pass.ts 都需要此逻辑——抽到此处避免重复。
|
|
5
|
+
|
|
6
|
+
/** execSync 抛出的异常可能包含的 stderr 字段(Node 内部行为)。 */
|
|
7
|
+
type ExecError = Error & { stderr?: Uint8Array | string };
|
|
8
|
+
|
|
9
|
+
/** 从 execSync 抛出的异常中提取 stderr 字符串。
|
|
10
|
+
* - e 非 Error 或无 stderr 字段 → 返 ""
|
|
11
|
+
* - stderr 为 Uint8Array → Buffer 转字符串
|
|
12
|
+
* - 其他类型 → String() 兜底 */
|
|
13
|
+
export function readStderr(e: unknown): string {
|
|
14
|
+
if (e instanceof Error && "stderr" in e) {
|
|
15
|
+
const stderr = (e as ExecError).stderr;
|
|
16
|
+
if (stderr instanceof Uint8Array) {
|
|
17
|
+
return Buffer.from(stderr).toString();
|
|
18
|
+
}
|
|
19
|
+
if (typeof stderr === "string") {
|
|
20
|
+
return stderr;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// src/verify/ref-check.ts — Profile→Blueprint→Domain 引用完整性校验
|
|
2
|
+
//
|
|
3
|
+
// pt 引用图是三层星型(非 OXN Domain→Domain 网状),无环风险。
|
|
4
|
+
// 校验重点是完整性:悬空引用检测。
|
|
5
|
+
// 不校验 Blueprint.groups[].modules(H2 段名是模块类型声明,非 Domain 引用)。
|
|
6
|
+
//
|
|
7
|
+
// v11.x:Profile 全局 domains 覆盖是主用例——"未实例化"警告在该模式下静默
|
|
8
|
+
// (未实例化 = 走全局分发,无 warning;只有 Profile 完全空 + 聚合组全空时才报 warning)。
|
|
9
|
+
|
|
10
|
+
import type { Blueprint, Domain, Profile } from "../schema.js";
|
|
11
|
+
|
|
12
|
+
export interface RefCheckResult {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
errors: string[];
|
|
15
|
+
warnings: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 校验单个 Profile 的引用完整性。
|
|
19
|
+
* 传入该 Profile 引用的 Blueprint(若存在)+ 全集 Domains。 */
|
|
20
|
+
export function checkProfileRefs(
|
|
21
|
+
profile: Profile,
|
|
22
|
+
blueprints: Blueprint[],
|
|
23
|
+
domains: Domain[]
|
|
24
|
+
): RefCheckResult {
|
|
25
|
+
const errors: string[] = [];
|
|
26
|
+
const warnings: string[] = [];
|
|
27
|
+
|
|
28
|
+
const domainNames = new Set(domains.map((d) => d.name));
|
|
29
|
+
const _blueprintNames = new Set(blueprints.map((b) => b.name));
|
|
30
|
+
|
|
31
|
+
// 1. Profile → Blueprint 存在性
|
|
32
|
+
const bp = blueprints.find((b) => b.name === profile.blueprint);
|
|
33
|
+
if (!bp) {
|
|
34
|
+
errors.push(`Profile "${profile.name}" 引用的 Blueprint "${profile.blueprint}" 不存在`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2. Profile → Domain(全局 domains)
|
|
38
|
+
for (const dn of profile.domains) {
|
|
39
|
+
if (!domainNames.has(dn)) {
|
|
40
|
+
errors.push(`Profile "${profile.name}" 的 domains 引用悬空 Domain "${dn}"`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 3. Profile.groups → Domain + 聚合组名匹配
|
|
45
|
+
for (const group of profile.groups) {
|
|
46
|
+
// 3a. 聚合组名应在 Blueprint 里有对应
|
|
47
|
+
if (bp) {
|
|
48
|
+
const bpGroup = bp.groups.find((bg) => bg.name === group.name);
|
|
49
|
+
if (!bpGroup) {
|
|
50
|
+
errors.push(
|
|
51
|
+
`Profile "${profile.name}" 的聚合组 "${group.name}" 在 Blueprint "${bp.name}" 里无对应`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 3b. 聚合组引用的 Domain 存在
|
|
57
|
+
for (const dn of group.domains) {
|
|
58
|
+
if (!domainNames.has(dn)) {
|
|
59
|
+
errors.push(`Profile "${profile.name}" 聚合组 "${group.name}" 引用悬空 Domain "${dn}"`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 4. 警告:Blueprint 声明了聚合组但 Profile 未实例化(非错误——Profile 可只实例化部分聚合组)
|
|
65
|
+
// v11.x:Profile 有全局 domains 时,聚合组未 H2 实例化是合法用法(全局分发到所有聚合组),
|
|
66
|
+
// 静默不报。只有 Profile 全空(无全局 domains + 无 H2 实例化)时才报"未实例化" warning。
|
|
67
|
+
if (bp && profile.domains.length === 0) {
|
|
68
|
+
for (const bpGroup of bp.groups) {
|
|
69
|
+
const hasProfileGroup = profile.groups.some((pg) => pg.name === bpGroup.name);
|
|
70
|
+
if (!hasProfileGroup) {
|
|
71
|
+
warnings.push(
|
|
72
|
+
`Blueprint "${bp.name}" 的聚合组 "${bpGroup.name}" 在 Profile "${profile.name}" 里未实例化`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 校验所有 Profile 的引用完整性(批量入口)。 */
|
|
82
|
+
export function checkAllRefs(
|
|
83
|
+
profiles: Profile[],
|
|
84
|
+
blueprints: Blueprint[],
|
|
85
|
+
domains: Domain[]
|
|
86
|
+
): RefCheckResult {
|
|
87
|
+
const allErrors: string[] = [];
|
|
88
|
+
const allWarnings: string[] = [];
|
|
89
|
+
|
|
90
|
+
for (const profile of profiles) {
|
|
91
|
+
const r = checkProfileRefs(profile, blueprints, domains);
|
|
92
|
+
allErrors.push(...r.errors);
|
|
93
|
+
allWarnings.push(...r.warnings);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { ok: allErrors.length === 0, errors: allErrors, warnings: allWarnings };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 把 RefCheckResult 格式化为人类可读文本(pt_check_refs tool 输出用)。 */
|
|
100
|
+
export function formatRefCheckResult(r: RefCheckResult): string {
|
|
101
|
+
const lines: string[] = [];
|
|
102
|
+
if (r.ok && r.warnings.length === 0) {
|
|
103
|
+
lines.push("✓ 引用完整性检查通过,无悬空引用");
|
|
104
|
+
return lines.join("\n");
|
|
105
|
+
}
|
|
106
|
+
if (r.errors.length > 0) {
|
|
107
|
+
lines.push(`✗ ${r.errors.length} 个悬空引用:`);
|
|
108
|
+
for (const e of r.errors) lines.push(` - ${e}`);
|
|
109
|
+
}
|
|
110
|
+
if (r.warnings.length > 0) {
|
|
111
|
+
lines.push(`⚠ ${r.warnings.length} 个警告:`);
|
|
112
|
+
for (const w of r.warnings) lines.push(` - ${w}`);
|
|
113
|
+
}
|
|
114
|
+
if (r.ok && r.warnings.length > 0) {
|
|
115
|
+
lines.unshift("✓ 引用完整性检查通过(有警告)");
|
|
116
|
+
}
|
|
117
|
+
return lines.join("\n");
|
|
118
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// src/verify/test-pass.ts — 验证测试通过
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import type { ProbeOutcome } from "../schema.js";
|
|
4
|
+
import { readStderr } from "./read-stderr.js";
|
|
5
|
+
|
|
6
|
+
export async function testPass(cwd: string, params: Record<string, string>): Promise<ProbeOutcome> {
|
|
7
|
+
const cmd = params.cmd ?? "npx vitest run tests/verify/";
|
|
8
|
+
try {
|
|
9
|
+
execSync(cmd, { cwd, stdio: "pipe", timeout: 120000 });
|
|
10
|
+
return { outcome: "COMPLETED", message: `测试通过: ${cmd}` };
|
|
11
|
+
} catch (e) {
|
|
12
|
+
return {
|
|
13
|
+
outcome: "DEVIATED",
|
|
14
|
+
message: `测试失败: ${cmd}`,
|
|
15
|
+
actual: readStderr(e).slice(0, 500),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// src/verify/ts-compiles.ts — 验证 TS 编译通过
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import type { ProbeOutcome } from "../schema.js";
|
|
4
|
+
import { readStderr } from "./read-stderr.js";
|
|
5
|
+
|
|
6
|
+
export async function tsCompiles(
|
|
7
|
+
cwd: string,
|
|
8
|
+
params: Record<string, string>
|
|
9
|
+
): Promise<ProbeOutcome> {
|
|
10
|
+
const cmd = params.cmd ?? "npx tsc --noEmit";
|
|
11
|
+
try {
|
|
12
|
+
execSync(cmd, { cwd, stdio: "pipe", timeout: 60000 });
|
|
13
|
+
return { outcome: "COMPLETED", message: `TS 编译通过: ${cmd}` };
|
|
14
|
+
} catch (e) {
|
|
15
|
+
return {
|
|
16
|
+
outcome: "DEVIATED",
|
|
17
|
+
message: `TS 编译失败: ${cmd}`,
|
|
18
|
+
actual: readStderr(e).slice(0, 500),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|