| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- /** Read-only acceptance of the public board against the workshop deliverables. */
- import assert from 'node:assert/strict';
- import fs from 'node:fs/promises';
- import path from 'node:path';
- import { pathToFileURL } from 'node:url';
- import { loadPublicBoard, publicBoardAction } from '../src/services/workshop-data.js';
- import { buildModel } from '../src/core/kfs/build.js';
- import { reportCenter } from '../src/core/library/reports.js';
- import { indexBoardMembers } from '../src/core/member-groups.js';
- const root = process.cwd();
- const work = path.join(root, 'work/workshop-board-acceptance-20261110');
- const out = path.join(root, 'docs/workshop-board-acceptance-20261110');
- await fs.mkdir(work, { recursive: true });
- await fs.mkdir(out, { recursive: true });
- const save = (filename, value) => fs.writeFile(filename, JSON.stringify(value, null, 2) + '\n');
- const endpoint = 'https://kangshifu.fmode.cn/api/functions';
- const appId = 'FKieRbIoGt_main';
- let raw, groups;
- if (process.argv.includes('--fetch')) {
- raw = await loadPublicBoard({ url: endpoint, appId, fetchImpl: fetch });
- groups = await publicBoardAction({ url: endpoint, appId, name: 'ksfGetPublicMemberGroups', params: {}, fetchImpl: fetch });
- await save(path.join(work, 'public-board.json'), raw);
- await save(path.join(work, 'public-groups.json'), groups);
- } else {
- raw = JSON.parse(await fs.readFile(path.join(work, 'public-board.json')));
- groups = JSON.parse(await fs.readFile(path.join(work, 'public-groups.json')));
- }
- const db = buildModel(raw);
- const center = reportCenter(db);
- const directory = indexBoardMembers(raw);
- const artifactEntries = center.entries.filter(e => e.kind === 'artifact');
- assert.deepEqual(artifactEntries.map(e => e.id).sort(), raw.artifact.map(a => a.objectId).sort());
- const groupId = row => groups.assignments[directory.memberId('artifact', row)] || '__ungrouped';
- const tally = key => raw.artifact.reduce((map, row) => {
- const value = row[key] || 'unspecified';
- map[value] = (map[value] || 0) + 1;
- return map;
- }, {});
- const live = raw.artifact.filter(a => a.dataMode === 'live');
- const audit = {
- schema: 'ksf.workshop.board-display-audit.v1', checkedAt: new Date().toISOString(),
- scope: 'public-read-only; enterprise onboarding deferred by user', businessWrites: 0,
- publicSnapshotAt: raw.loadedAt,
- counts: Object.fromEntries(['project', 'artifact', 'insight', 'source', 'session', 'opportunity'].map(k => [k, raw[k].length])),
- artifactTypes: tally('artifactType'), dataModes: tally('dataMode'), versions: tally('version'),
- publicArtifactIdsAllReachReportCenter: true,
- structuredReports: raw.artifact.filter(a => a.report).length,
- rawTextOnlyArtifacts: raw.artifact.filter(a => !a.report && a.rawText).length,
- groupStateRevision: groups.revision,
- groups: [...groups.groups, { id: '__ungrouped', name: '未分组' }].map(g => ({
- id: g.id, name: g.name,
- memberCount: db.members.filter(m => (groups.assignments[m.id] || '__ungrouped') === g.id).length,
- artifactCount: raw.artifact.filter(a => groupId(a) === g.id).length,
- liveArtifactCount: live.filter(a => groupId(a) === g.id).length,
- })),
- liveArtifactsWithoutGroup: live.filter(a => groupId(a) === '__ungrouped').map(a => a.objectId),
- publicRowsWithSeriesKey: raw.artifact.filter(a => a.reportSeriesKey).length,
- modelRowsRetainingVersion: db.artifacts.filter(a => Number.isInteger(a.version)).length,
- modelRowsRetainingDataMode: db.artifacts.filter(a => a.dataMode).length,
- findings: [
- { id: 'group-results', status: 'needs-change', detail: 'Shared member groups are readable; report lists have no group/workshop filter or group deliverable overview. Current live records are ungrouped.' },
- { id: 'version-and-course-mapping', status: 'needs-change', detail: 'Public rows carry version but the model discards it; public projection omits reportSeriesKey used by Skill taskKey. Chapter matching and latest-version selection cannot be established reliably.' },
- { id: 'seven-deliverables', status: 'needs-change', detail: 'Five generic report categories do not provide the seven workshop deliverables or their missing-item status. Two concept outputs and distinct VOC pools require explicit task/chapter mapping.' },
- { id: 'data-mode', status: 'needs-change', detail: 'Live/sample/unspecified modes are not retained in the artifact model or offered as list filters; existing mixed records must not be counted as a real group completion rate.' },
- { id: 'assessment-classification', status: 'needs-change', detail: 'assessment_report is mapped to the generic concept page, including the database acceptance report; this is not evidence of a product proposal.' },
- { id: 'independent-concept', status: 'needs-change', detail: 'The public board does not return KfConcept or formula-item details; retaining a project currentConceptRef does not display the independent concept record.' },
- ],
- productionDisplayAcceptancePassed: false,
- };
- if (process.argv.includes('--browser')) {
- const { chromium } = await import(pathToFileURL(path.join(root, 'work/workshop-manual-tools/node_modules/playwright/index.mjs')).href);
- const browser = await chromium.launch({ headless: true, executablePath: 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe' });
- const checks = [], errors = [], functionCalls = [], blockedActions = [];
- try {
- const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
- const page = await context.newPage();
- page.on('pageerror', e => errors.push(e.message));
- await page.route('**/api/functions', async route => {
- const req = route.request();
- if (req.method() !== 'POST') return route.continue();
- let name = '';
- try { name = req.postDataJSON().path; } catch { /* An unrecognized action is not part of this read-only audit. */ }
- if (!['/ksfGetPublicBoard', '/ksfGetPublicMemberGroups'].includes(name)) {
- blockedActions.push(name); return route.abort();
- }
- functionCalls.push(name); return route.continue();
- });
- assert.equal((await page.goto('https://kangshifu.fmode.cn/reports', { waitUntil: 'domcontentloaded' })).status(), 200);
- await page.locator('view-reports [data-types]').waitFor({ timeout: 45000 });
- const go = async (pathname, selector) => {
- await page.evaluate(async pathname => {
- const { navigate } = await import('/src/utils/router.js'); navigate(pathname);
- }, pathname);
- await page.locator(selector).first().waitFor({ timeout: 20000 });
- await page.evaluate(async () => { const app = document.querySelector('kf-app'); await app.updateComplete; });
- };
- for (const width of [1440, 390]) {
- await page.setViewportSize({ width, height: 1000 });
- for (const [pathname, selector] of [['/reports', 'view-reports [data-types]'], ['/members', 'view-members [data-member-group]'], ['/stages', 'view-stages'], ['/pool', 'view-pool']]) {
- await go(pathname, selector);
- const size = await page.evaluate(() => ({ width: document.documentElement.clientWidth, scroll: document.documentElement.scrollWidth }));
- checks.push({ kind: 'route', width, pathname, noPageOverflow: size.scroll <= size.width });
- if (pathname === '/members') {
- await page.waitForFunction(() => document.querySelector('kf-app')?.shadowRoot.querySelector('view-members')?._loading === false);
- await page.locator('view-members').getByRole('button', { name: /^G1/ }).click();
- const names = await page.locator('view-members [data-member-id]').evaluateAll(nodes => nodes.map(n => n.getAttribute('data-member-id')));
- const expected = db.members.filter(m => groups.assignments[m.id] === 'legacy-group:G1').map(m => m.id).sort();
- assert.deepEqual(names.sort(), expected);
- checks.push({ kind: 'member-group-filter', width, group: 'G1', members: names.length, matchesSharedAssignments: true });
- }
- await page.screenshot({ path: path.join(work, `${pathname.slice(1)}-${width}.png`), fullPage: true });
- }
- }
- await page.setViewportSize({ width: 1440, height: 1000 });
- const normalize = text => String(text || '').replace(/\s+/gu, '');
- for (const artifact of raw.artifact.filter(a => a.report)) {
- await go('/report/' + encodeURIComponent(artifact.objectId), 'view-report [data-chapter]');
- const text = await page.locator('view-report').evaluate(el => el.renderRoot.textContent);
- const expected = [artifact.report.title || artifact.title, ...(artifact.report.chapters || []).map(c => c.title)].filter(Boolean);
- const missing = expected.filter(title => !normalize(text).includes(normalize(title)));
- const cellValues = cell => {
- if (Array.isArray(cell)) return cell.flatMap(item => Array.isArray(item) ? [String(item[0]), String(item[1]) + '%'] : [String(item)]);
- if (typeof cell === 'number') return [cell.toLocaleString('zh-CN')];
- if (typeof cell === 'string' && artifact.report.evidence?.[cell]) return [artifact.report.evidence[cell].claim || cell];
- return [String(cell ?? '—')];
- };
- const bodyValues = (artifact.report.chapters || []).flatMap(c => c.sections || []).flatMap(s => {
- if (['para', 'longpara'].includes(s.kind)) return [s.text || ''];
- if (['table', 'matrix'].includes(s.kind)) return [...(s.data?.cols || []), ...(s.data?.rows || []).flatMap(row => row.flatMap(cellValues))];
- return [];
- }).filter(value => normalize(value));
- const missingBodyValues = bodyValues.filter(value => !normalize(text).includes(normalize(value)));
- checks.push({ kind: 'report-read', artifactId: artifact.objectId, chapterCount: expected.length - 1, missingTitles: missing,
- paragraphAndTableValuesChecked: bodyValues.length, missingBodyValueCount: missingBodyValues.length,
- passed: missing.length === 0 && missingBodyValues.length === 0 });
- }
- for (const artifact of raw.artifact.filter(a => !a.report && a.rawText)) {
- await go('/reports?artifact=' + encodeURIComponent(artifact.objectId), 'kf-evidence-chain .raw');
- const text = await page.locator('kf-evidence-chain .raw').textContent();
- checks.push({ kind: 'raw-text-read', artifactId: artifact.objectId, passed: normalize(text) === normalize(artifact.rawText) });
- }
- await context.close();
- } finally { await browser.close(); }
- audit.browser = { checks, errors, functionCalls, blockedActions, businessWrites: 0 };
- }
- await save(path.join(out, 'audit.json'), audit);
- console.log(JSON.stringify({ artifactCount: raw.artifact.length, allArtifactsInReportCenter: true,
- groups: audit.groups, browserChecks: audit.browser?.checks.length,
- browserFailureCount: audit.browser?.checks.filter(c => c.passed === false || c.noPageOverflow === false).length,
- browserErrors: audit.browser?.errors, blockedActions: audit.browser?.blockedActions,
- acceptancePassed: audit.productionDisplayAcceptancePassed }, null, 2));
|