mustflow 2.22.5 → 2.22.9
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 +8 -0
- package/dist/cli/commands/classify.js +2 -0
- package/dist/cli/commands/dashboard.js +9 -69
- package/dist/cli/commands/run/receipt.js +1 -0
- package/dist/cli/commands/run.js +14 -1
- package/dist/cli/commands/verify/evidence-input.js +269 -0
- package/dist/cli/commands/verify/input.js +212 -0
- package/dist/cli/commands/verify.js +23 -482
- package/dist/cli/i18n/en.js +3 -0
- package/dist/cli/i18n/es.js +3 -0
- package/dist/cli/i18n/fr.js +3 -0
- package/dist/cli/i18n/hi.js +3 -0
- package/dist/cli/i18n/ko.js +3 -0
- package/dist/cli/i18n/zh.js +3 -0
- package/dist/cli/lib/dashboard-export.js +2 -0
- package/dist/cli/lib/dashboard-mutations.js +79 -0
- package/dist/cli/lib/local-index/command-effect-index.js +25 -0
- package/dist/cli/lib/local-index/hashing.js +7 -0
- package/dist/cli/lib/local-index/index.js +127 -826
- package/dist/cli/lib/local-index/source-index.js +137 -0
- package/dist/cli/lib/local-index/verification-evidence.js +451 -0
- package/dist/cli/lib/local-index/workflow-documents.js +204 -0
- package/dist/cli/lib/run-root-trust.js +27 -0
- package/dist/core/change-classification-policy.js +47 -0
- package/dist/core/change-classification.js +10 -43
- package/dist/core/contract-lint.js +6 -2
- package/dist/core/correlation-id.js +16 -0
- package/dist/core/run-receipt.js +1 -0
- package/package.json +4 -1
- package/schemas/README.md +4 -0
- package/schemas/change-verification-report.schema.json +4 -0
- package/schemas/classify-report.schema.json +4 -0
- package/schemas/dashboard-export.schema.json +4 -0
- package/schemas/latest-run-pointer.schema.json +4 -0
- package/schemas/run-receipt.schema.json +4 -0
- package/schemas/verify-report.schema.json +4 -0
- package/schemas/verify-run-manifest.schema.json +4 -0
- package/templates/default/i18n.toml +3 -3
- package/templates/default/locales/en/.mustflow/skills/architecture-deepening-review/SKILL.md +25 -2
- package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +9 -1
- package/templates/default/locales/en/.mustflow/skills/test-design-guard/SKILL.md +9 -1
- package/templates/default/manifest.toml +1 -1
package/README.md
CHANGED
|
@@ -367,6 +367,12 @@ bun run docs:check
|
|
|
367
367
|
bun run check:install
|
|
368
368
|
```
|
|
369
369
|
|
|
370
|
+
When Bun is not available, maintainers can still run the core CLI and package metadata checks with Node/npm:
|
|
371
|
+
|
|
372
|
+
```sh
|
|
373
|
+
npm run check:core:node
|
|
374
|
+
```
|
|
375
|
+
|
|
370
376
|
Agents working in this repository should prefer the configured mustflow intents for routine verification.
|
|
371
377
|
|
|
372
378
|
```sh
|
|
@@ -376,6 +382,7 @@ mf run test_related
|
|
|
376
382
|
mf run test
|
|
377
383
|
mf run test_coverage
|
|
378
384
|
mf run test_release
|
|
385
|
+
mf run maintainer_check_node
|
|
379
386
|
mf run docs_validate_fast
|
|
380
387
|
mf run docs_validate
|
|
381
388
|
mf run mustflow_check
|
|
@@ -413,6 +420,7 @@ The npm package includes only:
|
|
|
413
420
|
dist/
|
|
414
421
|
templates/
|
|
415
422
|
schemas/
|
|
423
|
+
examples/
|
|
416
424
|
README.md
|
|
417
425
|
LICENSE
|
|
418
426
|
```
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { createChangeClassificationReport, } from '../../core/change-classification.js';
|
|
3
|
+
import { createCorrelationId } from '../../core/correlation-id.js';
|
|
3
4
|
import { writeJsonFileInsideWithoutSymlinks } from '../../core/safe-filesystem.js';
|
|
4
5
|
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
5
6
|
import { requireGitChangedFiles } from '../lib/git-changes.js';
|
|
@@ -71,6 +72,7 @@ export function createClassifyOutput(projectRoot, source, paths) {
|
|
|
71
72
|
return {
|
|
72
73
|
schema_version: CLASSIFY_SCHEMA_VERSION,
|
|
73
74
|
command: 'classify',
|
|
75
|
+
correlation_id: createCorrelationId('classify'),
|
|
74
76
|
mustflow_root: projectRoot,
|
|
75
77
|
...createChangeClassificationReport(source, files),
|
|
76
78
|
};
|
|
@@ -2,7 +2,7 @@ import { createHash, randomBytes } from 'node:crypto';
|
|
|
2
2
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
3
|
import http from 'node:http';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
5
|
+
import { openUrlInBrowser } from '../lib/browser-open.js';
|
|
6
6
|
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
7
7
|
import { renderDashboardHtml, } from '../lib/dashboard-html.js';
|
|
8
8
|
import { DashboardExportPathError, writeDashboardExport, } from '../lib/dashboard-export.js';
|
|
@@ -12,8 +12,9 @@ import { parseSkillIndexRoutes } from '../../core/skill-route-alignment.js';
|
|
|
12
12
|
import { getAgentContext } from '../lib/agent-context.js';
|
|
13
13
|
import { readGitChangedFiles } from '../lib/git-changes.js';
|
|
14
14
|
import { isRecord, readCommandContract, readPositiveInteger, readString, readStringArray, } from '../lib/command-contract.js';
|
|
15
|
-
import { readDashboardPreferences,
|
|
16
|
-
import { DOC_REVIEW_LEDGER_RELATIVE_PATH, isDocReviewStatus,
|
|
15
|
+
import { readDashboardPreferences, } from '../lib/dashboard-preferences.js';
|
|
16
|
+
import { DOC_REVIEW_LEDGER_RELATIVE_PATH, isDocReviewStatus, listDocReviewEntries, } from '../lib/doc-review-ledger.js';
|
|
17
|
+
import { markDashboardDocReviewFromPayload, openDashboardMustflowFolder, updateDashboardPreferencesFromPayload, } from '../lib/dashboard-mutations.js';
|
|
17
18
|
import { MANIFEST_LOCK_RELATIVE_PATH, inspectManifestLock } from '../lib/manifest-lock.js';
|
|
18
19
|
import { getLocalIndexDatabasePath, readLatestLocalVerificationReadModelQueries, readLocalCommandEffectGraphs, } from '../lib/local-index.js';
|
|
19
20
|
import { readPackageMetadata } from '../lib/package-info.js';
|
|
@@ -26,7 +27,6 @@ const DEFAULT_DASHBOARD_HOST = '127.0.0.1';
|
|
|
26
27
|
const DEFAULT_DASHBOARD_PORT = 0;
|
|
27
28
|
const MAX_REQUEST_BYTES = 64 * 1024;
|
|
28
29
|
const LOCAL_DASHBOARD_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
|
|
29
|
-
const DOC_REVIEW_BULK_PAYLOAD_FIELDS = ['paths', 'documents', 'entries'];
|
|
30
30
|
const RELEASE_FILE_PATTERNS = [
|
|
31
31
|
/^package\.json$/u,
|
|
32
32
|
/^bun\.lockb?$/u,
|
|
@@ -323,66 +323,6 @@ async function readRequestJson(request) {
|
|
|
323
323
|
const rawBody = Buffer.concat(chunks).toString('utf8');
|
|
324
324
|
return rawBody.trim().length === 0 ? {} : JSON.parse(rawBody);
|
|
325
325
|
}
|
|
326
|
-
function readUpdatePayload(value) {
|
|
327
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
328
|
-
throw new Error('Request body must be a JSON object.');
|
|
329
|
-
}
|
|
330
|
-
const updates = value.updates;
|
|
331
|
-
if (!Array.isArray(updates)) {
|
|
332
|
-
throw new Error('Request body must include an updates array.');
|
|
333
|
-
}
|
|
334
|
-
return updates.map((entry) => {
|
|
335
|
-
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
336
|
-
throw new Error('Each update must be a JSON object.');
|
|
337
|
-
}
|
|
338
|
-
const update = entry;
|
|
339
|
-
if (typeof update.id !== 'string' || update.id.trim().length === 0) {
|
|
340
|
-
throw new Error('Each update must include an id.');
|
|
341
|
-
}
|
|
342
|
-
return { id: update.id, value: update.value };
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
function readOptionalStringField(value, key) {
|
|
346
|
-
const field = value[key];
|
|
347
|
-
return typeof field === 'string' && field.trim().length > 0 ? field.trim() : undefined;
|
|
348
|
-
}
|
|
349
|
-
function readRequiredStringField(value, key) {
|
|
350
|
-
const field = readOptionalStringField(value, key);
|
|
351
|
-
if (!field) {
|
|
352
|
-
throw new Error(`${key} is required.`);
|
|
353
|
-
}
|
|
354
|
-
return field;
|
|
355
|
-
}
|
|
356
|
-
function readDocReviewPayload(value) {
|
|
357
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
358
|
-
throw new Error('Request body must be a JSON object.');
|
|
359
|
-
}
|
|
360
|
-
const payload = value;
|
|
361
|
-
for (const field of DOC_REVIEW_BULK_PAYLOAD_FIELDS) {
|
|
362
|
-
if (field in payload) {
|
|
363
|
-
throw new Error('Bulk documentation review updates require a separate confirmed flow.');
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
const status = readRequiredStringField(payload, 'status');
|
|
367
|
-
if (status !== 'approved' && status !== 'needs_human' && status !== 'ignored') {
|
|
368
|
-
throw new Error('status must be approved, needs_human, or ignored.');
|
|
369
|
-
}
|
|
370
|
-
const reviewerKind = readRequiredStringField(payload, 'reviewerKind');
|
|
371
|
-
if (!isReviewerKind(reviewerKind)) {
|
|
372
|
-
throw new Error('reviewerKind must be human, llm, tool, or external.');
|
|
373
|
-
}
|
|
374
|
-
return {
|
|
375
|
-
path: readRequiredStringField(payload, 'path'),
|
|
376
|
-
status,
|
|
377
|
-
reviewerKind,
|
|
378
|
-
reviewerId: readRequiredStringField(payload, 'reviewerId'),
|
|
379
|
-
reviewerLabel: readOptionalStringField(payload, 'reviewerLabel'),
|
|
380
|
-
reviewerProvider: readOptionalStringField(payload, 'reviewerProvider'),
|
|
381
|
-
reviewerModel: readOptionalStringField(payload, 'reviewerModel'),
|
|
382
|
-
reviewerCommandIntent: readOptionalStringField(payload, 'reviewerCommandIntent'),
|
|
383
|
-
summary: readOptionalStringField(payload, 'summary'),
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
326
|
function renderDocReviewResponse(projectRoot, requestUrl) {
|
|
387
327
|
const status = requestUrl.searchParams.get('status');
|
|
388
328
|
if (status && !isDocReviewStatus(status)) {
|
|
@@ -858,7 +798,7 @@ export async function runDashboard(args, reporter, lang = 'en') {
|
|
|
858
798
|
}
|
|
859
799
|
if (request.method === 'POST') {
|
|
860
800
|
const body = await readRequestJson(request);
|
|
861
|
-
sendJson(response, 200,
|
|
801
|
+
sendJson(response, 200, updateDashboardPreferencesFromPayload(projectRoot, body));
|
|
862
802
|
return;
|
|
863
803
|
}
|
|
864
804
|
}
|
|
@@ -871,12 +811,12 @@ export async function runDashboard(args, reporter, lang = 'en') {
|
|
|
871
811
|
sendText(response, 405, 'Method not allowed');
|
|
872
812
|
return;
|
|
873
813
|
}
|
|
874
|
-
const
|
|
875
|
-
if (
|
|
814
|
+
const openResult = openDashboardMustflowFolder(projectRoot);
|
|
815
|
+
if (openResult === 'missing') {
|
|
876
816
|
sendText(response, 404, '.mustflow folder not found');
|
|
877
817
|
return;
|
|
878
818
|
}
|
|
879
|
-
if (
|
|
819
|
+
if (openResult === 'unavailable') {
|
|
880
820
|
sendText(response, 500, 'No file manager opener is available for this platform');
|
|
881
821
|
return;
|
|
882
822
|
}
|
|
@@ -894,7 +834,7 @@ export async function runDashboard(args, reporter, lang = 'en') {
|
|
|
894
834
|
}
|
|
895
835
|
if (request.method === 'POST') {
|
|
896
836
|
const body = await readRequestJson(request);
|
|
897
|
-
|
|
837
|
+
markDashboardDocReviewFromPayload(projectRoot, body);
|
|
898
838
|
sendJson(response, 200, renderDocReviewResponse(projectRoot, requestUrl));
|
|
899
839
|
return;
|
|
900
840
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRunReceipt, createRunReceiptRelativePath, } from '../../../core/run-receipt.js';
|
|
2
2
|
export function assembleRunReceipt(input) {
|
|
3
3
|
return createRunReceipt({
|
|
4
|
+
correlationId: input.correlationId,
|
|
4
5
|
intent: input.intentName,
|
|
5
6
|
status: input.runStatus,
|
|
6
7
|
timedOut: input.runStatus === 'timed_out',
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { performance } from 'node:perf_hooks';
|
|
2
2
|
import { createCommandEnv } from '../../core/command-env.js';
|
|
3
|
+
import { createCorrelationId } from '../../core/correlation-id.js';
|
|
3
4
|
import { printUsageError, renderCliError, renderHelp } from '../lib/cli-output.js';
|
|
4
5
|
import { readCommandContract, readMustflowConfigIfExists } from '../../core/config-loading.js';
|
|
5
6
|
import { resolveRunReceiptRetentionPolicy } from '../../core/retention-policy.js';
|
|
6
7
|
import { t } from '../lib/i18n.js';
|
|
7
8
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
9
|
+
import { ALLOW_UNTRUSTED_ROOT_OPTION, assessRunRootTrust } from '../lib/run-root-trust.js';
|
|
8
10
|
import { createRunPlan, createRunPreview, renderRunPreviewText, } from '../lib/run-plan.js';
|
|
9
11
|
import { writeRunReceipt, } from '../../core/run-receipt.js';
|
|
10
12
|
import { recordRunPerformanceHistory } from '../../core/run-performance-history.js';
|
|
@@ -92,6 +94,7 @@ export function getRunHelp(lang = 'en') {
|
|
|
92
94
|
{ label: '--dry-run', description: t(lang, 'run.help.option.dryRun') },
|
|
93
95
|
{ label: '--plan-only', description: t(lang, 'run.help.option.planOnly') },
|
|
94
96
|
{ label: '--json', description: t(lang, 'run.help.option.json') },
|
|
97
|
+
{ label: ALLOW_UNTRUSTED_ROOT_OPTION, description: t(lang, 'run.help.option.allowUntrustedRoot') },
|
|
95
98
|
{ label: '-h, --help', description: t(lang, 'cli.option.help') },
|
|
96
99
|
],
|
|
97
100
|
examples: ['mf run test', 'mf run lint --json', 'mf run mustflow_check --dry-run --json'],
|
|
@@ -121,7 +124,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
121
124
|
reporter.stdout(getRunHelp(lang));
|
|
122
125
|
return 0;
|
|
123
126
|
}
|
|
124
|
-
const supportedOptions = new Set(['--json', '--dry-run', '--plan-only']);
|
|
127
|
+
const supportedOptions = new Set(['--json', '--dry-run', '--plan-only', ALLOW_UNTRUSTED_ROOT_OPTION]);
|
|
125
128
|
const unsupported = args.filter((arg) => arg.startsWith('-') && !supportedOptions.has(arg));
|
|
126
129
|
if (unsupported.length > 0) {
|
|
127
130
|
printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf run --help', getRunHelp(lang), lang);
|
|
@@ -130,6 +133,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
130
133
|
const json = args.includes('--json');
|
|
131
134
|
const dryRun = args.includes('--dry-run');
|
|
132
135
|
const planOnly = args.includes('--plan-only');
|
|
136
|
+
const allowUntrustedRoot = args.includes(ALLOW_UNTRUSTED_ROOT_OPTION);
|
|
133
137
|
const previewMode = dryRun ? 'dry-run' : planOnly ? 'plan-only' : null;
|
|
134
138
|
if (dryRun && planOnly) {
|
|
135
139
|
printUsageError(reporter, t(lang, 'run.error.conflictingPreviewModes'), 'mf run --help', getRunHelp(lang), lang);
|
|
@@ -146,6 +150,14 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
146
150
|
return 1;
|
|
147
151
|
}
|
|
148
152
|
const projectRoot = profiler.measure('root_detection', () => resolveMustflowRoot());
|
|
153
|
+
const rootTrust = profiler.measure('root_trust', () => assessRunRootTrust(projectRoot));
|
|
154
|
+
if (!previewMode && !allowUntrustedRoot && !rootTrust.trusted) {
|
|
155
|
+
const message = rootTrust.reason === 'manifest_lock_invalid'
|
|
156
|
+
? t(lang, 'run.error.untrustedRootInvalid', { detail: rootTrust.detail ?? rootTrust.manifestLockPath })
|
|
157
|
+
: t(lang, 'run.error.untrustedRootMissing', { path: rootTrust.detail ?? rootTrust.manifestLockPath });
|
|
158
|
+
reporter.stderr(renderCliError(message, 'mf run --help', lang));
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
149
161
|
const contract = profiler.measure('command_contract', () => readCommandContract(projectRoot));
|
|
150
162
|
const plan = profiler.measure('plan_creation', () => createRunPlan(projectRoot, contract, intentName, { testTargets: options.testTargets }));
|
|
151
163
|
if (previewMode) {
|
|
@@ -208,6 +220,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
208
220
|
}
|
|
209
221
|
}
|
|
210
222
|
const receipt = profiler.measure('receipt_create', () => assembleRunReceipt({
|
|
223
|
+
correlationId: options.correlationId ?? createCorrelationId('run'),
|
|
211
224
|
intentName,
|
|
212
225
|
runStatus,
|
|
213
226
|
startedAt,
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { readVerifyJsonInputFile } from './input.js';
|
|
2
|
+
function isPlainRecord(value) {
|
|
3
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function isExternalEvidenceStatus(value) {
|
|
6
|
+
return value === 'passed' || value === 'failed' || value === 'cancelled' || value === 'unknown';
|
|
7
|
+
}
|
|
8
|
+
function isLegacyReproEvidenceStatus(value) {
|
|
9
|
+
return value === 'present' || value === 'unavailable' || value === 'missing';
|
|
10
|
+
}
|
|
11
|
+
function isReproBeforeFixStatus(value) {
|
|
12
|
+
return value === 'reproduced' || value === 'unavailable' || value === 'missing';
|
|
13
|
+
}
|
|
14
|
+
function isReproBeforeFixOutcome(value) {
|
|
15
|
+
return value === 'failed_as_expected' || value === 'failed_differently' || value === 'passed_unexpectedly' || value === null;
|
|
16
|
+
}
|
|
17
|
+
function isReproAfterFixStatus(value) {
|
|
18
|
+
return value === 'passed' || value === 'failed' || value === 'unavailable' || value === 'missing';
|
|
19
|
+
}
|
|
20
|
+
function isReproAfterFixOutcome(value) {
|
|
21
|
+
return value === 'passed_expected_behavior' || value === 'failed_same_route' || value === 'failed_differently' || value === null;
|
|
22
|
+
}
|
|
23
|
+
function isReproRegressionGuardStatus(value) {
|
|
24
|
+
return value === 'passed' || value === 'failed' || value === 'unavailable' || value === 'missing';
|
|
25
|
+
}
|
|
26
|
+
function isReproRouteKind(value) {
|
|
27
|
+
return (value === 'test' ||
|
|
28
|
+
value === 'cli' ||
|
|
29
|
+
value === 'browser' ||
|
|
30
|
+
value === 'api' ||
|
|
31
|
+
value === 'manual' ||
|
|
32
|
+
value === 'unknown' ||
|
|
33
|
+
value === null);
|
|
34
|
+
}
|
|
35
|
+
function readOptionalString(value) {
|
|
36
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
37
|
+
}
|
|
38
|
+
function readRouteStep(value, index) {
|
|
39
|
+
if (!isPlainRecord(value)) {
|
|
40
|
+
return {
|
|
41
|
+
ordinal: index + 1,
|
|
42
|
+
action: null,
|
|
43
|
+
target: null,
|
|
44
|
+
input_digest: null,
|
|
45
|
+
observation_digest: null,
|
|
46
|
+
summary: null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const ordinal = typeof value.ordinal === 'number' && Number.isInteger(value.ordinal) && value.ordinal > 0 ? value.ordinal : index + 1;
|
|
50
|
+
return {
|
|
51
|
+
ordinal,
|
|
52
|
+
action: readOptionalString(value.action),
|
|
53
|
+
target: readOptionalString(value.target),
|
|
54
|
+
input_digest: readOptionalString(value.input_digest),
|
|
55
|
+
observation_digest: readOptionalString(value.observation_digest),
|
|
56
|
+
summary: readOptionalString(value.summary),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function readReproductionRoute(value) {
|
|
60
|
+
if (!isPlainRecord(value)) {
|
|
61
|
+
return {
|
|
62
|
+
route_id: null,
|
|
63
|
+
route_kind: null,
|
|
64
|
+
route_digest: null,
|
|
65
|
+
failure_oracle_hash: null,
|
|
66
|
+
steps: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const routeKind = value.route_kind ?? null;
|
|
70
|
+
if (!isReproRouteKind(routeKind)) {
|
|
71
|
+
throw new Error('invalid_repro_evidence_file');
|
|
72
|
+
}
|
|
73
|
+
const rawSteps = Array.isArray(value.steps) ? value.steps : [];
|
|
74
|
+
return {
|
|
75
|
+
route_id: readOptionalString(value.route_id),
|
|
76
|
+
route_kind: routeKind,
|
|
77
|
+
route_digest: readOptionalString(value.route_digest),
|
|
78
|
+
failure_oracle_hash: readOptionalString(value.failure_oracle_hash),
|
|
79
|
+
steps: rawSteps.map((step, index) => readRouteStep(step, index)),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function readLegacyReproEvidenceItem(value) {
|
|
83
|
+
if (!isPlainRecord(value)) {
|
|
84
|
+
return {
|
|
85
|
+
status: 'missing',
|
|
86
|
+
summary: null,
|
|
87
|
+
reason: null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (!isLegacyReproEvidenceStatus(value.status)) {
|
|
91
|
+
throw new Error('invalid_repro_evidence_file');
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
status: value.status,
|
|
95
|
+
summary: readOptionalString(value.summary),
|
|
96
|
+
reason: readOptionalString(value.reason),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function legacyBeforeFixEvidence(value) {
|
|
100
|
+
const item = readLegacyReproEvidenceItem(value);
|
|
101
|
+
return {
|
|
102
|
+
status: item.status === 'present' ? 'reproduced' : item.status,
|
|
103
|
+
outcome: item.status === 'present' ? 'failed_as_expected' : null,
|
|
104
|
+
receipt_path: null,
|
|
105
|
+
receipt_sha256: null,
|
|
106
|
+
verification_plan_id: null,
|
|
107
|
+
summary: item.summary,
|
|
108
|
+
reason: item.reason,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function legacyAfterFixEvidence(value) {
|
|
112
|
+
const item = readLegacyReproEvidenceItem(value);
|
|
113
|
+
return {
|
|
114
|
+
status: item.status === 'present' ? 'passed' : item.status,
|
|
115
|
+
outcome: item.status === 'present' ? 'passed_expected_behavior' : null,
|
|
116
|
+
same_route_as: null,
|
|
117
|
+
receipt_path: null,
|
|
118
|
+
receipt_sha256: null,
|
|
119
|
+
verification_plan_id: null,
|
|
120
|
+
summary: item.summary,
|
|
121
|
+
reason: item.reason,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function legacyRegressionGuardEvidence(value) {
|
|
125
|
+
const item = readLegacyReproEvidenceItem(value);
|
|
126
|
+
return {
|
|
127
|
+
status: item.status === 'present' ? 'passed' : item.status,
|
|
128
|
+
intent: null,
|
|
129
|
+
test_path: null,
|
|
130
|
+
receipt_path: null,
|
|
131
|
+
receipt_sha256: null,
|
|
132
|
+
verification_plan_id: null,
|
|
133
|
+
summary: item.summary,
|
|
134
|
+
reason: item.reason,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function readBeforeFixEvidence(value) {
|
|
138
|
+
if (!isPlainRecord(value)) {
|
|
139
|
+
return {
|
|
140
|
+
status: 'missing',
|
|
141
|
+
outcome: null,
|
|
142
|
+
receipt_path: null,
|
|
143
|
+
receipt_sha256: null,
|
|
144
|
+
verification_plan_id: null,
|
|
145
|
+
summary: null,
|
|
146
|
+
reason: null,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const outcome = value.outcome ?? null;
|
|
150
|
+
if (!isReproBeforeFixStatus(value.status) || !isReproBeforeFixOutcome(outcome)) {
|
|
151
|
+
throw new Error('invalid_repro_evidence_file');
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
status: value.status,
|
|
155
|
+
outcome,
|
|
156
|
+
receipt_path: readOptionalString(value.receipt_path),
|
|
157
|
+
receipt_sha256: readOptionalString(value.receipt_sha256),
|
|
158
|
+
verification_plan_id: readOptionalString(value.verification_plan_id),
|
|
159
|
+
summary: readOptionalString(value.summary),
|
|
160
|
+
reason: readOptionalString(value.reason),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function readAfterFixEvidence(value) {
|
|
164
|
+
if (!isPlainRecord(value)) {
|
|
165
|
+
return {
|
|
166
|
+
status: 'missing',
|
|
167
|
+
outcome: null,
|
|
168
|
+
same_route_as: null,
|
|
169
|
+
receipt_path: null,
|
|
170
|
+
receipt_sha256: null,
|
|
171
|
+
verification_plan_id: null,
|
|
172
|
+
summary: null,
|
|
173
|
+
reason: null,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const outcome = value.outcome ?? null;
|
|
177
|
+
if (!isReproAfterFixStatus(value.status) || !isReproAfterFixOutcome(outcome)) {
|
|
178
|
+
throw new Error('invalid_repro_evidence_file');
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
status: value.status,
|
|
182
|
+
outcome,
|
|
183
|
+
same_route_as: readOptionalString(value.same_route_as),
|
|
184
|
+
receipt_path: readOptionalString(value.receipt_path),
|
|
185
|
+
receipt_sha256: readOptionalString(value.receipt_sha256),
|
|
186
|
+
verification_plan_id: readOptionalString(value.verification_plan_id),
|
|
187
|
+
summary: readOptionalString(value.summary),
|
|
188
|
+
reason: readOptionalString(value.reason),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function readRegressionGuardEvidence(value) {
|
|
192
|
+
if (!isPlainRecord(value)) {
|
|
193
|
+
return {
|
|
194
|
+
status: 'missing',
|
|
195
|
+
intent: null,
|
|
196
|
+
test_path: null,
|
|
197
|
+
receipt_path: null,
|
|
198
|
+
receipt_sha256: null,
|
|
199
|
+
verification_plan_id: null,
|
|
200
|
+
summary: null,
|
|
201
|
+
reason: null,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (!isReproRegressionGuardStatus(value.status)) {
|
|
205
|
+
throw new Error('invalid_repro_evidence_file');
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
status: value.status,
|
|
209
|
+
intent: readOptionalString(value.intent),
|
|
210
|
+
test_path: readOptionalString(value.test_path),
|
|
211
|
+
receipt_path: readOptionalString(value.receipt_path),
|
|
212
|
+
receipt_sha256: readOptionalString(value.receipt_sha256),
|
|
213
|
+
verification_plan_id: readOptionalString(value.verification_plan_id),
|
|
214
|
+
summary: readOptionalString(value.summary),
|
|
215
|
+
reason: readOptionalString(value.reason),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
export function readReproEvidenceFile(projectRoot, inputPath) {
|
|
219
|
+
const parsed = readVerifyJsonInputFile(projectRoot, inputPath, 'invalid_repro_evidence_file');
|
|
220
|
+
if (!isPlainRecord(parsed) || parsed.schema_version !== '1' || parsed.command !== 'repro-evidence') {
|
|
221
|
+
throw new Error('unsupported_repro_evidence_source');
|
|
222
|
+
}
|
|
223
|
+
const regressionGuard = isPlainRecord(parsed.regression_guard) && isReproRegressionGuardStatus(parsed.regression_guard.status)
|
|
224
|
+
? readRegressionGuardEvidence(parsed.regression_guard)
|
|
225
|
+
: legacyRegressionGuardEvidence(parsed.regression_guard);
|
|
226
|
+
return {
|
|
227
|
+
source: 'repro_first_debug',
|
|
228
|
+
authority: 'claim_evidence',
|
|
229
|
+
reported_symptom: readOptionalString(parsed.reported_symptom),
|
|
230
|
+
expected_behavior: readOptionalString(parsed.expected_behavior),
|
|
231
|
+
observed_behavior: readOptionalString(parsed.observed_behavior),
|
|
232
|
+
reproduction_route: readReproductionRoute(parsed.reproduction_route),
|
|
233
|
+
before_fix: isPlainRecord(parsed.before_fix)
|
|
234
|
+
? readBeforeFixEvidence(parsed.before_fix)
|
|
235
|
+
: legacyBeforeFixEvidence(parsed.evidence_before_fix),
|
|
236
|
+
after_fix: isPlainRecord(parsed.after_fix)
|
|
237
|
+
? readAfterFixEvidence(parsed.after_fix)
|
|
238
|
+
: legacyAfterFixEvidence(parsed.evidence_after_fix),
|
|
239
|
+
regression_guard: regressionGuard,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
export function readExternalEvidenceFile(projectRoot, inputPath) {
|
|
243
|
+
const parsed = readVerifyJsonInputFile(projectRoot, inputPath, 'invalid_external_evidence_file');
|
|
244
|
+
if (!isPlainRecord(parsed) || parsed.schema_version !== '1' || parsed.command !== 'external-evidence') {
|
|
245
|
+
throw new Error('unsupported_external_evidence_source');
|
|
246
|
+
}
|
|
247
|
+
if (!Array.isArray(parsed.checks)) {
|
|
248
|
+
throw new Error('invalid_external_evidence_file');
|
|
249
|
+
}
|
|
250
|
+
return parsed.checks.map((check) => {
|
|
251
|
+
if (!isPlainRecord(check) ||
|
|
252
|
+
typeof check.provider !== 'string' ||
|
|
253
|
+
check.provider.length === 0 ||
|
|
254
|
+
typeof check.name !== 'string' ||
|
|
255
|
+
check.name.length === 0 ||
|
|
256
|
+
!isExternalEvidenceStatus(check.status)) {
|
|
257
|
+
throw new Error('invalid_external_evidence_file');
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
source: 'external_ci',
|
|
261
|
+
authority: 'supporting_only',
|
|
262
|
+
provider: check.provider,
|
|
263
|
+
name: check.name,
|
|
264
|
+
status: check.status,
|
|
265
|
+
url: readOptionalString(check.url),
|
|
266
|
+
summary: readOptionalString(check.summary),
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
}
|