feat: complete WorkBuddy parity and Windows release workflow
This commit is contained in:
1 parent
143516b3a8
commit
d5512df549
434 files changed
+54977
-1854
No files matched your search
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".user-menu-popover",
|
||||
".user-menu-items",
|
||||
"[data-settings-tab=\"buddyApps\"]",
|
||||
".settings-navigation__item--active[data-settings-tab=\"buddyApps\"]",
|
||||
".settings-modal__panel"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "click",
|
||||
"step_id": "open-account-menu",
|
||||
"selector": ".user-menu-trigger--workbuddy",
|
||||
"index": 0,
|
||||
"frames": 0
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "account-menu-ready",
|
||||
"duration_ms": 500,
|
||||
"frames": 0
|
||||
},
|
||||
{
|
||||
"kind": "click",
|
||||
"step_id": "open-settings-window",
|
||||
"selector": ".user-menu-items .user-menu-item",
|
||||
"index": 3,
|
||||
"frames": 0
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "settings-window-ready",
|
||||
"duration_ms": 1200,
|
||||
"frames": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"kind": "click",
|
||||
"step_id": "open-library",
|
||||
"selector": ".conversation-list-tab-button:nth-child(5)",
|
||||
"frames": 0
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "library-stable",
|
||||
"duration_ms": 1000,
|
||||
"frames": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "open-type-filter", "selector": ".wb-home-type-btn-pc", "frames": 1 },
|
||||
{ "kind": "key", "step_id": "close-type-filter", "key": "Escape", "frames": 0 },
|
||||
{ "kind": "click", "step_id": "open-personal-add", "selector": ".workspace-tree-section-add-btn-pc", "index": 0, "frames": 1 },
|
||||
{ "kind": "key", "step_id": "close-personal-add", "key": "Escape", "frames": 0 },
|
||||
{ "kind": "click", "step_id": "open-library-search", "selector": ".knowledge-sidebar-search-btn-pc", "frames": 1 },
|
||||
{ "kind": "key", "step_id": "close-library-search", "key": "Escape", "frames": 0 },
|
||||
{ "kind": "click", "step_id": "open-local-artifacts", "selector": ".memory-nav-item-pc", "index": 1, "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "local-artifacts-stable", "duration_ms": 1000, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"track_selectors": [
|
||||
".conversation-list-tab-button",
|
||||
".library-page",
|
||||
".library-nav-item",
|
||||
".my-files-flat-row--file",
|
||||
".my-files-flat-col-actions"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "click",
|
||||
"step_id": "open-library",
|
||||
"selector": ".conversation-list-tab-button",
|
||||
"index": 4,
|
||||
"frames": 1
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "library-ready",
|
||||
"duration_ms": 450,
|
||||
"frames": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"media_selectors": [
|
||||
".wb-home-guide-card-agent-pc .wb-home-guide-card-illustration-pc",
|
||||
".wb-home-guide-card-html-pc .wb-home-guide-card-illustration-pc"
|
||||
],
|
||||
"actions": []
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "library-assets-open", "selector": ".conversation-list-tab-button:nth-child(5)", "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "library-assets-ready", "duration_ms": 1000, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "restore-current-task", "selector": ".conversation-agent-card", "index": 0, "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "restored-current-task", "duration_ms": 600, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"track_selectors": [
|
||||
".my-files-flat-col-actions",
|
||||
".my-files-list-action-wrapper",
|
||||
".my-files-list-action-btn",
|
||||
"[role=dialog]",
|
||||
"[role=menu]",
|
||||
".wb-popover",
|
||||
".wb-dropdown"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "click",
|
||||
"step_id": "open-local-share",
|
||||
"selector": ".my-files-flat-row--file .my-files-list-action-btn",
|
||||
"index": 0,
|
||||
"frames": 1
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "local-share-open-stable",
|
||||
"duration_ms": 350,
|
||||
"frames": 1
|
||||
},
|
||||
{
|
||||
"kind": "key",
|
||||
"step_id": "close-local-share",
|
||||
"key": "Escape",
|
||||
"frames": 0
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "local-share-close-stable",
|
||||
"duration_ms": 180,
|
||||
"frames": 1
|
||||
},
|
||||
{
|
||||
"kind": "click",
|
||||
"step_id": "open-local-more",
|
||||
"selector": ".my-files-flat-row--file .my-files-list-action-btn",
|
||||
"index": 1,
|
||||
"frames": 1
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "local-more-open-stable",
|
||||
"duration_ms": 350,
|
||||
"frames": 1
|
||||
},
|
||||
{
|
||||
"kind": "key",
|
||||
"step_id": "close-local-more",
|
||||
"key": "Escape",
|
||||
"frames": 0
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "local-more-close-stable",
|
||||
"duration_ms": 180,
|
||||
"frames": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".memory-settings-panel"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "key",
|
||||
"step_id": "close-memory-modal",
|
||||
"key": "Escape",
|
||||
"frames": 2,
|
||||
"interval_ms": 120
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "memory-modal-closed-settled",
|
||||
"duration_ms": 300,
|
||||
"frames": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": true,
|
||||
"track_selectors": [],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "capture-import-dialog-text",
|
||||
"duration_ms": 10,
|
||||
"frames": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".settings-navigation__item--active",
|
||||
".appearance-card--dark",
|
||||
".memory-settings-panel"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "click",
|
||||
"step_id": "open-appearance",
|
||||
"selector": "[data-settings-tab=\"appearance\"]",
|
||||
"frames": 1
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "appearance-ready",
|
||||
"duration_ms": 250,
|
||||
"frames": 0
|
||||
},
|
||||
{
|
||||
"kind": "click",
|
||||
"step_id": "select-dark-theme",
|
||||
"selector": ".appearance-card--dark",
|
||||
"frames": 1
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "dark-theme-ready",
|
||||
"duration_ms": 700,
|
||||
"frames": 1
|
||||
},
|
||||
{
|
||||
"kind": "click",
|
||||
"step_id": "return-to-memory-in-dark-theme",
|
||||
"selector": "[data-settings-tab=\"memory\"]",
|
||||
"frames": 1
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "memory-dark-stable",
|
||||
"duration_ms": 700,
|
||||
"frames": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".memory-settings-panel",
|
||||
".memory-settings-panel__manage-section"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "click",
|
||||
"step_id": "open-memory-import",
|
||||
"selector": ".memory-settings-panel__manage-section .wb-button",
|
||||
"index": 0,
|
||||
"frames": 2,
|
||||
"interval_ms": 120
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "memory-import-open-settled",
|
||||
"duration_ms": 450,
|
||||
"frames": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".user-menu-popover",
|
||||
".user-menu-items",
|
||||
".settings-modal__panel",
|
||||
".settings-navigation__item"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "open-account-menu", "selector": ".user-menu-trigger--workbuddy", "index": 0, "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "account-menu-ready", "duration_ms": 500, "frames": 0 },
|
||||
{ "kind": "click", "step_id": "open-settings-from-confirmed-menu-index-3", "selector": ".user-menu-items .user-menu-item", "index": 3, "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "settings-window-ready", "duration_ms": 800, "frames": 0 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".settings-navigation__item--active",
|
||||
".appearance-card--light",
|
||||
".memory-settings-panel"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "click",
|
||||
"step_id": "open-appearance-from-memory",
|
||||
"selector": "[data-settings-tab=\"appearance\"]",
|
||||
"frames": 1
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "appearance-ready-from-dark",
|
||||
"duration_ms": 250,
|
||||
"frames": 0
|
||||
},
|
||||
{
|
||||
"kind": "click",
|
||||
"step_id": "restore-light-theme",
|
||||
"selector": ".appearance-card--light",
|
||||
"frames": 1
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "light-theme-ready",
|
||||
"duration_ms": 700,
|
||||
"frames": 1
|
||||
},
|
||||
{
|
||||
"kind": "click",
|
||||
"step_id": "return-to-memory-in-light-theme",
|
||||
"selector": "[data-settings-tab=\"memory\"]",
|
||||
"frames": 1
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "memory-light-stable",
|
||||
"duration_ms": 700,
|
||||
"frames": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".memory-settings-panel",
|
||||
".memory-settings-panel__generate-memory-group",
|
||||
".memory-settings-panel__manage-section"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "click",
|
||||
"step_id": "conversation-memory-off",
|
||||
"selector": ".memory-settings-panel .wb-switch",
|
||||
"index": 0,
|
||||
"frames": 2,
|
||||
"interval_ms": 120,
|
||||
"restore": true
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "conversation-memory-off-settled",
|
||||
"duration_ms": 350,
|
||||
"frames": 1
|
||||
},
|
||||
{
|
||||
"kind": "click",
|
||||
"step_id": "local-memory-skills-off",
|
||||
"selector": ".memory-settings-panel .wb-switch",
|
||||
"index": 1,
|
||||
"frames": 2,
|
||||
"interval_ms": 120,
|
||||
"restore": true
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "local-memory-skills-off-settled",
|
||||
"duration_ms": 350,
|
||||
"frames": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"kind": "click",
|
||||
"step_id": "open-discover-apps",
|
||||
"selector": ".industry-template-switcher__trigger",
|
||||
"frames": 0
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "discover-apps-ready",
|
||||
"duration_ms": 500,
|
||||
"frames": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"track_selectors": [
|
||||
".industry-template-switcher__trigger"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "key",
|
||||
"step_id": "close-discover-apps",
|
||||
"key": "Escape",
|
||||
"modifiers": 0,
|
||||
"frames": 0
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "home-ready",
|
||||
"duration_ms": 500,
|
||||
"frames": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"track_selectors": [
|
||||
".conversation-list-tab-button-more"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "click",
|
||||
"step_id": "open-more",
|
||||
"selector": ".conversation-list-tab-button-more",
|
||||
"frames": 0
|
||||
},
|
||||
{
|
||||
"kind": "wait",
|
||||
"step_id": "more-ready",
|
||||
"duration_ms": 500,
|
||||
"frames": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
|
||||
const routeEvidencePath = resolve('.tmp/rea-todo20-scheduled-create.json')
|
||||
const outputPath = resolve('src/renderer/src/automations/automation-source-assets.ts')
|
||||
const checkOnly = process.argv.includes('--check')
|
||||
|
||||
const expected = {
|
||||
automation: {
|
||||
sha256: '687f7b062b0df0f353dafd1f17b00cb939685da5a12c8d3ce865b920b94d6710',
|
||||
bytes: 131_537
|
||||
},
|
||||
src: {
|
||||
sha256: '747e41d215375e1214c366213aea7e077d85421923029c61cbb6e9562be58de0',
|
||||
bytes: 1_062_959
|
||||
}
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
throw new Error(`automation UI recovery: ${message}`)
|
||||
}
|
||||
|
||||
function sha256(value) {
|
||||
return createHash('sha256').update(value).digest('hex')
|
||||
}
|
||||
|
||||
function assertArtifact(label, artifact, sourceText) {
|
||||
const expectedArtifact = expected[label]
|
||||
if (!artifact || !sourceText) fail(`${label} source artifact is missing`)
|
||||
if (artifact.bytes !== expectedArtifact.bytes) {
|
||||
fail(`${label} byte count changed: ${artifact.bytes} != ${expectedArtifact.bytes}`)
|
||||
}
|
||||
if (artifact.sha256 !== expectedArtifact.sha256 || sha256(sourceText) !== expectedArtifact.sha256) {
|
||||
fail(`${label} SHA-256 does not match the pinned REA evidence`)
|
||||
}
|
||||
}
|
||||
|
||||
function findScript(evidence, suffix) {
|
||||
return evidence.normalized_result?.scripts?.items?.find((item) =>
|
||||
item.file_path?.endsWith(suffix)
|
||||
)
|
||||
}
|
||||
|
||||
function extractDataUri(source, variable) {
|
||||
const escaped = variable.replaceAll('$', '\\$')
|
||||
const match = new RegExp(`(?<![\\w$])${escaped}\\s*=\\s*\\x60(data:image/svg\\+xml,[^\\x60]+)\\x60`).exec(source)
|
||||
if (!match) fail(`could not recover ${variable} SVG data URI`)
|
||||
return match[1]
|
||||
}
|
||||
|
||||
function parseJsxPath(attributesSource) {
|
||||
const attributes = Object.fromEntries(
|
||||
[...attributesSource.matchAll(/([A-Za-z][\w-]*)\s*:\s*`([^`]*)`/g)]
|
||||
.map((match) => [match[1], match[2]])
|
||||
)
|
||||
if (!attributes.d) fail('a recovered path is missing its d attribute')
|
||||
return attributes
|
||||
}
|
||||
|
||||
function attributeName(name) {
|
||||
return name
|
||||
.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)
|
||||
}
|
||||
|
||||
function escapeAttribute(value) {
|
||||
return value
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
}
|
||||
|
||||
function pathsToSvg(paths, width, height, viewBox) {
|
||||
const markup = paths.map((attributes) => {
|
||||
const serialized = Object.entries(attributes)
|
||||
.filter(([name]) => name !== 'd')
|
||||
.map(([name, value]) => `${attributeName(name)}="${escapeAttribute(value)}"`)
|
||||
.join(' ')
|
||||
return `<path ${serialized} d="${escapeAttribute(attributes.d)}"/>`
|
||||
}).join('')
|
||||
return `data:image/svg+xml,${encodeURIComponent(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${viewBox}" fill="black">${markup}</svg>`
|
||||
)}`
|
||||
}
|
||||
|
||||
function extractPaths(source, startMarker, endMarker, jsxFactory) {
|
||||
const start = source.indexOf(startMarker)
|
||||
const end = source.indexOf(endMarker, start + startMarker.length)
|
||||
if (start < 0 || end < 0) fail(`could not locate ${startMarker}`)
|
||||
const block = source.slice(start, end)
|
||||
const pattern = new RegExp(`\\(0,${jsxFactory}\\.jsx\\)\\(\\x60path\\x60,\\s*\\{([^}]*)\\}\\)`, 'g')
|
||||
const paths = [...block.matchAll(pattern)].map((match) => parseJsxPath(match[1]))
|
||||
if (!paths.length) fail(`no SVG paths recovered from ${startMarker}`)
|
||||
return paths
|
||||
}
|
||||
|
||||
const evidence = JSON.parse(await readFile(routeEvidencePath, 'utf8'))
|
||||
const srcItem = findScript(evidence, 'src-DPDj7_qD.js')
|
||||
const automationItem = findScript(evidence, 'automation-qDy4JNlv.js')
|
||||
|
||||
assertArtifact('src', srcItem?.source?.artifact, srcItem?.source?.artifact?.text)
|
||||
assertArtifact(
|
||||
'automation',
|
||||
automationItem?.source?.artifact,
|
||||
automationItem?.source?.artifact?.text
|
||||
)
|
||||
|
||||
const src = srcItem.source.artifact.text
|
||||
const automation = automationItem.source.artifact.text
|
||||
|
||||
const templateIcons = {
|
||||
news: extractDataUri(src, 'xb'),
|
||||
languages: extractDataUri(src, 'Yy'),
|
||||
moon: extractDataUri(src, '_b'),
|
||||
'weekly-report': extractDataUri(src, 'Tb'),
|
||||
film: extractDataUri(src, 'Ly'),
|
||||
calendar: extractDataUri(src, 'Ny'),
|
||||
lightbulb: extractDataUri(src, '$y'),
|
||||
image: extractDataUri(src, 'Gy'),
|
||||
hospital: extractDataUri(src, 'Vy'),
|
||||
'messages-square': extractDataUri(src, 'sb'),
|
||||
'list-todo': extractDataUri(src, 'rb')
|
||||
}
|
||||
|
||||
const clockPaths = extractPaths(
|
||||
src,
|
||||
'Gc=(0,Uc.forwardRef)',
|
||||
'Gc.displayName',
|
||||
'Wc'
|
||||
)
|
||||
templateIcons['alarm-clock'] = pathsToSvg(clockPaths, 16, 16, '0 0 16 16')
|
||||
|
||||
const emptyIllustrationPaths = extractPaths(
|
||||
automation,
|
||||
'function TaskEmptyIllustration',
|
||||
'var Ta,Ea',
|
||||
'Ta'
|
||||
)
|
||||
const emptyIllustration = pathsToSvg(
|
||||
emptyIllustrationPaths,
|
||||
64,
|
||||
64,
|
||||
'0 0 64 64'
|
||||
)
|
||||
|
||||
const output = `/**
|
||||
* Generated by scripts/rea/recover-automation-ui.mjs.
|
||||
*
|
||||
* Source: fixed WorkBuddy 5.8.3 REA evidence
|
||||
* ${evidence.evidence_id}
|
||||
*
|
||||
* Do not edit by hand. Re-run the recovery script to verify or refresh the assets.
|
||||
*/
|
||||
export const AUTOMATION_EVIDENCE = {
|
||||
id: ${JSON.stringify(evidence.evidence_id)},
|
||||
automationSha256: ${JSON.stringify(expected.automation.sha256)},
|
||||
srcSha256: ${JSON.stringify(expected.src.sha256)}
|
||||
} as const
|
||||
|
||||
export const AUTOMATION_TEMPLATE_ICON_URLS = ${JSON.stringify(templateIcons, null, 2)} as const
|
||||
|
||||
export type AutomationTemplateIconKey = keyof typeof AUTOMATION_TEMPLATE_ICON_URLS
|
||||
|
||||
export const AUTOMATION_EMPTY_ILLUSTRATION_URL = ${JSON.stringify(emptyIllustration)}
|
||||
`
|
||||
|
||||
if (checkOnly) {
|
||||
const current = await readFile(outputPath, 'utf8').catch(() => '')
|
||||
if (current !== output) fail('generated asset file is stale')
|
||||
console.log(`PASS automation UI assets ${expected.automation.sha256}`)
|
||||
} else {
|
||||
await mkdir(dirname(outputPath), { recursive: true })
|
||||
await writeFile(outputPath, output, 'utf8')
|
||||
console.log(`Wrote ${outputPath}`)
|
||||
}
|
||||
@@ -528,7 +528,7 @@ manifest.adaptations.push('Three conversation-shell rules are recovered alongsid
|
||||
// --------------------------------------------------------------------------- types
|
||||
const types=`/** Typed host contract for the recovered WorkBuddy 5.8.2 model selector. */
|
||||
import type {ComponentType, ReactNode} from 'react'
|
||||
export type ModelEffort='low'|'medium'|'high'|'xhigh'|'max'
|
||||
export type ModelEffort='minimal'|'low'|'medium'|'high'|'xhigh'|'max'
|
||||
export type ModelBusyLevel='idle'|'normal'|'busy'|'congested'|'unavailable'|'unknown'
|
||||
export interface ModelBadgeSpec{label:string;color?:string;background?:string;icon?:string}
|
||||
export interface ModelTierBadgeSpec{label:string;level?:'flagship'|'member';color?:string;background?:string}
|
||||
@@ -644,7 +644,8 @@ manifest.hostBindings=[
|
||||
{kind:'npm',name:'dompurify',names:['default']},
|
||||
{kind:'npm',name:'react',names:['* as React','* as JSX (react/jsx-runtime)']},
|
||||
]
|
||||
const outputs=[[FOLDER+'source.mjs',moduleSource],[FOLDER+'source.d.mts',types],[FOLDER+'source.css',css]]
|
||||
const hostSource=moduleSource.replace('var xW={low:', '// Host extension: models.dev exposes a minimal effort supported by the local kernel.\nvar xW={minimal:`极低`,low:')
|
||||
const outputs=[[FOLDER+'source.mjs',hostSource],[FOLDER+'source.d.mts',types],[FOLDER+'source.css',css]]
|
||||
manifest.outputs=outputs.map(([file,text])=>({file,sha256:sha(text),bytes:Buffer.byteLength(text)}))
|
||||
outputs.push([FOLDER+'manifest.json',JSON.stringify(manifest,null,2)+'\n'])
|
||||
const check=process.argv.includes('--check')
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const crypto = require('node:crypto')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..', '..')
|
||||
const tmpDir = path.join(repoRoot, '.tmp')
|
||||
|
||||
const evidence = {
|
||||
home: {
|
||||
path: path.join(tmpDir, 'rea-four-modules-library-stable-20261017.json'),
|
||||
id: 'ev_82a9ad8ec38c807e465646d749e2cedbb150fec62e30234c97acd0bb309c349e'
|
||||
},
|
||||
local: {
|
||||
path: path.join(tmpDir, 'rea-four-modules-library-local-20261017.json'),
|
||||
id: 'ev_b7fe1b4adac2311b04dfa454126363ca382869d6a29664152d497a920c2e8cad'
|
||||
}
|
||||
}
|
||||
|
||||
const mediaEvidence = {
|
||||
path: path.join(
|
||||
tmpDir,
|
||||
'rea-css-evidence',
|
||||
'ev_d650991b41fe1831a4caf5dd225398e29db8e91205d00c2ac3047b00baa1998e.json'
|
||||
),
|
||||
id: 'ev_d650991b41fe1831a4caf5dd225398e29db8e91205d00c2ac3047b00baa1998e'
|
||||
}
|
||||
|
||||
const illustrations = [
|
||||
{
|
||||
component: 'RobotGuideIllustration',
|
||||
importName: 'agentGuideIllustration',
|
||||
assetFile: 'wb-home-guide-card-agent-illustration.svg',
|
||||
selector: '.wb-home-guide-card-agent-pc .wb-home-guide-card-illustration-pc',
|
||||
sourceUrl:
|
||||
'https://static.workbuddy.cn/web/library/5609e40689e0ddd5b7e8ede0a4e43acd7b1ba74d/js/federation/releases/e7fafd8eaa71ac3f/guide-card-agent-illustration.1abe0c58.svg',
|
||||
bytes: 2857,
|
||||
sha256: '1b0984754f470c4529b09d2fa56a957f23df3bd7fc16cc27d74989b1112ff815'
|
||||
},
|
||||
{
|
||||
component: 'HtmlGuideIllustration',
|
||||
importName: 'htmlGuideIllustration',
|
||||
assetFile: 'wb-home-guide-card-html-illustration.svg',
|
||||
selector: '.wb-home-guide-card-html-pc .wb-home-guide-card-illustration-pc',
|
||||
sourceUrl:
|
||||
'https://static.workbuddy.cn/web/library/5609e40689e0ddd5b7e8ede0a4e43acd7b1ba74d/js/federation/releases/e7fafd8eaa71ac3f/guide-card-html-illustration.0c0d6493.svg',
|
||||
bytes: 1413,
|
||||
sha256: '1af18b1901999725af4114997bba4c779c6d04f09712c44d34add010433d1e47'
|
||||
}
|
||||
]
|
||||
|
||||
const icons = [
|
||||
{
|
||||
name: 'ClockIcon',
|
||||
evidence: 'home',
|
||||
rootIndex: 872,
|
||||
parentClass: 'memory-nav-item-icon-pc'
|
||||
},
|
||||
{
|
||||
name: 'FolderLineIcon',
|
||||
evidence: 'home',
|
||||
rootIndex: 878,
|
||||
parentClass: 'memory-nav-item-icon-pc'
|
||||
},
|
||||
{
|
||||
name: 'MoreIcon',
|
||||
evidence: 'home',
|
||||
rootIndex: 1066,
|
||||
parentClass: 'wb-feed-more-btn-pc'
|
||||
},
|
||||
{
|
||||
name: 'InfoIcon',
|
||||
evidence: 'home',
|
||||
rootIndex: 1290,
|
||||
parentClass: 'wb-home-guide-title-pc'
|
||||
},
|
||||
{
|
||||
name: 'LightbulbIcon',
|
||||
evidence: 'home',
|
||||
rootIndex: 1323,
|
||||
parentClass: 'wb-home-guide-playbook-title-pc'
|
||||
},
|
||||
{
|
||||
name: 'BackIcon',
|
||||
evidence: 'local',
|
||||
rootIndex: 848,
|
||||
parentClass: 'kb-back-to-library__btn'
|
||||
},
|
||||
{
|
||||
name: 'SortArrowsIcon',
|
||||
evidence: 'local',
|
||||
rootIndex: 878,
|
||||
parentClass: 'my-files-flat-col-name my-files-flat-sortable',
|
||||
variant: 'sort'
|
||||
},
|
||||
{
|
||||
name: 'ShareIcon',
|
||||
evidence: 'local',
|
||||
rootIndex: 954,
|
||||
parentClass: 'my-files-list-action-btn'
|
||||
},
|
||||
{
|
||||
name: 'TableMoreIcon',
|
||||
evidence: 'local',
|
||||
rootIndex: 959,
|
||||
parentClass: 'my-files-list-action-btn'
|
||||
},
|
||||
{
|
||||
name: 'TaskIcon',
|
||||
evidence: 'local',
|
||||
rootIndex: 967,
|
||||
parentClass: 'my-files-flat-col-name'
|
||||
}
|
||||
]
|
||||
|
||||
const notes = {
|
||||
star_icon: {
|
||||
status: 'unresolved',
|
||||
reason:
|
||||
'No star/favorite SVG exists in the captured library UI. WorkBuddy uses a CSS checkbox for the favorite filter and renders share plus more actions in file rows.',
|
||||
evidence_ids: Object.values(evidence).map((item) => item.id),
|
||||
searched_classes: [
|
||||
'my-files-favorite-checkbox',
|
||||
'my-files-favorite-checkbox__label',
|
||||
'my-files-list-action-btn'
|
||||
]
|
||||
},
|
||||
rejected_reusable_home_media: {
|
||||
status: 'not_applicable',
|
||||
reason:
|
||||
'A previously archived image body exists, but it belongs to the older .wb-home-route__growth-buddy img selector and is not evidence for either current guide-card illustration.',
|
||||
evidence_id: 'ev_474129797ce3ab73eaaab2cd41247653e7f128c3b89b598158b57b74e1f17e88',
|
||||
selector: '.wb-home-route__growth-buddy img',
|
||||
source_url:
|
||||
'https://staging-download.codebuddy.cn/operation-platform/assets/_general/ffca4ecbc52c1e3e.png',
|
||||
artifact_sha256: '2713ed5bbf1288d56ebeaca49bfb0d622c967914ab9c98955cc995932049e7e8',
|
||||
bytes: 13426
|
||||
}
|
||||
}
|
||||
|
||||
const svgAttributeNames = new Map([
|
||||
['class', 'className'],
|
||||
['clip-path', 'clipPath'],
|
||||
['clip-rule', 'clipRule'],
|
||||
['fill-rule', 'fillRule'],
|
||||
['stroke-dasharray', 'strokeDasharray'],
|
||||
['stroke-dashoffset', 'strokeDashoffset'],
|
||||
['stroke-linecap', 'strokeLinecap'],
|
||||
['stroke-linejoin', 'strokeLinejoin'],
|
||||
['stroke-miterlimit', 'strokeMiterlimit'],
|
||||
['stroke-width', 'strokeWidth'],
|
||||
['transform-origin', 'transformOrigin']
|
||||
])
|
||||
|
||||
const svgTagNames = new Map([
|
||||
['clippath', 'clipPath'],
|
||||
['lineargradient', 'linearGradient'],
|
||||
['radialgradient', 'radialGradient']
|
||||
])
|
||||
|
||||
function sha256(value) {
|
||||
return crypto.createHash('sha256').update(value).digest('hex')
|
||||
}
|
||||
|
||||
function readEvidence(spec) {
|
||||
const value = JSON.parse(fs.readFileSync(spec.path, 'utf8').replace(/^\uFEFF/, ''))
|
||||
assert.equal(value.evidence_id, spec.id)
|
||||
assert.equal(value.operation, 'inspect_electron_page')
|
||||
return value
|
||||
}
|
||||
|
||||
function jsxAttributeName(name) {
|
||||
return svgAttributeNames.get(name) ?? name
|
||||
}
|
||||
|
||||
function jsxAttributes(attributes) {
|
||||
return Object.entries(attributes)
|
||||
.map(([name, value]) => `${jsxAttributeName(name)}=${JSON.stringify(value)}`)
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
function recoverIcon(document, spec) {
|
||||
const layoutNodes = new Map(document.nodes.map((node) => [node.node_index, node]))
|
||||
const svgByIndex = new Map(document.svg_nodes.map((node) => [node.node_index, node]))
|
||||
const childrenByParent = new Map()
|
||||
for (const node of document.svg_nodes) {
|
||||
const children = childrenByParent.get(node.parent_index) ?? []
|
||||
children.push(node)
|
||||
childrenByParent.set(node.parent_index, children)
|
||||
}
|
||||
|
||||
const root = svgByIndex.get(spec.rootIndex)
|
||||
assert.ok(root, `missing SVG root ${spec.rootIndex} for ${spec.name}`)
|
||||
assert.equal(root.node_name, 'svg')
|
||||
|
||||
const parent = layoutNodes.get(root.parent_index)
|
||||
assert.ok(parent, `missing SVG parent for ${spec.name}`)
|
||||
assert.equal(parent.attributes?.class, spec.parentClass)
|
||||
|
||||
const subtree = []
|
||||
const visit = (node) => {
|
||||
subtree.push({
|
||||
node_index: node.node_index,
|
||||
parent_index: node.parent_index,
|
||||
node_name: node.node_name,
|
||||
attributes: node.attributes
|
||||
})
|
||||
for (const child of childrenByParent.get(node.node_index) ?? []) visit(child)
|
||||
}
|
||||
visit(root)
|
||||
|
||||
return {
|
||||
root,
|
||||
parent,
|
||||
childrenByParent,
|
||||
hash: sha256(JSON.stringify(subtree))
|
||||
}
|
||||
}
|
||||
|
||||
function renderNode(recovery, node, isRoot = false, spec = null) {
|
||||
const tag = svgTagNames.get(node.node_name.toLowerCase()) ?? node.node_name
|
||||
const attributes = { ...node.attributes }
|
||||
|
||||
let prefix = ''
|
||||
if (isRoot) {
|
||||
delete attributes.width
|
||||
delete attributes.height
|
||||
if (spec?.variant === 'sort') {
|
||||
delete attributes.class
|
||||
prefix = `{...svgProps} width={width ?? ${JSON.stringify(recovery.root.attributes.width)}} height={height ?? ${JSON.stringify(recovery.root.attributes.height)}} className={mergeClassNames("my-files-flat-sort-icon", direction, className)} `
|
||||
} else if ('class' in attributes) {
|
||||
const sourceClass = attributes.class
|
||||
delete attributes.class
|
||||
prefix = `{...svgProps} width={width ?? ${JSON.stringify(recovery.root.attributes.width)}} height={height ?? ${JSON.stringify(recovery.root.attributes.height)}} className={mergeClassNames(${JSON.stringify(sourceClass)}, className)} `
|
||||
} else {
|
||||
prefix = `{...svgProps} width={width ?? ${JSON.stringify(recovery.root.attributes.width)}} height={height ?? ${JSON.stringify(recovery.root.attributes.height)}} `
|
||||
}
|
||||
}
|
||||
|
||||
const serialized = jsxAttributes(attributes)
|
||||
const opener = `<${tag} ${prefix}${serialized}`
|
||||
const children = recovery.childrenByParent.get(node.node_index) ?? []
|
||||
if (!children.length) return `${opener} />`
|
||||
return `${opener}>${children.map((child) => renderNode(recovery, child)).join('')}</${tag}>`
|
||||
}
|
||||
|
||||
function functionSource(name, recovery, evidenceId, version, spec) {
|
||||
const provenance = [
|
||||
`WorkBuddy ${version}`,
|
||||
`REA ${evidenceId}`,
|
||||
`parent .${spec.parentClass.replaceAll(' ', '.')}`,
|
||||
`root ${spec.rootIndex}`,
|
||||
`subtree sha256 ${recovery.hash}`
|
||||
].join('; ')
|
||||
|
||||
if (spec.variant === 'sort') {
|
||||
return `// Exact SVG recovery: ${provenance}\nexport function ${name}({ direction, className, width, height, ...svgProps }: SortIconProps): React.JSX.Element {\n return ${renderNode(recovery, recovery.root, true, spec)}\n}`
|
||||
}
|
||||
|
||||
return `// Exact SVG recovery: ${provenance}\nexport function ${name}(props: IconProps): React.JSX.Element {\n const { className, width, height, ...svgProps } = props\n return ${renderNode(recovery, recovery.root, true, spec)}\n}`
|
||||
}
|
||||
|
||||
function mergeClassNames(...values) {
|
||||
return values.filter(Boolean).join(' ') || undefined
|
||||
}
|
||||
|
||||
function readMediaEvidence(spec) {
|
||||
const value = JSON.parse(fs.readFileSync(spec.path, 'utf8').replace(/^\uFEFF/, ''))
|
||||
assert.equal(value.evidence_id, spec.id)
|
||||
const items = value.normalized_result?.visuals?.media_assets?.items
|
||||
assert.ok(Array.isArray(items))
|
||||
return items
|
||||
}
|
||||
|
||||
function recoverIllustration(item, spec) {
|
||||
assert.equal(item.selector, spec.selector)
|
||||
assert.equal(item.status, 'exported')
|
||||
assert.equal(item.source?.url, spec.sourceUrl)
|
||||
assert.equal(item.source?.mime_type, 'image/svg+xml')
|
||||
assert.equal(item.artifact?.mime_type, 'image/svg+xml')
|
||||
assert.equal(item.artifact?.bytes, spec.bytes)
|
||||
assert.equal(item.artifact?.sha256, spec.sha256)
|
||||
|
||||
const bytes = Buffer.from(item.artifact.data_base64, 'base64')
|
||||
assert.equal(bytes.length, spec.bytes)
|
||||
assert.equal(sha256(bytes), spec.sha256)
|
||||
const svg = bytes.toString('utf8')
|
||||
assert.match(svg.trimStart(), /^<svg\b/)
|
||||
|
||||
return {
|
||||
bytes,
|
||||
record: {
|
||||
component: spec.component,
|
||||
asset_file: `src/renderer/src/library/assets/${spec.assetFile}`,
|
||||
evidence_id: mediaEvidence.id,
|
||||
evidence_file: path.relative(repoRoot, mediaEvidence.path).replaceAll('\\', '/'),
|
||||
selector: item.selector,
|
||||
node_index: item.node_index,
|
||||
node_class: item.selected_node?.attributes?.class,
|
||||
source_url: item.source.url,
|
||||
source_url_sha256: item.source.url_sha256,
|
||||
status: item.status,
|
||||
visibility: item.visibility,
|
||||
mime_type: item.artifact.mime_type,
|
||||
bytes: item.artifact.bytes,
|
||||
sha256: item.artifact.sha256
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function illustrationSource(records) {
|
||||
const imports = records
|
||||
.map(({ spec }) => `import ${spec.importName} from './assets/${spec.assetFile}'`)
|
||||
.join('\n')
|
||||
const components = records.map(({ record, spec }) => {
|
||||
const provenance = [
|
||||
`REA ${mediaEvidence.id}`,
|
||||
`selector ${spec.selector}`,
|
||||
`source ${spec.sourceUrl}`,
|
||||
`${spec.bytes} bytes`,
|
||||
`sha256 ${spec.sha256}`
|
||||
].join('; ')
|
||||
return `// Exact WorkBuddy asset recovery: ${provenance}\nexport function ${spec.component}(): React.JSX.Element {\n return (\n <img\n className="wb-home-guide-card-illustration-pc library-guide__illustration"\n src={${spec.importName}}\n alt=""\n />\n )\n}`
|
||||
}).join('\n\n')
|
||||
|
||||
return `// Generated by scripts/rea/recover-library-icons.cjs. Do not hand edit.\n${imports}\n\n${components}\n`
|
||||
}
|
||||
|
||||
function main() {
|
||||
const loaded = Object.fromEntries(
|
||||
Object.entries(evidence).map(([name, spec]) => [name, readEvidence(spec)])
|
||||
)
|
||||
const iconRecords = []
|
||||
const iconSources = []
|
||||
|
||||
for (const spec of icons) {
|
||||
const value = loaded[spec.evidence]
|
||||
const document = value.normalized_result.visuals.layout.documents[0]
|
||||
const recovery = recoverIcon(document, spec)
|
||||
const version = value.normalized_result.browser.user_agent.match(/WorkBuddy\/([^\s]+)/)?.[1] ?? 'unknown'
|
||||
iconSources.push(functionSource(spec.name, recovery, value.evidence_id, version, spec))
|
||||
iconRecords.push({
|
||||
name: spec.name,
|
||||
evidence_id: value.evidence_id,
|
||||
evidence_file: path.relative(repoRoot, evidence[spec.evidence].path).replaceAll('\\', '/'),
|
||||
root_node_index: spec.rootIndex,
|
||||
parent_class: recovery.parent.attributes.class,
|
||||
source_sha256: recovery.hash,
|
||||
svg_attributes: recovery.root.attributes
|
||||
})
|
||||
}
|
||||
|
||||
const iconOutput = `// Generated by scripts/rea/recover-library-icons.cjs. Do not hand edit.\nimport type { SVGProps } from 'react'\n\ntype IconProps = SVGProps<SVGSVGElement>\ntype SortIconProps = IconProps & { direction?: 'asc' | 'desc' }\n\nfunction mergeClassNames(...values: Array<string | undefined>): string | undefined {\n return values.filter(Boolean).join(' ') || undefined\n}\n\n${iconSources.join('\n\n')}\n`
|
||||
const iconDestination = path.join(repoRoot, 'src', 'renderer', 'src', 'library', 'SourceLibraryIcons.tsx')
|
||||
const mediaItems = readMediaEvidence(mediaEvidence)
|
||||
const illustrationRecoveries = illustrations.map((spec) => {
|
||||
const item = mediaItems.find((candidate) => candidate.selector === spec.selector)
|
||||
assert.ok(item, `missing media item ${spec.selector}`)
|
||||
return { spec, ...recoverIllustration(item, spec) }
|
||||
})
|
||||
const illustrationOutput = illustrationSource(illustrationRecoveries)
|
||||
const illustrationDestination = path.join(
|
||||
repoRoot,
|
||||
'src',
|
||||
'renderer',
|
||||
'src',
|
||||
'library',
|
||||
'SourceLibraryIllustrations.tsx'
|
||||
)
|
||||
const assetDirectory = path.join(repoRoot, 'src', 'renderer', 'src', 'library', 'assets')
|
||||
const manifestPath = path.join(tmpDir, 'library-icon-recovery-manifest.json')
|
||||
const manifest = {
|
||||
icon_source_file: path.relative(repoRoot, iconDestination).replaceAll('\\', '/'),
|
||||
icon_file_sha256: sha256(iconOutput),
|
||||
illustration_source_file: path.relative(repoRoot, illustrationDestination).replaceAll('\\', '/'),
|
||||
illustration_file_sha256: sha256(illustrationOutput),
|
||||
icons: iconRecords,
|
||||
illustrations: illustrationRecoveries.map(({ record }) => record),
|
||||
notes
|
||||
}
|
||||
|
||||
if (process.argv.includes('--check')) {
|
||||
assert.equal(fs.readFileSync(iconDestination, 'utf8'), iconOutput)
|
||||
assert.equal(fs.readFileSync(illustrationDestination, 'utf8'), illustrationOutput)
|
||||
for (const { bytes, record } of illustrationRecoveries) {
|
||||
assert.deepEqual(fs.readFileSync(path.join(repoRoot, record.asset_file)), bytes)
|
||||
}
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(manifestPath, 'utf8')), manifest)
|
||||
} else {
|
||||
fs.mkdirSync(assetDirectory, { recursive: true })
|
||||
fs.writeFileSync(iconDestination, iconOutput)
|
||||
fs.writeFileSync(illustrationDestination, illustrationOutput)
|
||||
for (const { bytes, spec } of illustrationRecoveries) {
|
||||
fs.writeFileSync(path.join(assetDirectory, spec.assetFile), bytes)
|
||||
}
|
||||
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
||||
}
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
status: 'PASS',
|
||||
recovered_icons: iconRecords.length,
|
||||
recovered_illustrations: illustrationRecoveries.length,
|
||||
unresolved: Object.entries(notes)
|
||||
.filter(([, note]) => note.status === 'unresolved')
|
||||
.map(([name]) => name),
|
||||
icon_file: iconDestination,
|
||||
icon_file_sha256: manifest.icon_file_sha256,
|
||||
illustration_file: illustrationDestination,
|
||||
illustration_file_sha256: manifest.illustration_file_sha256,
|
||||
manifest: manifestPath
|
||||
}, null, 2)}\n`)
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,205 @@
|
||||
import { promises as fs } from 'node:fs'
|
||||
import { basename, dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = resolve(scriptDir, '..', '..')
|
||||
const tmpDir = join(repoRoot, '.tmp')
|
||||
|
||||
const evidenceFiles = {
|
||||
home: join(tmpDir, 'rea-four-modules-library-stable-20261017.json'),
|
||||
local: join(tmpDir, 'rea-four-modules-library-local-20261017.json')
|
||||
}
|
||||
|
||||
const prefixes = [
|
||||
'knowledge-sidebar',
|
||||
'main-layout-content',
|
||||
'workspace-tree',
|
||||
'wb-home',
|
||||
'doc-feed',
|
||||
'wb-feed',
|
||||
'my-files'
|
||||
]
|
||||
|
||||
const styleKeys = [
|
||||
'display',
|
||||
'position',
|
||||
'font-family',
|
||||
'font-size',
|
||||
'font-weight',
|
||||
'line-height',
|
||||
'color',
|
||||
'background-color',
|
||||
'border-radius',
|
||||
'border-top-width',
|
||||
'border-top-color',
|
||||
'padding-top',
|
||||
'padding-right',
|
||||
'padding-bottom',
|
||||
'padding-left',
|
||||
'margin-top',
|
||||
'margin-right',
|
||||
'margin-bottom',
|
||||
'margin-left',
|
||||
'gap',
|
||||
'align-items',
|
||||
'justify-content',
|
||||
'box-shadow',
|
||||
'overflow',
|
||||
'max-width',
|
||||
'min-height',
|
||||
'transition-property',
|
||||
'transition-duration',
|
||||
'transition-timing-function',
|
||||
'opacity',
|
||||
'transform'
|
||||
]
|
||||
|
||||
function stripComments(source) {
|
||||
return source.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
}
|
||||
|
||||
function cssRules(source) {
|
||||
const css = stripComments(source)
|
||||
const stack = []
|
||||
const rules = []
|
||||
let boundary = 0
|
||||
let quote = ''
|
||||
let escaped = false
|
||||
|
||||
for (let index = 0; index < css.length; index += 1) {
|
||||
const char = css[index]
|
||||
if (quote) {
|
||||
if (escaped) escaped = false
|
||||
else if (char === '\\') escaped = true
|
||||
else if (char === quote) quote = ''
|
||||
continue
|
||||
}
|
||||
if (char === '"' || char === "'") {
|
||||
quote = char
|
||||
continue
|
||||
}
|
||||
if (char === '{') {
|
||||
const prelude = css.slice(boundary, index).trim()
|
||||
stack.push({ prelude, bodyStart: index + 1 })
|
||||
boundary = index + 1
|
||||
continue
|
||||
}
|
||||
if (char === '}') {
|
||||
const entry = stack.pop()
|
||||
if (entry) {
|
||||
const body = css.slice(entry.bodyStart, index).trim()
|
||||
if (body && !entry.prelude.startsWith('@')) {
|
||||
rules.push({
|
||||
context: stack.map((item) => item.prelude).filter((item) => item.startsWith('@')),
|
||||
selector: entry.prelude,
|
||||
body
|
||||
})
|
||||
}
|
||||
}
|
||||
boundary = index + 1
|
||||
continue
|
||||
}
|
||||
if (char === ';' && stack.length === 0) boundary = index + 1
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
function matchesPrefix(selector) {
|
||||
return prefixes.some((prefix) => selector.includes(prefix))
|
||||
}
|
||||
|
||||
function relevantNode(node) {
|
||||
const bounds = node.bounds ?? []
|
||||
if (bounds.length < 4 || bounds[2] <= 0 || bounds[3] <= 0) return false
|
||||
if (!node.attributes || !Object.keys(node.attributes).length) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function layoutNode(node) {
|
||||
const styles = node.styles ?? {}
|
||||
return {
|
||||
nodeIndex: node.node_index,
|
||||
parentIndex: node.parent_index,
|
||||
tag: node.node_name,
|
||||
attributes: node.attributes,
|
||||
bounds: node.bounds,
|
||||
styles: Object.fromEntries(styleKeys.map((key) => [key, styles[key]]))
|
||||
}
|
||||
}
|
||||
|
||||
async function readEvidence(path) {
|
||||
return JSON.parse(await fs.readFile(path, 'utf8'))
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const evidence = {}
|
||||
for (const [name, path] of Object.entries(evidenceFiles)) {
|
||||
evidence[name] = await readEvidence(path)
|
||||
}
|
||||
|
||||
const seenRules = new Set()
|
||||
const recovered = []
|
||||
const styleSources = new Map()
|
||||
|
||||
for (const [name, entry] of Object.entries(evidence)) {
|
||||
for (const sheet of entry.normalized_result.styles.items ?? []) {
|
||||
const text = sheet.source?.artifact?.text
|
||||
if (!text || !prefixes.some((prefix) => text.includes(prefix))) continue
|
||||
const sheetName = basename(sheet.file_path ?? 'inline.css')
|
||||
styleSources.set(sheetName, {
|
||||
path: sheet.file_path,
|
||||
bytes: sheet.source.artifact.bytes,
|
||||
sha256: sheet.source.artifact.sha256
|
||||
})
|
||||
for (const rule of cssRules(text)) {
|
||||
if (!matchesPrefix(rule.selector)) continue
|
||||
const key = `${rule.context.join('>')}|${rule.selector}|${rule.body}`
|
||||
if (seenRules.has(key)) continue
|
||||
seenRules.add(key)
|
||||
recovered.push({
|
||||
evidence: name,
|
||||
sheet: sheetName,
|
||||
...rule
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const css = recovered
|
||||
.map((rule) => {
|
||||
const prefix = rule.context.length ? `${rule.context.join(' { ')} { ` : ''
|
||||
const suffix = rule.context.length ? ` ${'}'.repeat(rule.context.length)}` : ''
|
||||
return `/* ${rule.evidence}:${rule.sheet} */\n${prefix}${rule.selector} { ${rule.body} }${suffix}`
|
||||
})
|
||||
.join('\n\n')
|
||||
|
||||
const layout = {}
|
||||
for (const [name, entry] of Object.entries(evidence)) {
|
||||
layout[name] = (entry.normalized_result.visuals.layout.documents ?? [])
|
||||
.flatMap((document) => document.nodes ?? [])
|
||||
.filter(relevantNode)
|
||||
.map(layoutNode)
|
||||
}
|
||||
|
||||
await fs.mkdir(tmpDir, { recursive: true })
|
||||
await Promise.all([
|
||||
fs.writeFile(join(tmpDir, 'library-recovered-rules.css'), `${css}\n`, 'utf8'),
|
||||
fs.writeFile(join(tmpDir, 'library-layout-summary.json'), JSON.stringify({
|
||||
styleSources: Object.fromEntries(styleSources),
|
||||
layout
|
||||
}, null, 2), 'utf8')
|
||||
])
|
||||
|
||||
process.stdout.write(JSON.stringify({
|
||||
rules: recovered.length,
|
||||
css: join(tmpDir, 'library-recovered-rules.css'),
|
||||
layout: join(tmpDir, 'library-layout-summary.json'),
|
||||
styleSources
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
// Project the WorkBuddy 5.8.3 MemorySettingsPanel CSS recovered through REA.
|
||||
// This script only reads an archived REA Evidence record; it never contacts the app.
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
const evidenceId = 'ev_ccbabc0d8507e997eba15aab8b5effa66f7e82d6720c6131bfcd62157b9d6896'
|
||||
const evidencePath = path.resolve(
|
||||
process.env.YCODE_MEMORY_EVIDENCE ??
|
||||
`.tmp/rea-css-evidence/${evidenceId}.json`
|
||||
)
|
||||
const outputPath = path.resolve(
|
||||
'src/renderer/src/components/source-memory-settings-recovered.css'
|
||||
)
|
||||
const expectedSourceSha256 = '43e0b0207ff1a91ad6066efc772dad351b2b5e7afe588d38a5ee38537eb01aad'
|
||||
const expectedSourceBytes = 25213
|
||||
|
||||
const evidence = JSON.parse(await fs.readFile(evidencePath, 'utf8'))
|
||||
assert.equal(evidence.evidence_id, evidenceId, 'REA Evidence identity changed')
|
||||
assert.equal(evidence.operation, 'inspect_electron_page')
|
||||
assert.equal(evidence.normalized_result?.styles?.total, 52, 'unexpected WorkBuddy CSS source count')
|
||||
assert.equal(evidence.normalized_result?.styles?.truncated, false, 'CSS capture was truncated')
|
||||
assert.equal(evidence.normalized_result?.styles?.source_failures ?? 0, 0, 'CSS source failure')
|
||||
|
||||
const matches = evidence.normalized_result.styles.items.filter(item =>
|
||||
/MemorySettingsPanel-[^/\\]+\.css$/i.test(item.file_path ?? '')
|
||||
)
|
||||
assert.equal(matches.length, 1, 'expected exactly one MemorySettingsPanel CSS source')
|
||||
|
||||
const source = matches[0].source?.artifact
|
||||
assert.equal(source?.sha256, expectedSourceSha256, 'MemorySettingsPanel CSS digest changed')
|
||||
assert.equal(source?.bytes, expectedSourceBytes, 'MemorySettingsPanel CSS byte length changed')
|
||||
assert.equal(typeof source?.text, 'string', 'MemorySettingsPanel CSS text is unavailable')
|
||||
|
||||
const header = [
|
||||
'/*',
|
||||
` * REA Evidence ${evidenceId}`,
|
||||
` * Source ${path.basename(matches[0].file_path)}`,
|
||||
` * SHA-256 ${expectedSourceSha256}`,
|
||||
' * The declarations below are the unmodified recovered module source.',
|
||||
' */',
|
||||
''
|
||||
].join('\n')
|
||||
const output = `${header}${source.text.trim()}\n`
|
||||
const outputSha256 = createHash('sha256').update(output).digest('hex')
|
||||
|
||||
if (process.argv.includes('--check')) {
|
||||
const current = await fs.readFile(outputPath, 'utf8')
|
||||
assert.equal(current, output, 'recovered MemorySettingsPanel CSS is stale')
|
||||
} else {
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true })
|
||||
await fs.writeFile(outputPath, output)
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
evidenceId,
|
||||
sourceFile: path.basename(matches[0].file_path),
|
||||
sourceSha256: expectedSourceSha256,
|
||||
sourceBytes: expectedSourceBytes,
|
||||
output: path.relative(process.cwd(), outputPath).replaceAll('\\', '/'),
|
||||
outputBytes: Buffer.byteLength(output),
|
||||
outputSha256
|
||||
}, null, 2))
|
||||
@@ -0,0 +1,56 @@
|
||||
// Read retained REA exports only. Never accesses the installed WorkBuddy or CDP.
|
||||
const fs = require('node:fs')
|
||||
const crypto = require('node:crypto')
|
||||
const { parse } = require('@babel/parser')
|
||||
const traverse = require('@babel/traverse').default
|
||||
const postcss = require('postcss')
|
||||
const assert = require('node:assert/strict')
|
||||
const sha = text => crypto.createHash('sha256').update(text).digest('hex')
|
||||
const archive = JSON.parse(fs.readFileSync('.tmp/rea-runtime-scripts.json', 'utf8'))
|
||||
const source = archive.items.find(item => item.file_path.endsWith('src-DbE1_mgM.js')).source.artifact
|
||||
assert.equal(sha(source.text), '6c6ce63c93854783270e855572797d75e536e3b8d4548410e31fe9d147e1feb4')
|
||||
const css = fs.readFileSync('.tmp/rea-css-recovered/src-CyXHA0IC.css', 'utf8')
|
||||
assert.equal(sha(css), 'a5e26b537bb50323aefbf25abddad52e288dd3868598be8d255d70886fe77cfe')
|
||||
const primitiveCss = fs.readFileSync('.tmp/rea-css-recovered/src-Bh3YZ35H.css', 'utf8')
|
||||
assert.equal(sha(primitiveCss), '88dbdc1cd3f59d0e8b75824512a465a2be714bc7af481d790fe3a9484f85e01a')
|
||||
const ast = parse(source.text, { sourceType: 'module' })
|
||||
const wanted = new Set(['ShieldSecuredIcon', 'ShieldDangerIcon', 'ShieldErrorIcon'])
|
||||
const bodies = new Map()
|
||||
traverse(ast, {
|
||||
FunctionDeclaration(p) { if (wanted.has(p.node.id?.name)) bodies.set(p.node.id.name, p.node) },
|
||||
AssignmentExpression(p) { if (wanted.has(p.node.left.name)) bodies.set(p.node.left.name, p.node.right) },
|
||||
VariableDeclarator(p) { if (wanted.has(p.node.id.name) && p.node.init) bodies.set(p.node.id.name, p.node.init) }
|
||||
})
|
||||
assert.ok(bodies.has('ShieldSecuredIcon') && bodies.has('ShieldDangerIcon'))
|
||||
let icons = '// Generated from fixed REA ' + archive.evidence_id + '.\n// @ts-nocheck -- the original JSX runtime bodies are retained.\nimport { jsx, jsxs } from "react/jsx-runtime"\n'
|
||||
for (const [name, node] of bodies) {
|
||||
let body = source.text.slice(node.start, node.end).replace(/\(0,[\w$]+\.(jsx|jsxs)\)/g, '$1')
|
||||
assert.ok(!/\(0,[\w$]+\.(jsx|jsxs)\)/.test(body), `${name}: unresolved JSX runtime alias`)
|
||||
icons += node.type === 'FunctionDeclaration' ? `export ${body}\n` : `export const ${name} = ${body}\n`
|
||||
}
|
||||
const rules = []
|
||||
postcss.parse(css).walkRules(rule => {
|
||||
if (/\.cr-permission-(panel|setting-popover|confirm)/.test(rule.selector)) rules.push(rule.toString())
|
||||
})
|
||||
postcss.parse(css).walkAtRules('keyframes', rule => {
|
||||
if (rule.params === 'permissionPopIn') rules.push(rule.toString())
|
||||
})
|
||||
assert.ok(rules.length >= 20)
|
||||
// The existing workspace adapter only recovered the button variants it used.
|
||||
// Bring in the confirmation's actual danger/secondary states, including disabled.
|
||||
postcss.parse(primitiveCss).walkRules(rule => {
|
||||
if (!/\.wb-button(?:\b|--|__)/.test(rule.selector)) return
|
||||
const copy = rule.clone()
|
||||
copy.selectors = copy.selectors.map(selector => `.yc-source-permission ${selector}`)
|
||||
rules.push(copy.toString())
|
||||
})
|
||||
const outputs = [
|
||||
['src/renderer/src/workbuddy/composer/permission/SourcePermissionIcons.tsx', icons],
|
||||
['src/renderer/src/workbuddy/composer/permission/source-permission-recovered.css',
|
||||
`/* REA ${archive.evidence_id}; CSS ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94.\n * Source SHA-256 ${sha(css)}. Exact permission panel/confirmation declarations. */\n${rules.join('\n')}\n`]
|
||||
]
|
||||
for (const [filename, text] of outputs) {
|
||||
if (process.argv.includes('--check')) assert.equal(fs.readFileSync(filename, 'utf8'), text, filename)
|
||||
else { fs.mkdirSync(require('node:path').dirname(filename), { recursive: true }); fs.writeFileSync(filename, text) }
|
||||
}
|
||||
console.log(`PERMISSION_SOURCE_PASS ${bodies.size} icons, ${rules.length} rules`)
|
||||
@@ -0,0 +1,52 @@
|
||||
const fs = require('node:fs')
|
||||
const assert = require('node:assert/strict')
|
||||
const crypto = require('node:crypto')
|
||||
const postcss = require('postcss')
|
||||
const evidence = JSON.parse(fs.readFileSync('.tmp/rea-todo20-new-project-dialog.json', 'utf8').replace(/^\uFEFF/, ''))
|
||||
assert.equal(evidence.evidence_id, 'ev_bc0f5546bb714cad4634168969a115be8f34e307bc09f51caf1457f5d3ed61ec')
|
||||
const output = postcss.root()
|
||||
function sheet(name) {
|
||||
const item = evidence.normalized_result.styles.items.find(item => item.file_path.endsWith(name))
|
||||
assert.ok(item?.source.included)
|
||||
const text = item.source.artifact.text
|
||||
assert.equal(crypto.createHash('sha256').update(text).digest('hex'), item.source.artifact.sha256)
|
||||
return postcss.parse(text)
|
||||
}
|
||||
sheet('tokens-Df-i-zsO.css').walkRules(rule => {
|
||||
if (!rule.selector.includes(':root') && !rule.selector.includes('[data-theme=dark]')) return
|
||||
if (!rule.nodes.some(node => node.type === 'decl' && node.prop.startsWith('--'))) return
|
||||
const dark = rule.selector.includes('dark')
|
||||
const target = postcss.rule({ selector: dark ? '[data-theme=dark] .yc-project-surface' : '.yc-project-surface' })
|
||||
for (const node of rule.nodes) if (node.type === 'decl' && node.prop.startsWith('--')) target.append(node.clone())
|
||||
output.append(target)
|
||||
})
|
||||
assert.ok(output.nodes.length >= 2)
|
||||
function select(container) {
|
||||
const result = container.clone({ nodes: [] })
|
||||
for (const node of container.nodes || []) {
|
||||
if (node.type === 'rule' && /\.wb-(?:button|input|textarea)(?:[\s.:#[\]>+~,-]|__|--|$)/.test(node.selector)) {
|
||||
const copy = node.clone()
|
||||
copy.selectors = copy.selectors.map(selector => `:where(.yc-project-surface) ${selector}`)
|
||||
result.append(copy)
|
||||
} else if (node.type === 'atrule' && node.nodes && !/keyframes$/.test(node.name)) {
|
||||
const nested = select(node)
|
||||
if (nested.nodes.length) result.append(nested)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
const controls = sheet('src-Bh3YZ35H.css')
|
||||
controls.walkRules(rule => {
|
||||
if (!rule.selector.includes(':root') && !rule.selector.includes('[data-theme=dark]')) return
|
||||
const declarations = rule.nodes.filter(node => node.type === 'decl' && node.prop.startsWith('--'))
|
||||
if (!declarations.length) return
|
||||
const target = postcss.rule({ selector: rule.selector.includes('dark') ? '[data-theme=dark] .yc-project-surface' : '.yc-project-surface' })
|
||||
declarations.forEach(node => target.append(node.clone()))
|
||||
output.append(target)
|
||||
})
|
||||
output.append(select(controls).nodes)
|
||||
const text = `/* WorkBuddy 5.8.3 shared controls/tokens; REA ${evidence.evidence_id}. */\n${output.toString()}\n`
|
||||
const file = 'src/renderer/src/projects/source-project-controls.css'
|
||||
if (process.argv.includes('--check')) assert.equal(fs.readFileSync(file, 'utf8'), text)
|
||||
else fs.writeFileSync(file, text)
|
||||
console.log(`PASS project controls/tokens: ${output.nodes.length} top-level blocks, hashes verified`)
|
||||
@@ -0,0 +1,20 @@
|
||||
// Recover the observed project-detail selectors from REA retained evidence.
|
||||
const fs=require('node:fs'),path=require('node:path'),crypto=require('node:crypto'),postcss=require('postcss'),assert=require('node:assert/strict')
|
||||
const evidence=JSON.parse(fs.readFileSync('.tmp/rea-todo20-project-tasks-full.json','utf8').replace(/^\uFEFF/,''))
|
||||
assert.equal(evidence.evidence_id,'ev_8782905c492acd1618e137480a3da8a8d7eba56e8f328e4856b008e1dd7f34ea')
|
||||
const source=evidence.normalized_result.styles.items.find(item=>item.file_path.endsWith('project-detail-page-aED_k-Ym.css'))
|
||||
assert.ok(source?.source.included)
|
||||
const css=source.source.artifact.text
|
||||
const sha=crypto.createHash('sha256').update(css).digest('hex')
|
||||
assert.equal(sha,source.source.artifact.sha256)
|
||||
const root=postcss.parse(css),out=postcss.root();let count=0
|
||||
function select(container){const result=container.clone({nodes:[]});for(const node of container.nodes||[]){
|
||||
if(node.type==='rule'&&/(?:project-detail-view|project-task-panel|project-instruction-drawer|context-panel|wb-config-card|task-item|wb-list-item|wb-status-dot|wb-avatar|collaborator-avatar-stack)/.test(node.selector)){
|
||||
const rule=node.clone();rule.selectors=rule.selectors.map(selector=>`:where(.yc-project-surface) ${selector}`);result.append(rule);count++
|
||||
}else if(node.type==='atrule'&&node.nodes&&!/keyframes$/i.test(node.name)){const nested=select(node);if(nested.nodes.length)result.append(nested)}
|
||||
}return result}
|
||||
out.append(select(root).nodes)
|
||||
const text=`/* WorkBuddy 5.8.3 project detail; REA ${evidence.evidence_id}; source SHA-256 ${sha}. */\n${out.toString()}\n`
|
||||
const destination=path.resolve('src/renderer/src/projects/source-project-detail-recovered.css')
|
||||
if(process.argv.includes('--check'))assert.equal(fs.readFileSync(destination,'utf8'),text);else fs.writeFileSync(destination,text)
|
||||
console.log(`PASS project-detail CSS: ${count} scoped rules; SHA verified`)
|
||||
@@ -0,0 +1,22 @@
|
||||
// Export only the image already retained by REA. No installation-directory access.
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const assert = require('node:assert/strict')
|
||||
const crypto = require('node:crypto')
|
||||
const evidence = JSON.parse(fs.readFileSync('.tmp/rea-todo20-project-hero.json', 'utf8').replace(/^\uFEFF/, ''))
|
||||
assert.equal(evidence.evidence_id, 'ev_d740e73903dd69444105e2509b238e5912f2c93d3adaa183b05ec7221512ef7a')
|
||||
const item = evidence.normalized_result.visuals.media_assets.items.find(item => item.selector === '.landing-hero')
|
||||
assert.ok(item?.artifact)
|
||||
assert.equal(item.artifact.mime_type, 'image/png')
|
||||
assert.ok(item.source.url.endsWith('/landing-hero-B6659kdy.png'))
|
||||
const bytes = Buffer.from(item.artifact.data_base64, 'base64')
|
||||
assert.equal(bytes.length, item.artifact.bytes)
|
||||
assert.equal(crypto.createHash('sha256').update(bytes).digest('hex'), item.artifact.sha256)
|
||||
assert.equal(bytes.subarray(0, 8).toString('hex'), '89504e470d0a1a0a')
|
||||
const destination = path.resolve('src/renderer/src/projects/assets/landing-hero.png')
|
||||
if (process.argv.includes('--check')) assert.deepEqual(fs.readFileSync(destination), bytes)
|
||||
else {
|
||||
fs.mkdirSync(path.dirname(destination), { recursive: true })
|
||||
fs.writeFileSync(destination, bytes)
|
||||
}
|
||||
console.log(`PASS project hero: ${bytes.length} bytes; SHA-256 ${item.artifact.sha256}`)
|
||||
@@ -0,0 +1,24 @@
|
||||
const fs = require('node:fs')
|
||||
const assert = require('node:assert/strict')
|
||||
const evidence = JSON.parse(fs.readFileSync('.tmp/rea-todo20-project-page.json', 'utf8').replace(/^\uFEFF/, ''))
|
||||
assert.equal(evidence.evidence_id, 'ev_89843a8b67f411d3297b5f3e428830393cd0fe8712603816cf8b7bbe442064d9')
|
||||
const document = evidence.normalized_result.visuals.layout.documents[0]
|
||||
const parent = document.nodes.find(node => node.attributes.class === 'landing-template-icon')
|
||||
const svg = document.svg_nodes.find(node => node.parent_index === parent.node_index)
|
||||
assert.equal(svg.node_name, 'svg')
|
||||
const names = { 'clip-path': 'clipPath', 'fill-rule': 'fillRule', 'class': 'className' }
|
||||
function jsx(node) {
|
||||
assert.ok(['svg', 'clipPath', 'path', 'g'].includes(node.node_name))
|
||||
const attrs = Object.entries(node.attributes).map(([key, value]) => {
|
||||
if (key === 'id') return 'id={clipId}'
|
||||
if (key === 'clip-path') return 'clipPath={`url(#${clipId})`}'
|
||||
return `${names[key] || key}=${JSON.stringify(value)}`
|
||||
}).join(' ')
|
||||
const children = document.svg_nodes.filter(child => child.parent_index === node.node_index).map(jsx).join('')
|
||||
return `<${node.node_name} ${attrs}>${children}</${node.node_name}>`
|
||||
}
|
||||
const output = `// WorkBuddy 5.8.3, REA ${evidence.evidence_id}; literal SVG recovery.\nimport { useId } from 'react'\nexport default function ProjectIcon(): React.JSX.Element {\nconst clipId = useId()\nreturn ${jsx(svg)}\n}\n`
|
||||
const destination = 'src/renderer/src/projects/ProjectIcon.tsx'
|
||||
if (process.argv.includes('--check')) assert.equal(fs.readFileSync(destination, 'utf8'), output)
|
||||
else fs.writeFileSync(destination, output)
|
||||
console.log('PASS project SVG recovery, per-instance clip ID')
|
||||
@@ -0,0 +1,60 @@
|
||||
// Consume retained REA evidence only; never reads the WorkBuddy installation.
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const crypto = require('node:crypto')
|
||||
const postcss = require('postcss')
|
||||
const assert = require('node:assert/strict')
|
||||
const evidence = JSON.parse(fs.readFileSync(path.resolve('.tmp/rea-todo20-new-project-dialog.json'), 'utf8').replace(/^\uFEFF/, ''))
|
||||
assert.equal(evidence.evidence_id, 'ev_bc0f5546bb714cad4634168969a115be8f34e307bc09f51caf1457f5d3ed61ec')
|
||||
const sheet = evidence.normalized_result.styles.items.find(item => item.file_path.endsWith('collab-YMJYRnW_.css'))
|
||||
assert.ok(sheet?.source.included)
|
||||
const source = sheet.source.artifact.text
|
||||
const sha = crypto.createHash('sha256').update(source).digest('hex')
|
||||
assert.equal(sha, '4737f6a8f8f993de933e5ebae0d94b96cb39788e160470490f607508537e5377')
|
||||
const root = postcss.parse(source)
|
||||
// The shared modal lives outside collab's chunk; recover its rules from the same evidence.
|
||||
let modalRules = 0
|
||||
for (const item of evidence.normalized_result.styles.items) {
|
||||
if (item === sheet || !item.source?.included || !item.source.artifact?.text) continue
|
||||
const text = item.source.artifact.text
|
||||
if (!text.includes('.collab-modal__') && !text.includes('.workbuddy-topbar')) continue
|
||||
assert.equal(crypto.createHash('sha256').update(text).digest('hex'), item.source.artifact.sha256)
|
||||
const parsed = postcss.parse(text)
|
||||
function select(container) {
|
||||
const result = container.clone({ nodes: [] })
|
||||
for (const node of container.nodes || []) {
|
||||
if (node.type === 'rule' && /\.collab-modal__|\.workbuddy-topbar(?:[\s.:#[\]>+~,-]|-|$)/.test(node.selector)) {
|
||||
result.append(node.clone()); modalRules++
|
||||
} else if (node.type === 'atrule' && node.nodes && !/keyframes$/i.test(node.name)) {
|
||||
const nested = select(node)
|
||||
if (nested.nodes.length) result.append(nested)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
root.append(select(parsed).nodes)
|
||||
}
|
||||
let count = 0
|
||||
root.walkRules(rule => {
|
||||
// Preserve keyframe selectors and all enclosing media/supports conditions.
|
||||
for (let parent = rule.parent; parent; parent = parent.parent) {
|
||||
if (parent.type === 'atrule' && /keyframes$/i.test(parent.name)) return
|
||||
}
|
||||
rule.selectors = rule.selectors.map(selector => {
|
||||
// Body/theme conditions are ancestors of the surface, never its descendants.
|
||||
const ancestor = selector.match(/^(body(?:\[[^\]]+\]|\.[\w-]+|:[\w-]+(?:\([^)]*\))?)*|html(?:\[[^\]]+\]|\.[\w-]+)*|:root)\s+/)
|
||||
return ancestor
|
||||
? `${ancestor[1]} :where(.yc-project-surface) ${selector.slice(ancestor[0].length)}`
|
||||
: `:where(.yc-project-surface) ${selector}`
|
||||
})
|
||||
count++
|
||||
})
|
||||
const output = `/* Generated from WorkBuddy 5.8.3; REA ${evidence.evidence_id}; source SHA-256 ${sha}. */\n${root.toString()}\n`
|
||||
const destination = path.resolve('src/renderer/src/projects/source-projects-recovered.css')
|
||||
if (process.argv.includes('--check')) {
|
||||
assert.equal(fs.readFileSync(destination, 'utf8'), output)
|
||||
} else {
|
||||
fs.mkdirSync(path.dirname(destination), { recursive: true })
|
||||
fs.writeFileSync(destination, output)
|
||||
}
|
||||
console.log(`PASS project styles: ${count} scoped rules (${modalRules} shared modal); source SHA verified${process.argv.includes('--check') ? '; generated file unchanged' : ''}`)
|
||||
@@ -0,0 +1,66 @@
|
||||
// Static AST decoding of retained REA sources; no recovered code is executed.
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const assert = require('node:assert/strict')
|
||||
const crypto = require('node:crypto')
|
||||
const parser = require('@babel/parser')
|
||||
const ts = {
|
||||
isStringLiteral: n => n?.type === 'StringLiteral',
|
||||
isNoSubstitutionTemplateLiteral: n => n?.type === 'TemplateLiteral' && n.expressions.length === 0,
|
||||
isArrayLiteralExpression: n => n?.type === 'ArrayExpression',
|
||||
isObjectLiteralExpression: n => n?.type === 'ObjectExpression',
|
||||
isPropertyAssignment: n => n?.type === 'ObjectProperty',
|
||||
isBinaryExpression: n => n?.type === 'AssignmentExpression',
|
||||
isIdentifier: n => n?.type === 'Identifier',
|
||||
forEachChild: (node, callback) => { for (const value of Object.values(node)) { if (Array.isArray(value)) value.forEach(item => { if (item?.type) callback(item) }); else if (value?.type) callback(value) } }
|
||||
}
|
||||
const literal = node => node.type === 'TemplateLiteral' ? node.quasis[0].value.cooked : node.value
|
||||
const evidence = JSON.parse(fs.readFileSync('.tmp/rea-todo20-project-page.json', 'utf8').replace(/^\uFEFF/, ''))
|
||||
assert.equal(evidence.evidence_id, 'ev_89843a8b67f411d3297b5f3e428830393cd0fe8712603816cf8b7bbe442064d9')
|
||||
function source(name) {
|
||||
const item = evidence.normalized_result.scripts.items.find(item => item.file_path.endsWith(name))
|
||||
assert.ok(item?.source.included)
|
||||
const text = item.source.artifact.text
|
||||
assert.equal(crypto.createHash('sha256').update(text).digest('hex'), item.source.artifact.sha256)
|
||||
return parser.parse(text, { sourceType: 'module' })
|
||||
}
|
||||
function decode(node) {
|
||||
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return literal(node)
|
||||
if (ts.isArrayLiteralExpression(node)) return node.elements.map(decode)
|
||||
if (ts.isObjectLiteralExpression(node)) {
|
||||
const result = {}
|
||||
for (const property of node.properties) {
|
||||
assert.ok(ts.isPropertyAssignment(property))
|
||||
const key = property.key.name ?? property.key.value
|
||||
if (key !== 'icon') result[key] = decode(property.value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
throw Error(`Nonliteral template node: ${node.type}`)
|
||||
}
|
||||
const definitions = new Map()
|
||||
function visit(node) {
|
||||
if (ts.isBinaryExpression(node) && node.operator === '=' && ts.isIdentifier(node.left) && ['vt', 'gt', 'St', 'bt', 'mt'].includes(node.left.name) && ts.isObjectLiteralExpression(node.right)) definitions.set(node.left.name, decode(node.right))
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
visit(source('project-list-page-DpHRTtk8.js'))
|
||||
assert.equal(definitions.size, 5)
|
||||
const translations = new Map()
|
||||
function strings(node) {
|
||||
if (ts.isPropertyAssignment(node) && ts.isStringLiteral(node.key) && node.key.value.startsWith('collab.list.template.') && (ts.isStringLiteral(node.value) || ts.isNoSubstitutionTemplateLiteral(node.value))) translations.set(node.key.value, literal(node.value))
|
||||
ts.forEachChild(node, strings)
|
||||
}
|
||||
strings(source('zh-cn-C7BTHfrQ.js'))
|
||||
const templates = ['vt', 'gt', 'St', 'bt', 'mt'].map(key => {
|
||||
const item = definitions.get(key)
|
||||
for (const field of ['title', 'desc', 'prompt']) {
|
||||
assert.ok(translations.has(item[field]), `Missing translation: ${item[field]}`)
|
||||
item[field] = translations.get(item[field])
|
||||
}
|
||||
return item
|
||||
})
|
||||
const output = JSON.stringify(templates, null, 2) + '\n'
|
||||
const destination = path.resolve('src/renderer/src/projects/project-templates.json')
|
||||
if (process.argv.includes('--check')) assert.equal(fs.readFileSync(destination, 'utf8'), output)
|
||||
else fs.writeFileSync(destination, output)
|
||||
console.log(`PASS project templates: ${templates.length} source configurations and Chinese instructions recovered`)
|
||||
@@ -0,0 +1,49 @@
|
||||
// Consume pinned, retained REA Evidence only. No access to the installed WorkBuddy or target protocols.
|
||||
import fs from 'node:fs/promises'
|
||||
import crypto from 'node:crypto'
|
||||
import { parse } from '@babel/parser'
|
||||
import postcss from 'postcss'
|
||||
const evidence = JSON.parse(await fs.readFile('.tmp/rea-runtime-scripts.json', 'utf8'))
|
||||
if (evidence.evidence_id !== 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb') throw Error('REA Evidence changed')
|
||||
const item = evidence.items.find(item => item.file_path?.endsWith('src-DbE1_mgM.js'))
|
||||
const source = item?.source?.artifact?.text
|
||||
if (!source || crypto.createHash('sha256').update(source).digest('hex') !== '6c6ce63c93854783270e855572797d75e536e3b8d4548410e31fe9d147e1feb4') throw Error('REA source digest changed')
|
||||
const names = ['MessageQuoteTableIcon', 'FileArchiveIcon', 'FileArdotIcon', 'FileAudioIcon', 'FileCodeIcon', 'FileDiagramIcon', 'FileDocIcon', 'FileFolderIcon', 'FileHtmlIcon', 'FileImageIcon', 'FileMarkdownIcon', 'FilePdfIcon', 'FileSheetIcon', 'FileSlideIcon', 'FileTxtIcon', 'FileUnknownIcon', 'FileVideoIcon', 'Close12Icon', 'ErrorIcon$1']
|
||||
const functions = new Map(), literals = new Map()
|
||||
function visit(node) {
|
||||
if (!node || typeof node !== 'object') return
|
||||
if (node.type === 'FunctionDeclaration' && names.includes(node.id?.name)) functions.set(node.id.name, source.slice(node.start, node.end))
|
||||
if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier') {
|
||||
if (names.includes(node.left.name) && node.right.type === 'ArrowFunctionExpression') functions.set(node.left.name, `const ${node.left.name} = ${source.slice(node.right.start, node.right.end)}`)
|
||||
if (['TemplateLiteral', 'StringLiteral'].includes(node.right.type) && !node.right.expressions?.length) literals.set(node.left.name, source.slice(node.right.start, node.right.end))
|
||||
}
|
||||
for (const value of Object.values(node)) if (Array.isArray(value)) value.forEach(visit); else if (value && typeof value === 'object') visit(value)
|
||||
}
|
||||
visit(parse(source, { sourceType: 'module' }))
|
||||
const constants = new Map()
|
||||
const declarations = names.map(name => {
|
||||
const definition = functions.get(name)
|
||||
if (!definition) throw Error('Missing icon ' + name)
|
||||
for (const match of definition.matchAll(/\bd:([A-Za-z_$][\w$]*)[,}]/g)) {
|
||||
const literal = literals.get(match[1])
|
||||
if (!literal) throw Error('Missing inert path constant ' + match[1])
|
||||
constants.set(match[1], literal)
|
||||
}
|
||||
return 'export ' + definition.replace(/\(0,[\w$]+\.(jsx|jsxs)\)/g, ' $1').replace(/ErrorIcon\$1/g, 'ReferenceErrorIcon')
|
||||
})
|
||||
const directory = 'src/renderer/src/workbuddy/references'
|
||||
await fs.mkdir(directory, { recursive: true })
|
||||
await fs.writeFile(`${directory}/SourceReferenceIcons.tsx`, `// @ts-nocheck\n// Inert SVG geometry recovered from REA ${evidence.evidence_id}, src-DbE1_mgM.js.\nimport { jsx, jsxs } from 'react/jsx-runtime'\n${[...constants].map(([key, value]) => `const ${key} = ${value}`).join('\n')}\n${declarations.join('\n')}\n`)
|
||||
const css = await fs.readFile('.tmp/rea-css-recovered/src-CyXHA0IC.css', 'utf8')
|
||||
if (crypto.createHash('sha256').update(css).digest('hex') !== 'a5e26b537bb50323aefbf25abddad52e288dd3868598be8d255d70886fe77cfe') throw Error('REA CSS digest changed')
|
||||
const selected = []
|
||||
postcss.parse(css).walkRules(rule => {
|
||||
const selectors = rule.selectors.filter(selector => /1krka/.test(selector) && !/sceneTag|urlTag|base64UrlImg/.test(selector) || /^\.cr-input-editor-host \[data-slate-editor\]/.test(selector))
|
||||
if (!selectors.length) return
|
||||
const node = rule.clone(); node.selectors = selectors
|
||||
let tree = node
|
||||
for (let parent = rule.parent; parent?.type === 'atrule'; parent = parent.parent) { const outer = parent.clone({ nodes: [] }); outer.append(tree); tree = outer }
|
||||
selected.push(tree.toString())
|
||||
})
|
||||
await fs.writeFile(`${directory}/source-reference-tags.css`, `/* REA ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94; src-CyXHA0IC.css sha256 a5e26b537bb50323aefbf25abddad52e288dd3868598be8d255d70886fe77cfe. Exact declarations with parent conditions retained. */\n${selected.join('\n')}\n`)
|
||||
console.log(JSON.stringify({ icons: names.length, rules: selected.length, sourceEvidence: evidence.evidence_id }))
|
||||
@@ -1,9 +1,12 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { parse } from '@babel/parser'
|
||||
import traverseModule from '@babel/traverse'
|
||||
import postcss from 'postcss'
|
||||
const traverse=traverseModule.default??traverseModule, evidence=JSON.parse(fs.readFileSync('.tmp/rea-runtime-scripts.json','utf8')),cache=new Map()
|
||||
const traverse=traverseModule.default??traverseModule, evidence=JSON.parse(fs.readFileSync('.tmp/rea-runtime-scripts.json','utf8')),cache=new Map(),checkOnly=process.argv.includes('--check')
|
||||
const HELP_FEEDBACK_EVIDENCE={id:'ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e',fileSha256:'87511827e6cf8f016c9c073fb654ffcc13520b0770dc2ef997564efc1ffe6ba7',pathLength:534,pathSha256:'d84f185b0d52e3b7eb61c96efc21a34887c213746b12132ddd3595d33e332287'}
|
||||
const hash=(value)=>createHash('sha256').update(value).digest('hex')
|
||||
function source(base){return evidence.items.find((item)=>item.file_path.endsWith(base))?.source.artifact.text}
|
||||
function ast(base){if(!cache.has(base))cache.set(base,parse(source(base),{sourceType:'module'}));return cache.get(base)}
|
||||
const context='contexts-BMeWSUHk.js',staticValue=(node)=>!node?undefined:node.type==='TemplateLiteral'&&!node.expressions.length?node.quasis[0].value.cooked:node.type==='StringLiteral'||node.type==='NumericLiteral'?node.value:undefined
|
||||
@@ -30,9 +33,28 @@ function resolve(base,name,depth=0){
|
||||
return next?resolve(base,next,depth+1):null
|
||||
}
|
||||
const icons={};for(const[id,name]of Object.entries(names)){const svg=resolve(context,name);if(svg)icons[id]=svg}
|
||||
fs.writeFileSync('src/renderer/src/components/source-settings-navigation-reference.ts','// REA '+evidence.evidence_id+'; SettingsNavigation/DC/cXe and source SVG geometry only.\nexport const sourceSettingsTabs = '+JSON.stringify(tabs,null,2)+'\nexport const sourceSettingsGroups = '+JSON.stringify(groups,null,2)+'\nexport const sourceSettingsIcons: Record<string,{props:Record<string,string|number>;children:{tag:string;props:Record<string,string|number>}[]}> = '+JSON.stringify(icons,null,2)+'\nconst labels:Record<string,string> = '+JSON.stringify(labels,null,2)+'\nexport const sourceSettingsLabel=(key:string):string=>labels[key]??key\n')
|
||||
const fixedEvidenceFile=path.join('.tmp','rea-css-evidence',HELP_FEEDBACK_EVIDENCE.id+'.json'),fixedEvidenceBytes=fs.readFileSync(fixedEvidenceFile),fixedEvidenceSha=hash(fixedEvidenceBytes)
|
||||
if(fixedEvidenceSha!==HELP_FEEDBACK_EVIDENCE.fileSha256)throw new Error('Fixed settings navigation Evidence changed: '+fixedEvidenceSha)
|
||||
const fixedEvidence=JSON.parse(fixedEvidenceBytes.toString('utf8'))
|
||||
if(fixedEvidence.evidence_id!==HELP_FEEDBACK_EVIDENCE.id)throw new Error('Fixed settings navigation Evidence identity mismatch')
|
||||
const fixedDocument=fixedEvidence.normalized_result.visuals.layout.documents.find((document)=>document.nodes.some((node)=>String(node.attributes?.class??'').split(/\s+/).includes('settings-navigation__group--about')))
|
||||
if(!fixedDocument)throw new Error('settings-navigation__group--about is absent from fixed Evidence')
|
||||
const fixedChildren=new Map();for(const node of fixedDocument.nodes){const list=fixedChildren.get(node.parent_index)??[];list.push(node);fixedChildren.set(node.parent_index,list)}
|
||||
const descendants=(root)=>{const output=[],queue=[...(fixedChildren.get(root.node_index)??[])];while(queue.length){const node=queue.shift();output.push(node);queue.push(...(fixedChildren.get(node.node_index)??[]))}return output}
|
||||
const aboutGroup=fixedDocument.nodes.find((node)=>String(node.attributes?.class??'').split(/\s+/).includes('settings-navigation__group--about')),aboutNodes=descendants(aboutGroup)
|
||||
const aboutItem=aboutNodes.find((node)=>String(node.attributes?.class??'').split(/\s+/).includes('settings-navigation__item')),aboutSvg=aboutNodes.find((node)=>node.node_name.toLowerCase()==='svg')
|
||||
if(!aboutItem||!aboutSvg)throw new Error('About navigation item or SVG is absent from fixed Evidence')
|
||||
const fixedSvgRoot=fixedDocument.svg_nodes.find((node)=>node.node_index===aboutSvg.node_index&&node.node_name==='svg'),fixedPath=fixedDocument.svg_nodes.find((node)=>node.parent_index===aboutSvg.node_index&&node.node_name==='path')
|
||||
if(!fixedSvgRoot)throw new Error('About navigation SVG root is absent from fixed Evidence')
|
||||
if(!fixedPath||typeof fixedPath.attributes.d!=='string')throw new Error('About navigation SVG path is absent from fixed Evidence')
|
||||
const pathLength=Buffer.byteLength(fixedPath.attributes.d,'utf16le')/2,pathSha=hash(fixedPath.attributes.d)
|
||||
if(pathLength!==HELP_FEEDBACK_EVIDENCE.pathLength||pathSha!==HELP_FEEDBACK_EVIDENCE.pathSha256)throw new Error('About navigation path fingerprint mismatch: '+pathLength+' '+pathSha)
|
||||
icons.helpFeedback={props:{viewBox:fixedSvgRoot.attributes.viewBox,fill:fixedSvgRoot.attributes.fill,xmlns:'http://www.w3.org/2000/svg'},children:[{tag:'path',props:{transform:fixedPath.attributes.transform,fill:fixedPath.attributes.fill,fillRule:fixedPath.attributes['fill-rule'],d:fixedPath.attributes.d}}]}
|
||||
const generatedReference='// REA '+evidence.evidence_id+'; SettingsNavigation/DC/cXe and source SVG geometry only.\n// About path: REA '+HELP_FEEDBACK_EVIDENCE.id+'; evidence sha256='+fixedEvidenceSha+'; path sha256='+pathSha+'; length='+pathLength+'.\nexport const sourceSettingsTabs = '+JSON.stringify(tabs,null,2)+'\nexport const sourceSettingsGroups = '+JSON.stringify(groups,null,2)+'\nexport const sourceSettingsIcons: Record<string,{props:Record<string,string|number>;children:{tag:string;props:Record<string,string|number>}[]}> = '+JSON.stringify(icons,null,2)+'\nconst labels:Record<string,string> = '+JSON.stringify(labels,null,2)+'\nexport const sourceSettingsLabel=(key:string):string=>labels[key]??key\n'
|
||||
const root=postcss.parse(fs.readFileSync('.tmp/rea-css-recovered/contexts-BkjwYSZT.css','utf8')),css=[]
|
||||
root.walkRules((rule)=>{if(!/\.settings-(?:navigation|search)/.test(rule.selector))return;let copy=rule.clone();copy.selectors=copy.selectors.map((s)=>'.source-settings-navigation '+s);let tree=copy;for(let parent=rule.parent;parent?.type==='atrule';parent=parent.parent){const wrap=parent.clone({nodes:[]});wrap.append(tree);tree=wrap}css.push(tree.toString())})
|
||||
const variables=new Set();const collect=(text)=>{for(const match of text.matchAll(/var\((--[\w-]+)/g))variables.add(match[1])};collect(css.join(''));const tokenRules=[];postcss.parse(fs.readFileSync('.tmp/rea-css-recovered/tokens-Df-i-zsO.css','utf8')).walkRules((rule)=>tokenRules.push(rule));for(let pass=0;pass<10;pass++)for(const rule of tokenRules)rule.walkDecls((decl)=>{if(variables.has(decl.prop))collect(decl.value)});const light=new Map(),dark=new Map();for(const rule of tokenRules){const target=rule.selector.includes('dark')?dark:rule.selector.includes(':root')?light:null;if(target)rule.walkDecls((decl)=>{if(variables.has(decl.prop))target.set(decl.prop,decl.value)})}const serialize=(map)=>[...map].map(([key,value])=>key+':'+value).join(';');css.unshift('.source-settings-navigation{'+serialize(light)+'}',':root[data-theme=dark] .source-settings-navigation{'+serialize(dark)+'}');
|
||||
fs.writeFileSync('src/renderer/src/components/source-settings-navigation-recovered.css','/* REA ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94; media conditions retained. */\n'+css.join('\n')+'\n')
|
||||
console.log(JSON.stringify({tabs:tabs.length,groups:groups.length,icons:Object.keys(icons),unknownIcons:Object.keys(names).filter((key)=>!icons[key]),rules:css.length}))
|
||||
const generatedCss='/* REA ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94; media conditions retained. */\n'+css.join('\n')+'\n',outputs=new Map([['src/renderer/src/components/source-settings-navigation-reference.ts',generatedReference],['src/renderer/src/components/source-settings-navigation-recovered.css',generatedCss]]),drift=[]
|
||||
for(const[file,content]of outputs){if(checkOnly){if(!fs.existsSync(file)||fs.readFileSync(file,'utf8')!==content)drift.push(file)}else fs.writeFileSync(file,content)}
|
||||
if(drift.length){console.error('settings navigation recovery drift: '+drift.join(', '));process.exitCode=1}
|
||||
console.log(JSON.stringify({check:checkOnly,idempotent:drift.length===0,tabs:tabs.length,groups:groups.length,icons:Object.keys(icons),unknownIcons:Object.keys(names).filter((key)=>!icons[key]),rules:css.length,helpFeedbackEvidence:HELP_FEEDBACK_EVIDENCE.id,helpFeedbackPathLength:pathLength,helpFeedbackPathSha256:pathSha}))
|
||||
@@ -0,0 +1,52 @@
|
||||
// Only retained REA evidence is read; this script never opens WorkBuddy or its installation.
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { createHash } from 'node:crypto'
|
||||
import postcss from 'postcss'
|
||||
|
||||
const sha = text => createHash('sha256').update(text).digest('hex')
|
||||
const archive = JSON.parse(fs.readFileSync('.tmp/rea-runtime-scripts.json', 'utf8'))
|
||||
const source = archive.items.find(item => item.file_path.endsWith('contexts-BMeWSUHk.js'))?.source.artifact
|
||||
const sourcePin = '5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b'
|
||||
if (!source || source.sha256 !== sourcePin || sha(source.text) !== sourcePin) throw Error('REA runtime source digest mismatch')
|
||||
const cssManifest = JSON.parse(fs.readFileSync('.tmp/rea-css-recovered/manifest.json', 'utf8'))
|
||||
const cssIdentity = cssManifest.files.find(file => file.file === 'contexts-BkjwYSZT.css')
|
||||
const css = fs.readFileSync(path.join('.tmp/rea-css-recovered', cssIdentity.file), 'utf8')
|
||||
if (sha(css) !== cssIdentity.sha256) throw Error('REA CSS source digest mismatch')
|
||||
const output = postcss.root()
|
||||
postcss.parse(css).walkRules(rule => {
|
||||
if (!/\.conversation-(?:team-collaboration|member-detail)(?:[\s_.:#\[>+-]|$)/.test(rule.selector)) return
|
||||
let copy = rule.clone()
|
||||
for (let parent = rule.parent; parent?.type === 'atrule'; parent = parent.parent) {
|
||||
copy = parent.clone({ nodes: [copy] })
|
||||
}
|
||||
output.append(copy)
|
||||
})
|
||||
if (!output.nodes.length) throw Error('REA member CSS was not found')
|
||||
const folder = 'src/renderer/src/workbuddy/subagents'
|
||||
const header = `/* WorkBuddy 5.8.2; retained REA ${cssManifest.sourceEvidenceId}.\n * Exact selected source rules; host primitive adapters are kept separately.\n */\n`
|
||||
const result = header + output.toString().replaceAll('}', '}\n').trimEnd() + '\n'
|
||||
// The conversation's generated theme is scoped to .chat-pane. Give the sibling
|
||||
// member surface the same retained token declarations without leaking them globally.
|
||||
const memberTheme = postcss.root()
|
||||
postcss.parse(fs.readFileSync('src/renderer/src/workbuddy/conversation/source-styles.css', 'utf8')).walkRules(rule => {
|
||||
const selectors = rule.selectors.filter(selector => /^\.chat-pane\.cr-theme(?:\[data-cr-theme=(?:light|dark)\])?$/.test(selector))
|
||||
if (!selectors.length) return
|
||||
const declarations = rule.nodes.filter(node => node.type === 'decl' && node.prop.startsWith('--')).map(node => node.clone())
|
||||
if (declarations.length) memberTheme.append(postcss.rule({ selector: selectors.map(selector => selector.replace('.chat-pane', '.conversation-member-detail')).join(','), nodes: declarations }))
|
||||
})
|
||||
if (!memberTheme.nodes.length) throw Error('Scoped conversation theme tokens were not found')
|
||||
const themeResult = '/* Same retained conversation token scopes for the sibling member detail surface. */\n' + memberTheme.toString().replaceAll('}', '}\n').trimEnd() + '\n'
|
||||
const manifest = { version: '5.8.2', runtimeEvidenceId: archive.evidence_id, runtimeFile: 'contexts-BMeWSUHk.js', runtimeSha256: sourcePin,
|
||||
cssEvidenceId: cssManifest.sourceEvidenceId, cssFile: cssIdentity.file, cssSha256: cssIdentity.sha256, rules: output.nodes.length,
|
||||
sourceRanges: { members: [6187325, 6193670], detail: [5913738, 5917850], finishedFooter: [6891900, 6892500], streamingFooter: [6977800, 6978800] },
|
||||
adaptations: ['YCode SubagentSnapshot supplies identity, status and events.', 'The existing workbench owns member selection and its detail surface.', 'Source button/avatar/icon primitives bind to local React elements; adapters are separate.', 'Cancellation remains available from the member detail header.'],
|
||||
limitations: ['No current live WorkBuddy capture; full-state pixel and motion equivalence remains unverified.'], outputSha256: sha(result), themeOutputSha256: sha(themeResult) }
|
||||
fs.mkdirSync(folder, { recursive: true })
|
||||
for (const [file, content] of [['source.css', result], ['theme.css', themeResult], ['manifest.json', JSON.stringify(manifest, null, 2) + '\n']]) {
|
||||
const target = path.join(folder, file)
|
||||
if (process.argv.includes('--check')) {
|
||||
if (fs.readFileSync(target, 'utf8') !== content) throw Error('Recovered source drift: ' + target)
|
||||
} else fs.writeFileSync(target, content)
|
||||
}
|
||||
console.log(JSON.stringify({ rules: output.nodes.length, cssEvidenceId: manifest.cssEvidenceId, outputSha256: manifest.outputSha256 }))
|
||||
@@ -0,0 +1,70 @@
|
||||
// SVG recovery from the fixed REA script export only; no original installation/runtime access.
|
||||
const fs = require('node:fs'), crypto = require('node:crypto')
|
||||
const { parse } = require('@babel/parser')
|
||||
const traverse = require('@babel/traverse').default
|
||||
const archive = JSON.parse(fs.readFileSync('.tmp/rea-runtime-scripts.json', 'utf8'))
|
||||
const cache = new Map()
|
||||
const literal = node => !node ? undefined : node.type === 'TemplateLiteral' && !node.expressions.length ? node.quasis[0].value.cooked : ['StringLiteral', 'NumericLiteral', 'BooleanLiteral'].includes(node.type) ? node.value : undefined
|
||||
const object = node => Object.fromEntries((node?.properties || []).filter(prop => prop.type === 'ObjectProperty').flatMap(prop => {
|
||||
const value = literal(prop.value)
|
||||
return value !== undefined ? [[prop.key.name ?? prop.key.value, value]] : []
|
||||
}))
|
||||
function get(file) {
|
||||
if (cache.has(file)) return cache.get(file)
|
||||
const source = archive.items.find(item => item.file_path.endsWith(file))?.source?.artifact
|
||||
if (!source) return
|
||||
if (crypto.createHash('sha256').update(source.text).digest('hex') !== source.sha256) throw Error('REA digest mismatch')
|
||||
if (file === 'contexts-BMeWSUHk.js' && source.sha256 !== '5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b') throw Error('REA context identity changed')
|
||||
if (file === 'src-DPDj7_qD.js' && source.sha256 !== '747e41d215375e1214c366213aea7e077d85421923029c61cbb6e9562be58de0') throw Error('REA icon identity changed')
|
||||
const ast = parse(source.text, { sourceType: 'module' })
|
||||
const result = { source, ast }
|
||||
cache.set(file, result)
|
||||
return result
|
||||
}
|
||||
function resolve(file, name, depth = 0) {
|
||||
const source = get(file)
|
||||
if (!source || depth > 8) return
|
||||
let imported, declaration
|
||||
traverse(source.ast, {
|
||||
ImportDeclaration(p) { for (const spec of p.node.specifiers) if (spec.local.name === name && spec.type === 'ImportSpecifier') imported = { file: p.node.source.value.replace('./', ''), member: spec.imported.name } },
|
||||
FunctionDeclaration(p) { if (p.node.id?.name === name) declaration = p },
|
||||
AssignmentExpression(p) { if (p.node.left.name === name) declaration = p },
|
||||
VariableDeclarator(p) { if (p.node.id?.name === name && p.node.init) declaration = p }
|
||||
})
|
||||
if (imported && get(imported.file)) {
|
||||
let local
|
||||
traverse(get(imported.file).ast, { ExportNamedDeclaration(p) { for (const spec of p.node.specifiers) if (spec.exported.name === imported.member) local = spec.local.name } })
|
||||
if (local) return resolve(imported.file, local, depth + 1)
|
||||
}
|
||||
if (!declaration) return
|
||||
const right = declaration.node.right ?? declaration.node.init
|
||||
if (right?.type === 'CallExpression' && typeof literal(right.arguments[0]) === 'string' && right.arguments[1]?.type === 'Identifier') {
|
||||
let nodes
|
||||
traverse(source.ast, { AssignmentExpression(p) { if (p.node.left.name === right.arguments[1].name && p.node.right.type === 'ArrayExpression') nodes = p.node.right.elements } })
|
||||
if (nodes) return { file, sha256: source.source.sha256, props: { viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }, children: nodes.map(entry => ({ tag: literal(entry.elements[0]), props: object(entry.elements[1]) })) }
|
||||
}
|
||||
if (right?.type === 'CallExpression' && right.arguments[0]?.type === 'Identifier') return resolve(file, right.arguments[0].name, depth + 1)
|
||||
let svg, paths = [], next
|
||||
declaration.traverse({ CallExpression(p) {
|
||||
const tag = p.node.arguments[0]
|
||||
if (tag?.type === 'Identifier') { next = tag.name; return }
|
||||
const kind = literal(tag)
|
||||
if (!['svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse'].includes(kind)) return
|
||||
const props = object(p.node.arguments[1])
|
||||
if (kind === 'svg') svg = props
|
||||
else paths.push({ tag: kind, props })
|
||||
} })
|
||||
if (svg && paths.length) return { file, sha256: source.source.sha256, props: svg, children: paths }
|
||||
return next ? resolve(file, next, depth + 1) : undefined
|
||||
}
|
||||
const icons = {}
|
||||
for (const [key, binding] of Object.entries({ chevronDown: 'Oo', chevronRight: 'yo', loading: 'xo', check: 'Ea', failure: 'Ba', close: 'Da', assignment: 'Pte' })) {
|
||||
icons[key] = resolve('contexts-BMeWSUHk.js', binding) || null
|
||||
}
|
||||
if (Object.values(icons).some(icon => !icon)) throw Error('A selected source member icon was not recovered')
|
||||
const output = 'src/renderer/src/workbuddy/subagents/icons.json'
|
||||
const content = JSON.stringify({ version: '5.8.2', evidenceId: archive.evidence_id, icons }, null, 2) + '\n'
|
||||
if (process.argv.includes('--check')) {
|
||||
if (fs.readFileSync(output, 'utf8') !== content) throw Error('Recovered member icon drift')
|
||||
} else fs.writeFileSync(output, content)
|
||||
console.log(JSON.stringify({ icons: Object.keys(icons).length, sha256: crypto.createHash('sha256').update(content).digest('hex') }))
|
||||
Reference in new issue
Block a user