@lmzhen/dsh-tool-skill-manage 0.4.1 → 0.6.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/README.md +9 -0
- package/lib/index.js +102 -11
- package/lib/types/index.d.ts +43 -0
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -20,6 +20,15 @@ Mutations (create/edit/update/patch/delete/write_file/remove_file/restructure) p
|
|
|
20
20
|
|
|
21
21
|
`pin` and `unpin` are deliberately **outside** the approval seam. Pinning only lifts/restores the curator-lifecycle freeze (a lifecycle flag, never content) and is fully reversible by the same tool. Routing it through `policy:'ask'` would let a staged-but-never-approved request hold the library in a pinned state invisibly. Tradeoff accepted: no approval on a lifecycle-flag flip; if product policy changes, pin/unpin should be wired into the same staging path as `patch`.
|
|
22
22
|
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
- `maxSkillContentChars` / `maxSkillFileBytes` bound SKILL.md reads and support-file writes on this row.
|
|
26
|
+
- Deprecated name (G0/S0.3): `maxSkillContentChars` is the legacy spelling of the
|
|
27
|
+
policy row's `skillContentChars` (canonical id). The two are not auto-synchronised
|
|
28
|
+
yet — this row's value is what the write paths use (the G3 unification closes
|
|
29
|
+
that gap). Reading the legacy name still works; writing it is refused, and it is
|
|
30
|
+
removed in 0.7.0.
|
|
31
|
+
|
|
23
32
|
## Known limitations
|
|
24
33
|
|
|
25
34
|
- No known durable consumer gaps at this time. Runtime contracts are covered by package and boundary tests.
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { effectiveSessionPolicy } from "@lmzhen/dsh-evolution-approval";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
4
|
-
import { DEFAULT_SKILL_LIMITS, DSH_AUTHORING_STANDARDS, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, authoringFeedback, callingScope, clampedNumber, computeDedupGroups, contentHash, evolutionIoAdapter, isPresent, isUnknown, newSkillLibrary, parseFrontmatter, probePresent, probeUnknown, resolveExecOrigins } from "@lmzhen/dsh-evolution-core";
|
|
4
|
+
import { DEFAULT_ARCHIVE_RETENTION_POLICY, DEFAULT_CITATION_POLICY, DEFAULT_REFERENCE_REWRITE_POLICY, DEFAULT_SKILL_LIMITS, DEFAULT_SUPPORT_FILE_CHAR_POLICY, DSH_AUTHORING_STANDARDS, PARAM_NAMESPACES, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, authoringFeedback, callingScope, clampedNumber, computeDedupGroups, contentHash, evolutionIoAdapter, installParamSection, isPresent, isUnknown, newSkillLibrary, parseFrontmatter, policyStageLimits, probePresent, probeUnknown, readNumberParam, resolveExecOrigins } from "@lmzhen/dsh-evolution-core";
|
|
5
5
|
//#region lib/types/index.js
|
|
6
6
|
/**
|
|
7
7
|
* Model-facing skill_manage tool over ctx.evolutionIo + ctx.skillUsage.
|
|
@@ -41,6 +41,37 @@ const Config = z.object({
|
|
|
41
41
|
threatExemptLabels: z.array(z.string()).default([]),
|
|
42
42
|
strictCrossSource: z.boolean().default(false)
|
|
43
43
|
});
|
|
44
|
+
/** Namespace the write caps and the four stage policies live in (core's PARAM_NAMESPACES). */
|
|
45
|
+
const SKILLS_SETTINGS_NAMESPACE = "evolution-skills";
|
|
46
|
+
/** Schema the platform validates the user layer against; defaults mirror the core
|
|
47
|
+
* constants and the row schema, so an empty document resolves to today's behaviour. */
|
|
48
|
+
const SKILLS_SETTINGS_SCHEMA = z.object({
|
|
49
|
+
skillContentChars: z.number().min(1).default(DEFAULT_SKILL_LIMITS.maxSkillContentChars),
|
|
50
|
+
maxSkillFileBytes: z.number().min(1).default(DEFAULT_SKILL_LIMITS.maxSkillFileBytes),
|
|
51
|
+
maxSkillNameLength: z.number().min(1).default(DEFAULT_SKILL_LIMITS.maxNameLength),
|
|
52
|
+
maxDescriptionLength: z.number().min(1).default(DEFAULT_SKILL_LIMITS.maxDescriptionLength),
|
|
53
|
+
descriptionStrict: z.boolean().default(false),
|
|
54
|
+
strictCrossSource: z.boolean().default(false),
|
|
55
|
+
citationPolicy: z.union([z.const("verify"), z.const("refuse")]).default(DEFAULT_CITATION_POLICY),
|
|
56
|
+
supportFileCharPolicy: z.union([z.const("report"), z.const("enforce")]).default(DEFAULT_SUPPORT_FILE_CHAR_POLICY)
|
|
57
|
+
});
|
|
58
|
+
/** The four caps a user may only tighten, in schema order (the sentry reads it). */
|
|
59
|
+
const SKILL_SETTINGS_CAPS = [
|
|
60
|
+
"skillContentChars",
|
|
61
|
+
"maxSkillFileBytes",
|
|
62
|
+
"maxSkillNameLength",
|
|
63
|
+
"maxDescriptionLength"
|
|
64
|
+
];
|
|
65
|
+
/**
|
|
66
|
+
* Refuse a resolved section whose cap sits ABOVE the deployment's allocation.
|
|
67
|
+
* Windows have no single-field bound the schema could express, and a user who
|
|
68
|
+
* widened one would spend a budget the deployment set for the whole library.
|
|
69
|
+
* @param value - the resolved section the platform hands the owner.
|
|
70
|
+
* @param ceilings - the deployment values (the plugin row, after its clamps).
|
|
71
|
+
*/
|
|
72
|
+
function validateSkillSettings(value, ceilings) {
|
|
73
|
+
for (const key of SKILL_SETTINGS_CAPS) if (value[key] > ceilings[key]) throw new Error(`${key} may only be tightened: ${value[key]} exceeds the deployment value ${ceilings[key]}`);
|
|
74
|
+
}
|
|
44
75
|
/** v30 REV-02: read the protected-skill list off the (optional) policy
|
|
45
76
|
* snapshot through an `unknown` boundary — the Context augmentation types the
|
|
46
77
|
* getter non-optionally, but at runtime the row can be absent. */
|
|
@@ -73,26 +104,86 @@ function apply(ctx, rawConfig = {}) {
|
|
|
73
104
|
if (value !== void 0 && result !== value) numericClamped.push(name);
|
|
74
105
|
return result;
|
|
75
106
|
};
|
|
107
|
+
const libraryLimits = {
|
|
108
|
+
...policyStageLimits(policySnapshotOf(ctx.get("evolutionPolicy"))),
|
|
109
|
+
maxNameLength: limit("maxSkillNameLength", rawConfig.maxSkillNameLength, DEFAULT_SKILL_LIMITS.maxNameLength),
|
|
110
|
+
maxDescriptionLength: limit("maxDescriptionLength", rawConfig.maxDescriptionLength, DEFAULT_SKILL_LIMITS.maxDescriptionLength),
|
|
111
|
+
maxSkillContentChars: limit("maxSkillContentChars", readNumberParam(rawConfig, "skillContentChars"), DEFAULT_SKILL_LIMITS.maxSkillContentChars),
|
|
112
|
+
maxSkillFileBytes: limit("maxSkillFileBytes", rawConfig.maxSkillFileBytes, DEFAULT_SKILL_LIMITS.maxSkillFileBytes)
|
|
113
|
+
};
|
|
76
114
|
const library = newSkillLibrary({
|
|
77
115
|
config: rawConfig,
|
|
78
116
|
io,
|
|
79
|
-
limits:
|
|
80
|
-
maxNameLength: limit("maxSkillNameLength", rawConfig.maxSkillNameLength, DEFAULT_SKILL_LIMITS.maxNameLength),
|
|
81
|
-
maxDescriptionLength: limit("maxDescriptionLength", rawConfig.maxDescriptionLength, DEFAULT_SKILL_LIMITS.maxDescriptionLength),
|
|
82
|
-
maxSkillContentChars: limit("maxSkillContentChars", rawConfig.maxSkillContentChars, DEFAULT_SKILL_LIMITS.maxSkillContentChars),
|
|
83
|
-
maxSkillFileBytes: limit("maxSkillFileBytes", rawConfig.maxSkillFileBytes, DEFAULT_SKILL_LIMITS.maxSkillFileBytes)
|
|
84
|
-
},
|
|
117
|
+
limits: libraryLimits,
|
|
85
118
|
ctx,
|
|
86
119
|
threatExemptLabels: rawConfig.threatExemptLabels
|
|
87
120
|
});
|
|
88
121
|
if (numericClamped.length > 0) ctx.logger.warn(`tool-skill-manage: ${numericClamped.join(", ")} provided an invalid value; falling back to the default`);
|
|
122
|
+
const settingsBase = {
|
|
123
|
+
skillContentChars: libraryLimits.maxSkillContentChars,
|
|
124
|
+
maxSkillFileBytes: libraryLimits.maxSkillFileBytes,
|
|
125
|
+
maxSkillNameLength: libraryLimits.maxNameLength,
|
|
126
|
+
maxDescriptionLength: libraryLimits.maxDescriptionLength,
|
|
127
|
+
descriptionStrict: rawConfig.descriptionStrict ?? false,
|
|
128
|
+
strictCrossSource: rawConfig.strictCrossSource ?? false,
|
|
129
|
+
citationPolicy: libraryLimits.citationPolicy ?? DEFAULT_CITATION_POLICY,
|
|
130
|
+
supportFileCharPolicy: libraryLimits.supportFileCharPolicy ?? DEFAULT_SUPPORT_FILE_CHAR_POLICY
|
|
131
|
+
};
|
|
132
|
+
const section = {};
|
|
133
|
+
const settings = () => {
|
|
134
|
+
const stages = policyStageLimits(policySnapshotOf(ctx.get("evolutionPolicy")));
|
|
135
|
+
const overridden = (key) => section.overrides?.get(key);
|
|
136
|
+
const pick = (key) => overridden(key) ?? settingsBase[key];
|
|
137
|
+
const cap = (value, lower) => clampedNumber(value, lower, { min: 1 });
|
|
138
|
+
return {
|
|
139
|
+
skillContentChars: cap(pick("skillContentChars"), settingsBase.skillContentChars),
|
|
140
|
+
maxSkillFileBytes: cap(pick("maxSkillFileBytes"), settingsBase.maxSkillFileBytes),
|
|
141
|
+
maxSkillNameLength: cap(pick("maxSkillNameLength"), settingsBase.maxSkillNameLength),
|
|
142
|
+
maxDescriptionLength: cap(pick("maxDescriptionLength"), settingsBase.maxDescriptionLength),
|
|
143
|
+
descriptionStrict: pick("descriptionStrict"),
|
|
144
|
+
strictCrossSource: pick("strictCrossSource"),
|
|
145
|
+
citationPolicy: overridden("citationPolicy") ?? stages.citationPolicy ?? settingsBase.citationPolicy,
|
|
146
|
+
supportFileCharPolicy: overridden("supportFileCharPolicy") ?? stages.supportFileCharPolicy ?? settingsBase.supportFileCharPolicy
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
const applyLimits = () => {
|
|
150
|
+
const resolved = settings();
|
|
151
|
+
const stages = policyStageLimits(policySnapshotOf(ctx.get("evolutionPolicy")));
|
|
152
|
+
Object.assign(libraryLimits, {
|
|
153
|
+
maxNameLength: resolved.maxSkillNameLength,
|
|
154
|
+
maxDescriptionLength: resolved.maxDescriptionLength,
|
|
155
|
+
maxSkillContentChars: resolved.skillContentChars,
|
|
156
|
+
maxSkillFileBytes: resolved.maxSkillFileBytes,
|
|
157
|
+
citationPolicy: resolved.citationPolicy,
|
|
158
|
+
referenceRewrite: stages.referenceRewrite ?? DEFAULT_REFERENCE_REWRITE_POLICY,
|
|
159
|
+
archiveRetention: stages.archiveRetention ?? DEFAULT_ARCHIVE_RETENTION_POLICY,
|
|
160
|
+
supportFileCharPolicy: resolved.supportFileCharPolicy
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
section.overrides = installParamSection(ctx, PARAM_NAMESPACES["tool-skill-manage"] ?? "evolution-skills", SKILLS_SETTINGS_SCHEMA, settingsBase, {
|
|
164
|
+
warn: (message) => {
|
|
165
|
+
ctx.logger.warn("dsh-evolution-skills: " + message);
|
|
166
|
+
},
|
|
167
|
+
validate: (value) => {
|
|
168
|
+
validateSkillSettings(value, {
|
|
169
|
+
skillContentChars: settingsBase.skillContentChars,
|
|
170
|
+
maxSkillFileBytes: settingsBase.maxSkillFileBytes,
|
|
171
|
+
maxSkillNameLength: settingsBase.maxSkillNameLength,
|
|
172
|
+
maxDescriptionLength: settingsBase.maxDescriptionLength
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
onChange: () => {
|
|
176
|
+
applyLimits();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
applyLimits();
|
|
89
180
|
let crossSourceViewWarned = false;
|
|
90
181
|
const crossSourceRefusal = async (rawName, phase = "stage") => {
|
|
91
182
|
const replay = phase === "replay";
|
|
92
183
|
if (typeof rawName !== "string" || rawName === "") return null;
|
|
93
184
|
const probe = await catalogWinner(rawName);
|
|
94
185
|
if (isUnknown(probe)) {
|
|
95
|
-
if (
|
|
186
|
+
if (settings().strictCrossSource) return replay ? `Refused: the cross-source check could not be performed — ${probe.reason}. Reject this staged write, fix the catalog view, and re-stage "${rawName}".` : `Refused: the cross-source check could not be performed — ${probe.reason}. Fix the catalog view (or drop strictCrossSource) before writing "${rawName}".`;
|
|
96
187
|
if (!crossSourceViewWarned) {
|
|
97
188
|
crossSourceViewWarned = true;
|
|
98
189
|
ctx.logger.warn(`skill_manage: cross-source check skipped for this session — ${probe.reason}`);
|
|
@@ -102,7 +193,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
102
193
|
if (isPresent(probe) && probe.value !== void 0 && probe.value.provider !== "dsh-evolution") {
|
|
103
194
|
const winner = probe.value;
|
|
104
195
|
const message = `skill "${rawName}" resolves to a higher-priority "${winner.source}" skill (provider "${winner.provider}"); this write lands on the family copy, which the catalog does NOT serve — edit the "${winner.source}" copy or rename.`;
|
|
105
|
-
if (
|
|
196
|
+
if (settings().strictCrossSource) return `Refused: ${message}`;
|
|
106
197
|
ctx.logger.warn(`skill_manage: ${message}`);
|
|
107
198
|
}
|
|
108
199
|
return null;
|
|
@@ -182,7 +273,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
182
273
|
if (parsed) {
|
|
183
274
|
const feedback = authoringFeedback(parsed.frontmatter);
|
|
184
275
|
feedbackLines = feedback.lines;
|
|
185
|
-
if (
|
|
276
|
+
if (settings().descriptionStrict && feedback.over60) return {
|
|
186
277
|
ok: false,
|
|
187
278
|
message: `Authoring check: description ${feedback.descriptionChars}/60 characters exceeds the strict bar; tighten it to <=60 or set descriptionStrict=false.`,
|
|
188
279
|
skills: []
|
|
@@ -416,4 +507,4 @@ function apply(ctx, rawConfig = {}) {
|
|
|
416
507
|
});
|
|
417
508
|
}
|
|
418
509
|
//#endregion
|
|
419
|
-
export { Config, apply, inject, name };
|
|
510
|
+
export { Config, SKILLS_SETTINGS_NAMESPACE, SKILLS_SETTINGS_SCHEMA, SKILL_SETTINGS_CAPS, apply, inject, name, validateSkillSettings };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import type { Context } from '@deepseek-ai/cordis';
|
|
21
21
|
import z from '@deepseek-ai/schemastery';
|
|
22
|
+
import type { CitationPolicy, SupportFileCharPolicy } from '@lmzhen/dsh-evolution-core';
|
|
22
23
|
export declare const name = "tool-skill-manage";
|
|
23
24
|
export declare const inject: string[];
|
|
24
25
|
export interface Config {
|
|
@@ -26,6 +27,11 @@ export interface Config {
|
|
|
26
27
|
root?: string;
|
|
27
28
|
maxSkillNameLength?: number;
|
|
28
29
|
maxDescriptionLength?: number;
|
|
30
|
+
/** Read cap for SKILL.md writes. Same semantic as the policy row's
|
|
31
|
+
* `skillContentChars` (the canonical id, G0/S0.2) — but NO automatic
|
|
32
|
+
* shadowing today: this row's value is what the write paths use, which is
|
|
33
|
+
* what the G3 unification has to close. Deprecated alias (G0/S0.3): still
|
|
34
|
+
* readable, refused by writes; removed in 0.7.0. */
|
|
29
35
|
maxSkillContentChars?: number;
|
|
30
36
|
maxSkillFileBytes?: number;
|
|
31
37
|
/** When true, create/update refuse a description over the 60-char authoring bar (default: advisory feedback only). */
|
|
@@ -42,5 +48,42 @@ export interface Config {
|
|
|
42
48
|
strictCrossSource?: boolean;
|
|
43
49
|
}
|
|
44
50
|
export declare const Config: z<Config>;
|
|
51
|
+
/** Namespace the write caps and the four stage policies live in (core's PARAM_NAMESPACES). */
|
|
52
|
+
export declare const SKILLS_SETTINGS_NAMESPACE = "evolution-skills";
|
|
53
|
+
/** Write behaviour a user may change (G3/S3.4). Field names are the CANONICAL
|
|
54
|
+
* parameter ids from the registry. The four caps are TIGHTEN-ONLY: the settings
|
|
55
|
+
* `validate` hook refuses a value above what the deployment allocated, because
|
|
56
|
+
* raising them would let one user widen a shared library's write budget. */
|
|
57
|
+
export interface SkillSettings {
|
|
58
|
+
/** Character cap on a SKILL.md body. */
|
|
59
|
+
skillContentChars: number;
|
|
60
|
+
/** Byte cap on one support file. */
|
|
61
|
+
maxSkillFileBytes: number;
|
|
62
|
+
/** Character cap on a skill name. */
|
|
63
|
+
maxSkillNameLength: number;
|
|
64
|
+
/** Character cap on a skill description. */
|
|
65
|
+
maxDescriptionLength: number;
|
|
66
|
+
/** Refuse a description over the authoring bar instead of advising. */
|
|
67
|
+
descriptionStrict: boolean;
|
|
68
|
+
/** Refuse writes whose catalog entry resolves outside the family. */
|
|
69
|
+
strictCrossSource: boolean;
|
|
70
|
+
/** Refuse a move that would leave a dangling reference, or verify it. */
|
|
71
|
+
citationPolicy: CitationPolicy;
|
|
72
|
+
/** Warn about an oversize support file, or refuse the write. */
|
|
73
|
+
supportFileCharPolicy: SupportFileCharPolicy;
|
|
74
|
+
}
|
|
75
|
+
/** Schema the platform validates the user layer against; defaults mirror the core
|
|
76
|
+
* constants and the row schema, so an empty document resolves to today's behaviour. */
|
|
77
|
+
export declare const SKILLS_SETTINGS_SCHEMA: z<SkillSettings>;
|
|
78
|
+
/** The four caps a user may only tighten, in schema order (the sentry reads it). */
|
|
79
|
+
export declare const SKILL_SETTINGS_CAPS: readonly ["skillContentChars", "maxSkillFileBytes", "maxSkillNameLength", "maxDescriptionLength"];
|
|
80
|
+
/**
|
|
81
|
+
* Refuse a resolved section whose cap sits ABOVE the deployment's allocation.
|
|
82
|
+
* Windows have no single-field bound the schema could express, and a user who
|
|
83
|
+
* widened one would spend a budget the deployment set for the whole library.
|
|
84
|
+
* @param value - the resolved section the platform hands the owner.
|
|
85
|
+
* @param ceilings - the deployment values (the plugin row, after its clamps).
|
|
86
|
+
*/
|
|
87
|
+
export declare function validateSkillSettings(value: SkillSettings, ceilings: Pick<SkillSettings, (typeof SKILL_SETTINGS_CAPS)[number]>): void;
|
|
45
88
|
export declare function apply(ctx: Context, rawConfig?: Config): void;
|
|
46
89
|
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-tool-skill-manage",
|
|
3
3
|
"description": "Model-facing skill_manage tool (community build)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -27,24 +27,24 @@
|
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
30
|
-
"@lmzhen/dsh-evolution-approval": "^0.
|
|
31
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
30
|
+
"@lmzhen/dsh-evolution-approval": "^0.6.0",
|
|
31
|
+
"@lmzhen/dsh-evolution-core": "^0.6.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
35
35
|
"@deepseek-ai/dsh-skill": "^0.1.5-rc.2",
|
|
36
36
|
"@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.2",
|
|
37
37
|
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
38
|
-
"@lmzhen/dsh-evolution-io": "^0.
|
|
39
|
-
"@lmzhen/dsh-skill-usage": "^0.
|
|
38
|
+
"@lmzhen/dsh-evolution-io": "^0.6.0",
|
|
39
|
+
"@lmzhen/dsh-skill-usage": "^0.6.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@deepseek-ai/dsh-agent-loop-testkit": "^0.1.5-rc.2",
|
|
43
43
|
"@deepseek-ai/dsh-skill": "^0.1.5-rc.2",
|
|
44
44
|
"@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.2",
|
|
45
45
|
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
46
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
47
|
-
"@lmzhen/dsh-evolution-io": "^0.
|
|
48
|
-
"@lmzhen/dsh-skill-usage": "^0.
|
|
46
|
+
"@lmzhen/dsh-evolution-core": "^0.6.0",
|
|
47
|
+
"@lmzhen/dsh-evolution-io": "^0.6.0",
|
|
48
|
+
"@lmzhen/dsh-skill-usage": "^0.6.0"
|
|
49
49
|
}
|
|
50
50
|
}
|