@angar2/taskery 0.7.0 → 0.8.1
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/bin/close.js +13 -1
- package/bin/legacy-quotes.js +60 -0
- package/bin/lib.js +159 -7
- package/bin/migrate.js +261 -86
- package/bin/update.js +13 -7
- package/package.json +1 -1
- package/template/.codex/agents/task-tester.toml +10 -4
- package/template/.codex/agents/test-plan-author.toml +34 -0
- package/template/.project/rules/CHANGELOG_RULE.md +26 -20
- package/template/.project/rules/GIT_RULE.md +2 -0
- package/template/.project/rules/TASKERY_RULE.md +4 -4
- package/template/.project/rules/TASK_DOC_RULE.md +11 -1
- package/template/shared/skills/project-init/SKILL.md +1 -0
- package/template/shared/skills/task-close/SKILL.md +11 -8
- package/template/shared/skills/task-plan/SKILL.md +121 -6
- package/template/shared/skills/task-test/SKILL.md +22 -3
package/bin/migrate.js
CHANGED
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 이행 4종:
|
|
7
7
|
* 1. ingestActivePlan — 구형 AGENT-GUIDE.md '## 활성 plan 버전' → manifest.activePlan (1회)
|
|
8
|
-
* 2.
|
|
9
|
-
* 새 AGENTS.md
|
|
8
|
+
* 2. recomposeEntryDocs — 진입 문서 **재조립**. 현 진입 문서(AGENTS.md, 없으면 구 CLAUDE.md)에서
|
|
9
|
+
* 리포 내용을 건져 새 AGENTS.md 템플릿에 끼워넣는다. 최초 이행 때는
|
|
10
|
+
* CLAUDE.md를 `@AGENTS.md` 한 줄로 축약한다.
|
|
11
|
+
* **매 update 항상 실행한다(1회성 아님).** AGENTS.md는 리포 값을 품어
|
|
12
|
+
* 템플릿과 바이트 일치가 원리적으로 불가능하므로, 본 루프의 복사 대상이
|
|
13
|
+
* 되어선 안 된다(0.7.0 결함: 이행 후 본 루프가 값을 통째로 덮거나,
|
|
14
|
+
* 거절하면 구판 문안에 영구 고착됐다).
|
|
10
15
|
* **update 본 루프보다 먼저 실행하고, 처리한 경로를 본 루프에서 제외한다.**
|
|
11
|
-
* 본 루프에 맡기면 '사용자 customize 검출' 프롬프트가 뜨는데,
|
|
12
|
-
* 덮으면 리포 값이 .bak에만 남고 안 덮으면 구판 전문이 남아 새 판과 충돌한다.
|
|
13
16
|
* 3. migrateGitignore — AGENTS.md가 agnostic으로 바뀌어 모든 리포에 새로 깔리는데
|
|
14
17
|
* .gitignore 기록은 init에만 있어 누락된다. 누락 패턴 append 제안.
|
|
15
18
|
* 4. migrateTestGuide — 폐지되는 TEST-GUIDE.md의 축적 내용을 TEST_RULE.local.md로 1회 이관.
|
|
@@ -20,7 +23,8 @@
|
|
|
20
23
|
const fs = require('fs');
|
|
21
24
|
const path = require('path');
|
|
22
25
|
|
|
23
|
-
const { sha256, copyFile, gitignorePatternsFor } = require('./lib');
|
|
26
|
+
const { sha256, sha256Text, copyFile, gitignorePatternsFor } = require('./lib');
|
|
27
|
+
const { LEGACY_TEMPLATE_QUOTES } = require('./legacy-quotes');
|
|
24
28
|
|
|
25
29
|
// 진입 문서에서 이식할 *리포 값* 섹션. 헤딩 텍스트 접두로 매칭한다 —
|
|
26
30
|
// 실제 문서의 헤딩은 '## 검수 실행 명령 (해당 시 — 앱 실행·검수가 필요한 프로젝트)'처럼
|
|
@@ -59,52 +63,144 @@ function splitSections(content) {
|
|
|
59
63
|
return out;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
// 문서를 { 머리말, 섹션[] }로 쪼갠다. splitSections와 달리 첫 '## ' 앞의 머리말을 버리지 않는다 —
|
|
67
|
+
// 재조립은 문서 전체를 다시 써내므로 머리말(제목·최상위 인용문)도 결과물에 들어가야 한다.
|
|
68
|
+
function parseDoc(content) {
|
|
69
|
+
const preamble = [];
|
|
70
|
+
const sections = [];
|
|
71
|
+
let cur = null;
|
|
72
|
+
let inFence = false;
|
|
73
|
+
for (const line of content.split('\n')) {
|
|
74
|
+
if (/^\s*(```|~~~)/.test(line)) inFence = !inFence;
|
|
75
|
+
const m = inFence ? null : line.match(/^##\s+(.*)$/);
|
|
76
|
+
if (m) {
|
|
77
|
+
cur = { heading: line, title: m[1].trim(), body: [] };
|
|
78
|
+
sections.push(cur);
|
|
79
|
+
} else if (cur) {
|
|
80
|
+
cur.body.push(line);
|
|
81
|
+
} else {
|
|
82
|
+
preamble.push(line);
|
|
83
|
+
}
|
|
69
84
|
}
|
|
70
|
-
|
|
71
|
-
return body.join('\n');
|
|
85
|
+
return { preamble, sections };
|
|
72
86
|
}
|
|
73
87
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (/^\s
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
// taskery가 쓴 인용문 집합 = 과거 템플릿 전량(고정 목록) ∪ *현재* 템플릿(실시간 추출).
|
|
89
|
+
// 현재 템플릿을 함께 넣는 것이 핵심이다 — 그래야 다음 버전의 새 문안도 그 다음 update에서
|
|
90
|
+
// 자기 자신을 중복 없이 걷어낸다(목록을 손으로 관리하지 않아도 영구히 자기유지된다).
|
|
91
|
+
function boilerplateQuotes(templateContent) {
|
|
92
|
+
const set = new Set(LEGACY_TEMPLATE_QUOTES.map((s) => s.trim()));
|
|
93
|
+
for (const l of templateContent.split('\n')) {
|
|
94
|
+
if (/^\s*>/.test(l)) set.add(l.trim());
|
|
95
|
+
}
|
|
96
|
+
return set;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 우리가 수행한 파일 이름 변경 — 리포가 직접 쓴 문장 안의 죽은 경로를 대응 경로로 바꾼다.
|
|
100
|
+
// 1:1로 확정된 것만 넣는다(추측 치환 금지).
|
|
101
|
+
const DEAD_PATH_REWRITES = [['.project/TEST-GUIDE.md', '.project/rules/TEST_RULE.local.md']];
|
|
102
|
+
|
|
103
|
+
// 인용문 블록(헤딩 직후 연속된 '>' 줄)과 꼬리(끝의 빈 줄·구분선)를 템플릿 본문에서 뽑는다.
|
|
104
|
+
function splitTemplateBody(body) {
|
|
84
105
|
let i = 0;
|
|
85
|
-
while (i <
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
106
|
+
while (i < body.length && body[i].trim() === '') i++;
|
|
107
|
+
const quotes = [];
|
|
108
|
+
while (i < body.length && /^\s*>/.test(body[i])) quotes.push(body[i++]);
|
|
109
|
+
const tail = [];
|
|
110
|
+
let j = body.length - 1;
|
|
111
|
+
while (j >= i && (body[j].trim() === '' || body[j].trim() === '---')) tail.unshift(body[j--]);
|
|
112
|
+
return { quotes, tail };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* AGENTS.md 재조립 — 새 템플릿의 지침 + 현 문서의 리포 내용.
|
|
117
|
+
*
|
|
118
|
+
* 절마다: 새 템플릿 헤딩 + 새 템플릿 인용문 + 현 문서 본문(단, taskery 인용문은 제거).
|
|
119
|
+
* 값 줄·표·산문은 판정 대상이 아니라 전부 보존된다. 리포가 직접 쓴 인용문도 보존된다.
|
|
120
|
+
*
|
|
121
|
+
* @returns {{ content, kept, dropped, rewritten, omitted, missing }}
|
|
122
|
+
* kept — 보존한 리포 인용문 / dropped — 걷어낸 taskery 구 문안
|
|
123
|
+
* rewritten — 죽은 경로를 치환한 줄 / omitted — 현 문서에 없어 결과물에서 뺀 절
|
|
124
|
+
* missing — omitted 중 필수 절(호출자가 보류 판정에 쓴다)
|
|
125
|
+
*/
|
|
126
|
+
function composeAgentsDoc(currentContent, templateContent) {
|
|
127
|
+
const tpl = parseDoc(templateContent);
|
|
128
|
+
const cur = parseDoc(currentContent);
|
|
129
|
+
const boiler = boilerplateQuotes(templateContent);
|
|
130
|
+
const kept = [];
|
|
131
|
+
const dropped = [];
|
|
132
|
+
const rewritten = [];
|
|
133
|
+
const omitted = [];
|
|
134
|
+
const out = [...tpl.preamble];
|
|
135
|
+
|
|
136
|
+
for (const sec of tpl.sections) {
|
|
137
|
+
const key = REPO_VALUE_SECTIONS.find((k) => sec.title.startsWith(k));
|
|
138
|
+
if (!key) {
|
|
139
|
+
out.push(sec.heading, ...sec.body);
|
|
91
140
|
continue;
|
|
92
141
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
142
|
+
const hit = cur.sections.find((s) => s.title.startsWith(key));
|
|
143
|
+
const { quotes, tail } = splitTemplateBody(sec.body);
|
|
144
|
+
if (!hit) {
|
|
145
|
+
// 리포가 지운 절은 되살리지 않는다(삭제 의사 존중). 절 사이 구분선만 유지한다.
|
|
146
|
+
omitted.push(key);
|
|
147
|
+
out.push(...tail);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const repoBody = [];
|
|
151
|
+
for (const raw of hit.body) {
|
|
152
|
+
const isQuote = /^\s*>/.test(raw);
|
|
153
|
+
if (isQuote && boiler.has(raw.trim())) {
|
|
154
|
+
dropped.push(raw.trim());
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
let line = raw;
|
|
158
|
+
for (const [from, to] of DEAD_PATH_REWRITES) {
|
|
159
|
+
if (line.includes(from)) line = line.split(from).join(to);
|
|
160
|
+
}
|
|
161
|
+
if (line !== raw) rewritten.push({ before: raw.trim(), after: line.trim() });
|
|
162
|
+
if (isQuote) kept.push(line.trim());
|
|
163
|
+
repoBody.push(line);
|
|
164
|
+
}
|
|
165
|
+
while (repoBody.length && repoBody[0].trim() === '') repoBody.shift();
|
|
166
|
+
while (
|
|
167
|
+
repoBody.length &&
|
|
168
|
+
(repoBody[repoBody.length - 1].trim() === '' || repoBody[repoBody.length - 1].trim() === '---')
|
|
169
|
+
) {
|
|
170
|
+
repoBody.pop();
|
|
100
171
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
172
|
+
out.push(sec.heading);
|
|
173
|
+
if (repoBody.length === 0) {
|
|
174
|
+
// 절은 있는데 내용이 비었다 → 새 템플릿 본문(플레이스홀더)을 그대로 쓴다.
|
|
175
|
+
out.push(...sec.body);
|
|
176
|
+
continue;
|
|
104
177
|
}
|
|
105
|
-
out.push('',
|
|
178
|
+
if (quotes.length) out.push('', ...quotes);
|
|
179
|
+
out.push('', ...repoBody, ...tail);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 결과물에 실리지 않은 현 문서의 내용 — 리포 값 4개 절 *밖*에 사람이 적어 둔 줄이 여기 걸린다.
|
|
183
|
+
// 그 자리는 taskery 소유라 템플릿 판으로 덮이는 것이 설계지만, 재조립은 매 update 도는 상시
|
|
184
|
+
// 동작이므로 조용히 사라지면 안 된다. 버리되 반드시 알린다(.bak이 원본 참조처).
|
|
185
|
+
const inResult = new Set(out.map((l) => l.trim()));
|
|
186
|
+
const rewrittenBefore = new Set(rewritten.map((r) => r.before));
|
|
187
|
+
const unmapped = [];
|
|
188
|
+
for (const line of currentContent.split('\n')) {
|
|
189
|
+
const l = line.trim();
|
|
190
|
+
if (!l || l === '---') continue;
|
|
191
|
+
if (inResult.has(l) || boiler.has(l) || rewrittenBefore.has(l)) continue;
|
|
192
|
+
unmapped.push(l);
|
|
106
193
|
}
|
|
107
|
-
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
content: out.join('\n'),
|
|
197
|
+
kept,
|
|
198
|
+
dropped,
|
|
199
|
+
rewritten,
|
|
200
|
+
omitted,
|
|
201
|
+
unmapped,
|
|
202
|
+
missing: omitted.filter((k) => !OPTIONAL_SECTIONS.has(k)),
|
|
203
|
+
};
|
|
108
204
|
}
|
|
109
205
|
|
|
110
206
|
/**
|
|
@@ -128,7 +224,7 @@ function ingestActivePlan(cwd, oldManifest) {
|
|
|
128
224
|
* handled — update 본 루프에서 제외할 설치 경로
|
|
129
225
|
* entries — manifest.files에 기록할 항목 (설치 경로 → { hash, core, managed })
|
|
130
226
|
*/
|
|
131
|
-
function
|
|
227
|
+
function recomposeEntryDocs(cwd, templateDir, platforms, oldManifest) {
|
|
132
228
|
const notes = [];
|
|
133
229
|
const entries = {};
|
|
134
230
|
const skip = { handled: [], entries, notes, held: false, done: false };
|
|
@@ -137,63 +233,86 @@ function migrateEntryDocs(cwd, templateDir, platforms, oldManifest) {
|
|
|
137
233
|
const claudePath = path.join(cwd, 'CLAUDE.md');
|
|
138
234
|
const hasAgents = fs.existsSync(agentsPath);
|
|
139
235
|
const hasClaude = fs.existsSync(claudePath);
|
|
140
|
-
|
|
236
|
+
// 최초 이행 여부는 manifest 마커 하나로만 판정한다. 파일 구조로 추정하면 플랫폼별로 오판하고
|
|
237
|
+
// (codex 단독은 CLAUDE.md가 없다), 버전으로 거르면 보류된 리포가 영영 재시도되지 않는다.
|
|
238
|
+
const migrated = oldManifest.entryDocs === ENTRY_DOCS_MARKER;
|
|
141
239
|
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
// 버전 게이트를 두면 보류된 리포가 영영 재시도되지 않는다("다음 update가 다시 시도합니다"가 거짓이 된다).
|
|
147
|
-
// - 신규 설치는 init이 마커를 박아 두므로 이 한 줄로 걸러진다.
|
|
148
|
-
if (oldManifest.entryDocs === ENTRY_DOCS_MARKER) return skip;
|
|
149
|
-
|
|
150
|
-
// 소스 선정: AGENTS.md 우선(양쪽은 미러라 값이 같다), 없으면 CLAUDE.md.
|
|
151
|
-
const srcPath = hasAgents ? agentsPath : claudePath;
|
|
152
|
-
const srcName = hasAgents ? 'AGENTS.md' : 'CLAUDE.md';
|
|
153
|
-
const sections = splitSections(fs.readFileSync(srcPath, 'utf8'));
|
|
154
|
-
|
|
155
|
-
const extracted = {};
|
|
156
|
-
const missing = [];
|
|
157
|
-
for (const key of REPO_VALUE_SECTIONS) {
|
|
158
|
-
const body = findSectionBody(sections, key);
|
|
159
|
-
if (body == null) missing.push(key);
|
|
160
|
-
else extracted[key] = body;
|
|
161
|
-
}
|
|
162
|
-
const fatalMissing = missing.filter((k) => !OPTIONAL_SECTIONS.has(k));
|
|
240
|
+
// 조립 소스: 현 AGENTS.md가 최우선(이행 후의 정상 경로). 없으면 미이행 리포의 구 CLAUDE.md.
|
|
241
|
+
const srcPath = hasAgents ? agentsPath : hasClaude && !migrated ? claudePath : null;
|
|
242
|
+
if (!srcPath) return skip; // 진입 문서 자체가 없음 → 본 루프의 '신규' 분기가 템플릿을 깐다
|
|
243
|
+
const srcName = path.basename(srcPath);
|
|
163
244
|
|
|
164
245
|
const newAgentsSrc = path.join(templateDir, 'AGENTS.md');
|
|
165
246
|
const newAgents = fs.readFileSync(newAgentsSrc, 'utf8');
|
|
247
|
+
const current = fs.readFileSync(srcPath, 'utf8');
|
|
248
|
+
const composed = composeAgentsDoc(current, newAgents);
|
|
166
249
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
250
|
+
// 최초 이행에서 필수 절을 못 찾으면 소스 판정이 틀렸다는 뜻이다 → 어느 파일도 덮지 않고 보류.
|
|
251
|
+
// (이행 후의 필수 절 부재는 사용자가 지운 것이므로 보류하지 않고 그 삭제를 존중한다.)
|
|
252
|
+
if (!migrated && composed.missing.length > 0) {
|
|
170
253
|
fs.writeFileSync(path.join(cwd, 'AGENTS.md.new'), newAgents);
|
|
171
254
|
notes.push(
|
|
172
|
-
`⚠ 진입 문서 이행 보류 — ${srcName}에서 필수 섹션을 찾지 못함 (${
|
|
255
|
+
`⚠ 진입 문서 이행 보류 — ${srcName}에서 필수 섹션을 찾지 못함 (${composed.missing.join(', ')}).`,
|
|
173
256
|
` 새 판을 AGENTS.md.new 로 두었습니다. 리포 값을 직접 옮긴 후 AGENTS.md 로 바꿔주세요.`,
|
|
174
257
|
` (기존 파일은 그대로 보존되었고 manifest도 갱신하지 않아 다음 update가 다시 시도합니다.)`,
|
|
175
258
|
);
|
|
176
259
|
return { handled: ['AGENTS.md', 'CLAUDE.md'], entries, notes, held: true, done: false };
|
|
177
260
|
}
|
|
178
261
|
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
fs.
|
|
262
|
+
// 결과물이 현 파일과 다를 때만 쓴다. .bak은 *최초 1회만* — 이미 있으면 그게 사용자의 원본이다.
|
|
263
|
+
const changed = !hasAgents || composed.content !== current;
|
|
264
|
+
if (changed) {
|
|
265
|
+
if (hasAgents && !fs.existsSync(agentsPath + '.bak')) {
|
|
266
|
+
fs.copyFileSync(agentsPath, agentsPath + '.bak');
|
|
267
|
+
notes.push(` 직전 AGENTS.md는 AGENTS.md.bak 으로 백업.`);
|
|
268
|
+
}
|
|
269
|
+
fs.writeFileSync(agentsPath, composed.content);
|
|
183
270
|
}
|
|
184
|
-
|
|
185
|
-
|
|
271
|
+
// manifest에는 **조립 결과물**의 해시를 기록한다. 템플릿 원본 해시를 적으면 실제 파일과
|
|
272
|
+
// 영구 불일치해 매 update마다 '사용자 customize' 오검출이 뜬다(0.7.0 결함).
|
|
273
|
+
entries['AGENTS.md'] = { hash: sha256Text(composed.content), core: true, managed: true };
|
|
186
274
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
275
|
+
if (!migrated) {
|
|
276
|
+
notes.push(
|
|
277
|
+
`진입 문서 재편 — ${srcName}의 리포 내용을 새 AGENTS.md로 재조립.`,
|
|
278
|
+
` 구 문서의 taskery 지침 본문은 새 체계(.project/rules/)로 대체됩니다 — 원본은 ${srcName}.bak.`,
|
|
279
|
+
);
|
|
280
|
+
} else if (changed) {
|
|
281
|
+
notes.push(`AGENTS.md 재조립 — 리포 내용 보존, taskery 지침만 새 판으로 교체.`);
|
|
282
|
+
}
|
|
283
|
+
if (changed) {
|
|
284
|
+
if (composed.dropped.length) {
|
|
285
|
+
notes.push(` 구 taskery 문안 ${composed.dropped.length}줄 제거(새 문안이 대체).`);
|
|
286
|
+
}
|
|
287
|
+
if (composed.kept.length) {
|
|
288
|
+
notes.push(` 리포가 직접 쓴 인용문 ${composed.kept.length}줄 보존.`);
|
|
289
|
+
}
|
|
290
|
+
for (const r of composed.rewritten) {
|
|
291
|
+
notes.push(` 경로 갱신: ${r.before}`);
|
|
292
|
+
notes.push(` → ${r.after}`);
|
|
293
|
+
}
|
|
294
|
+
if (composed.omitted.length) {
|
|
295
|
+
notes.push(` 리포에 없어 결과물에서 뺀 절: ${composed.omitted.join(', ')} (되살리지 않음)`);
|
|
296
|
+
}
|
|
297
|
+
if (composed.unmapped.length && migrated) {
|
|
298
|
+
// 리포 값 칸 밖의 내용은 AGENTS.md가 소유하지 않는다(TASKERY_RULE — 규칙은 rules/, 커스텀은 *.local.md).
|
|
299
|
+
// 그래도 사람이 적어 둔 것이므로 전량을 보여주고 원본 위치를 알린다.
|
|
300
|
+
// 최초 이행에서는 알리지 않는다 — 그때의 미반영분은 전량이 *구 taskery 문서 본문*이라
|
|
301
|
+
// (0.6.1 기준 88줄) 경고가 아니라 노이즈다. 그 경우는 아래 한 줄과 .bak으로 갈음한다.
|
|
302
|
+
notes.push(
|
|
303
|
+
` ⚠ 리포 값 칸(프로젝트 메타 · 각 명령) 밖의 아래 ${composed.unmapped.length}줄은 결과물에 싣지 않았습니다.`,
|
|
304
|
+
` 그 자리는 taskery 지침 영역이라 새 판으로 교체됩니다. 남길 내용이면 값 칸이나 *.local.md로 옮기세요.`,
|
|
305
|
+
` (직전 원본: AGENTS.md.bak)`,
|
|
306
|
+
);
|
|
307
|
+
for (const l of composed.unmapped) notes.push(` · ${l.length > 96 ? l.slice(0, 96) + '…' : l}`);
|
|
308
|
+
}
|
|
191
309
|
}
|
|
192
310
|
|
|
193
311
|
const handled = ['AGENTS.md'];
|
|
194
312
|
|
|
195
|
-
// CLAUDE.md는 `@AGENTS.md` 한 줄로 축약 (claude 플랫폼일 때만).
|
|
196
|
-
|
|
313
|
+
// CLAUDE.md는 `@AGENTS.md` 한 줄로 축약 — 최초 이행 때 1회(claude 플랫폼일 때만).
|
|
314
|
+
// 이행 후에는 순수 템플릿 파일이라 본 루프가 정상 관리한다.
|
|
315
|
+
if (!migrated && platforms.includes('claude')) {
|
|
197
316
|
const claudeSrc = path.join(templateDir, 'CLAUDE.md');
|
|
198
317
|
if (hasClaude && !fs.existsSync(claudePath + '.bak')) {
|
|
199
318
|
fs.copyFileSync(claudePath, claudePath + '.bak');
|
|
@@ -208,6 +327,22 @@ function migrateEntryDocs(cwd, templateDir, platforms, oldManifest) {
|
|
|
208
327
|
return { handled, entries, notes, held: false, done: true };
|
|
209
328
|
}
|
|
210
329
|
|
|
330
|
+
/**
|
|
331
|
+
* 2-b. 구형 AGENT-GUIDE.md 정리 — 활성 plan이 manifest로 이관된 뒤에도 남아 있으면
|
|
332
|
+
* 같은 정보를 두 곳에 보유하게 되고, manifest 값이 비는 순간 lib의 구형 폴백이
|
|
333
|
+
* 낡은 값을 되살린다. 삭제는 하지 않고(사용자 파일 불가침) 이름을 바꿔 폴백 경로에서 뺀다.
|
|
334
|
+
* @returns {string|null} 처리 안내 (해당 없으면 null)
|
|
335
|
+
*/
|
|
336
|
+
function retireLegacyAgentGuide(cwd, activePlan) {
|
|
337
|
+
if (!activePlan) return null; // manifest가 아직 활성 plan을 못 들었으면 폴백을 남겨둔다
|
|
338
|
+
const guidePath = path.join(cwd, '.project', 'AGENT-GUIDE.md');
|
|
339
|
+
if (!fs.existsSync(guidePath)) return null;
|
|
340
|
+
const retired = guidePath + '.obsolete';
|
|
341
|
+
if (fs.existsSync(retired)) return null;
|
|
342
|
+
fs.renameSync(guidePath, retired);
|
|
343
|
+
return `구형 .project/AGENT-GUIDE.md 는 활성 plan이 manifest로 이관돼 역할이 끝났습니다 — AGENT-GUIDE.md.obsolete 로 이름 변경(내용 보존). 확인 후 삭제해도 됩니다.`;
|
|
344
|
+
}
|
|
345
|
+
|
|
211
346
|
/**
|
|
212
347
|
* 3. .gitignore 누락 패턴 — 이미 taskery 패턴을 쓰는 리포에만 제안한다.
|
|
213
348
|
* (init에서 등록을 거부한 리포는 패턴이 하나도 없으므로 건드리지 않는다.)
|
|
@@ -231,11 +366,39 @@ function planGitignore(cwd, platforms) {
|
|
|
231
366
|
};
|
|
232
367
|
}
|
|
233
368
|
|
|
369
|
+
// TEST_RULE.local.md 필수 절 골격(TASKERY_RULE §8) — 이관 본문에 없으면 append한다.
|
|
370
|
+
// 문안은 project-init 골격과 동일 유지. '방식별 실행 경로'는 이관 본문 자체가 그 내용이라 제외.
|
|
371
|
+
const TEST_RULE_REQUIRED_SECTIONS = [
|
|
372
|
+
{
|
|
373
|
+
key: '테스트 실행 환경',
|
|
374
|
+
block: [
|
|
375
|
+
'## 테스트 실행 환경 (격리 실행 경로)',
|
|
376
|
+
'',
|
|
377
|
+
'> 실제 앱을 띄워 UI를 자동 조작하는 스위트를 **사용자의 화면·마우스·키보드를 뺏지 않고** 돌리는 경로.',
|
|
378
|
+
'> 여기가 비어 있으면 자동 게이트·격리 세션은 그 스위트를 실행하지 않고 사용자 승인을 받는다.',
|
|
379
|
+
'> 적을 수 있는 것: 창 숨김·헤드리스 플래그 / 가상 디스플레이·컨테이너 / VM / CI 러너 위임 / 점유 스위트를 제외하는 한정 실행 옵션.',
|
|
380
|
+
'> 결정 자체도 기록 대상 — 격리 경로를 두지 않기로 했다면 *"없음 — 매번 승인(사용자 결정 YYYY-MM-DD)"*로 적어 미결정 상태와 구분한다.',
|
|
381
|
+
'',
|
|
382
|
+
'<아직 미정 — 여기에 이 리포의 격리 실행 경로를 채운다>',
|
|
383
|
+
].join('\n'),
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
key: '범위·방식 정책',
|
|
387
|
+
block: [
|
|
388
|
+
'## 범위·방식 정책',
|
|
389
|
+
'',
|
|
390
|
+
'> 무엇을 얼마나 검증하나 — 수정 루프에서의 실행 범위, 테스트 신설 상한 등.',
|
|
391
|
+
'',
|
|
392
|
+
'<아직 미정 — 여기에 이 리포의 범위 정책을 채운다>',
|
|
393
|
+
].join('\n'),
|
|
394
|
+
},
|
|
395
|
+
];
|
|
396
|
+
|
|
234
397
|
/**
|
|
235
398
|
* 4. TEST-GUIDE.md → TEST_RULE.local.md 이관.
|
|
236
399
|
* 폐지되는 문서라 그대로 두면 새 참조처들이 읽지 않아 축적 내용이 고아가 된다.
|
|
237
400
|
* 원본은 지우지 않는다(사용자 파일 불가침).
|
|
238
|
-
* @returns {{ available: boolean, apply: function }}
|
|
401
|
+
* @returns {{ available: boolean, apply: function }} — apply()는 append한 필수 절 key 목록을 반환
|
|
239
402
|
*/
|
|
240
403
|
function planTestGuide(cwd) {
|
|
241
404
|
const guidePath = path.join(cwd, '.project', 'TEST-GUIDE.md');
|
|
@@ -260,8 +423,18 @@ function planTestGuide(cwd) {
|
|
|
260
423
|
'> `*.local.md`는 taskery update가 건드리지 않는다 — 이 리포가 소유한다.',
|
|
261
424
|
'',
|
|
262
425
|
].join('\n');
|
|
426
|
+
// 필수 절 골격 보강 — TEST-GUIDE에는 없던 절(TASKERY_RULE §8)이라 이관만으로는 빠진다.
|
|
427
|
+
// 헤딩은 접두 일치로 검사한다(완전 일치 요구 금지 — 뒤에 부가 텍스트가 붙는다).
|
|
428
|
+
const titles = splitSections(stripped).map((s) => s.title);
|
|
429
|
+
const appended = TEST_RULE_REQUIRED_SECTIONS.filter(
|
|
430
|
+
(sec) => !titles.some((t) => t.startsWith(sec.key)),
|
|
431
|
+
);
|
|
432
|
+
const tail = appended.length
|
|
433
|
+
? `${stripped.endsWith('\n') ? '' : '\n'}\n${appended.map((s) => s.block).join('\n\n')}\n`
|
|
434
|
+
: '';
|
|
263
435
|
fs.mkdirSync(path.dirname(localPath), { recursive: true });
|
|
264
|
-
fs.writeFileSync(localPath, header + stripped);
|
|
436
|
+
fs.writeFileSync(localPath, header + stripped + tail);
|
|
437
|
+
return appended.map((s) => s.key);
|
|
265
438
|
},
|
|
266
439
|
};
|
|
267
440
|
}
|
|
@@ -297,10 +470,12 @@ module.exports = {
|
|
|
297
470
|
mergeGitignore,
|
|
298
471
|
isBefore070,
|
|
299
472
|
splitSections,
|
|
300
|
-
|
|
301
|
-
|
|
473
|
+
parseDoc,
|
|
474
|
+
boilerplateQuotes,
|
|
475
|
+
composeAgentsDoc,
|
|
302
476
|
ingestActivePlan,
|
|
303
|
-
|
|
477
|
+
retireLegacyAgentGuide,
|
|
478
|
+
recomposeEntryDocs,
|
|
304
479
|
planGitignore,
|
|
305
480
|
planTestGuide,
|
|
306
481
|
};
|
package/bin/update.js
CHANGED
|
@@ -42,7 +42,8 @@ const {
|
|
|
42
42
|
ENTRY_DOCS_MARKER,
|
|
43
43
|
mergeGitignore,
|
|
44
44
|
ingestActivePlan,
|
|
45
|
-
|
|
45
|
+
retireLegacyAgentGuide,
|
|
46
|
+
recomposeEntryDocs,
|
|
46
47
|
planGitignore,
|
|
47
48
|
planTestGuide,
|
|
48
49
|
} = require('./migrate');
|
|
@@ -91,11 +92,11 @@ async function main() {
|
|
|
91
92
|
const newFiles = {};
|
|
92
93
|
const summary = { unchanged: 0, new: 0, autoUpdated: 0, customizedReplaced: 0, skipped: 0, removed: 0 };
|
|
93
94
|
|
|
94
|
-
// 2.5.
|
|
95
|
-
//
|
|
96
|
-
// 덮으면 리포 값이 .bak에만 남고
|
|
97
|
-
//
|
|
98
|
-
const entryMig =
|
|
95
|
+
// 2.5. 진입 문서 재조립 — **본 루프보다 먼저, 그리고 매 update 항상.**
|
|
96
|
+
// AGENTS.md는 리포 값을 품어 템플릿과 바이트 일치가 원리적으로 불가능하다. 본 루프에 맡기면
|
|
97
|
+
// '사용자 customize 검출'이 떠서, 덮으면 리포 값이 .bak에만 남고 거절하면 구판 문안에 고착된다.
|
|
98
|
+
// 재조립이 최종 상태까지 기록하고, 처리한 경로는 아래 루프에서 제외한다.
|
|
99
|
+
const entryMig = recomposeEntryDocs(cwd, templateDir, platforms, oldManifest);
|
|
99
100
|
const entryHandled = new Set(entryMig.handled);
|
|
100
101
|
for (const line of entryMig.notes) console.log(` ${line}`);
|
|
101
102
|
Object.assign(newFiles, entryMig.entries);
|
|
@@ -212,6 +213,8 @@ async function main() {
|
|
|
212
213
|
// 소리 없이 사라지고 구형 AGENT-GUIDE 폴백의 낡은 값으로 되돌아간다.
|
|
213
214
|
const { activePlan, ingested: activePlanIngested } = ingestActivePlan(cwd, oldManifest);
|
|
214
215
|
if (activePlanIngested) migration.activePlan = true;
|
|
216
|
+
const retiredGuide = retireLegacyAgentGuide(cwd, activePlan);
|
|
217
|
+
if (retiredGuide) console.log(`\n ${retiredGuide}`);
|
|
215
218
|
|
|
216
219
|
const newManifest = {
|
|
217
220
|
version: getPackageVersion(),
|
|
@@ -252,8 +255,11 @@ async function main() {
|
|
|
252
255
|
console.log(`\n .project/TEST-GUIDE.md는 0.7.0에서 폐지되고 역할이 TEST_RULE.local.md로 옮겨졌습니다.`);
|
|
253
256
|
console.log(` 지금 옮기지 않으면 /task-plan·/task-dev가 기존 내용을 읽지 않습니다. (원본은 보존)`);
|
|
254
257
|
if (await confirm(` 내용을 .project/rules/TEST_RULE.local.md로 이관할까?`)) {
|
|
255
|
-
tg.apply();
|
|
258
|
+
const appended = tg.apply();
|
|
256
259
|
console.log(` 이관 완료 — .project/rules/TEST_RULE.local.md (원본 TEST-GUIDE.md 보존)`);
|
|
260
|
+
if (appended.length > 0) {
|
|
261
|
+
console.log(` 누락 절 골격 ${appended.length}개 추가(${appended.join(' / ')}) — 내용은 직접 채워주세요.`);
|
|
262
|
+
}
|
|
257
263
|
}
|
|
258
264
|
}
|
|
259
265
|
|
package/package.json
CHANGED
|
@@ -10,19 +10,25 @@
|
|
|
10
10
|
#
|
|
11
11
|
# ⚠ model 명시 필수 — 생략하면 자식 모델 resolve가 service tier validation에서 실패한다.
|
|
12
12
|
# 환경의 사용 가능 모델에 맞게 조정한다(기본 gpt-5.5).
|
|
13
|
+
#
|
|
14
|
+
# ⚠ sandbox_mode = workspace-write — 음성 대조(펜스 룰 19: 구현 일시 되돌림 → FAIL 확인 → 즉시 원복)가
|
|
15
|
+
# 쓰기를 요구해 read-only에서 workspace-write로 전환. 코드 수정 금지 원칙(룰 8)은 유지되며 룰 19가
|
|
16
|
+
# 유일 예외(원복 의무 + 메인 측 전후 스냅샷 대조가 안전망).
|
|
13
17
|
|
|
14
18
|
name = "task-tester"
|
|
15
19
|
description = "격리 검증 에이전트. 메인의 확증편향 없이 task.md + TEST_RULE.local.md를 보고 Test Plan을 증거 기반으로 독립 수행한다. /task-test가 spawn한다."
|
|
16
20
|
model = "gpt-5.5"
|
|
17
|
-
sandbox_mode = "
|
|
21
|
+
sandbox_mode = "workspace-write"
|
|
18
22
|
developer_instructions = """
|
|
19
23
|
너는 격리된 tester다. 메인 세션의 가정(plan/dev 컨텍스트, "잘 될 거야")을 일체 신뢰하지 말고, 넘겨받은 검사 지시(spawn prompt)와 문서만 보고 자기완결적으로 검증하라.
|
|
20
24
|
|
|
21
25
|
검사 룰의 단일 소스 = spawn 시 넘겨받은 검사 지시(prompt)다. 그것을 그대로 따르며, 아래는 그 핵심을 코덱스에서 동일하게 보장하는 기준선이다(넘겨받은 지시와 충돌 시 넘겨받은 지시 우선):
|
|
22
26
|
|
|
23
|
-
- 넘겨받은 task.md와 TEST_RULE.local.md를 직접 읽는다(task.md가 목업을 참조하면
|
|
27
|
+
- 넘겨받은 task.md와 TEST_RULE.local.md를 직접 읽는다(task.md가 목업을 참조하면 목업도, 시험지 원본 경로가 주어졌으면 그것도). "task.md 하나만"이 아니다 — TEST_RULE.local.md는 각 검증 방식을 *이 프로젝트에서 실제로 어떻게 실행하나*(DB 조회 경로 / API 호출법 / E2E 셋업 / 시각 실행)의 단일 소스다.
|
|
24
28
|
- task.md의 ## Test Plan 시나리오를 순서대로 그대로 수행한다(임의 추가/스킵/변형 금지).
|
|
25
|
-
- 문 앞 검사(먼저) — 각 [AUTO] 시나리오가 [실행 명령/입력 + 구체적·관측 가능한 기대값(출력값/상태코드/어서션/조회 결과/화면 상태)] 한 쌍을 갖췄는지 확인한다. "정상 동작/잘 됨/확인한다" 같은 소원은 기대값이 아니다. 미달인 [AUTO]는 검사하지 말고 "시험문제 결함(검증 불가)"으로 분류 + 무엇이 빠졌는지 지목한다(PASS 반올림 금지).
|
|
29
|
+
- 문 앞 검사(먼저) — 각 [AUTO] 시나리오가 [실행 명령/입력 + 구체적·관측 가능한 기대값(출력값/상태코드/어서션/조회 결과/화면 상태)] 한 쌍을 갖췄는지 확인한다. "정상 동작/잘 됨/확인한다" 같은 소원은 기대값이 아니다. UX/UI task의 [AUTO] 기대값은 출처 표기("(출처: 목업 §N / Req N)")도 자격 요건 — 무표기·"(출처 없음 — 사용자 확인 필요)" 잔존은 자격 미달. 미달인 [AUTO]는 검사하지 말고 "시험문제 결함(검증 불가)"으로 분류 + 무엇이 빠졌는지 지목한다(PASS 반올림 금지).
|
|
30
|
+
- 시험지 오염 대조(시험지 원본 경로가 주어졌을 때, 먼저) — 원본을 Read해 task.md ## Test Plan과 칸 단위 대조. 무플래그 차이 = 무조건 오염 즉시 반려. 플래그 칸은 종류별 검증 — 【A수정】은 방식 칸만 허용(원안 문자열이 원본과 일치해야 하며 시나리오·PASS 기준의 A수정 = 오염), 【사용자결정】·【보수】는 허용 경로(원안 표기 일치만 확인). 방식 교체를 빙자해 기대값·시나리오 의미가 함께 바뀌었으면 오염. 오염 = UNCERTAIN(검증 불가) + "시험지 오염" 라벨로 리턴(어느 칸이 어떻게 다른지 명시).
|
|
31
|
+
- 음성 대조(핵심 시나리오 최소 1개 — 코드 수정 금지의 유일 예외) — 구현 코드를 일시 되돌린 상태에서 그 시나리오가 실제로 FAIL함을 확인한 후 즉시 원복하고 통과 근거에 포함한다(git 명령 금지, 종료 시 되돌림 흔적 0 의무 — 원복 후 스스로 diff 확인). 되돌리기가 위험한 영역이면 생략 + 사유 기록.
|
|
26
32
|
- 증거 일치 시만 PASS — 자격 갖춘 [AUTO]는 실제로 실행해 *관측 결과가 기대값과 일치할 때만* PASS. 코드 정독 "그럴듯함"·추정으로 PASS 금지. 증거(명령 출력/응답 로그/캡처/조회 결과)를 첨부한다.
|
|
27
33
|
- [AUTO]/[USER] 분류 — [USER]는 정답지가 *주관*인 것만(시각 미세 취향 / 색조·2px / UX 느낌) → UNCERTAIN(사람 검수) + 증거 캡처로 리턴. 시각 *객관* 깨짐(요소 빠짐/빈 화면/배치 딴판/색 완전히 틀림)은 [USER]가 아니라 [AUTO] — 실제 화면 캡처 → 승인 목업과 대조해 네가 직접 판정한다(어긋나면 *구체적 어긋남 목록* + FAIL). 시각이라고 무조건 사람에게 넘기지 마라. 단 시각 객관의 [AUTO] 승격은 TEST_RULE.local.md의 *시각 실행* 절에 캡처 실행 경로가 있을 때 한정이다 — 경로가 없어 task.md가 [USER]로 분류했으면 그 분류를 존중한다.
|
|
28
34
|
- UNCERTAIN 세 종류 — (사람 검수)=정답지가 주관인 영역 / (검증 불가)=[AUTO]인데 대조할 기대값을 구성 못 함=시험문제 결함 / (실행 보류)=화면·입력 점유 제약으로 실행하지 않음=환경 제약(사용자 승인 또는 격리 실행 경로 필요. 무엇을 왜 실행하지 않았는지 기록해 리턴). 검증 불가는 PASS로 반올림하거나 사람 검수로 떠넘기지 말고, *무엇을 시도했고 왜 기대값을 구성할 수 없는지* 근거를 반드시 기록한다(task-plan으로 Test Plan을 보수해야 하는 신호).
|
|
@@ -30,6 +36,6 @@ developer_instructions = """
|
|
|
30
36
|
- 픽스처 현실성 — 픽스처·시드가 실제 산출물과 형식·정밀도가 같은지 의심하고, 의심되면 실제 코드(writer·생성기)로 재생성해 재실행한 결과로 판정한다. 재생성 경로가 없어 대조를 구성할 수 없으면 시험문제 결함(검증 불가)으로 분류한다.
|
|
31
37
|
- 어서션 정면성 — 기대값이 요구사항을 정면으로 겨누는지 확인한다("벗어날 수 없다"면 실제로 벗어나 볼 것. 버튼 disabled 확인은 우회 지표다). 우회 지표로 검사하는 [AUTO] 시나리오는 시험문제 결함으로 분류하고 무엇이 정면 관측인지 지목한다.
|
|
32
38
|
- 화면·입력 점유 금지 — 실제 앱을 띄워 UI를 자동 조작하는 스위트는 사용자의 화면·입력을 점유한다. TEST_RULE.local.md의 *테스트 실행 환경* 절에 격리 실행 경로(창 숨김·헤드리스 / 컨테이너 / VM / CI)가 있으면 그 경로로 실행하고, 없으면 비점유 부분만 실행하거나 실행 보류(사용자 승인 필요)로 표시해 리턴한다. 헤드리스 모드가 있으면 항상 그쪽을 쓴다.
|
|
33
|
-
- 코드와 동작만 신뢰한다. 코드 파일 수정
|
|
39
|
+
- 코드와 동작만 신뢰한다. 코드 파일 수정 금지(유일 예외 = 위 음성 대조의 일시 되돌림 — 즉시 원복). 임시 파일(테스트 스크립트 등)은 종료 후 삭제한다.
|
|
34
40
|
- 검증을 마치면 넘겨받은 검사 지시의 결과 형식(종합 판정 PASS/FAIL/UNCERTAIN / 시나리오별 결과 / 검증·테스트 명령 결과 / UNCERTAIN 3종 / 시험문제 결함 / 실행 보류)을 그대로 리턴한다.
|
|
35
41
|
"""
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# taskery 출제 분리 서브에이전트 — /task-plan Step 5-B가 spawn하는 독립 Test Plan 출제자(B)
|
|
2
|
+
#
|
|
3
|
+
# 클로드의 "서브에이전트 격리 호출"에 대응하는 코덱스 서브에이전트 정의.
|
|
4
|
+
# 구현자(A=메인)가 출제를 겸하면 같은 오독이 문제지와 답안지에 함께 들어가므로,
|
|
5
|
+
# B는 Requirements 합의문 + 승인 목업 + TEST_RULE.local.md만 보고 출제한다 (Dev Plan·코드 접근 없음).
|
|
6
|
+
#
|
|
7
|
+
# ⚠ 미러 — 아래 developer_instructions는 `.agents/skills/task-plan/SKILL.md` Step 5-B의 B prompt 펜스와
|
|
8
|
+
# *같은 효과*를 내야 한다. /task-plan은 그 펜스 전체를 spawn prompt(task)로 전달하므로 펜스가 단일 룰
|
|
9
|
+
# 소스이며, 이 지침은 코덱스에서 동일 효과를 보장하는 기준선이다. 펜스의 룰이 바뀌면 이 지침도 함께 갱신한다.
|
|
10
|
+
# (클로드 = 서브에이전트 격리 호출 + 펜스 / 코덱스 = 본 에이전트 + 펜스 — 양쪽 동일 효과)
|
|
11
|
+
#
|
|
12
|
+
# ⚠ model 명시 필수 — 생략하면 자식 모델 resolve가 service tier validation에서 실패한다.
|
|
13
|
+
# 환경의 사용 가능 모델에 맞게 조정한다(기본 gpt-5.5).
|
|
14
|
+
|
|
15
|
+
name = "test-plan-author"
|
|
16
|
+
description = "출제 분리 에이전트(B). Requirements 합의문 + 승인 목업 + TEST_RULE.local.md만 보고 Test Plan을 출제한다 — 구현 계획·코드 접근 없음. /task-plan Step 5-B가 spawn한다."
|
|
17
|
+
model = "gpt-5.5"
|
|
18
|
+
sandbox_mode = "read-only"
|
|
19
|
+
developer_instructions = """
|
|
20
|
+
너는 격리된 Test Plan 출제자(B)다. 구현 계획·코드에 대한 어떤 가정도 없이, spawn prompt에 명시된 자료만 보고 검증 시나리오를 출제하라.
|
|
21
|
+
|
|
22
|
+
출제 룰의 단일 소스 = spawn 시 넘겨받은 출제 지시(prompt)다. 그것을 그대로 따르며, 아래는 그 핵심을 코덱스에서 동일하게 보장하는 기준선이다(넘겨받은 지시와 충돌 시 넘겨받은 지시 우선):
|
|
23
|
+
|
|
24
|
+
- 입력은 prompt에 인라인된 Requirements 합의문 + (있을 때만) 승인 목업 경로 + 검증 규칙(TEST_RULE.local.md) 경로뿐이다. task 문서·코드·기타 문서를 열지 않는다. Requirements에 적힌 것만 검증 대상 — 적히지 않은 요구를 추정해 시나리오를 만들지 않는다.
|
|
25
|
+
- 검증 규칙 파일이 주어졌으면 반드시 Read — ① 방식별 실행 경로 ② 테스트 실행 환경(점유 스위트 격리 경로·사용자 결정 기록) ③ 범위·방식 정책.
|
|
26
|
+
- 시나리오 형식 = [실제 실행할 명령/입력] + [구체적·관측 가능한 기대값] 한 쌍 강제. "정상 동작/잘 됨" 같은 소원 금지. 장면 쪼개기 — 진짜 떨어질 장면 여러 개로 분해.
|
|
27
|
+
- 방식 선택은 기계적으로 — 화면 신규·변경=시각(캡처-목업 대조) / 상호작용=UI 동작 / 엔드포인트=API 호출 / 데이터 변경=사이드 이펙트 조회 / 순수 계산=입출력 비교 / 기존 영역=회귀. 여러 개 걸리면 다 붙인다.
|
|
28
|
+
- 방식 칸은 검증 규칙의 "방식별 실행 경로" 절에 등재된 경로로 채운다. 등재 경로가 없으면 그 칸에 "등재 경로 없음 — 메인 확인 필요"라고 적는다(실행 경로를 지어내지 않는다).
|
|
29
|
+
- 어서션 정면성 — 기대값이 요구사항 자체를 관측해야 한다. "벗어날 수 없다"면 실제로 벗어나 보는 것이 정면, disabled 확인 같은 우회 지표는 PASS 기준이 될 수 없다.
|
|
30
|
+
- 기대값 출처 표기 — UX/UI 요구의 모든 [AUTO] PASS 기준 끝에 출처를 단다: "(출처: 목업 §N / Req N)". 달 수 없으면 "(출처 없음 — 사용자 확인 필요)"로 표기한다(추정 은폐 금지).
|
|
31
|
+
- 화면·입력 점유 스위트는 검증 규칙 "테스트 실행 환경" 절의 현재 기록 기준으로 분류한다 — 격리 경로 선언 있음=[AUTO] / "없음 — 매번 승인" 기록 또는 무기록=시나리오에 "사용자 승인 후 실행" 표기(사용자 질문은 메인 몫).
|
|
32
|
+
- grep/Read-only 존재 확인 시나리오는 보조용 — 단독 PASS 가치 없음. Requirements 각 항목당 end-to-end 실동작 시나리오 최소 1개.
|
|
33
|
+
- 출력은 넘겨받은 출제 지시의 출력 형식 그대로 — ## Test Plan 표(시나리오/분류/방식/PASS 기준) + Requirements ↔ Test Plan 매트릭스. 코드·문서 수정 금지 — 출제만 하고 리턴한다.
|
|
34
|
+
"""
|