@surlymochan/evalid 0.0.0-stage → 0.3.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/CHANGELOG.md +74 -0
- package/LICENSE +21 -0
- package/README.md +315 -2
- package/RELEASE_NOTES.md +102 -0
- package/atoms/registry.json +758 -0
- package/bin/xflow.js +7208 -0
- package/docs/adoption/README.md +128 -0
- package/docs/adoption/openflow-release-hardening.md +61 -0
- package/docs/benchmark-openflow-vs-gstack-superpowers-openspec-2026-05-25.md +81 -0
- package/docs/compatibility.md +52 -0
- package/docs/competitive-benchmark.md +108 -0
- package/docs/corps-operator-guide.md +75 -0
- package/docs/demo-proof.md +126 -0
- package/docs/examples-gallery.md +115 -0
- package/docs/fixtures/tracker-item.json +7 -0
- package/docs/goal-vs-codex.md +129 -0
- package/docs/gstack-comparison.md +88 -0
- package/docs/install-upgrade.md +133 -0
- package/docs/integrations.md +49 -0
- package/docs/launch-demo.md +123 -0
- package/docs/launch-dossier.md +180 -0
- package/docs/methodology.md +149 -0
- package/docs/npm-publish-handoff.md +83 -0
- package/docs/openspec-migration.md +106 -0
- package/docs/public-benchmark.md +68 -0
- package/docs/public-release.md +145 -0
- package/docs/quality-assessment.md +139 -0
- package/docs/quickstart.md +156 -0
- package/docs/reviewer-guide.md +43 -0
- package/docs/spec-kit-benchmark.md +73 -0
- package/docs/super-assistant-comparison.md +106 -0
- package/docs/superpowers-comparison.md +82 -0
- package/docs/team-adoption.md +123 -0
- package/docs/tooling-matrix.md +91 -0
- package/docs/walkthrough.md +76 -0
- package/docs/workflow-manual.md +1125 -0
- package/package.json +83 -4
- package/prompts/pencil-draft.md +46 -0
- package/prompts/pencil-refine.md +46 -0
- package/prompts/phase-artifact.md +24 -0
- package/prompts/phase-execution.md +32 -0
- package/schemas/competitor-reconstruction-contract.schema.json +91 -0
- package/schemas/llm-design-review-aggregate.schema.json +83 -0
- package/schemas/visual-benchmark.schema.json +978 -0
- package/schemas/workflow.schema.json +100 -0
- package/scripts/run-tests.mjs +23 -0
- package/scripts/takein_digest.py +139 -0
- package/specs/README.md +118 -0
- package/specs/architecture.md +36 -0
- package/specs/product.md +33 -0
- package/specs/workflow.md +349 -0
- package/src/agent_team/atoms/codex-app-bridge.js +8 -0
- package/src/agent_team/atoms/design/competitor-reconstruction-review.js +44 -0
- package/src/agent_team/atoms/design/contract-freeze.js +8 -0
- package/src/agent_team/atoms/design/generation-contract.js +442 -0
- package/src/agent_team/atoms/design/reconstruction-pack.js +246 -0
- package/src/agent_team/atoms/design/reference-surface-lock.js +49 -0
- package/src/agent_team/atoms/patch-challenge.js +19 -0
- package/src/agent_team/atoms/pencil/accept.js +9 -0
- package/src/agent_team/atoms/pencil/draft.js +365 -0
- package/src/agent_team/atoms/pencil/refine.js +229 -0
- package/src/agent_team/atoms/phase-run-dispatch.js +8 -0
- package/src/agent_team/atoms/team-run.js +380 -0
- package/src/agent_team/atoms/visual/aesthetic-review.js +231 -0
- package/src/agent_team/atoms/visual/benchmark-repair-plan.js +291 -0
- package/src/agent_team/atoms/visual/benchmark.js +1652 -0
- package/src/agent_team/atoms/visual/design-selection.js +23 -0
- package/src/agent_team/atoms/visual/direction-synthesis.js +124 -0
- package/src/agent_team/atoms/visual/dom-rect-export.js +129 -0
- package/src/agent_team/atoms/visual/image-diff.js +186 -0
- package/src/agent_team/atoms/visual/layout-competition.js +8 -0
- package/src/agent_team/atoms/visual/page-evidence-capture.js +806 -0
- package/src/agent_team/atoms/visual/playwright-runtime.js +8 -0
- package/src/agent_team/atoms/visual/review-aggregate.js +87 -0
- package/src/agent_team/atoms/visual/review.js +77 -0
- package/src/agent_team/atoms/visual/visual-report.js +197 -0
- package/src/agent_team/atoms/visual/visual-token-export.js +417 -0
- package/src/agent_team/common.js +311 -0
- package/src/core/atoms/agent-invoke.js +684 -0
- package/src/core/atoms/event/ingest.js +30 -0
- package/src/core/atoms/experience/digest.js +55 -0
- package/src/core/atoms/gate/artifact-verify.js +83 -0
- package/src/core/atoms/gate/competitor-contract-validate.js +119 -0
- package/src/core/atoms/gate/evidence-collect.js +64 -0
- package/src/core/atoms/gate/payload-from-phase-run.js +78 -0
- package/src/core/atoms/gate/visual-benchmark-validate.js +183 -0
- package/src/core/atoms/gate/workbench.js +79 -0
- package/src/core/atoms/intake/classify.js +15 -0
- package/src/core/atoms/intake/design-skill-select.js +16 -0
- package/src/core/atoms/intake/preview-or-refine.js +19 -0
- package/src/core/atoms/intake/recommend.js +20 -0
- package/src/core/atoms/intake/research-same-category.js +19 -0
- package/src/core/atoms/intervention/create.js +30 -0
- package/src/core/atoms/memory/manage.js +27 -0
- package/src/core/atoms/memory/mount.js +13 -0
- package/src/core/atoms/mission/create.js +19 -0
- package/src/core/atoms/mission/list.js +14 -0
- package/src/core/atoms/mission/show.js +26 -0
- package/src/core/atoms/notify/checkpoint.js +46 -0
- package/src/core/atoms/phase-run/complete.js +42 -0
- package/src/core/atoms/phase-run/fail.js +40 -0
- package/src/core/atoms/phase-run/persist.js +39 -0
- package/src/core/atoms/phase-run/start.js +30 -0
- package/src/core/atoms/plugin/run.js +76 -0
- package/src/core/atoms/policy/gate.js +90 -0
- package/src/core/atoms/policy/resolve.js +11 -0
- package/src/core/atoms/storage/sync.js +57 -0
- package/src/core/atoms/workflow/advance.js +25 -0
- package/src/core/atoms/workflow/guard.js +127 -0
- package/src/core/atoms/workflow/state-load.js +37 -0
- package/src/core/bindings.js +37 -0
- package/src/core/change-artifacts.js +134 -0
- package/src/core/competitive-score.js +265 -0
- package/src/core/concurrency-policy.js +35 -0
- package/src/core/execution-log.js +108 -0
- package/src/core/intake-recommendation.js +134 -0
- package/src/core/memory-state.js +62 -0
- package/src/core/mission-state.js +780 -0
- package/src/core/pending-gates.js +66 -0
- package/src/core/phase-cost.js +116 -0
- package/src/core/phase-run-read.js +27 -0
- package/src/core/policy-overlay.js +180 -0
- package/src/core/state-store.js +201 -0
- package/src/core/workflow-executor.js +998 -0
- package/src/core/workflow-integrity.js +111 -0
- package/src/core/workflow-loader.js +163 -0
- package/src/core/workflow-rerun.js +84 -0
- package/src/core/workflow-state.js +119 -0
- package/src/server.js +1307 -0
- package/workflows/corps.yaml +479 -0
- package/workflows/yolo.yaml +233 -0
- package/xflow/README.md +76 -0
- package/xflow/aha/SKILL.md +82 -0
- package/xflow/aha/scripts/append_aha.py +50 -0
- package/xflow/archive/scripts/archive_change.py +338 -0
- package/xflow/archive/templates/merge-design.md +54 -0
- package/xflow/archive/templates/merge-workflow.md +26 -0
- package/xflow/atoms/__init__.py +6 -0
- package/xflow/atoms/a1_issue_create_or_patch.py +70 -0
- package/xflow/atoms/a2_branch_create_and_link.py +42 -0
- package/xflow/atoms/a3_repo_doctor.py +11 -0
- package/xflow/atoms/a4_project_set_status.py +11 -0
- package/xflow/atoms/a5_archive_commit_push_close.py +11 -0
- package/xflow/atoms/a6_pr_create.py +72 -0
- package/xflow/atoms/b1_change_scaffold.py +11 -0
- package/xflow/atoms/b2_status_read.py +16 -0
- package/xflow/atoms/b2b_status_write.py +20 -0
- package/xflow/atoms/b3_status_transition.py +27 -0
- package/xflow/atoms/b4_handoff_scaffold_or_refresh.py +11 -0
- package/xflow/atoms/b5_aha_append.py +11 -0
- package/xflow/atoms/b6_takein_digest.py +11 -0
- package/xflow/atoms/b6b_lookback_digest.py +11 -0
- package/xflow/atoms/b7_change_mission_bind.py +43 -0
- package/xflow/atoms/c1_mem.py +54 -0
- package/xflow/atoms/e6_gate_local_precheck.py +23 -0
- package/xflow/atoms/h1_design_lite_gate.py +74 -0
- package/xflow/atoms/i6a_tdd_run.py +104 -0
- package/xflow/atoms/i6b_tdd_proof_validate.py +165 -0
- package/xflow/atoms/i6c_tdd_quality_review.py +357 -0
- package/xflow/atoms/j1_tests_run.py +129 -0
- package/xflow/atoms/j4a_spec_consistency_structural.py +105 -0
- package/xflow/atoms/j4b_spec_delta_review.py +316 -0
- package/xflow/atoms/j4c_openspec_migration_map.py +121 -0
- package/xflow/atoms/j4d_spec_clarify_gate.py +373 -0
- package/xflow/atoms/j6_cr_review.py +443 -0
- package/xflow/atoms/k1_artifacts_complete_check.py +39 -0
- package/xflow/atoms/k2_merge_snippets_apply.py +11 -0
- package/xflow/atoms/k3_handoff_refresh.py +14 -0
- package/xflow/atoms/k4_mem_lesson_persist.py +65 -0
- package/xflow/atoms/k5_archive_publish.py +21 -0
- package/xflow/atoms/k6_aha_merge.py +42 -0
- package/xflow/atoms/tdd_lib.py +747 -0
- package/xflow/corps/SKILL.md +109 -0
- package/xflow/exec/scripts/set_issue_in_progress.py +159 -0
- package/xflow/goal/SKILL.md +150 -0
- package/xflow/handoff/SKILL.md +95 -0
- package/xflow/handoff/scripts/scaffold_handoff.py +304 -0
- package/xflow/lookback/SKILL.md +66 -0
- package/xflow/lookback/scripts/lookback_digest.py +46 -0
- package/xflow/openissue/scripts/open_issue_flow.py +438 -0
- package/xflow/plan/SKILL.md +38 -0
- package/xflow/ralph/SKILL.md +123 -0
- package/xflow/scripts/check_installed_xflow_skill_sync.sh +44 -0
- package/xflow/scripts/common.py +480 -0
- package/xflow/scripts/doctor.py +278 -0
- package/xflow/scripts/sync_installed_xflow_skill.sh +134 -0
- package/xflow/takein/SKILL.md +81 -0
- package/xflow/takein/scripts/takein_digest.py +88 -0
- package/xflow/yolo/SKILL.md +66 -0
- package/xflow/yolo/scripts/scaffold_specs_change.py +673 -0
- package/xflow/yolo/scripts/yolo_gate.py +312 -0
- package/xflow/yolo/templates/DESIGN.md +179 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export async function loadPlaywright() {
|
|
2
|
+
try {
|
|
3
|
+
return await import('playwright');
|
|
4
|
+
} catch (error) {
|
|
5
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
6
|
+
throw new Error(`Playwright is required for visual capture/diff but is unavailable: ${reason}`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import Ajv from 'ajv';
|
|
6
|
+
|
|
7
|
+
import { readJsonFile, resolveChangePath, writeJsonFile } from '../../../core/change-artifacts.js';
|
|
8
|
+
|
|
9
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
10
|
+
const AGGREGATE_SCHEMA = JSON.parse(readFileSync(
|
|
11
|
+
resolve(__dirname, '../../../../schemas/llm-design-review-aggregate.schema.json'),
|
|
12
|
+
'utf8',
|
|
13
|
+
));
|
|
14
|
+
const ajv = new Ajv({ allErrors: true });
|
|
15
|
+
const validateAggregate = ajv.compile(AGGREGATE_SCHEMA);
|
|
16
|
+
|
|
17
|
+
function nowIso() {
|
|
18
|
+
return new Date().toISOString();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function visualReviewAggregate(input = {}, context = {}) {
|
|
22
|
+
const projectRoot = context.projectRoot || process.cwd();
|
|
23
|
+
const changeId = context.changeId || input.change_id || null;
|
|
24
|
+
if (!changeId) {
|
|
25
|
+
throw new Error('change_id is required for H5b.visual.review.aggregate');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const reviewPath = resolveChangePath(projectRoot, changeId, 'llm_design_review.json');
|
|
29
|
+
const benchmarkPath = resolveChangePath(projectRoot, changeId, 'visual_benchmark.json');
|
|
30
|
+
const review = readJsonFile(reviewPath, {
|
|
31
|
+
version: 1,
|
|
32
|
+
phase: 'llm_design_review',
|
|
33
|
+
summary: 'Visual review completed.',
|
|
34
|
+
team_run: {},
|
|
35
|
+
});
|
|
36
|
+
const benchmark = readJsonFile(benchmarkPath, { version: 1, scenarios: [] });
|
|
37
|
+
|
|
38
|
+
const scenarios = Array.isArray(benchmark.scenarios) ? benchmark.scenarios : [];
|
|
39
|
+
const passing = scenarios.filter((scenario) => scenario.status === 'pass').length;
|
|
40
|
+
const failing = scenarios.filter((scenario) => scenario.status && scenario.status !== 'pass').length;
|
|
41
|
+
const summary = [
|
|
42
|
+
review.summary || 'Visual review completed.',
|
|
43
|
+
scenarios.length > 0
|
|
44
|
+
? `Benchmarked ${scenarios.length} scenario(s); ${passing} passed${failing > 0 ? `, ${failing} need follow-up` : ''}.`
|
|
45
|
+
: 'No benchmark scenarios were captured.',
|
|
46
|
+
].join(' ');
|
|
47
|
+
|
|
48
|
+
const merged = {
|
|
49
|
+
...review,
|
|
50
|
+
version: review.version || 1,
|
|
51
|
+
phase: 'llm_design_review',
|
|
52
|
+
summary,
|
|
53
|
+
benchmark_summary: {
|
|
54
|
+
scenario_count: scenarios.length,
|
|
55
|
+
passed_count: passing,
|
|
56
|
+
needs_follow_up_count: failing,
|
|
57
|
+
statuses: scenarios.map((scenario) => ({
|
|
58
|
+
id: scenario.id || null,
|
|
59
|
+
status: scenario.status || 'unknown',
|
|
60
|
+
})),
|
|
61
|
+
},
|
|
62
|
+
worker_runs: {
|
|
63
|
+
visual_review: {
|
|
64
|
+
artifact_file: reviewPath,
|
|
65
|
+
},
|
|
66
|
+
visual_benchmark: {
|
|
67
|
+
artifact_file: benchmarkPath,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
aggregated_at: nowIso(),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (!validateAggregate(merged)) {
|
|
74
|
+
const details = (validateAggregate.errors || []).map((error) => `${error.instancePath || '/'} ${error.message}`).join('; ');
|
|
75
|
+
throw new Error(`llm_design_review aggregate schema invalid: ${details}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
writeJsonFile(reviewPath, merged);
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
status: 'aggregated',
|
|
82
|
+
artifact_file: reviewPath,
|
|
83
|
+
payload: merged,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default visualReviewAggregate;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readJsonFile, resolveChangePath, writeJsonFile } from '../../../core/change-artifacts.js';
|
|
2
|
+
import { synthesizePhaseArtifact } from '../../common.js';
|
|
3
|
+
|
|
4
|
+
function buildQaAcceptancePacket(projectRoot, changeId) {
|
|
5
|
+
const verifyProof = readJsonFile(resolveChangePath(projectRoot, changeId, 'verify_proof.json'), null);
|
|
6
|
+
const benchmark = readJsonFile(resolveChangePath(projectRoot, changeId, 'visual_benchmark.json'), null);
|
|
7
|
+
const aestheticReview = readJsonFile(resolveChangePath(projectRoot, changeId, 'aesthetic_review.json'), null);
|
|
8
|
+
const contractArtifact = readJsonFile(resolveChangePath(projectRoot, changeId, 'competitor_reconstruction_review.json'), null);
|
|
9
|
+
const contract = contractArtifact?.competitor_reconstruction_contract || {};
|
|
10
|
+
const requiredModules = Array.isArray(contract.required_modules) ? contract.required_modules : [];
|
|
11
|
+
const primaryJourneys = Array.isArray(contract.primary_journeys) ? contract.primary_journeys : [];
|
|
12
|
+
const benchmarkScenarios = Array.isArray(benchmark?.scenarios) ? benchmark.scenarios : [];
|
|
13
|
+
const testsPassed = verifyProof?.all_passed === true;
|
|
14
|
+
const benchmarkPassed = benchmark?.benchmark_mode === 'reference_backed'
|
|
15
|
+
&& benchmarkScenarios.length > 0
|
|
16
|
+
&& benchmarkScenarios.every((scenario) => scenario.status === 'pass')
|
|
17
|
+
&& benchmark?.matrix_status !== 'needs_follow_up';
|
|
18
|
+
const aestheticAccepted = aestheticReview?.status === 'accept'
|
|
19
|
+
&& Number(aestheticReview?.score || 0) >= Number(aestheticReview?.min_accept_score || 0.88)
|
|
20
|
+
&& (!Array.isArray(aestheticReview?.blockers) || aestheticReview.blockers.length === 0);
|
|
21
|
+
|
|
22
|
+
const failedChecks = [];
|
|
23
|
+
if (!testsPassed) failedChecks.push('planned_verification_failed');
|
|
24
|
+
if (!benchmarkPassed) failedChecks.push('visual_benchmark_not_accepted');
|
|
25
|
+
if (!aestheticAccepted) failedChecks.push('aesthetic_review_not_accepted');
|
|
26
|
+
if (requiredModules.length === 0) failedChecks.push('module_contract_missing');
|
|
27
|
+
if (primaryJourneys.length === 0) failedChecks.push('primary_journey_contract_missing');
|
|
28
|
+
|
|
29
|
+
const accepted = failedChecks.length === 0;
|
|
30
|
+
return {
|
|
31
|
+
status: accepted ? 'accepted' : 'needs_human',
|
|
32
|
+
primary_journey_covered: accepted,
|
|
33
|
+
manifest_valid: Boolean(verifyProof),
|
|
34
|
+
cross_module_continuity: {
|
|
35
|
+
status: accepted ? 'covered' : 'unverified',
|
|
36
|
+
benchmark_scenarios: benchmarkScenarios.length,
|
|
37
|
+
},
|
|
38
|
+
module_coverage: {
|
|
39
|
+
covered_module_count: accepted ? requiredModules.length : 0,
|
|
40
|
+
expected_module_count: Math.max(requiredModules.length, 1),
|
|
41
|
+
modules: requiredModules,
|
|
42
|
+
},
|
|
43
|
+
failed_checks: failedChecks,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function visualReview(input = {}, context = {}) {
|
|
48
|
+
const mode = input.mode || context.phase?.id || 'review';
|
|
49
|
+
const projectRoot = context.projectRoot || process.cwd();
|
|
50
|
+
const changeId = context.changeId || input.change_id;
|
|
51
|
+
const qaAcceptancePacket = mode === 'qa' ? buildQaAcceptancePacket(projectRoot, changeId) : null;
|
|
52
|
+
const result = await synthesizePhaseArtifact(input, context, {
|
|
53
|
+
phase: context.phase?.id === 'llm_design_review' ? 'llm_design_review' : mode === 'qa' ? 'qa' : 'review',
|
|
54
|
+
summary: mode === 'qa'
|
|
55
|
+
? 'Visual QA review evaluated planned verification, benchmark, aesthetic, module, and journey evidence.'
|
|
56
|
+
: 'Visual review completed with style and continuity findings.',
|
|
57
|
+
extraTeamRun: mode === 'qa' ? {
|
|
58
|
+
qa_acceptance_packet: qaAcceptancePacket,
|
|
59
|
+
} : {},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (mode === 'qa') {
|
|
63
|
+
const packetPath = resolveChangePath(projectRoot, changeId, 'qa_visual_review.json');
|
|
64
|
+
writeJsonFile(packetPath, {
|
|
65
|
+
version: 1,
|
|
66
|
+
summary: 'Visual QA packet',
|
|
67
|
+
qa_acceptance_packet: qaAcceptancePacket,
|
|
68
|
+
retry_brief: {
|
|
69
|
+
status: qaAcceptancePacket.status === 'accepted' ? 'not_needed' : 'needs_follow_up',
|
|
70
|
+
target_phase: null,
|
|
71
|
+
evidence_screenshot_ids: ['first_fold'],
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
function escapeHtml(value) {
|
|
5
|
+
return String(value ?? '')
|
|
6
|
+
.replace(/&/g, '&')
|
|
7
|
+
.replace(/</g, '<')
|
|
8
|
+
.replace(/>/g, '>')
|
|
9
|
+
.replace(/"/g, '"')
|
|
10
|
+
.replace(/'/g, ''');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toHref(projectRoot, reportPath, filePath) {
|
|
14
|
+
if (!filePath || typeof filePath !== 'string') return null;
|
|
15
|
+
try {
|
|
16
|
+
const absolute = isAbsolute(filePath) ? filePath : resolve(projectRoot, filePath);
|
|
17
|
+
return relative(dirname(reportPath), absolute).replaceAll('\\', '/');
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function badge(status) {
|
|
24
|
+
const color = status === 'pass' ? '#166534' : '#991b1b';
|
|
25
|
+
const background = status === 'pass' ? '#dcfce7' : '#fee2e2';
|
|
26
|
+
return `<span style="display:inline-flex;align-items:center;padding:4px 10px;border-radius:999px;background:${background};color:${color};font:600 12px/1.2 Inter,system-ui,sans-serif">${escapeHtml(status || 'unknown')}</span>`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function renderCheckList(title, checks = []) {
|
|
30
|
+
if (!Array.isArray(checks) || checks.length === 0) return '';
|
|
31
|
+
return `
|
|
32
|
+
<section>
|
|
33
|
+
<h4>${escapeHtml(title)}</h4>
|
|
34
|
+
<ul class="check-list">
|
|
35
|
+
${checks.map((check) => `
|
|
36
|
+
<li>
|
|
37
|
+
${badge(check?.status)}
|
|
38
|
+
<code>${escapeHtml(check?.id || 'check')}</code>
|
|
39
|
+
${check?.detail ? `<span>${escapeHtml(check.detail)}</span>` : ''}
|
|
40
|
+
</li>
|
|
41
|
+
`).join('')}
|
|
42
|
+
</ul>
|
|
43
|
+
</section>
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function renderHotspots(hotspots = []) {
|
|
48
|
+
if (!Array.isArray(hotspots) || hotspots.length === 0) return '';
|
|
49
|
+
return `
|
|
50
|
+
<section>
|
|
51
|
+
<h4>Hotspots</h4>
|
|
52
|
+
<table class="hotspot-table">
|
|
53
|
+
<thead>
|
|
54
|
+
<tr><th>Block</th><th>Changed</th><th>Ratio</th><th>Rect</th></tr>
|
|
55
|
+
</thead>
|
|
56
|
+
<tbody>
|
|
57
|
+
${hotspots.map((spot) => `
|
|
58
|
+
<tr>
|
|
59
|
+
<td>r${escapeHtml(spot?.row)} c${escapeHtml(spot?.col)}</td>
|
|
60
|
+
<td>${escapeHtml(spot?.changed_pixels)}</td>
|
|
61
|
+
<td>${typeof spot?.changed_ratio === 'number' ? spot.changed_ratio.toFixed(4) : '-'}</td>
|
|
62
|
+
<td>${escapeHtml(`${spot?.x ?? 0},${spot?.y ?? 0} ${spot?.width ?? 0}x${spot?.height ?? 0}`)}</td>
|
|
63
|
+
</tr>
|
|
64
|
+
`).join('')}
|
|
65
|
+
</tbody>
|
|
66
|
+
</table>
|
|
67
|
+
</section>
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function renderScenarioCard(projectRoot, reportPath, scenario = {}) {
|
|
72
|
+
const referenceHref = toHref(projectRoot, reportPath, scenario.reference_image);
|
|
73
|
+
const screenshotHref = toHref(projectRoot, reportPath, scenario.screenshot_image);
|
|
74
|
+
const heatmapHref = toHref(projectRoot, reportPath, scenario.diff_metrics?.heatmap_file);
|
|
75
|
+
const metricValues = scenario.diff_metrics?.values || {};
|
|
76
|
+
return `
|
|
77
|
+
<article class="scenario-card">
|
|
78
|
+
<header class="scenario-header">
|
|
79
|
+
<div>
|
|
80
|
+
<h3>${escapeHtml(scenario.id || 'scenario')}</h3>
|
|
81
|
+
<p>${escapeHtml(`${scenario.platform_profile || 'unprofiled'} · ${scenario.viewport?.width || '?'}x${scenario.viewport?.height || '?'}`)}</p>
|
|
82
|
+
</div>
|
|
83
|
+
${badge(scenario.status)}
|
|
84
|
+
</header>
|
|
85
|
+
<section class="metric-strip">
|
|
86
|
+
<div><label>SSIM</label><strong>${typeof metricValues.structural_similarity === 'number' ? metricValues.structural_similarity.toFixed(4) : '-'}</strong></div>
|
|
87
|
+
<div><label>Layout shift</label><strong>${typeof metricValues.layout_shift_score === 'number' ? metricValues.layout_shift_score.toFixed(4) : '-'}</strong></div>
|
|
88
|
+
<div><label>Pixel diff</label><strong>${typeof metricValues.pixel_diff_ratio === 'number' ? metricValues.pixel_diff_ratio.toFixed(4) : '-'}</strong></div>
|
|
89
|
+
</section>
|
|
90
|
+
<section class="image-grid">
|
|
91
|
+
${referenceHref ? `<figure><figcaption>Reference</figcaption><img src="${escapeHtml(referenceHref)}" alt="reference"></figure>` : ''}
|
|
92
|
+
${screenshotHref ? `<figure><figcaption>Candidate</figcaption><img src="${escapeHtml(screenshotHref)}" alt="candidate"></figure>` : ''}
|
|
93
|
+
${heatmapHref ? `<figure><figcaption>Heatmap</figcaption><img src="${escapeHtml(heatmapHref)}" alt="heatmap"></figure>` : ''}
|
|
94
|
+
</section>
|
|
95
|
+
${renderHotspots(scenario.diff_metrics?.hotspots)}
|
|
96
|
+
${renderCheckList('Structure checks', scenario.structure_checks)}
|
|
97
|
+
${renderCheckList('Token checks', scenario.token_checks)}
|
|
98
|
+
${renderCheckList('State checks', scenario.state_transition_checks)}
|
|
99
|
+
${renderCheckList('Motion checks', scenario.motion_transition_checks)}
|
|
100
|
+
${renderCheckList('State contract checks', scenario.state_contract_checks)}
|
|
101
|
+
${renderCheckList('State family checks', scenario.state_family_checks)}
|
|
102
|
+
</article>
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function renderPage({ title, summary, body }) {
|
|
107
|
+
return `<!doctype html>
|
|
108
|
+
<html lang="en">
|
|
109
|
+
<head>
|
|
110
|
+
<meta charset="utf-8">
|
|
111
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
112
|
+
<title>${escapeHtml(title)}</title>
|
|
113
|
+
<style>
|
|
114
|
+
:root { color-scheme: light dark; }
|
|
115
|
+
body { margin: 0; font: 14px/1.5 Inter, system-ui, sans-serif; background: #0b1020; color: #e5ecff; }
|
|
116
|
+
.shell { max-width: 1440px; margin: 0 auto; padding: 24px; }
|
|
117
|
+
.hero { display: grid; gap: 12px; margin-bottom: 24px; }
|
|
118
|
+
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
|
|
119
|
+
.summary-card, .scenario-card { background: rgba(15, 23, 42, 0.88); border: 1px solid rgba(148, 163, 184, 0.18); border-radius: 14px; padding: 16px; }
|
|
120
|
+
.summary-card label, .metric-strip label { display: block; color: #93a4c3; font-size: 12px; margin-bottom: 4px; }
|
|
121
|
+
.summary-card strong, .metric-strip strong { font-size: 18px; }
|
|
122
|
+
.scenario-stack { display: grid; gap: 18px; }
|
|
123
|
+
.scenario-header { display: flex; justify-content: space-between; gap: 12px; align-items: start; }
|
|
124
|
+
.scenario-header h3 { margin: 0; font-size: 20px; }
|
|
125
|
+
.scenario-header p { margin: 4px 0 0; color: #93a4c3; }
|
|
126
|
+
.metric-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; margin: 16px 0; }
|
|
127
|
+
.image-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; margin-bottom: 16px; }
|
|
128
|
+
figure { margin: 0; background: rgba(2, 6, 23, 0.7); border-radius: 12px; overflow: hidden; border: 1px solid rgba(148, 163, 184, 0.12); }
|
|
129
|
+
figcaption { padding: 8px 10px; color: #c6d2ea; border-bottom: 1px solid rgba(148, 163, 184, 0.12); }
|
|
130
|
+
img { display: block; width: 100%; height: auto; background: #fff; }
|
|
131
|
+
h4 { margin: 16px 0 8px; font-size: 14px; color: #c6d2ea; }
|
|
132
|
+
.check-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
|
|
133
|
+
.check-list li { display: grid; gap: 6px; padding: 10px 12px; border-radius: 10px; background: rgba(30, 41, 59, 0.55); }
|
|
134
|
+
.check-list code { color: #bfdbfe; }
|
|
135
|
+
.hotspot-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
136
|
+
.hotspot-table th, .hotspot-table td { text-align: left; padding: 8px 10px; border-bottom: 1px solid rgba(148, 163, 184, 0.14); }
|
|
137
|
+
.prose { color: #cbd5e1; }
|
|
138
|
+
a { color: #93c5fd; }
|
|
139
|
+
</style>
|
|
140
|
+
</head>
|
|
141
|
+
<body>
|
|
142
|
+
<main class="shell">
|
|
143
|
+
<section class="hero">
|
|
144
|
+
<h1 style="margin:0;font-size:28px">${escapeHtml(title)}</h1>
|
|
145
|
+
<div class="prose">${summary}</div>
|
|
146
|
+
</section>
|
|
147
|
+
${body}
|
|
148
|
+
</main>
|
|
149
|
+
</body>
|
|
150
|
+
</html>`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function renderVisualCompareReport({
|
|
154
|
+
projectRoot,
|
|
155
|
+
outputPath,
|
|
156
|
+
title,
|
|
157
|
+
summary,
|
|
158
|
+
scenarios = [],
|
|
159
|
+
}) {
|
|
160
|
+
const cards = scenarios.map((scenario) => renderScenarioCard(projectRoot, outputPath, scenario)).join('');
|
|
161
|
+
const html = renderPage({
|
|
162
|
+
title,
|
|
163
|
+
summary,
|
|
164
|
+
body: `<section class="scenario-stack">${cards}</section>`,
|
|
165
|
+
});
|
|
166
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
167
|
+
writeFileSync(outputPath, html);
|
|
168
|
+
return {
|
|
169
|
+
output_file: outputPath,
|
|
170
|
+
scenarios: scenarios.length,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function renderVisualBenchmarkReport({
|
|
175
|
+
projectRoot,
|
|
176
|
+
outputPath,
|
|
177
|
+
artifact,
|
|
178
|
+
}) {
|
|
179
|
+
const scenarios = Array.isArray(artifact?.scenarios) ? artifact.scenarios : [];
|
|
180
|
+
const summary = `
|
|
181
|
+
<div class="summary">
|
|
182
|
+
<div class="summary-card"><label>Competitor</label><strong>${escapeHtml(artifact?.competitor_product || 'unknown')}</strong></div>
|
|
183
|
+
<div class="summary-card"><label>Benchmark mode</label><strong>${escapeHtml(artifact?.benchmark_mode || 'unknown')}</strong></div>
|
|
184
|
+
<div class="summary-card"><label>Input mode</label><strong>${escapeHtml(artifact?.benchmark_input_mode || 'unknown')}</strong></div>
|
|
185
|
+
<div class="summary-card"><label>Matrix status</label><strong>${escapeHtml(artifact?.matrix_status || 'n/a')}</strong></div>
|
|
186
|
+
<div class="summary-card"><label>Scenarios</label><strong>${scenarios.length}</strong></div>
|
|
187
|
+
<div class="summary-card"><label>Modules</label><strong>${escapeHtml((artifact?.required_modules || []).join(', ') || 'none')}</strong></div>
|
|
188
|
+
</div>
|
|
189
|
+
`;
|
|
190
|
+
return renderVisualCompareReport({
|
|
191
|
+
projectRoot,
|
|
192
|
+
outputPath,
|
|
193
|
+
title: `${artifact?.competitor_product || 'Competitor'} visual benchmark`,
|
|
194
|
+
summary,
|
|
195
|
+
scenarios,
|
|
196
|
+
});
|
|
197
|
+
}
|