@surlymochan/evalid 0.0.0-stage → 0.4.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 +94 -0
- package/LICENSE +21 -0
- package/README.md +315 -2
- package/RELEASE_NOTES.md +124 -0
- package/atoms/registry.json +758 -0
- package/bin/xflow.js +7261 -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 +96 -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 +1130 -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 +1014 -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 +481 -0
- package/workflows/yolo.yaml +241 -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 +107 -0
- package/xflow/atoms/i6b_tdd_proof_validate.py +184 -0
- package/xflow/atoms/i6c_tdd_quality_review.py +372 -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 +956 -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 +47 -0
- package/xflow/ralph/SKILL.md +123 -0
- package/xflow/scripts/check_installed_xflow_skill_sync.sh +44 -0
- package/xflow/scripts/common.py +483 -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 +75 -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,23 @@
|
|
|
1
|
+
import { synthesizePhaseArtifact } from '../../common.js';
|
|
2
|
+
import { readJsonFile, resolveChangePath } from '../../../core/change-artifacts.js';
|
|
3
|
+
|
|
4
|
+
export async function visualDesignSelection(input = {}, context = {}) {
|
|
5
|
+
const projectRoot = context.projectRoot || process.cwd();
|
|
6
|
+
const changeId = context.changeId || input.change_id || null;
|
|
7
|
+
const synthesis = readJsonFile(resolveChangePath(projectRoot, changeId, 'visual_direction_synthesis.json'), {}) || {};
|
|
8
|
+
const visualDirection = synthesis.visual_direction_synthesis || {};
|
|
9
|
+
const directions = Array.isArray(visualDirection.directions) ? visualDirection.directions : [];
|
|
10
|
+
const selected = directions.find((direction) => direction.id === visualDirection.recommended_direction) || directions[0] || null;
|
|
11
|
+
return synthesizePhaseArtifact(input, context, {
|
|
12
|
+
phase: 'design_selection',
|
|
13
|
+
summary: 'A winning high-aesthetic direction and fallback candidate were selected.',
|
|
14
|
+
extraEnvelope: {
|
|
15
|
+
design_selection: {
|
|
16
|
+
status: selected ? 'selected' : 'needs_input',
|
|
17
|
+
selected_direction: selected,
|
|
18
|
+
fallback_direction: directions.find((direction) => direction.id !== selected?.id) || null,
|
|
19
|
+
selection_rule: 'Prefer reference-faithful product credibility before decorative novelty.',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { synthesizePhaseArtifact } from '../../common.js';
|
|
2
|
+
import { readJsonFile, resolveChangePath, writeJsonFile, writeTextFile } from '../../../core/change-artifacts.js';
|
|
3
|
+
|
|
4
|
+
function buildDirection(id, label, emphasis, aestheticStandard = {}, imageReference = {}, designSystemPack = {}) {
|
|
5
|
+
const practiceSources = Array.isArray(designSystemPack.practice_sources)
|
|
6
|
+
? designSystemPack.practice_sources.map((source) => source.id).filter(Boolean)
|
|
7
|
+
: [];
|
|
8
|
+
return {
|
|
9
|
+
id,
|
|
10
|
+
label,
|
|
11
|
+
emphasis,
|
|
12
|
+
aesthetic_dimensions: aestheticStandard.dimensions || [],
|
|
13
|
+
design_system_practices: practiceSources,
|
|
14
|
+
image_reference_brief: imageReference.enabled === false ? null : {
|
|
15
|
+
model_capability: imageReference.model_capability || 'gpt_image_v2_style_reference',
|
|
16
|
+
prompt_focus: [
|
|
17
|
+
`Create a high-aesthetic ${label.toLowerCase()} product UI reference frame.`,
|
|
18
|
+
`Emphasize ${emphasis}.`,
|
|
19
|
+
'Preserve the locked surface decomposition, hierarchy, and product credibility.',
|
|
20
|
+
`Honor the design-system pack profile: ${designSystemPack.profile || 'commercial_product_design_system_pack'}.`,
|
|
21
|
+
'Output should guide editable UI primitives, not replace implementation.',
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function materializeImageReferenceSet(projectRoot, changeId, imageReference = {}, directions = [], designSystemPack = {}) {
|
|
28
|
+
if (imageReference.enabled === false) return null;
|
|
29
|
+
|
|
30
|
+
const requiredOutputs = Array.isArray(imageReference.required_outputs)
|
|
31
|
+
? imageReference.required_outputs
|
|
32
|
+
: [];
|
|
33
|
+
const references = requiredOutputs.map((output, index) => {
|
|
34
|
+
const direction = directions[index % Math.max(directions.length, 1)] || directions[0] || {};
|
|
35
|
+
const artifactPath = resolveChangePath(projectRoot, changeId, `image-references/${output.id}.md`);
|
|
36
|
+
const lines = [
|
|
37
|
+
`# ${output.id}`,
|
|
38
|
+
'',
|
|
39
|
+
`model_capability: ${imageReference.model_capability || 'gpt_image_v2_style_reference'}`,
|
|
40
|
+
`purpose: ${output.purpose || ''}`,
|
|
41
|
+
`direction: ${direction.label || 'Reference-faithful'}`,
|
|
42
|
+
`design_system_profile: ${designSystemPack.profile || 'commercial_product_design_system_pack'}`,
|
|
43
|
+
'',
|
|
44
|
+
'## Prompt',
|
|
45
|
+
'',
|
|
46
|
+
...(direction.image_reference_brief?.prompt_focus || [
|
|
47
|
+
'Create a high-aesthetic product UI reference artifact from the frozen generation contract.',
|
|
48
|
+
'Keep the result translatable into editable Pencil and DOM primitives.',
|
|
49
|
+
]).map((line) => `- ${line}`),
|
|
50
|
+
'',
|
|
51
|
+
'## Translation Contract',
|
|
52
|
+
'',
|
|
53
|
+
'- Use this as an aesthetic reference target, not as the shipped implementation.',
|
|
54
|
+
'- Preserve the design-system component whitelist, token policy, and required state set.',
|
|
55
|
+
'- Preserve editable UI primitives and benchmarkable DOM/canvas evidence downstream.',
|
|
56
|
+
];
|
|
57
|
+
writeTextFile(artifactPath, lines.join('\n'));
|
|
58
|
+
return {
|
|
59
|
+
id: output.id,
|
|
60
|
+
status: 'ready',
|
|
61
|
+
kind: 'image_reference_brief',
|
|
62
|
+
model_capability: imageReference.model_capability || 'gpt_image_v2_style_reference',
|
|
63
|
+
artifact_path: `specs/changes/${changeId}/image-references/${output.id}.md`,
|
|
64
|
+
purpose: output.purpose || null,
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const payload = {
|
|
69
|
+
version: 1,
|
|
70
|
+
phase: 'image_reference_generation',
|
|
71
|
+
change_id: changeId,
|
|
72
|
+
status: references.length >= 3 ? 'ready' : 'needs_refine',
|
|
73
|
+
model_capability: imageReference.model_capability || 'gpt_image_v2_style_reference',
|
|
74
|
+
references,
|
|
75
|
+
translation_contract: {
|
|
76
|
+
editable_target: 'Pencil and DOM primitives',
|
|
77
|
+
bitmap_non_goal: true,
|
|
78
|
+
benchmark_required: true,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
writeJsonFile(resolveChangePath(projectRoot, changeId, 'image_reference_set.json'), payload);
|
|
82
|
+
return payload;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function visualDirectionSynthesis(input = {}, context = {}) {
|
|
86
|
+
const projectRoot = context.projectRoot || process.cwd();
|
|
87
|
+
const changeId = context.changeId || input.change_id || null;
|
|
88
|
+
const generation = readJsonFile(resolveChangePath(projectRoot, changeId, 'generation_contract.json'), {})?.generation_contract || {};
|
|
89
|
+
const designSystemPack = readJsonFile(resolveChangePath(projectRoot, changeId, 'design_system_pack.json'), {}) || {};
|
|
90
|
+
const visualConstraints = generation.visual_constraints || {};
|
|
91
|
+
const aestheticStandard = visualConstraints.aesthetic_standard || {};
|
|
92
|
+
const imageReference = visualConstraints.image_reference_generation || {};
|
|
93
|
+
const directions = [
|
|
94
|
+
buildDirection('reference-faithful', 'Reference-faithful', 'reference structure, density, and material restraint', aestheticStandard, imageReference, designSystemPack),
|
|
95
|
+
buildDirection('clarity-first', 'Clarity-first', 'information hierarchy and repeated-component rhythm', aestheticStandard, imageReference, designSystemPack),
|
|
96
|
+
buildDirection('state-polish', 'State-polish', 'interaction states, affordance contrast, and motion readiness', aestheticStandard, imageReference, designSystemPack),
|
|
97
|
+
];
|
|
98
|
+
const imageReferenceSet = materializeImageReferenceSet(projectRoot, changeId, imageReference, directions, designSystemPack);
|
|
99
|
+
return synthesizePhaseArtifact(input, context, {
|
|
100
|
+
phase: 'visual_direction_synthesis',
|
|
101
|
+
summary: 'Multiple high-aesthetic visual directions were synthesized with image-reference prompts and product-surface constraints.',
|
|
102
|
+
extraEnvelope: {
|
|
103
|
+
visual_direction_synthesis: {
|
|
104
|
+
status: 'ready',
|
|
105
|
+
aesthetic_standard: aestheticStandard,
|
|
106
|
+
image_reference_generation: imageReference,
|
|
107
|
+
design_system_pack: {
|
|
108
|
+
status: designSystemPack.status || 'missing',
|
|
109
|
+
profile: designSystemPack.profile || null,
|
|
110
|
+
practice_sources: (designSystemPack.practice_sources || []).map((source) => source.id),
|
|
111
|
+
},
|
|
112
|
+
image_reference_set: imageReferenceSet
|
|
113
|
+
? {
|
|
114
|
+
status: imageReferenceSet.status,
|
|
115
|
+
artifact: 'image_reference_set.json',
|
|
116
|
+
reference_count: imageReferenceSet.references.length,
|
|
117
|
+
}
|
|
118
|
+
: null,
|
|
119
|
+
directions,
|
|
120
|
+
recommended_direction: directions[0].id,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
function normalizeAttributes(node = {}, panelAttr = 'data-panel') {
|
|
5
|
+
if (node.attributes && typeof node.attributes === 'object' && !Array.isArray(node.attributes)) {
|
|
6
|
+
return node.attributes;
|
|
7
|
+
}
|
|
8
|
+
if (node.attrs && typeof node.attrs === 'object' && !Array.isArray(node.attrs)) {
|
|
9
|
+
return node.attrs;
|
|
10
|
+
}
|
|
11
|
+
if (Array.isArray(node.attributes)) {
|
|
12
|
+
return Object.fromEntries(node.attributes
|
|
13
|
+
.map((entry) => Array.isArray(entry) ? entry : [entry?.name, entry?.value])
|
|
14
|
+
.filter(([name]) => typeof name === 'string'));
|
|
15
|
+
}
|
|
16
|
+
if (Array.isArray(node.attribute_list)) {
|
|
17
|
+
return Object.fromEntries(node.attribute_list
|
|
18
|
+
.map((entry) => [entry?.name, entry?.value])
|
|
19
|
+
.filter(([name]) => typeof name === 'string'));
|
|
20
|
+
}
|
|
21
|
+
if (node.dataset && typeof node.dataset === 'object' && !Array.isArray(node.dataset)) {
|
|
22
|
+
const datasetEntries = Object.entries(node.dataset).map(([key, value]) => [`data-${key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)}`, value]);
|
|
23
|
+
return Object.fromEntries(datasetEntries);
|
|
24
|
+
}
|
|
25
|
+
return panelAttr in node ? { [panelAttr]: node[panelAttr] } : {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolvePanelId(node = {}, panelAttr = 'data-panel') {
|
|
29
|
+
if (typeof node.panel_id === 'string' && node.panel_id.trim()) return node.panel_id.trim();
|
|
30
|
+
const attributes = normalizeAttributes(node, panelAttr);
|
|
31
|
+
if (typeof attributes[panelAttr] === 'string' && attributes[panelAttr].trim()) return attributes[panelAttr].trim();
|
|
32
|
+
if (typeof node.id === 'string' && node.id.trim()) return node.id.trim();
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeRect(node = {}) {
|
|
37
|
+
if (node.boundingClientRect && typeof node.boundingClientRect === 'object') {
|
|
38
|
+
const rect = node.boundingClientRect;
|
|
39
|
+
if (Number.isFinite(rect.left) && Number.isFinite(rect.top) && Number.isFinite(rect.width) && Number.isFinite(rect.height)) {
|
|
40
|
+
return { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (node.rect && typeof node.rect === 'object') {
|
|
44
|
+
const rect = node.rect;
|
|
45
|
+
if (Number.isFinite(rect.left) && Number.isFinite(rect.top) && Number.isFinite(rect.width) && Number.isFinite(rect.height)) {
|
|
46
|
+
return { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (node.box && typeof node.box === 'object') {
|
|
50
|
+
const box = node.box;
|
|
51
|
+
if (Number.isFinite(box.x) && Number.isFinite(box.y) && Number.isFinite(box.width) && Number.isFinite(box.height)) {
|
|
52
|
+
return { left: box.x, top: box.y, width: box.width, height: box.height };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (Number.isFinite(node.left) && Number.isFinite(node.top) && Number.isFinite(node.width) && Number.isFinite(node.height)) {
|
|
56
|
+
return { left: node.left, top: node.top, width: node.width, height: node.height };
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function pickNodes(snapshot) {
|
|
62
|
+
if (Array.isArray(snapshot)) return snapshot;
|
|
63
|
+
const collected = [];
|
|
64
|
+
if (Array.isArray(snapshot?.nodes)) collected.push(...snapshot.nodes);
|
|
65
|
+
if (Array.isArray(snapshot?.states)) {
|
|
66
|
+
for (const state of snapshot.states) {
|
|
67
|
+
if (Array.isArray(state?.nodes)) collected.push(...state.nodes);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (collected.length > 0) return collected;
|
|
71
|
+
if (Array.isArray(snapshot?.dom_snapshot?.nodes)) return snapshot.dom_snapshot.nodes;
|
|
72
|
+
if (Array.isArray(snapshot?.snapshot?.nodes)) return snapshot.snapshot.nodes;
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function exportDomRectsFromSnapshot(snapshot, options = {}) {
|
|
77
|
+
const panelAttr = options.panelAttr || 'data-panel';
|
|
78
|
+
const nodes = pickNodes(snapshot);
|
|
79
|
+
const byPanelId = new Map();
|
|
80
|
+
|
|
81
|
+
for (const node of nodes) {
|
|
82
|
+
const panelId = resolvePanelId(node, panelAttr);
|
|
83
|
+
const rect = normalizeRect(node);
|
|
84
|
+
if (!panelId || !rect) continue;
|
|
85
|
+
if (!(rect.width > 0 && rect.height > 0)) continue;
|
|
86
|
+
const area = rect.width * rect.height;
|
|
87
|
+
const selector = typeof node.selector === 'string' ? node.selector : null;
|
|
88
|
+
const candidate = {
|
|
89
|
+
panel_id: panelId,
|
|
90
|
+
selector,
|
|
91
|
+
left: rect.left,
|
|
92
|
+
top: rect.top,
|
|
93
|
+
width: rect.width,
|
|
94
|
+
height: rect.height,
|
|
95
|
+
_area: area,
|
|
96
|
+
};
|
|
97
|
+
const current = byPanelId.get(panelId);
|
|
98
|
+
if (!current || candidate._area > current._area) {
|
|
99
|
+
byPanelId.set(panelId, candidate);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const domRects = Array.from(byPanelId.values())
|
|
104
|
+
.sort((left, right) => {
|
|
105
|
+
if (left.left !== right.left) return left.left - right.left;
|
|
106
|
+
if (left.top !== right.top) return left.top - right.top;
|
|
107
|
+
return left.panel_id.localeCompare(right.panel_id);
|
|
108
|
+
})
|
|
109
|
+
.map(({ _area, ...entry }) => entry);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
dom_rects: domRects,
|
|
113
|
+
panel_attr: panelAttr,
|
|
114
|
+
input_nodes: nodes.length,
|
|
115
|
+
extracted_panels: domRects.length,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function exportDomRectsToFile({ inputPath, outputPath, panelAttr = 'data-panel' }) {
|
|
120
|
+
const snapshot = JSON.parse(readFileSync(inputPath, 'utf8'));
|
|
121
|
+
const payload = exportDomRectsFromSnapshot(snapshot, { panelAttr });
|
|
122
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
123
|
+
writeFileSync(outputPath, JSON.stringify(payload, null, 2));
|
|
124
|
+
return payload;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function resolveCliPath(projectRoot, maybeRelativePath) {
|
|
128
|
+
return resolve(projectRoot, maybeRelativePath);
|
|
129
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, extname } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { loadPlaywright } from './playwright-runtime.js';
|
|
5
|
+
|
|
6
|
+
function toNumber(value, fallback) {
|
|
7
|
+
const parsed = Number(value);
|
|
8
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function fileToDataUrl(filePath) {
|
|
12
|
+
const extension = extname(filePath).toLowerCase();
|
|
13
|
+
const mimeType = extension === '.svg'
|
|
14
|
+
? 'image/svg+xml'
|
|
15
|
+
: extension === '.png'
|
|
16
|
+
? 'image/png'
|
|
17
|
+
: extension === '.jpg' || extension === '.jpeg'
|
|
18
|
+
? 'image/jpeg'
|
|
19
|
+
: extension === '.webp'
|
|
20
|
+
? 'image/webp'
|
|
21
|
+
: 'application/octet-stream';
|
|
22
|
+
const encoded = readFileSync(filePath).toString('base64');
|
|
23
|
+
return `data:${mimeType};base64,${encoded}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function computeImageDiffMetrics({
|
|
27
|
+
referencePath,
|
|
28
|
+
candidatePath,
|
|
29
|
+
pixelThreshold = 16,
|
|
30
|
+
blockRows = 12,
|
|
31
|
+
blockCols = 12,
|
|
32
|
+
heatmapOutputPath = null,
|
|
33
|
+
}) {
|
|
34
|
+
if (!referencePath) throw new Error('referencePath is required for computeImageDiffMetrics');
|
|
35
|
+
if (!candidatePath) throw new Error('candidatePath is required for computeImageDiffMetrics');
|
|
36
|
+
|
|
37
|
+
const playwright = await loadPlaywright();
|
|
38
|
+
const browser = await playwright.chromium.launch({ headless: true });
|
|
39
|
+
try {
|
|
40
|
+
const page = await browser.newPage({ viewport: { width: 64, height: 64 } });
|
|
41
|
+
await page.setContent('<html><body></body></html>');
|
|
42
|
+
const metrics = await page.evaluate(async ({ referenceUrl, candidateUrl, pixelThresholdValue, blockRowsValue, blockColsValue }) => {
|
|
43
|
+
const loadImage = (src) => new Promise((resolve, reject) => {
|
|
44
|
+
const image = new Image();
|
|
45
|
+
image.onload = () => resolve(image);
|
|
46
|
+
image.onerror = () => reject(new Error(`Failed to load image: ${src}`));
|
|
47
|
+
image.src = src;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const [referenceImage, candidateImage] = await Promise.all([
|
|
51
|
+
loadImage(referenceUrl),
|
|
52
|
+
loadImage(candidateUrl),
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const width = Math.max(referenceImage.naturalWidth, candidateImage.naturalWidth);
|
|
56
|
+
const height = Math.max(referenceImage.naturalHeight, candidateImage.naturalHeight);
|
|
57
|
+
const makeImageData = (image) => {
|
|
58
|
+
const canvas = document.createElement('canvas');
|
|
59
|
+
canvas.width = width;
|
|
60
|
+
canvas.height = height;
|
|
61
|
+
const context = canvas.getContext('2d');
|
|
62
|
+
context.clearRect(0, 0, width, height);
|
|
63
|
+
context.drawImage(image, 0, 0);
|
|
64
|
+
return context.getImageData(0, 0, width, height).data;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const referenceData = makeImageData(referenceImage);
|
|
68
|
+
const candidateData = makeImageData(candidateImage);
|
|
69
|
+
const totalPixels = width * height;
|
|
70
|
+
const blockChanged = new Array(blockRowsValue * blockColsValue).fill(false);
|
|
71
|
+
const blockChangeCounts = new Array(blockRowsValue * blockColsValue).fill(0);
|
|
72
|
+
let changedPixels = 0;
|
|
73
|
+
let luminanceSquaredError = 0;
|
|
74
|
+
|
|
75
|
+
for (let pixelIndex = 0; pixelIndex < totalPixels; pixelIndex += 1) {
|
|
76
|
+
const base = pixelIndex * 4;
|
|
77
|
+
const redDiff = Math.abs(referenceData[base] - candidateData[base]);
|
|
78
|
+
const greenDiff = Math.abs(referenceData[base + 1] - candidateData[base + 1]);
|
|
79
|
+
const blueDiff = Math.abs(referenceData[base + 2] - candidateData[base + 2]);
|
|
80
|
+
const alphaDiff = Math.abs(referenceData[base + 3] - candidateData[base + 3]);
|
|
81
|
+
const maxChannelDiff = Math.max(redDiff, greenDiff, blueDiff, alphaDiff);
|
|
82
|
+
if (maxChannelDiff > pixelThresholdValue) {
|
|
83
|
+
changedPixels += 1;
|
|
84
|
+
const x = pixelIndex % width;
|
|
85
|
+
const y = Math.floor(pixelIndex / width);
|
|
86
|
+
const blockX = Math.min(blockColsValue - 1, Math.floor((x / Math.max(width, 1)) * blockColsValue));
|
|
87
|
+
const blockY = Math.min(blockRowsValue - 1, Math.floor((y / Math.max(height, 1)) * blockRowsValue));
|
|
88
|
+
const blockIndex = (blockY * blockColsValue) + blockX;
|
|
89
|
+
blockChanged[blockIndex] = true;
|
|
90
|
+
blockChangeCounts[blockIndex] += 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const referenceLuminance = (0.2126 * referenceData[base]) + (0.7152 * referenceData[base + 1]) + (0.0722 * referenceData[base + 2]);
|
|
94
|
+
const candidateLuminance = (0.2126 * candidateData[base]) + (0.7152 * candidateData[base + 1]) + (0.0722 * candidateData[base + 2]);
|
|
95
|
+
const luminanceDiff = referenceLuminance - candidateLuminance;
|
|
96
|
+
luminanceSquaredError += luminanceDiff * luminanceDiff;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const changedBlocks = blockChanged.filter(Boolean).length;
|
|
100
|
+
const pixelDiffRatio = totalPixels > 0 ? changedPixels / totalPixels : 0;
|
|
101
|
+
const layoutShiftScore = blockChanged.length > 0 ? changedBlocks / blockChanged.length : 0;
|
|
102
|
+
const rmse = totalPixels > 0 ? Math.sqrt(luminanceSquaredError / totalPixels) / 255 : 0;
|
|
103
|
+
const structuralSimilarity = Math.max(0, 1 - rmse);
|
|
104
|
+
const hotspots = blockChangeCounts
|
|
105
|
+
.map((changedCount, index) => {
|
|
106
|
+
const row = Math.floor(index / blockColsValue);
|
|
107
|
+
const col = index % blockColsValue;
|
|
108
|
+
const blockWidth = width / blockColsValue;
|
|
109
|
+
const blockHeight = height / blockRowsValue;
|
|
110
|
+
return {
|
|
111
|
+
row,
|
|
112
|
+
col,
|
|
113
|
+
changed_pixels: changedCount,
|
|
114
|
+
changed_ratio: totalPixels > 0 ? changedCount / totalPixels : 0,
|
|
115
|
+
x: Math.round(col * blockWidth),
|
|
116
|
+
y: Math.round(row * blockHeight),
|
|
117
|
+
width: Math.round(blockWidth),
|
|
118
|
+
height: Math.round(blockHeight),
|
|
119
|
+
};
|
|
120
|
+
})
|
|
121
|
+
.filter((entry) => entry.changed_pixels > 0)
|
|
122
|
+
.sort((left, right) => right.changed_pixels - left.changed_pixels)
|
|
123
|
+
.slice(0, 8);
|
|
124
|
+
|
|
125
|
+
const heatmapCanvas = document.createElement('canvas');
|
|
126
|
+
heatmapCanvas.width = width;
|
|
127
|
+
heatmapCanvas.height = height;
|
|
128
|
+
const heatmapContext = heatmapCanvas.getContext('2d');
|
|
129
|
+
heatmapContext.clearRect(0, 0, width, height);
|
|
130
|
+
heatmapContext.drawImage(candidateImage, 0, 0);
|
|
131
|
+
const maxChangedCount = Math.max(...blockChangeCounts, 0);
|
|
132
|
+
for (let row = 0; row < blockRowsValue; row += 1) {
|
|
133
|
+
for (let col = 0; col < blockColsValue; col += 1) {
|
|
134
|
+
const index = (row * blockColsValue) + col;
|
|
135
|
+
const changedCount = blockChangeCounts[index];
|
|
136
|
+
if (changedCount <= 0 || maxChangedCount <= 0) continue;
|
|
137
|
+
const intensity = changedCount / maxChangedCount;
|
|
138
|
+
const x = (col / blockColsValue) * width;
|
|
139
|
+
const y = (row / blockRowsValue) * height;
|
|
140
|
+
const blockWidth = width / blockColsValue;
|
|
141
|
+
const blockHeight = height / blockRowsValue;
|
|
142
|
+
heatmapContext.fillStyle = `rgba(255, 0, 0, ${Math.max(0.12, intensity * 0.65)})`;
|
|
143
|
+
heatmapContext.fillRect(x, y, blockWidth, blockHeight);
|
|
144
|
+
heatmapContext.strokeStyle = 'rgba(255,255,255,0.2)';
|
|
145
|
+
heatmapContext.lineWidth = 1;
|
|
146
|
+
heatmapContext.strokeRect(x, y, blockWidth, blockHeight);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const heatmapDataUrl = heatmapCanvas.toDataURL('image/png');
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
width,
|
|
153
|
+
height,
|
|
154
|
+
changed_pixels: changedPixels,
|
|
155
|
+
total_pixels: totalPixels,
|
|
156
|
+
changed_blocks: changedBlocks,
|
|
157
|
+
total_blocks: blockChanged.length,
|
|
158
|
+
pixel_diff_ratio: pixelDiffRatio,
|
|
159
|
+
layout_shift_score: layoutShiftScore,
|
|
160
|
+
structural_similarity: structuralSimilarity,
|
|
161
|
+
hotspots,
|
|
162
|
+
heatmap_png_base64: heatmapDataUrl.replace(/^data:image\/png;base64,/, ''),
|
|
163
|
+
};
|
|
164
|
+
}, {
|
|
165
|
+
referenceUrl: fileToDataUrl(referencePath),
|
|
166
|
+
candidateUrl: fileToDataUrl(candidatePath),
|
|
167
|
+
pixelThresholdValue: toNumber(pixelThreshold, 16),
|
|
168
|
+
blockRowsValue: toNumber(blockRows, 12),
|
|
169
|
+
blockColsValue: toNumber(blockCols, 12),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const result = {
|
|
173
|
+
...metrics,
|
|
174
|
+
heatmap_file: null,
|
|
175
|
+
};
|
|
176
|
+
if (heatmapOutputPath && typeof heatmapOutputPath === 'string') {
|
|
177
|
+
mkdirSync(dirname(heatmapOutputPath), { recursive: true });
|
|
178
|
+
writeFileSync(heatmapOutputPath, Buffer.from(metrics.heatmap_png_base64, 'base64'));
|
|
179
|
+
result.heatmap_file = heatmapOutputPath;
|
|
180
|
+
}
|
|
181
|
+
delete result.heatmap_png_base64;
|
|
182
|
+
return result;
|
|
183
|
+
} finally {
|
|
184
|
+
await browser.close();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { synthesizePhaseArtifact } from '../../common.js';
|
|
2
|
+
|
|
3
|
+
export async function visualLayoutCompetition(input = {}, context = {}) {
|
|
4
|
+
return synthesizePhaseArtifact(input, context, {
|
|
5
|
+
phase: 'layout_competition',
|
|
6
|
+
summary: 'Competing first-fold layout directions were compared and ranked.',
|
|
7
|
+
});
|
|
8
|
+
}
|