mustflow 2.18.0 → 2.18.3
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 +2 -2
- package/dist/cli/commands/explain-verify.js +2 -2
- package/dist/cli/commands/run/builtin-dispatch.js +92 -0
- package/dist/cli/commands/run/executor.js +112 -0
- package/dist/cli/commands/run/output.js +59 -0
- package/dist/cli/commands/run/process-tree.js +91 -0
- package/dist/cli/commands/run/receipt.js +42 -0
- package/dist/cli/commands/run.js +22 -414
- package/dist/cli/commands/verify/args.js +262 -0
- package/dist/cli/commands/verify.js +106 -263
- package/dist/cli/i18n/en.js +3 -1
- package/dist/cli/i18n/es.js +3 -1
- package/dist/cli/i18n/fr.js +3 -1
- package/dist/cli/i18n/hi.js +3 -1
- package/dist/cli/i18n/ko.js +3 -1
- package/dist/cli/i18n/zh.js +3 -1
- package/dist/cli/index.js +6 -72
- package/dist/cli/lib/command-registry.js +27 -0
- package/dist/cli/lib/repo-map.js +10 -3
- package/dist/core/atomic-state-write.js +31 -0
- package/dist/core/bounded-output.js +23 -1
- package/dist/core/check-issues.js +1 -0
- package/dist/core/command-contract-validation.js +57 -7
- package/dist/core/completion-verdict.js +2 -1
- package/dist/core/public-json-contracts.js +1 -1
- package/dist/core/run-receipt.js +20 -13
- package/dist/core/source-anchors.js +96 -24
- package/dist/core/verification-evidence.js +4 -1
- package/package.json +1 -1
- package/schemas/README.md +1 -1
- package/schemas/run-receipt.schema.json +26 -3
- package/schemas/verify-report.schema.json +1 -1
- package/schemas/verify-run-manifest.schema.json +1 -1
- package/templates/default/i18n.toml +7 -1
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +2 -1
- package/templates/default/locales/en/.mustflow/skills/routes.toml +6 -0
- package/templates/default/locales/en/.mustflow/skills/source-anchor-authoring/SKILL.md +147 -0
- package/templates/default/manifest.toml +8 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { mkdirSync, readFileSync,
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createClassifyOutput } from './classify.js';
|
|
5
5
|
import { runRun } from './run.js';
|
|
6
6
|
import { createChangeVerificationReport, } from '../../core/change-verification.js';
|
|
7
7
|
import { createVerifyCompletionVerdict, } from '../../core/completion-verdict.js';
|
|
8
|
+
import { atomicWriteJsonFile, createStateRunId } from '../../core/atomic-state-write.js';
|
|
8
9
|
import { createExternalEvidenceRisks, } from '../../core/external-evidence.js';
|
|
9
10
|
import { createRepeatedFailureRisks, createVerificationFailureFingerprint, updateRepeatedFailureState, } from '../../core/repeated-failure.js';
|
|
10
11
|
import { countReproEvidenceVerdictEffects, createReproEvidenceRisks, } from '../../core/repro-evidence.js';
|
|
@@ -12,14 +13,14 @@ import { createVerifyEvidenceModel } from '../../core/verification-evidence.js';
|
|
|
12
13
|
import { createScopeDiffRisks } from '../../core/scope-risk.js';
|
|
13
14
|
import { countValidationRatchetVerdictEffects, createValidationRatchetRisks, } from '../../core/validation-ratchet.js';
|
|
14
15
|
import { readCommandContract } from '../../core/config-loading.js';
|
|
16
|
+
import { DEFAULT_VERIFY_PARALLELISM, parseVerifyArgs } from './verify/args.js';
|
|
15
17
|
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
16
18
|
import { t } from '../lib/i18n.js';
|
|
17
19
|
import { readLocalCommandEffectGraph, readLocalPathSurfaces, readLocalSourceAnchorVerdictRisks, } from '../lib/local-index.js';
|
|
18
20
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
19
21
|
const VERIFY_SCHEMA_VERSION = '1';
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const LATEST_RUN_RECEIPT_PATH = path.join('.mustflow', 'state', 'runs', 'latest.json');
|
|
22
|
+
const RUN_STATE_DIR = path.join('.mustflow', 'state', 'runs');
|
|
23
|
+
const LATEST_RUN_RECEIPT_PATH = path.join(RUN_STATE_DIR, 'latest.json');
|
|
23
24
|
function createBufferedOutput() {
|
|
24
25
|
const stdout = [];
|
|
25
26
|
const stderr = [];
|
|
@@ -52,6 +53,7 @@ export function getVerifyHelp(lang = 'en') {
|
|
|
52
53
|
{ label: '--write-plan <path>', description: t(lang, 'verify.help.option.writePlan') },
|
|
53
54
|
{ label: '--repro-evidence <path>', description: t(lang, 'verify.help.option.reproEvidence') },
|
|
54
55
|
{ label: '--external-evidence <path>', description: t(lang, 'verify.help.option.externalEvidence') },
|
|
56
|
+
{ label: '--parallel <count>', description: t(lang, 'verify.help.option.parallel') },
|
|
55
57
|
{ label: '--plan-only', description: t(lang, 'verify.help.option.planOnly') },
|
|
56
58
|
{ label: '--json', description: t(lang, 'cli.option.json') },
|
|
57
59
|
{ label: '-h, --help', description: t(lang, 'cli.option.help') },
|
|
@@ -71,232 +73,24 @@ export function getVerifyHelp(lang = 'en') {
|
|
|
71
73
|
],
|
|
72
74
|
}, lang);
|
|
73
75
|
}
|
|
74
|
-
function parseVerifyArgs(args) {
|
|
75
|
-
let reason;
|
|
76
|
-
let fromClassification;
|
|
77
|
-
let fromPlan;
|
|
78
|
-
let writePlan;
|
|
79
|
-
let reproEvidence;
|
|
80
|
-
let externalEvidence;
|
|
81
|
-
let json = false;
|
|
82
|
-
let planOnly = false;
|
|
83
|
-
let changed = false;
|
|
84
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
85
|
-
const arg = args[index];
|
|
86
|
-
if (arg === '--json') {
|
|
87
|
-
json = true;
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
if (arg === '--plan-only') {
|
|
91
|
-
planOnly = true;
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
if (arg === '--changed') {
|
|
95
|
-
changed = true;
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (arg === '--reason') {
|
|
99
|
-
const value = args[index + 1];
|
|
100
|
-
if (!value || value.startsWith('-')) {
|
|
101
|
-
return { json, planOnly, changed, reason, error: 'missing_reason_value' };
|
|
102
|
-
}
|
|
103
|
-
reason = value;
|
|
104
|
-
index += 1;
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
if (arg === '--from-plan') {
|
|
108
|
-
const value = args[index + 1];
|
|
109
|
-
if (!value || value.startsWith('-')) {
|
|
110
|
-
return { json, planOnly, changed, reason, fromClassification, fromPlan, error: 'missing_from_plan_value' };
|
|
111
|
-
}
|
|
112
|
-
fromPlan = value;
|
|
113
|
-
index += 1;
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
if (arg === '--from-classification') {
|
|
117
|
-
const value = args[index + 1];
|
|
118
|
-
if (!value || value.startsWith('-')) {
|
|
119
|
-
return {
|
|
120
|
-
json,
|
|
121
|
-
planOnly,
|
|
122
|
-
changed,
|
|
123
|
-
reason,
|
|
124
|
-
fromClassification,
|
|
125
|
-
fromPlan,
|
|
126
|
-
error: 'missing_from_classification_value',
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
fromClassification = value;
|
|
130
|
-
index += 1;
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
if (arg === '--write-plan') {
|
|
134
|
-
const value = args[index + 1];
|
|
135
|
-
if (!value || value.startsWith('-')) {
|
|
136
|
-
return {
|
|
137
|
-
json,
|
|
138
|
-
planOnly,
|
|
139
|
-
changed,
|
|
140
|
-
reason,
|
|
141
|
-
fromClassification,
|
|
142
|
-
fromPlan,
|
|
143
|
-
writePlan,
|
|
144
|
-
error: 'missing_write_plan_value',
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
writePlan = value;
|
|
148
|
-
index += 1;
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
if (arg === '--external-evidence') {
|
|
152
|
-
const value = args[index + 1];
|
|
153
|
-
if (!value || value.startsWith('-')) {
|
|
154
|
-
return {
|
|
155
|
-
json,
|
|
156
|
-
planOnly,
|
|
157
|
-
changed,
|
|
158
|
-
reason,
|
|
159
|
-
fromClassification,
|
|
160
|
-
fromPlan,
|
|
161
|
-
writePlan,
|
|
162
|
-
externalEvidence,
|
|
163
|
-
error: 'missing_external_evidence_value',
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
externalEvidence = value;
|
|
167
|
-
index += 1;
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
if (arg === '--repro-evidence') {
|
|
171
|
-
const value = args[index + 1];
|
|
172
|
-
if (!value || value.startsWith('-')) {
|
|
173
|
-
return {
|
|
174
|
-
json,
|
|
175
|
-
planOnly,
|
|
176
|
-
changed,
|
|
177
|
-
reason,
|
|
178
|
-
fromClassification,
|
|
179
|
-
fromPlan,
|
|
180
|
-
writePlan,
|
|
181
|
-
reproEvidence,
|
|
182
|
-
externalEvidence,
|
|
183
|
-
error: 'missing_repro_evidence_value',
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
reproEvidence = value;
|
|
187
|
-
index += 1;
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
if (arg.startsWith('--reason=')) {
|
|
191
|
-
const value = arg.slice('--reason='.length);
|
|
192
|
-
if (value.length === 0) {
|
|
193
|
-
return { json, planOnly, changed, reason, error: 'missing_reason_value' };
|
|
194
|
-
}
|
|
195
|
-
reason = value;
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
if (arg.startsWith('--from-plan=')) {
|
|
199
|
-
const value = arg.slice('--from-plan='.length);
|
|
200
|
-
if (value.length === 0) {
|
|
201
|
-
return { json, planOnly, changed, reason, fromClassification, fromPlan, error: 'missing_from_plan_value' };
|
|
202
|
-
}
|
|
203
|
-
fromPlan = value;
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
if (arg.startsWith('--from-classification=')) {
|
|
207
|
-
const value = arg.slice('--from-classification='.length);
|
|
208
|
-
if (value.length === 0) {
|
|
209
|
-
return {
|
|
210
|
-
json,
|
|
211
|
-
planOnly,
|
|
212
|
-
changed,
|
|
213
|
-
reason,
|
|
214
|
-
fromClassification,
|
|
215
|
-
fromPlan,
|
|
216
|
-
error: 'missing_from_classification_value',
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
fromClassification = value;
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
if (arg.startsWith('--write-plan=')) {
|
|
223
|
-
const value = arg.slice('--write-plan='.length);
|
|
224
|
-
if (value.length === 0) {
|
|
225
|
-
return {
|
|
226
|
-
json,
|
|
227
|
-
planOnly,
|
|
228
|
-
changed,
|
|
229
|
-
reason,
|
|
230
|
-
fromClassification,
|
|
231
|
-
fromPlan,
|
|
232
|
-
writePlan,
|
|
233
|
-
error: 'missing_write_plan_value',
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
writePlan = value;
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
if (arg.startsWith('--external-evidence=')) {
|
|
240
|
-
const value = arg.slice('--external-evidence='.length);
|
|
241
|
-
if (value.length === 0) {
|
|
242
|
-
return {
|
|
243
|
-
json,
|
|
244
|
-
planOnly,
|
|
245
|
-
changed,
|
|
246
|
-
reason,
|
|
247
|
-
fromClassification,
|
|
248
|
-
fromPlan,
|
|
249
|
-
writePlan,
|
|
250
|
-
externalEvidence,
|
|
251
|
-
error: 'missing_external_evidence_value',
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
externalEvidence = value;
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
if (arg.startsWith('--repro-evidence=')) {
|
|
258
|
-
const value = arg.slice('--repro-evidence='.length);
|
|
259
|
-
if (value.length === 0) {
|
|
260
|
-
return {
|
|
261
|
-
json,
|
|
262
|
-
planOnly,
|
|
263
|
-
changed,
|
|
264
|
-
reason,
|
|
265
|
-
fromClassification,
|
|
266
|
-
fromPlan,
|
|
267
|
-
writePlan,
|
|
268
|
-
reproEvidence,
|
|
269
|
-
externalEvidence,
|
|
270
|
-
error: 'missing_repro_evidence_value',
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
reproEvidence = value;
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
if (arg.startsWith('-')) {
|
|
277
|
-
return { json, planOnly, changed, reason, fromClassification, fromPlan, writePlan, reproEvidence, externalEvidence, error: arg };
|
|
278
|
-
}
|
|
279
|
-
return {
|
|
280
|
-
json,
|
|
281
|
-
planOnly,
|
|
282
|
-
changed,
|
|
283
|
-
reason,
|
|
284
|
-
fromClassification,
|
|
285
|
-
fromPlan,
|
|
286
|
-
writePlan,
|
|
287
|
-
reproEvidence,
|
|
288
|
-
externalEvidence,
|
|
289
|
-
error: `unexpected:${arg}`,
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
return { json, planOnly, changed, reason, fromClassification, fromPlan, writePlan, reproEvidence, externalEvidence };
|
|
293
|
-
}
|
|
294
76
|
function uniqueStrings(values) {
|
|
295
77
|
return [...new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))];
|
|
296
78
|
}
|
|
297
79
|
function toPosixPath(value) {
|
|
298
80
|
return value.split(path.sep).join('/');
|
|
299
81
|
}
|
|
82
|
+
function createVerifyRunStatePaths(projectRoot) {
|
|
83
|
+
const runDir = toPosixPath(path.join(RUN_STATE_DIR, createStateRunId('verify')));
|
|
84
|
+
const manifestPath = toPosixPath(path.join(runDir, 'manifest.json'));
|
|
85
|
+
const absoluteRunDir = path.join(projectRoot, runDir);
|
|
86
|
+
return {
|
|
87
|
+
runDir,
|
|
88
|
+
manifestPath,
|
|
89
|
+
absoluteRunDir,
|
|
90
|
+
absoluteIntentDir: path.join(absoluteRunDir, 'intents'),
|
|
91
|
+
absoluteManifestPath: path.join(projectRoot, manifestPath),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
300
94
|
function sanitizeIntentFilePart(value) {
|
|
301
95
|
const sanitized = value.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^_+|_+$/g, '');
|
|
302
96
|
return sanitized.length > 0 ? sanitized.slice(0, 80) : 'intent';
|
|
@@ -445,7 +239,7 @@ function resolvePlanPath(projectRoot, inputPath) {
|
|
|
445
239
|
}
|
|
446
240
|
return resolved;
|
|
447
241
|
}
|
|
448
|
-
export function
|
|
242
|
+
export function readInputFromClassificationReport(projectRoot, inputPath) {
|
|
449
243
|
let parsed;
|
|
450
244
|
const planPath = resolvePlanPath(projectRoot, inputPath);
|
|
451
245
|
try {
|
|
@@ -836,7 +630,8 @@ async function runVerificationIntent(intent, lang, verificationPlanId, testTarge
|
|
|
836
630
|
if (receiptStatus === 'passed' ||
|
|
837
631
|
receiptStatus === 'failed' ||
|
|
838
632
|
receiptStatus === 'timed_out' ||
|
|
839
|
-
receiptStatus === 'start_failed'
|
|
633
|
+
receiptStatus === 'start_failed' ||
|
|
634
|
+
receiptStatus === 'output_limit_exceeded') {
|
|
840
635
|
status = receiptStatus;
|
|
841
636
|
}
|
|
842
637
|
}
|
|
@@ -856,12 +651,52 @@ async function runVerificationIntent(intent, lang, verificationPlanId, testTarge
|
|
|
856
651
|
receipt,
|
|
857
652
|
};
|
|
858
653
|
}
|
|
654
|
+
function entriesForScheduleBatch(entries, batch) {
|
|
655
|
+
const batchIntents = new Set(batch.intents);
|
|
656
|
+
return entries.filter((entry) => batchIntents.has(entry.intent));
|
|
657
|
+
}
|
|
658
|
+
async function runVerificationEntriesSequentially(entries, lang, verificationPlanId, scheduledTestTargets) {
|
|
659
|
+
const results = [];
|
|
660
|
+
for (const entry of entries) {
|
|
661
|
+
results.push(await runVerificationIntent(entry.intent, lang, verificationPlanId, scheduledTestTargets.get(entry.intent) ?? []));
|
|
662
|
+
}
|
|
663
|
+
return results;
|
|
664
|
+
}
|
|
665
|
+
async function runVerificationEntriesInParallelChunks(entries, parallelism, lang, verificationPlanId, scheduledTestTargets) {
|
|
666
|
+
const results = [];
|
|
667
|
+
for (let index = 0; index < entries.length; index += parallelism) {
|
|
668
|
+
const chunk = entries.slice(index, index + parallelism);
|
|
669
|
+
results.push(...(await Promise.all(chunk.map((entry) => runVerificationIntent(entry.intent, lang, verificationPlanId, scheduledTestTargets.get(entry.intent) ?? [])))));
|
|
670
|
+
}
|
|
671
|
+
return results;
|
|
672
|
+
}
|
|
673
|
+
async function runScheduledVerificationIntents(report, lang, verificationPlanId, scheduledTestTargets, parallelism) {
|
|
674
|
+
if (parallelism <= DEFAULT_VERIFY_PARALLELISM) {
|
|
675
|
+
return runVerificationEntriesSequentially(report.schedule.entries, lang, verificationPlanId, scheduledTestTargets);
|
|
676
|
+
}
|
|
677
|
+
const results = [];
|
|
678
|
+
for (const batch of report.schedule.batches) {
|
|
679
|
+
const entries = entriesForScheduleBatch(report.schedule.entries, batch);
|
|
680
|
+
if (entries.length === 0) {
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
if (entries.length > 1 && entries.every((entry) => entry.parallelEligible)) {
|
|
684
|
+
results.push(...(await runVerificationEntriesInParallelChunks(entries, parallelism, lang, verificationPlanId, scheduledTestTargets)));
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
results.push(...(await runVerificationEntriesSequentially(entries, lang, verificationPlanId, scheduledTestTargets)));
|
|
688
|
+
}
|
|
689
|
+
return results;
|
|
690
|
+
}
|
|
859
691
|
function summarizeResults(results) {
|
|
860
692
|
const ran = results.filter((result) => !result.skipped).length;
|
|
861
693
|
const passed = results.filter((result) => result.status === 'passed').length;
|
|
862
694
|
const skipped = results.filter((result) => result.skipped).length;
|
|
863
695
|
const failed = results.filter((result) => !result.skipped &&
|
|
864
|
-
(result.status === 'failed' ||
|
|
696
|
+
(result.status === 'failed' ||
|
|
697
|
+
result.status === 'timed_out' ||
|
|
698
|
+
result.status === 'start_failed' ||
|
|
699
|
+
result.status === 'output_limit_exceeded')).length;
|
|
865
700
|
return {
|
|
866
701
|
matched: results.filter((result) => result.intent !== null).length,
|
|
867
702
|
ran,
|
|
@@ -910,11 +745,15 @@ function timedOutForResult(result) {
|
|
|
910
745
|
return result.status === 'timed_out' || resultSummary?.timed_out === true;
|
|
911
746
|
}
|
|
912
747
|
function errorKindForResult(result) {
|
|
913
|
-
return stringField(resultSummaryForResult(result)?.error_kind) ??
|
|
748
|
+
return (stringField(resultSummaryForResult(result)?.error_kind) ??
|
|
749
|
+
(result.status === 'start_failed' || result.status === 'output_limit_exceeded' ? result.status : null));
|
|
914
750
|
}
|
|
915
751
|
function failedResults(results) {
|
|
916
752
|
return results.filter((result) => !result.skipped &&
|
|
917
|
-
(result.status === 'failed' ||
|
|
753
|
+
(result.status === 'failed' ||
|
|
754
|
+
result.status === 'timed_out' ||
|
|
755
|
+
result.status === 'start_failed' ||
|
|
756
|
+
result.status === 'output_limit_exceeded'));
|
|
918
757
|
}
|
|
919
758
|
function createFailureFingerprintForVerify(input) {
|
|
920
759
|
const failures = failedResults(input.results);
|
|
@@ -1026,7 +865,10 @@ function createCriteriaEvidence(report, results) {
|
|
|
1026
865
|
.filter((intent) => intent !== null);
|
|
1027
866
|
const gapCount = report.gaps.filter((gap) => gap.reason === requirement.reason).length;
|
|
1028
867
|
const selectedResults = selectedIntents.map((intent) => resultForSelectedIntent(results, intent));
|
|
1029
|
-
if (selectedResults.some((result) => result?.status === 'failed' ||
|
|
868
|
+
if (selectedResults.some((result) => result?.status === 'failed' ||
|
|
869
|
+
result?.status === 'timed_out' ||
|
|
870
|
+
result?.status === 'start_failed' ||
|
|
871
|
+
result?.status === 'output_limit_exceeded')) {
|
|
1030
872
|
return { ...current, contradicted: current.contradicted + 1 };
|
|
1031
873
|
}
|
|
1032
874
|
if (gapCount > 0 || (selectedIntents.length === 0 && skippedIntents.length > 0)) {
|
|
@@ -1181,20 +1023,18 @@ function createVerificationPlanId(report, contract) {
|
|
|
1181
1023
|
return hashTextSha256(stableJson(fingerprintSource));
|
|
1182
1024
|
}
|
|
1183
1025
|
function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks, scopeDiffRisks, validationRatchetRisks, reproEvidence, externalChecks) {
|
|
1184
|
-
const
|
|
1185
|
-
const intentDir = path.join(runDir, 'intents');
|
|
1026
|
+
const statePaths = createVerifyRunStatePaths(projectRoot);
|
|
1186
1027
|
const receipts = [];
|
|
1187
1028
|
const results = [];
|
|
1188
|
-
|
|
1189
|
-
mkdirSync(intentDir, { recursive: true });
|
|
1029
|
+
mkdirSync(statePaths.absoluteIntentDir, { recursive: true });
|
|
1190
1030
|
for (const [index, result] of output.results.entries()) {
|
|
1191
1031
|
let receiptPath = null;
|
|
1192
1032
|
let receiptSha256 = null;
|
|
1193
1033
|
let receipt = result.receipt;
|
|
1194
1034
|
if (result.intent && result.receipt) {
|
|
1195
1035
|
const fileName = `${String(index + 1).padStart(3, '0')}-${sanitizeIntentFilePart(result.intent)}.json`;
|
|
1196
|
-
const absoluteReceiptPath = path.join(
|
|
1197
|
-
receiptPath = toPosixPath(path.join(
|
|
1036
|
+
const absoluteReceiptPath = path.join(statePaths.absoluteIntentDir, fileName);
|
|
1037
|
+
receiptPath = toPosixPath(path.join(statePaths.runDir, 'intents', fileName));
|
|
1198
1038
|
receipt = {
|
|
1199
1039
|
...result.receipt,
|
|
1200
1040
|
verification_plan_id: output.verification_plan_id,
|
|
@@ -1202,7 +1042,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
|
|
|
1202
1042
|
};
|
|
1203
1043
|
const receiptContent = `${JSON.stringify(receipt, null, 2)}\n`;
|
|
1204
1044
|
receiptSha256 = hashTextSha256(receiptContent);
|
|
1205
|
-
|
|
1045
|
+
atomicWriteJsonFile(absoluteReceiptPath, receipt);
|
|
1206
1046
|
}
|
|
1207
1047
|
receipts.push({
|
|
1208
1048
|
intent: result.intent,
|
|
@@ -1272,6 +1112,8 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
|
|
|
1272
1112
|
completion_verdict: completionVerdict,
|
|
1273
1113
|
failure_fingerprint: failureFingerprint,
|
|
1274
1114
|
repeated_failure_summary: repeatedFailureSummary,
|
|
1115
|
+
run_dir: statePaths.runDir,
|
|
1116
|
+
manifest_path: statePaths.manifestPath,
|
|
1275
1117
|
results,
|
|
1276
1118
|
evidence_model: createVerifyEvidenceModel({
|
|
1277
1119
|
report,
|
|
@@ -1306,7 +1148,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
|
|
|
1306
1148
|
...(outputWithReceiptPaths.external_checks ? { external_checks: outputWithReceiptPaths.external_checks } : {}),
|
|
1307
1149
|
receipts,
|
|
1308
1150
|
};
|
|
1309
|
-
|
|
1151
|
+
atomicWriteJsonFile(statePaths.absoluteManifestPath, manifest);
|
|
1310
1152
|
const latest = {
|
|
1311
1153
|
schema_version: '1',
|
|
1312
1154
|
command: 'verify',
|
|
@@ -1324,13 +1166,13 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
|
|
|
1324
1166
|
summary: outputWithReceiptPaths.summary,
|
|
1325
1167
|
...(outputWithReceiptPaths.repro_evidence ? { repro_evidence: outputWithReceiptPaths.repro_evidence } : {}),
|
|
1326
1168
|
...(outputWithReceiptPaths.external_checks ? { external_checks: outputWithReceiptPaths.external_checks } : {}),
|
|
1327
|
-
run_dir:
|
|
1328
|
-
manifest_path:
|
|
1169
|
+
run_dir: statePaths.runDir,
|
|
1170
|
+
manifest_path: statePaths.manifestPath,
|
|
1329
1171
|
};
|
|
1330
|
-
|
|
1172
|
+
atomicWriteJsonFile(path.join(projectRoot, LATEST_RUN_RECEIPT_PATH), latest);
|
|
1331
1173
|
return outputWithReceiptPaths;
|
|
1332
1174
|
}
|
|
1333
|
-
async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvidence = null, externalChecks = []) {
|
|
1175
|
+
async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvidence = null, externalChecks = [], parallelism = DEFAULT_VERIFY_PARALLELISM) {
|
|
1334
1176
|
const contract = readCommandContract(projectRoot);
|
|
1335
1177
|
const report = createChangeVerificationReport(input.classificationReport, contract, projectRoot);
|
|
1336
1178
|
const verificationPlanId = createVerificationPlanId(report, contract);
|
|
@@ -1343,10 +1185,7 @@ async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvi
|
|
|
1343
1185
|
const reproEvidenceRisks = createReproEvidenceRisks(reproEvidence, { verificationPlanId });
|
|
1344
1186
|
const reproEvidenceVerdictEffects = countReproEvidenceVerdictEffects(reproEvidenceRisks);
|
|
1345
1187
|
const externalEvidenceRisks = createExternalEvidenceRisks(externalChecks);
|
|
1346
|
-
const results =
|
|
1347
|
-
for (const entry of report.schedule.entries) {
|
|
1348
|
-
results.push(await runVerificationIntent(entry.intent, lang, verificationPlanId, scheduledTestTargets.get(entry.intent) ?? []));
|
|
1349
|
-
}
|
|
1188
|
+
const results = await runScheduledVerificationIntents(report, lang, verificationPlanId, scheduledTestTargets, parallelism);
|
|
1350
1189
|
results.push(...createSkippedResults(report.candidates, scheduledIntents, report.gaps));
|
|
1351
1190
|
const summary = summarizeResults(results);
|
|
1352
1191
|
const status = getVerificationStatus(summary);
|
|
@@ -1416,8 +1255,8 @@ async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvi
|
|
|
1416
1255
|
summary,
|
|
1417
1256
|
...(reproEvidence ? { repro_evidence: reproEvidence } : {}),
|
|
1418
1257
|
...(externalChecks.length > 0 ? { external_checks: externalChecks } : {}),
|
|
1419
|
-
run_dir:
|
|
1420
|
-
manifest_path:
|
|
1258
|
+
run_dir: '',
|
|
1259
|
+
manifest_path: '',
|
|
1421
1260
|
results,
|
|
1422
1261
|
};
|
|
1423
1262
|
return writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks, scopeDiffRisks, validationRatchetRisks, reproEvidence, externalChecks);
|
|
@@ -1491,19 +1330,23 @@ export async function runVerify(args, reporter, lang = 'en') {
|
|
|
1491
1330
|
if (parsed.error) {
|
|
1492
1331
|
const message = parsed.error === 'missing_reason_value'
|
|
1493
1332
|
? t(lang, 'cli.error.missingValue', { option: '--reason' })
|
|
1494
|
-
: parsed.error === '
|
|
1495
|
-
? t(lang, 'cli.error.missingValue', { option: '--
|
|
1496
|
-
: parsed.error === '
|
|
1497
|
-
? t(lang, '
|
|
1498
|
-
: parsed.error === '
|
|
1499
|
-
? t(lang, 'cli.error.missingValue', { option: '--
|
|
1500
|
-
: parsed.error === '
|
|
1501
|
-
? t(lang, 'cli.error.missingValue', { option: '--
|
|
1502
|
-
: parsed.error === '
|
|
1503
|
-
? t(lang, 'cli.error.missingValue', { option: '--
|
|
1504
|
-
: parsed.error
|
|
1505
|
-
? t(lang, 'cli.error.
|
|
1506
|
-
:
|
|
1333
|
+
: parsed.error === 'missing_parallel_value'
|
|
1334
|
+
? t(lang, 'cli.error.missingValue', { option: '--parallel' })
|
|
1335
|
+
: parsed.error === 'invalid_parallel_value'
|
|
1336
|
+
? t(lang, 'verify.error.invalidParallel')
|
|
1337
|
+
: parsed.error === 'missing_from_classification_value'
|
|
1338
|
+
? t(lang, 'cli.error.missingValue', { option: '--from-classification' })
|
|
1339
|
+
: parsed.error === 'missing_from_plan_value'
|
|
1340
|
+
? t(lang, 'cli.error.missingValue', { option: '--from-plan' })
|
|
1341
|
+
: parsed.error === 'missing_write_plan_value'
|
|
1342
|
+
? t(lang, 'cli.error.missingValue', { option: '--write-plan' })
|
|
1343
|
+
: parsed.error === 'missing_repro_evidence_value'
|
|
1344
|
+
? t(lang, 'cli.error.missingValue', { option: '--repro-evidence' })
|
|
1345
|
+
: parsed.error === 'missing_external_evidence_value'
|
|
1346
|
+
? t(lang, 'cli.error.missingValue', { option: '--external-evidence' })
|
|
1347
|
+
: parsed.error.startsWith('unexpected:')
|
|
1348
|
+
? t(lang, 'cli.error.unexpectedArgument', { argument: parsed.error.slice('unexpected:'.length) })
|
|
1349
|
+
: t(lang, 'cli.error.unknownOption', { option: parsed.error });
|
|
1507
1350
|
printUsageError(reporter, message, 'mf verify --help', getVerifyHelp(lang), lang);
|
|
1508
1351
|
return 1;
|
|
1509
1352
|
}
|
|
@@ -1552,7 +1395,7 @@ export async function runVerify(args, reporter, lang = 'en') {
|
|
|
1552
1395
|
changedPlan = changedInput.plan;
|
|
1553
1396
|
}
|
|
1554
1397
|
else if (parsed.fromClassification || parsed.fromPlan) {
|
|
1555
|
-
input =
|
|
1398
|
+
input = readInputFromClassificationReport(projectRoot, (parsed.fromClassification ?? parsed.fromPlan));
|
|
1556
1399
|
}
|
|
1557
1400
|
else {
|
|
1558
1401
|
input = {
|
|
@@ -1588,7 +1431,7 @@ export async function runVerify(args, reporter, lang = 'en') {
|
|
|
1588
1431
|
reporter.stdout(JSON.stringify(await createPlanOnlyOutput(input, projectRoot), null, 2));
|
|
1589
1432
|
return 0;
|
|
1590
1433
|
}
|
|
1591
|
-
const output = await createVerifyOutput(input, parsed.fromClassification ?? parsed.fromPlan ?? (parsed.changed ? 'changed' : null), projectRoot, lang, reproEvidence, externalChecks);
|
|
1434
|
+
const output = await createVerifyOutput(input, parsed.fromClassification ?? parsed.fromPlan ?? (parsed.changed ? 'changed' : null), projectRoot, lang, reproEvidence, externalChecks, parsed.parallelism ?? DEFAULT_VERIFY_PARALLELISM);
|
|
1592
1435
|
if (parsed.json) {
|
|
1593
1436
|
reporter.stdout(JSON.stringify(output, null, 2));
|
|
1594
1437
|
}
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -750,11 +750,12 @@ Read these files before working:
|
|
|
750
750
|
"verify.help.summary": "Run configured verification intents selected by required_after metadata.",
|
|
751
751
|
"verify.help.option.reason": "Select the required_after reason to verify",
|
|
752
752
|
"verify.help.option.fromClassification": "Read verification reasons from an mf classify report inside this repository",
|
|
753
|
-
"verify.help.option.fromPlan": "
|
|
753
|
+
"verify.help.option.fromPlan": "Deprecated compatibility alias for --from-classification; it still expects an mf classify report",
|
|
754
754
|
"verify.help.option.changed": "Classify current Git changes and verify the matching reasons",
|
|
755
755
|
"verify.help.option.writePlan": "Compatibility option that writes the changed-file classification report",
|
|
756
756
|
"verify.help.option.reproEvidence": "Read structured bug-fix reproduction evidence from a repository-local JSON summary",
|
|
757
757
|
"verify.help.option.externalEvidence": "Read lower-authority external CI evidence from a repository-local JSON summary",
|
|
758
|
+
"verify.help.option.parallel": "Run safe non-conflicting schedule batches with up to this many commands; default is 1",
|
|
758
759
|
"verify.help.option.planOnly": "Print the verification plan without running commands; requires --json",
|
|
759
760
|
"verify.help.exit.ok": "All selected verification intents passed",
|
|
760
761
|
"verify.help.exit.fail": "Verification failed, was partial, was blocked, or input was invalid",
|
|
@@ -769,6 +770,7 @@ Read these files before working:
|
|
|
769
770
|
"verify.error.planOnlyJson": "--plan-only requires --json",
|
|
770
771
|
"verify.error.reproEvidenceRequiresRun": "--repro-evidence cannot be used with --plan-only",
|
|
771
772
|
"verify.error.externalEvidenceRequiresRun": "--external-evidence cannot be used with --plan-only",
|
|
773
|
+
"verify.error.invalidParallel": "--parallel must be a positive integer",
|
|
772
774
|
"verify.error.invalid_plan_file": "Classification report must be a readable JSON file",
|
|
773
775
|
"verify.error.unsupported_plan_source": "Verification input must be an mf classify report",
|
|
774
776
|
"verify.error.plan_root_mismatch": "Classification report must come from this mustflow root",
|
package/dist/cli/i18n/es.js
CHANGED
|
@@ -750,11 +750,12 @@ Lee estos archivos antes de trabajar:
|
|
|
750
750
|
"verify.help.summary": "Ejecuta intenciones de verificación configuradas seleccionadas por metadatos required_after.",
|
|
751
751
|
"verify.help.option.reason": "Selecciona la razón required_after que se debe verificar",
|
|
752
752
|
"verify.help.option.fromClassification": "Lee razones de verificación desde un informe de mf classify dentro de este repositorio",
|
|
753
|
-
"verify.help.option.fromPlan": "Alias de compatibilidad para --from-classification",
|
|
753
|
+
"verify.help.option.fromPlan": "Alias de compatibilidad obsoleto para --from-classification; todavía espera un informe de mf classify",
|
|
754
754
|
"verify.help.option.changed": "Clasifica los cambios actuales de Git y verifica las razones correspondientes",
|
|
755
755
|
"verify.help.option.writePlan": "Opción de compatibilidad que escribe el informe de clasificación de cambios",
|
|
756
756
|
"verify.help.option.reproEvidence": "Lee evidencia estructurada de reproducción de errores desde un resumen JSON local del repositorio",
|
|
757
757
|
"verify.help.option.externalEvidence": "Lee evidencia de CI externa de menor autoridad desde un resumen JSON local del repositorio",
|
|
758
|
+
"verify.help.option.parallel": "Ejecuta lotes programados seguros y sin conflictos con hasta esta cantidad de comandos; el valor predeterminado es 1",
|
|
758
759
|
"verify.help.option.planOnly": "Imprime el plan de verificación sin ejecutar comandos; requiere --json",
|
|
759
760
|
"verify.help.exit.ok": "Todas las intenciones de verificación seleccionadas pasaron",
|
|
760
761
|
"verify.help.exit.fail": "La verificación falló, fue parcial, quedó bloqueada o la entrada no fue válida",
|
|
@@ -769,6 +770,7 @@ Lee estos archivos antes de trabajar:
|
|
|
769
770
|
"verify.error.planOnlyJson": "--plan-only requiere --json",
|
|
770
771
|
"verify.error.reproEvidenceRequiresRun": "--repro-evidence no se puede usar con --plan-only",
|
|
771
772
|
"verify.error.externalEvidenceRequiresRun": "--external-evidence no se puede usar con --plan-only",
|
|
773
|
+
"verify.error.invalidParallel": "--parallel debe ser un entero positivo",
|
|
772
774
|
"verify.error.invalid_plan_file": "El informe de clasificación debe ser un archivo JSON legible",
|
|
773
775
|
"verify.error.unsupported_plan_source": "La entrada de verificación debe ser un informe de mf classify",
|
|
774
776
|
"verify.error.plan_root_mismatch": "El informe de clasificación debe provenir de esta raíz mustflow",
|
package/dist/cli/i18n/fr.js
CHANGED
|
@@ -750,11 +750,12 @@ Lisez ces fichiers avant de travailler :
|
|
|
750
750
|
"verify.help.summary": "Exécute les intentions de vérification configurées sélectionnées par les métadonnées required_after.",
|
|
751
751
|
"verify.help.option.reason": "Sélectionne la raison required_after à vérifier",
|
|
752
752
|
"verify.help.option.fromClassification": "Lit les raisons de vérification depuis un rapport mf classify dans ce dépôt",
|
|
753
|
-
"verify.help.option.fromPlan": "Alias de compatibilité pour --from-classification",
|
|
753
|
+
"verify.help.option.fromPlan": "Alias de compatibilité obsolète pour --from-classification; il attend toujours un rapport mf classify",
|
|
754
754
|
"verify.help.option.changed": "Classe les changements Git actuels et vérifie les raisons correspondantes",
|
|
755
755
|
"verify.help.option.writePlan": "Option de compatibilité qui écrit le rapport de classification des changements",
|
|
756
756
|
"verify.help.option.reproEvidence": "Lit une preuve structurée de reproduction de bogue depuis un résumé JSON local au dépôt",
|
|
757
757
|
"verify.help.option.externalEvidence": "Lit une preuve CI externe de moindre autorité depuis un résumé JSON local au dépôt",
|
|
758
|
+
"verify.help.option.parallel": "Exécute les lots planifiés sûrs et sans conflit avec au plus ce nombre de commandes ; valeur par défaut : 1",
|
|
758
759
|
"verify.help.option.planOnly": "Affiche le plan de vérification sans exécuter de commandes; nécessite --json",
|
|
759
760
|
"verify.help.exit.ok": "Toutes les intentions de vérification sélectionnées ont réussi",
|
|
760
761
|
"verify.help.exit.fail": "La vérification a échoué, est partielle, est bloquée ou l'entrée est invalide",
|
|
@@ -769,6 +770,7 @@ Lisez ces fichiers avant de travailler :
|
|
|
769
770
|
"verify.error.planOnlyJson": "--plan-only nécessite --json",
|
|
770
771
|
"verify.error.reproEvidenceRequiresRun": "--repro-evidence ne peut pas être utilisé avec --plan-only",
|
|
771
772
|
"verify.error.externalEvidenceRequiresRun": "--external-evidence ne peut pas être utilisé avec --plan-only",
|
|
773
|
+
"verify.error.invalidParallel": "--parallel doit être un entier positif",
|
|
772
774
|
"verify.error.invalid_plan_file": "Le rapport de classification doit être un fichier JSON lisible",
|
|
773
775
|
"verify.error.unsupported_plan_source": "L'entrée de vérification doit être un rapport mf classify",
|
|
774
776
|
"verify.error.plan_root_mismatch": "Le rapport de classification doit venir de cette racine mustflow",
|
package/dist/cli/i18n/hi.js
CHANGED
|
@@ -750,11 +750,12 @@ export const hiMessages = {
|
|
|
750
750
|
"verify.help.summary": "required_after metadata से चुने गए configured verification intents चलाएँ।",
|
|
751
751
|
"verify.help.option.reason": "Verify करने के लिए required_after reason चुनें",
|
|
752
752
|
"verify.help.option.fromClassification": "इस repository के अंदर mf classify report से verification reasons पढ़ें",
|
|
753
|
-
"verify.help.option.fromPlan": "--from-classification का compatibility alias",
|
|
753
|
+
"verify.help.option.fromPlan": "--from-classification का deprecated compatibility alias; input अब भी mf classify report होना चाहिए",
|
|
754
754
|
"verify.help.option.changed": "Current Git changes classify करके matching reasons verify करें",
|
|
755
755
|
"verify.help.option.writePlan": "Changed-file classification report लिखने वाला compatibility option",
|
|
756
756
|
"verify.help.option.reproEvidence": "Repository-local JSON summary से structured bug reproduction evidence पढ़ें",
|
|
757
757
|
"verify.help.option.externalEvidence": "Repository-local JSON summary से lower-authority external CI evidence पढ़ें",
|
|
758
|
+
"verify.help.option.parallel": "Safe और non-conflicting schedule batches को इतने commands तक साथ चलाएं; default 1 है",
|
|
758
759
|
"verify.help.option.planOnly": "Commands चलाए बिना verification plan print करें; --json चाहिए",
|
|
759
760
|
"verify.help.exit.ok": "सभी selected verification intents pass हुए",
|
|
760
761
|
"verify.help.exit.fail": "Verification fail हुआ, partial रहा, blocked रहा, या input invalid था",
|
|
@@ -769,6 +770,7 @@ export const hiMessages = {
|
|
|
769
770
|
"verify.error.planOnlyJson": "--plan-only के लिए --json चाहिए",
|
|
770
771
|
"verify.error.reproEvidenceRequiresRun": "--repro-evidence को --plan-only के साथ इस्तेमाल नहीं किया जा सकता",
|
|
771
772
|
"verify.error.externalEvidenceRequiresRun": "--external-evidence को --plan-only के साथ इस्तेमाल नहीं किया जा सकता",
|
|
773
|
+
"verify.error.invalidParallel": "--parallel positive integer होना चाहिए",
|
|
772
774
|
"verify.error.invalid_plan_file": "Classification report readable JSON file होना चाहिए",
|
|
773
775
|
"verify.error.unsupported_plan_source": "Verification input mf classify report होना चाहिए",
|
|
774
776
|
"verify.error.plan_root_mismatch": "Classification report इसी mustflow root से आना चाहिए",
|
package/dist/cli/i18n/ko.js
CHANGED
|
@@ -750,11 +750,12 @@ export const koMessages = {
|
|
|
750
750
|
"verify.help.summary": "required_after 메타데이터로 선택된 설정된 검증 의도를 실행합니다.",
|
|
751
751
|
"verify.help.option.reason": "검증할 required_after 이유를 지정합니다",
|
|
752
752
|
"verify.help.option.fromClassification": "이 저장소 안의 mf classify 보고서에서 검증 이유를 읽습니다",
|
|
753
|
-
"verify.help.option.fromPlan": "--from-classification과 같은 호환
|
|
753
|
+
"verify.help.option.fromPlan": "--from-classification과 같은 폐기 예정 호환 옵션입니다. 입력은 여전히 mf classify 보고서여야 합니다",
|
|
754
754
|
"verify.help.option.changed": "현재 Git 변경을 분류하고 맞는 검증 이유를 실행합니다",
|
|
755
755
|
"verify.help.option.writePlan": "변경 파일 분류 보고서를 쓰는 호환 옵션입니다",
|
|
756
756
|
"verify.help.option.reproEvidence": "저장소 안의 JSON 요약에서 구조화된 버그 재현 증거를 읽습니다",
|
|
757
757
|
"verify.help.option.externalEvidence": "저장소 안의 JSON 요약에서 낮은 권한의 외부 CI 증거를 읽습니다",
|
|
758
|
+
"verify.help.option.parallel": "안전하고 서로 충돌하지 않는 예정 실행 묶음을 이 개수까지 함께 실행합니다. 기본값은 1입니다",
|
|
758
759
|
"verify.help.option.planOnly": "명령을 실행하지 않고 검증 계획만 출력합니다. --json이 필요합니다",
|
|
759
760
|
"verify.help.exit.ok": "선택된 모든 검증 의도가 통과했습니다",
|
|
760
761
|
"verify.help.exit.fail": "검증이 실패했거나, 일부만 실행됐거나, 막혔거나, 입력이 올바르지 않습니다",
|
|
@@ -769,6 +770,7 @@ export const koMessages = {
|
|
|
769
770
|
"verify.error.planOnlyJson": "--plan-only에는 --json이 필요합니다",
|
|
770
771
|
"verify.error.reproEvidenceRequiresRun": "--repro-evidence는 --plan-only와 함께 사용할 수 없습니다",
|
|
771
772
|
"verify.error.externalEvidenceRequiresRun": "--external-evidence는 --plan-only와 함께 사용할 수 없습니다",
|
|
773
|
+
"verify.error.invalidParallel": "--parallel 값은 양의 정수여야 합니다",
|
|
772
774
|
"verify.error.invalid_plan_file": "분류 보고서는 읽을 수 있는 JSON 파일이어야 합니다",
|
|
773
775
|
"verify.error.unsupported_plan_source": "검증 입력은 mf classify 보고서여야 합니다",
|
|
774
776
|
"verify.error.plan_root_mismatch": "분류 보고서는 현재 mustflow 루트에서 나온 것이어야 합니다",
|