rcf-lite 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/blueprints/application-admin-console/CHANGELOG.md +11 -0
- package/blueprints/application-admin-console/README.md +98 -0
- package/blueprints/application-admin-console/blueprint.json +226 -0
- package/blueprints/application-admin-console/contributions/adrs/adr-2201-application-admin-console-capability-vocabulary.json +25 -0
- package/blueprints/application-admin-console/contributions/adrs/adr-2202-application-admin-console-baseline-roles.json +25 -0
- package/blueprints/application-admin-console/contributions/adrs/adr-2203-application-admin-console-invite-transport.json +20 -0
- package/blueprints/application-admin-console/contributions/adrs/adr-2204-application-admin-console-audit-retention.json +20 -0
- package/blueprints/application-admin-console/contributions/requirements/application-admin-console-req-001.json +18 -0
- package/blueprints/application-admin-console/contributions/requirements/application-admin-console-req-002.json +18 -0
- package/blueprints/application-admin-console/contributions/requirements/application-admin-console-req-003.json +18 -0
- package/blueprints/application-admin-console/contributions/requirements/application-admin-console-req-004.json +18 -0
- package/blueprints/application-admin-console/contributions/requirements/application-admin-console-req-005.json +18 -0
- package/blueprints/application-admin-console/contributions/requirements/application-admin-console-req-006.json +18 -0
- package/blueprints/application-admin-console/contributions/tacs/tac-2201-application-admin-console-shell.json +40 -0
- package/blueprints/application-admin-console/contributions/tacs/tac-2202-application-admin-console-capability-discovery.json +44 -0
- package/blueprints/application-admin-console/contributions/tacs/tac-2203-application-admin-console-permission-matrix.json +34 -0
- package/blueprints/application-admin-console/contributions/tacs/tac-2204-application-admin-console-audit-view.json +40 -0
- package/blueprints/application-admin-console/contributions/user-stories/application-admin-console-us-21101.json +34 -0
- package/blueprints/application-admin-console/contributions/user-stories/application-admin-console-us-21102.json +34 -0
- package/blueprints/application-admin-console/contributions/user-stories/application-admin-console-us-21103.json +34 -0
- package/blueprints/application-admin-console/contributions/user-stories/application-admin-console-us-21104.json +28 -0
- package/blueprints/application-admin-console/contributions/user-stories/application-admin-console-us-21105.json +34 -0
- package/blueprints/application-admin-console/contributions/user-stories/application-admin-console-us-21106.json +27 -0
- package/blueprints/application-admin-console/contributions/user-stories/application-admin-console-us-21107.json +27 -0
- package/blueprints/application-admin-console/contributions/user-stories/application-admin-console-us-21108.json +27 -0
- package/blueprints/application-admin-console/contributions/user-stories/application-admin-console-us-21109.json +33 -0
- package/blueprints/application-admin-console/docs/topics.md +35 -0
- package/blueprints/application-admin-console/guide/application-admin-console.md +29 -0
- package/blueprints/application-admin-console/probe-packs/application-admin-console.pack.mjs +182 -0
- package/blueprints/application-api-rest/docs/topics.md +5 -0
- package/blueprints/application-charts/CHANGELOG.md +12 -0
- package/blueprints/application-charts/README.md +73 -0
- package/blueprints/application-charts/blueprint.json +26 -0
- package/blueprints/application-charts/contributions/adrs/adr-1901-application-charts-engine-choice.json +25 -0
- package/blueprints/application-charts/contributions/adrs/adr-1902-application-charts-accessible-palette.json +25 -0
- package/blueprints/application-charts/contributions/adrs/adr-1903-application-charts-motion.json +25 -0
- package/blueprints/application-charts/contributions/requirements/application-charts-req-001.json +15 -0
- package/blueprints/application-charts/contributions/requirements/application-charts-req-002.json +15 -0
- package/blueprints/application-charts/contributions/requirements/application-charts-req-003.json +15 -0
- package/blueprints/application-charts/contributions/requirements/application-charts-req-004.json +15 -0
- package/blueprints/application-charts/contributions/tacs/tac-1901-application-charts-render-shell.json +42 -0
- package/blueprints/application-charts/contributions/tacs/tac-1902-application-charts-keyboard-traversal.json +40 -0
- package/blueprints/application-charts/contributions/user-stories/application-charts-us-18101.json +35 -0
- package/blueprints/application-charts/contributions/user-stories/application-charts-us-18102.json +35 -0
- package/blueprints/application-charts/contributions/user-stories/application-charts-us-18103.json +35 -0
- package/blueprints/application-charts/contributions/user-stories/application-charts-us-18104.json +35 -0
- package/blueprints/application-charts/contributions/user-stories/application-charts-us-18105.json +35 -0
- package/blueprints/application-charts/contributions/user-stories/application-charts-us-18106.json +35 -0
- package/blueprints/application-charts/docs/topics.md +37 -0
- package/blueprints/application-charts/guide/application-charts.md +44 -0
- package/blueprints/application-charts/probe-packs/application-charts.pack.mjs +191 -0
- package/blueprints/application-dashboard/CHANGELOG.md +10 -0
- package/blueprints/application-dashboard/README.md +102 -0
- package/blueprints/application-dashboard/assets/guidance/dashboard-design.md +124 -0
- package/blueprints/application-dashboard/blueprint.json +29 -0
- package/blueprints/application-dashboard/contributions/adrs/adr-2001-application-dashboard-primary-kpi-enum.json +25 -0
- package/blueprints/application-dashboard/contributions/adrs/adr-2002-application-dashboard-timeframe-presets.json +25 -0
- package/blueprints/application-dashboard/contributions/adrs/adr-2003-application-dashboard-export-formats.json +25 -0
- package/blueprints/application-dashboard/contributions/requirements/application-dashboard-req-001.json +15 -0
- package/blueprints/application-dashboard/contributions/requirements/application-dashboard-req-002.json +15 -0
- package/blueprints/application-dashboard/contributions/requirements/application-dashboard-req-003.json +15 -0
- package/blueprints/application-dashboard/contributions/requirements/application-dashboard-req-004.json +15 -0
- package/blueprints/application-dashboard/contributions/requirements/application-dashboard-req-005.json +15 -0
- package/blueprints/application-dashboard/contributions/tacs/tac-2001-application-dashboard-tile-grid.json +47 -0
- package/blueprints/application-dashboard/contributions/tacs/tac-2002-application-dashboard-filter-chrome.json +41 -0
- package/blueprints/application-dashboard/contributions/tacs/tac-2003-application-dashboard-export-handle.json +35 -0
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19101.json +35 -0
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19102.json +35 -0
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19103.json +35 -0
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19104.json +35 -0
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19105.json +35 -0
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19106.json +35 -0
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19107.json +35 -0
- package/blueprints/application-dashboard/docs/topics.md +43 -0
- package/blueprints/application-dashboard/guide/application-dashboard.md +48 -0
- package/blueprints/application-dashboard/probe-packs/application-dashboard.pack.mjs +231 -0
- package/blueprints/application-datatable/CHANGELOG.md +8 -0
- package/blueprints/application-datatable/README.md +43 -0
- package/blueprints/application-datatable/blueprint.json +33 -0
- package/blueprints/application-datatable/contributions/adrs/adr-1801-application-datatable-pattern-choice.json +25 -0
- package/blueprints/application-datatable/contributions/adrs/adr-1802-application-datatable-url-state.json +20 -0
- package/blueprints/application-datatable/contributions/adrs/adr-1803-application-datatable-page-size.json +20 -0
- package/blueprints/application-datatable/contributions/adrs/adr-1804-application-datatable-selection-persistence.json +20 -0
- package/blueprints/application-datatable/contributions/requirements/application-datatable-req-001.json +15 -0
- package/blueprints/application-datatable/contributions/requirements/application-datatable-req-002.json +15 -0
- package/blueprints/application-datatable/contributions/requirements/application-datatable-req-003.json +15 -0
- package/blueprints/application-datatable/contributions/requirements/application-datatable-req-004.json +15 -0
- package/blueprints/application-datatable/contributions/requirements/application-datatable-req-005.json +15 -0
- package/blueprints/application-datatable/contributions/requirements/application-datatable-req-006.json +15 -0
- package/blueprints/application-datatable/contributions/tacs/tac-1801-application-datatable-shell.json +50 -0
- package/blueprints/application-datatable/contributions/tacs/tac-1802-application-datatable-query-adapter.json +39 -0
- package/blueprints/application-datatable/contributions/tacs/tac-1803-application-datatable-selection-model.json +28 -0
- package/blueprints/application-datatable/contributions/user-stories/application-datatable-us-17101.json +44 -0
- package/blueprints/application-datatable/contributions/user-stories/application-datatable-us-17102.json +44 -0
- package/blueprints/application-datatable/contributions/user-stories/application-datatable-us-17103.json +44 -0
- package/blueprints/application-datatable/contributions/user-stories/application-datatable-us-17104.json +44 -0
- package/blueprints/application-datatable/contributions/user-stories/application-datatable-us-17105.json +44 -0
- package/blueprints/application-datatable/contributions/user-stories/application-datatable-us-17106.json +44 -0
- package/blueprints/application-datatable/contributions/user-stories/application-datatable-us-17107.json +44 -0
- package/blueprints/application-datatable/contributions/user-stories/application-datatable-us-17108.json +44 -0
- package/blueprints/application-datatable/contributions/user-stories/application-datatable-us-17109.json +44 -0
- package/blueprints/application-datatable/docs/topics.md +35 -0
- package/blueprints/application-datatable/guide/application-datatable.md +38 -0
- package/blueprints/application-datatable/probe-packs/application-datatable.pack.mjs +240 -0
- package/blueprints/application-error-handling/docs/topics.md +5 -0
- package/blueprints/application-notifications-in-app/CHANGELOG.md +9 -0
- package/blueprints/application-notifications-in-app/README.md +80 -0
- package/blueprints/application-notifications-in-app/blueprint.json +30 -0
- package/blueprints/application-notifications-in-app/contributions/adrs/adr-2101-application-notifications-in-app-live-region-role-mapping.json +25 -0
- package/blueprints/application-notifications-in-app/contributions/adrs/adr-2102-application-notifications-in-app-toast-timeout-floor.json +25 -0
- package/blueprints/application-notifications-in-app/contributions/adrs/adr-2103-application-notifications-in-app-retention-window.json +25 -0
- package/blueprints/application-notifications-in-app/contributions/requirements/application-notifications-in-app-req-001.json +15 -0
- package/blueprints/application-notifications-in-app/contributions/requirements/application-notifications-in-app-req-002.json +15 -0
- package/blueprints/application-notifications-in-app/contributions/requirements/application-notifications-in-app-req-003.json +15 -0
- package/blueprints/application-notifications-in-app/contributions/requirements/application-notifications-in-app-req-004.json +15 -0
- package/blueprints/application-notifications-in-app/contributions/requirements/application-notifications-in-app-req-005.json +15 -0
- package/blueprints/application-notifications-in-app/contributions/tacs/tac-2101-application-notifications-in-app-live-region.json +46 -0
- package/blueprints/application-notifications-in-app/contributions/tacs/tac-2102-application-notifications-in-app-centre.json +52 -0
- package/blueprints/application-notifications-in-app/contributions/tacs/tac-2103-application-notifications-in-app-preferences.json +40 -0
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20101.json +35 -0
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20102.json +35 -0
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20103.json +35 -0
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20104.json +35 -0
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20105.json +35 -0
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20106.json +26 -0
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20107.json +26 -0
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20108.json +26 -0
- package/blueprints/application-notifications-in-app/docs/topics.md +39 -0
- package/blueprints/application-notifications-in-app/guide/application-notifications-in-app.md +47 -0
- package/blueprints/application-notifications-in-app/probe-packs/application-notifications-in-app.pack.mjs +328 -0
- package/blueprints/application-spa/docs/topics.md +5 -0
- package/blueprints/delivery-ci-workflows/docs/topics.md +5 -0
- package/blueprints/deploy-cloudflare-workers/docs/topics.md +5 -0
- package/blueprints/email-smtp-resend/docs/topics.md +5 -0
- package/blueprints/observability-essentials/docs/topics.md +5 -0
- package/blueprints/observability-logging/CHANGELOG.md +5 -0
- package/blueprints/observability-logging/README.md +1 -1
- package/blueprints/observability-logging/blueprint.json +113 -21
- package/blueprints/observability-logging/docs/topics.md +5 -0
- package/blueprints/observability-probe-endpoints/docs/topics.md +5 -0
- package/blueprints/persistence-data-d1/docs/topics.md +5 -0
- package/blueprints/persistence-data-sqlite/docs/topics.md +5 -0
- package/blueprints/security-auth-clerk/CHANGELOG.md +5 -0
- package/blueprints/security-auth-clerk/blueprint.json +2 -1
- package/blueprints/security-auth-clerk/docs/topics.md +5 -0
- package/blueprints/security-auth-keycloak/CHANGELOG.md +5 -0
- package/blueprints/security-auth-keycloak/blueprint.json +2 -1
- package/blueprints/security-auth-keycloak/docs/topics.md +5 -0
- package/blueprints/security-auth-magic-link/CHANGELOG.md +5 -0
- package/blueprints/security-auth-magic-link/blueprint.json +2 -1
- package/blueprints/security-auth-magic-link/docs/topics.md +5 -0
- package/blueprints/security-auth-oauth2/CHANGELOG.md +5 -0
- package/blueprints/security-auth-oauth2/blueprint.json +2 -1
- package/blueprints/security-auth-oauth2/docs/topics.md +5 -0
- package/blueprints/security-secrets-management/docs/topics.md +5 -0
- package/fixtures/canary-manifest.json +9 -9
- package/package.json +2 -2
- package/rcf/code-nodes/cn-106.json +13 -0
- package/rcf/code-nodes/cn-107.json +14 -0
- package/rcf/code-nodes/cn-108.json +13 -0
- package/rcf/code-nodes/cn-109.json +12 -0
- package/rcf/code-nodes/cn-110.json +12 -0
- package/rcf/code-nodes/cn-111.json +14 -0
- package/rcf/code-nodes/cn-112.json +14 -0
- package/rcf/code-nodes/cn-113.json +12 -0
- package/rcf/code-nodes/cn-114.json +13 -0
- package/rcf/code-nodes/cn-115.json +13 -0
- package/rcf/code-nodes/cn-116.json +12 -0
- package/rcf/code-nodes/cn-117.json +13 -0
- package/rcf/code-nodes/cn-118.json +13 -0
- package/rcf/code-nodes/cn-119.json +13 -0
- package/rcf/code-nodes/cn-120.json +13 -0
- package/rcf/code-nodes/cn-121.json +12 -0
- package/rcf/code-nodes/cn-122.json +12 -0
- package/rcf/code-nodes/cn-123.json +13 -0
- package/rcf/code-nodes/cn-124.json +13 -0
- package/rcf/code-nodes/cn-125.json +13 -0
- package/rcf/code-nodes/cn-126.json +13 -0
- package/rcf/code-nodes/cn-127.json +12 -0
- package/rcf/code-nodes/cn-128.json +14 -0
- package/rcf/code-nodes/cn-129.json +14 -0
- package/rcf/code-nodes/cn-130.json +14 -0
- package/rcf/code-nodes/cn-131.json +15 -0
- package/rcf/code-nodes/cn-132.json +12 -0
- package/rcf/code-nodes/cn-133.json +13 -0
- package/rcf/fbs/fbs-036.json +22 -0
- package/rcf/fbs/fbs-037.json +19 -0
- package/rcf/fbs/fbs-038.json +19 -0
- package/rcf/fbs/fbs-039.json +21 -0
- package/rcf/fbs/fbs-040.json +20 -0
- package/rcf/fbs/fbs-041.json +13 -0
- package/rcf/fbs/fbs-042.json +13 -0
- package/rcf/requirements/req-017.json +18 -0
- package/rcf/requirements/req-018.json +18 -0
- package/rcf/requirements/req-019.json +38 -0
- package/rcf/requirements/req-020.json +13 -0
- package/rcf/requirements/req-021.json +13 -0
- package/rcf/requirements/req-022.json +13 -0
- package/rcf/requirements/req-023.json +14 -0
- package/rcf/test-suites/ts-046.json +78 -0
- package/rcf/test-suites/ts-047.json +54 -0
- package/rcf/test-suites/ts-048.json +54 -0
- package/rcf/test-suites/ts-049.json +70 -0
- package/rcf/test-suites/ts-050.json +69 -0
- package/rcf/test-suites/ts-051.json +22 -0
- package/rcf/test-suites/ts-052.json +20 -0
- package/rcf/user-stories/us-1701.json +56 -0
- package/rcf/user-stories/us-1801.json +40 -0
- package/rcf/user-stories/us-1901.json +40 -0
- package/rcf/user-stories/us-2001.json +50 -0
- package/rcf/user-stories/us-2101.json +45 -0
- package/rcf/user-stories/us-2201.json +60 -0
- package/rcf/user-stories/us-2301.json +51 -0
- package/releases/releases.yaml +11 -1
- package/src/blueprint/apply.js +98 -1
- package/src/blueprint/capabilities.js +318 -0
- package/src/blueprint/loader.js +247 -0
- package/src/browser-verify/boot.js +180 -0
- package/src/browser-verify/index.js +13 -0
- package/src/browser-verify/manifest-writer.js +31 -6
- package/src/browser-verify/pack-browser.js +294 -0
- package/src/browser-verify/pack-loader.js +176 -0
- package/src/browser-verify/pack-runner.js +193 -0
- package/src/browser-verify/pack-schema.js +155 -0
- package/src/browser-verify/runner.js +24 -1
- package/src/cli/blueprint.js +92 -0
- package/src/cli/browser-verify.js +147 -7
- package/src/core/errors/index.js +7 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// packBrowser: a real headless browser exposed to blueprint-shipped
|
|
2
|
+
// probe packs (visual-round-spec-2026-09-04 §3.3). The browser is
|
|
3
|
+
// provisioned through the pinned Playwright MCP server as a JSON-RPC
|
|
4
|
+
// 2.0 stdio client, with an optional cheaper route via a `playwright`
|
|
5
|
+
// installation already resolved from the consuming project's own
|
|
6
|
+
// node_modules. Both routes expose the same small stable API:
|
|
7
|
+
//
|
|
8
|
+
// goto(url) navigate to a URL
|
|
9
|
+
// snapshot() accessibility-tree text (searchable)
|
|
10
|
+
// evaluate(fn, arg?) run JS in page context; returns parsed value
|
|
11
|
+
// click(selectorOrRef, hint) click an element by CSS selector or a11y ref
|
|
12
|
+
// type(selectorOrRef, text) type into an editable element
|
|
13
|
+
// press(key) press a keyboard key
|
|
14
|
+
// resize(width, height) set viewport dimensions (breakpoint reflow probes)
|
|
15
|
+
// screenshot(filename?) capture a PNG; returns descriptor / path
|
|
16
|
+
// close() close the page + terminate the browser process
|
|
17
|
+
//
|
|
18
|
+
// Zero new npm dependencies: MCP over stdio uses only Node built-ins;
|
|
19
|
+
// the cheap route uses `playwright` peer dep if the project already
|
|
20
|
+
// installed it. No LLM call anywhere.
|
|
21
|
+
|
|
22
|
+
import { spawn } from 'node:child_process';
|
|
23
|
+
import { createRequire } from 'node:module';
|
|
24
|
+
import { pathToFileURL } from 'node:url';
|
|
25
|
+
import { resolve as resolvePath } from 'node:path';
|
|
26
|
+
|
|
27
|
+
import { PLAYWRIGHT_MCP_VERSION } from '../verify/engine/launcher.js';
|
|
28
|
+
|
|
29
|
+
const DEFAULT_TOOL_TIMEOUT_MS = 30000;
|
|
30
|
+
const INIT_TIMEOUT_MS = 45000;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Compose a packBrowser for use by probe packs.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} [options]
|
|
36
|
+
* @param {string} [options.projectRoot] consuming project root; used to try the cheap route
|
|
37
|
+
* @param {string} [options.playwrightMcpVersion] pinned MCP semver (defaults to PLAYWRIGHT_MCP_VERSION)
|
|
38
|
+
* @param {'auto'|'mcp'|'project'} [options.route] force a route (default 'auto')
|
|
39
|
+
* @param {(line: string) => void} [options.logger]
|
|
40
|
+
* @param {NodeJS.ProcessEnv} [options.env]
|
|
41
|
+
* @param {number} [options.timeoutMs] per-RPC timeout
|
|
42
|
+
* @returns {Promise<object>} the packBrowser handle
|
|
43
|
+
*/
|
|
44
|
+
export async function createPackBrowser(options = {}) {
|
|
45
|
+
const {
|
|
46
|
+
projectRoot,
|
|
47
|
+
playwrightMcpVersion = PLAYWRIGHT_MCP_VERSION,
|
|
48
|
+
route = 'auto',
|
|
49
|
+
logger = () => {},
|
|
50
|
+
env = process.env,
|
|
51
|
+
timeoutMs = DEFAULT_TOOL_TIMEOUT_MS,
|
|
52
|
+
} = options;
|
|
53
|
+
|
|
54
|
+
if (route === 'project' || (route === 'auto' && projectRoot && await tryResolveProjectPlaywright(projectRoot))) {
|
|
55
|
+
const handle = await createProjectPlaywrightHandle({ projectRoot, logger });
|
|
56
|
+
if (handle) return handle;
|
|
57
|
+
if (route === 'project') throw new Error(`packBrowser: project route requested but 'playwright' is not installed under ${projectRoot}`);
|
|
58
|
+
}
|
|
59
|
+
return createMcpPackBrowser({ playwrightMcpVersion, logger, env, timeoutMs });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function tryResolveProjectPlaywright(projectRoot) {
|
|
63
|
+
try {
|
|
64
|
+
const req = createRequire(resolvePath(projectRoot, 'package.json'));
|
|
65
|
+
req.resolve('playwright');
|
|
66
|
+
return true;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function createProjectPlaywrightHandle({ projectRoot, logger }) {
|
|
73
|
+
try {
|
|
74
|
+
const req = createRequire(resolvePath(projectRoot, 'package.json'));
|
|
75
|
+
const playwrightPath = req.resolve('playwright');
|
|
76
|
+
const mod = await import(pathToFileURL(playwrightPath).href);
|
|
77
|
+
const chromium = mod?.chromium ?? mod?.default?.chromium;
|
|
78
|
+
if (!chromium) return null;
|
|
79
|
+
const browser = await chromium.launch({ headless: true });
|
|
80
|
+
const context = await browser.newContext();
|
|
81
|
+
const page = await context.newPage();
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
provisioning: { via: 'project-playwright', playwrightPath },
|
|
84
|
+
async goto(url) { await page.goto(url, { waitUntil: 'load' }); },
|
|
85
|
+
async snapshot() {
|
|
86
|
+
const tree = await page.accessibility.snapshot({ interestingOnly: false });
|
|
87
|
+
return renderA11yTree(tree);
|
|
88
|
+
},
|
|
89
|
+
async evaluate(fn, arg) {
|
|
90
|
+
if (typeof fn === 'function') return page.evaluate(fn, arg);
|
|
91
|
+
return page.evaluate(String(fn));
|
|
92
|
+
},
|
|
93
|
+
async click(selector) { await page.click(selector); },
|
|
94
|
+
async type(selector, text) { await page.fill(selector, text); },
|
|
95
|
+
async press(key) { await page.keyboard.press(key); },
|
|
96
|
+
async resize(width, height) {
|
|
97
|
+
const w = normaliseDim(width, 'width');
|
|
98
|
+
const h = normaliseDim(height, 'height');
|
|
99
|
+
await page.setViewportSize({ width: w, height: h });
|
|
100
|
+
},
|
|
101
|
+
async screenshot(filename) {
|
|
102
|
+
const target = filename ? { path: filename } : undefined;
|
|
103
|
+
const buf = await page.screenshot(target);
|
|
104
|
+
return filename ?? `data:image/png;base64,${buf.toString('base64')}`;
|
|
105
|
+
},
|
|
106
|
+
async close() {
|
|
107
|
+
logger('[pack-browser] closing project-playwright browser');
|
|
108
|
+
try { await page.close(); } catch { /* fine */ }
|
|
109
|
+
try { await context.close(); } catch { /* fine */ }
|
|
110
|
+
try { await browser.close(); } catch { /* fine */ }
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
} catch (err) {
|
|
114
|
+
logger(`[pack-browser] project-playwright route failed: ${err.message}`);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function renderA11yTree(node, depth = 0) {
|
|
120
|
+
if (!node) return '';
|
|
121
|
+
const pad = ' '.repeat(depth);
|
|
122
|
+
const parts = [];
|
|
123
|
+
parts.push(`${pad}${node.role ?? '?'} ${node.name ? JSON.stringify(node.name) : ''}`.trimEnd());
|
|
124
|
+
for (const child of node.children ?? []) parts.push(renderA11yTree(child, depth + 1));
|
|
125
|
+
return parts.join('\n');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function createMcpPackBrowser({ playwrightMcpVersion, logger, env, timeoutMs }) {
|
|
129
|
+
const child = spawn('npx', ['-y', `@playwright/mcp@${playwrightMcpVersion}`, '--headless', '--isolated'], {
|
|
130
|
+
env: { ...env },
|
|
131
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/** @type {Map<number, { resolve: (v: any) => void, reject: (e: Error) => void, timer: NodeJS.Timeout|null }>} */
|
|
135
|
+
const pending = new Map();
|
|
136
|
+
let nextId = 1;
|
|
137
|
+
let buffer = '';
|
|
138
|
+
let exited = false;
|
|
139
|
+
|
|
140
|
+
child.stdout.on('data', (chunk) => {
|
|
141
|
+
buffer += chunk.toString('utf8');
|
|
142
|
+
for (;;) {
|
|
143
|
+
const nl = buffer.indexOf('\n');
|
|
144
|
+
if (nl === -1) break;
|
|
145
|
+
const line = buffer.slice(0, nl).trim();
|
|
146
|
+
buffer = buffer.slice(nl + 1);
|
|
147
|
+
if (!line) continue;
|
|
148
|
+
let msg;
|
|
149
|
+
try { msg = JSON.parse(line); } catch { continue; }
|
|
150
|
+
if (msg.id != null && pending.has(msg.id)) {
|
|
151
|
+
const p = pending.get(msg.id);
|
|
152
|
+
pending.delete(msg.id);
|
|
153
|
+
if (p.timer) clearTimeout(p.timer);
|
|
154
|
+
if (msg.error) p.reject(new Error(msg.error.message ?? `MCP error ${JSON.stringify(msg.error)}`));
|
|
155
|
+
else p.resolve(msg.result);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
child.stderr.on('data', (chunk) => logger(`[playwright-mcp] ${chunk.toString('utf8').trimEnd()}`));
|
|
160
|
+
child.on('exit', (code) => {
|
|
161
|
+
exited = true;
|
|
162
|
+
for (const [, p] of pending) {
|
|
163
|
+
if (p.timer) clearTimeout(p.timer);
|
|
164
|
+
p.reject(new Error(`Playwright MCP exited (code=${code})`));
|
|
165
|
+
}
|
|
166
|
+
pending.clear();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
function rpc(method, params, { timeout = timeoutMs } = {}) {
|
|
170
|
+
if (exited) return Promise.reject(new Error('Playwright MCP process already exited'));
|
|
171
|
+
const id = nextId++;
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
const timer = setTimeout(() => {
|
|
174
|
+
if (pending.has(id)) {
|
|
175
|
+
pending.delete(id);
|
|
176
|
+
reject(new Error(`MCP method ${method} timed out after ${timeout}ms`));
|
|
177
|
+
}
|
|
178
|
+
}, timeout);
|
|
179
|
+
pending.set(id, { resolve, reject, timer });
|
|
180
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function tool(name, args) {
|
|
185
|
+
const result = await rpc('tools/call', { name, arguments: args ?? {} });
|
|
186
|
+
if (result?.isError) {
|
|
187
|
+
const text = textOf(result);
|
|
188
|
+
throw new Error(`Playwright MCP ${name} failed: ${text || JSON.stringify(result)}`);
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
await rpc('initialize', {
|
|
194
|
+
protocolVersion: '2024-11-05',
|
|
195
|
+
capabilities: {},
|
|
196
|
+
clientInfo: { name: 'rcf-lite-pack-browser', version: '0.0.0' },
|
|
197
|
+
}, { timeout: INIT_TIMEOUT_MS });
|
|
198
|
+
// Best-effort initialized notification (some servers ignore, that is fine).
|
|
199
|
+
try {
|
|
200
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' })}\n`);
|
|
201
|
+
} catch { /* fine */ }
|
|
202
|
+
|
|
203
|
+
return Object.freeze({
|
|
204
|
+
provisioning: { via: 'playwright-mcp', version: playwrightMcpVersion },
|
|
205
|
+
async goto(url) {
|
|
206
|
+
await tool('browser_navigate', { url });
|
|
207
|
+
},
|
|
208
|
+
async snapshot() {
|
|
209
|
+
return textOf(await tool('browser_snapshot', {}));
|
|
210
|
+
},
|
|
211
|
+
async evaluate(fn, arg) {
|
|
212
|
+
const src = composeEvaluateSource(fn, arg);
|
|
213
|
+
const text = textOf(await tool('browser_evaluate', { function: src }));
|
|
214
|
+
return parseEvaluateResult(text);
|
|
215
|
+
},
|
|
216
|
+
async click(selector, hint) {
|
|
217
|
+
await tool('browser_click', { target: selector, element: hint ?? selector });
|
|
218
|
+
},
|
|
219
|
+
async type(selector, text, hint) {
|
|
220
|
+
await tool('browser_type', { target: selector, text, element: hint ?? selector });
|
|
221
|
+
},
|
|
222
|
+
async press(key) {
|
|
223
|
+
await tool('browser_press_key', { key });
|
|
224
|
+
},
|
|
225
|
+
async resize(width, height) {
|
|
226
|
+
const w = normaliseDim(width, 'width');
|
|
227
|
+
const h = normaliseDim(height, 'height');
|
|
228
|
+
await tool('browser_resize', { width: w, height: h });
|
|
229
|
+
},
|
|
230
|
+
async screenshot(filename) {
|
|
231
|
+
const args = filename ? { filename } : {};
|
|
232
|
+
return textOf(await tool('browser_take_screenshot', args));
|
|
233
|
+
},
|
|
234
|
+
async close() {
|
|
235
|
+
logger('[pack-browser] closing playwright-mcp browser');
|
|
236
|
+
try { await tool('browser_close', {}); } catch { /* fine */ }
|
|
237
|
+
try { child.stdin.end(); } catch { /* fine */ }
|
|
238
|
+
await new Promise((resolve) => {
|
|
239
|
+
if (exited) return resolve();
|
|
240
|
+
const timer = setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* fine */ } resolve(); }, 5000);
|
|
241
|
+
child.once('exit', () => { clearTimeout(timer); resolve(); });
|
|
242
|
+
});
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Coerce a viewport dimension argument to a positive integer.
|
|
249
|
+
* Exposed for unit tests through resize() call sites.
|
|
250
|
+
*/
|
|
251
|
+
function normaliseDim(value, label) {
|
|
252
|
+
const n = Number(value);
|
|
253
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
254
|
+
throw new Error(`packBrowser.resize: ${label} must be a positive number (got ${JSON.stringify(value)})`);
|
|
255
|
+
}
|
|
256
|
+
return Math.round(n);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function textOf(result) {
|
|
260
|
+
const parts = Array.isArray(result?.content) ? result.content : [];
|
|
261
|
+
return parts.map((c) => (c?.type === 'text' ? (c.text ?? '') : '')).join('\n');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function composeEvaluateSource(fn, arg) {
|
|
265
|
+
if (typeof fn !== 'function') return String(fn);
|
|
266
|
+
if (arg === undefined) return fn.toString();
|
|
267
|
+
return `() => (${fn.toString()})(${JSON.stringify(arg)})`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The MCP browser_evaluate response is a human-readable text block, usually of
|
|
272
|
+
* the shape "- Ran Playwright code:\n... - Result: <json-ish>". Parse the trailing
|
|
273
|
+
* value out of it and JSON-parse when possible. Exported for unit tests.
|
|
274
|
+
*/
|
|
275
|
+
export function parseEvaluateResult(text) {
|
|
276
|
+
if (typeof text !== 'string') return text;
|
|
277
|
+
// MCP 0.0.80+ returns "### Result\n<value>\n### Ran Playwright code\n```js..."
|
|
278
|
+
const headed = text.match(/###\s*Result\s*\n([\s\S]*?)(?=\n###\s|\n?$)/);
|
|
279
|
+
if (headed) {
|
|
280
|
+
const inner = headed[1].trim().replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/i, '').trim();
|
|
281
|
+
try { return JSON.parse(inner); } catch { return inner; }
|
|
282
|
+
}
|
|
283
|
+
// Older MCP shape: "Result: <value>" trailing block.
|
|
284
|
+
const marker = text.lastIndexOf('Result:');
|
|
285
|
+
const tail = marker >= 0 ? text.slice(marker + 'Result:'.length).trim() : text.trim();
|
|
286
|
+
const stripped = tail.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/i, '').trim();
|
|
287
|
+
try { return JSON.parse(stripped); } catch { return stripped; }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Internals exposed to unit tests. Not part of the public seam a
|
|
292
|
+
* probe pack sees; do not import from a pack.
|
|
293
|
+
*/
|
|
294
|
+
export const PACK_BROWSER_INTERNALS = Object.freeze({ normaliseDim });
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Pack loader for blueprint-shipped probe packs
|
|
2
|
+
// (visual round T-0, US-1701 AC-1701-1 / 2 / 3). CN-107.
|
|
3
|
+
//
|
|
4
|
+
// Given a list of applied blueprints, discovers each blueprint's
|
|
5
|
+
// probe packs under `<blueprintAbsPath>/probe-packs/*.pack.{js,mjs}`,
|
|
6
|
+
// imports each module dynamically, validates against the pack schema
|
|
7
|
+
// (`pack-schema.js`), and cross-checks every check id against the AC
|
|
8
|
+
// ids the blueprint contributes via `blueprint.json:contributions[]`.
|
|
9
|
+
//
|
|
10
|
+
// The loader is pure with respect to the filesystem it reads: no
|
|
11
|
+
// writes, no side effects on the imported modules. It returns an
|
|
12
|
+
// aggregate result (`packs`, `warnings`, `errors`) so the CLI can
|
|
13
|
+
// emit one refusal diagnostic per fault rather than stop at the first.
|
|
14
|
+
|
|
15
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
16
|
+
import { existsSync } from 'node:fs';
|
|
17
|
+
import { join, resolve, isAbsolute } from 'node:path';
|
|
18
|
+
import { pathToFileURL } from 'node:url';
|
|
19
|
+
|
|
20
|
+
import { validatePackModule } from './pack-schema.js';
|
|
21
|
+
|
|
22
|
+
const PACK_FILENAME_RE = /\.pack\.(?:js|mjs)$/;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load and validate probe packs under a set of applied blueprints.
|
|
26
|
+
*
|
|
27
|
+
* @param {object} args
|
|
28
|
+
* @param {Array<{ slug: string, absPath: string }>} args.appliedBlueprints
|
|
29
|
+
* List of blueprints on the applied shelf. `absPath` is the
|
|
30
|
+
* directory containing the blueprint's `blueprint.json`.
|
|
31
|
+
* @param {string} [args.projectRoot] reserved; unused today.
|
|
32
|
+
* @returns {Promise<{ packs: Array<LoadedPack>, warnings: Array<{ packAbsPath: string, message: string }>, errors: Array<{ packAbsPath: string, message: string }> }>}
|
|
33
|
+
*/
|
|
34
|
+
export async function loadProbePacks({ appliedBlueprints, projectRoot: _projectRoot }) {
|
|
35
|
+
const packs = [];
|
|
36
|
+
const warnings = [];
|
|
37
|
+
const errors = [];
|
|
38
|
+
|
|
39
|
+
for (const bp of Array.isArray(appliedBlueprints) ? appliedBlueprints : []) {
|
|
40
|
+
if (!bp || typeof bp.slug !== 'string' || typeof bp.absPath !== 'string' || !isAbsolute(bp.absPath)) {
|
|
41
|
+
errors.push({
|
|
42
|
+
packAbsPath: '(no path)',
|
|
43
|
+
message: `pack-loader: blueprint entry must carry { slug: string, absPath: absolute path } (got ${JSON.stringify(bp)})`,
|
|
44
|
+
});
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const packsDir = join(bp.absPath, 'probe-packs');
|
|
48
|
+
if (!existsSync(packsDir)) continue;
|
|
49
|
+
|
|
50
|
+
let contributedAcIds;
|
|
51
|
+
try {
|
|
52
|
+
contributedAcIds = await readContributedAcIds({ blueprintAbsPath: bp.absPath });
|
|
53
|
+
} catch (err) {
|
|
54
|
+
errors.push({ packAbsPath: bp.absPath, message: `pack-loader: cannot read blueprint contributions for '${bp.slug}': ${err.message}` });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let entries;
|
|
59
|
+
try {
|
|
60
|
+
entries = await readdir(packsDir, { withFileTypes: true });
|
|
61
|
+
} catch (err) {
|
|
62
|
+
errors.push({ packAbsPath: packsDir, message: `pack-loader: cannot list probe-packs dir for '${bp.slug}': ${err.message}` });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const packFiles = entries
|
|
67
|
+
.filter((e) => e.isFile() && PACK_FILENAME_RE.test(e.name))
|
|
68
|
+
.map((e) => join(packsDir, e.name))
|
|
69
|
+
.sort();
|
|
70
|
+
|
|
71
|
+
for (const packAbsPath of packFiles) {
|
|
72
|
+
let mod;
|
|
73
|
+
try {
|
|
74
|
+
mod = await import(pathToFileURL(packAbsPath).href);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
errors.push({ packAbsPath, message: `pack-loader: import failed for ${packAbsPath}: ${err.message}` });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const validation = validatePackModule({ mod, blueprintSlug: bp.slug, packAbsPath });
|
|
80
|
+
if (!validation.ok) {
|
|
81
|
+
for (const e of validation.errors) errors.push({ packAbsPath, message: e.message });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const pack = validation.pack;
|
|
85
|
+
|
|
86
|
+
const badCheckIds = pack.checks
|
|
87
|
+
.map((c) => c.id)
|
|
88
|
+
.filter((id) => !contributedAcIds.has(id));
|
|
89
|
+
if (badCheckIds.length > 0) {
|
|
90
|
+
errors.push({
|
|
91
|
+
packAbsPath,
|
|
92
|
+
message: `pack-loader: pack '${pack.packName}' names checks [${badCheckIds.join(', ')}] that the blueprint '${bp.slug}' does not contribute; contributed AC ids on this blueprint: ${contributedAcIds.size === 0 ? '(none)' : [...contributedAcIds].sort().join(', ')}`,
|
|
93
|
+
});
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
packs.push({
|
|
98
|
+
packName: pack.packName,
|
|
99
|
+
version: pack.version,
|
|
100
|
+
blueprintSlug: bp.slug,
|
|
101
|
+
packAbsPath,
|
|
102
|
+
appliesTo: pack.appliesTo,
|
|
103
|
+
boot: pack.boot ?? null,
|
|
104
|
+
checks: pack.checks,
|
|
105
|
+
preChecks: Array.isArray(pack.preChecks) ? pack.preChecks : [],
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { packs, warnings, errors };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Read every AC id contributed by a blueprint. A blueprint contributes ACs
|
|
115
|
+
* inside its US contribution files (`contributions[].kind === 'us'`).
|
|
116
|
+
*
|
|
117
|
+
* @param {object} args
|
|
118
|
+
* @param {string} args.blueprintAbsPath
|
|
119
|
+
* @returns {Promise<Set<string>>}
|
|
120
|
+
*/
|
|
121
|
+
export async function readContributedAcIds({ blueprintAbsPath }) {
|
|
122
|
+
const bpJsonPath = join(blueprintAbsPath, 'blueprint.json');
|
|
123
|
+
const bpRaw = await readFile(bpJsonPath, 'utf8');
|
|
124
|
+
let bpJson;
|
|
125
|
+
try { bpJson = JSON.parse(bpRaw); } catch (err) {
|
|
126
|
+
throw new Error(`invalid JSON in ${bpJsonPath}: ${err.message}`);
|
|
127
|
+
}
|
|
128
|
+
const contributions = Array.isArray(bpJson?.contributions) ? bpJson.contributions : [];
|
|
129
|
+
const acIds = new Set();
|
|
130
|
+
for (const c of contributions) {
|
|
131
|
+
if (c?.kind !== 'us' || typeof c.path !== 'string') continue;
|
|
132
|
+
// blueprint.json contribution paths are relative to the blueprint's
|
|
133
|
+
// `contributions/` subdirectory (see src/blueprint/loader.js). Older
|
|
134
|
+
// tests supplied paths that included `contributions/` at the root
|
|
135
|
+
// instead; probe for both shapes so shelf blueprints and hand-rolled
|
|
136
|
+
// fixtures both load without a per-caller convention.
|
|
137
|
+
const insideContributions = resolve(blueprintAbsPath, 'contributions', c.path);
|
|
138
|
+
const legacyRootRelative = resolve(blueprintAbsPath, c.path);
|
|
139
|
+
let usAbs = insideContributions;
|
|
140
|
+
let usRaw;
|
|
141
|
+
try {
|
|
142
|
+
usRaw = await readFile(insideContributions, 'utf8');
|
|
143
|
+
} catch (primaryErr) {
|
|
144
|
+
try {
|
|
145
|
+
usRaw = await readFile(legacyRootRelative, 'utf8');
|
|
146
|
+
usAbs = legacyRootRelative;
|
|
147
|
+
} catch {
|
|
148
|
+
throw new Error(`cannot read US contribution ${c.path}: ${primaryErr.message}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
let usJson;
|
|
152
|
+
try {
|
|
153
|
+
usJson = JSON.parse(usRaw);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
throw new Error(`cannot read US contribution ${c.path}: ${err.message}`);
|
|
156
|
+
}
|
|
157
|
+
void usAbs;
|
|
158
|
+
const inlineAcs = Array.isArray(usJson?.acceptanceCriteria) ? usJson.acceptanceCriteria : [];
|
|
159
|
+
for (const ac of inlineAcs) {
|
|
160
|
+
if (typeof ac?.id === 'string') acIds.add(ac.id);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return acIds;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @typedef {object} LoadedPack
|
|
168
|
+
* @property {string} packName
|
|
169
|
+
* @property {string} version
|
|
170
|
+
* @property {string} blueprintSlug
|
|
171
|
+
* @property {string} packAbsPath
|
|
172
|
+
* @property {(ctx: { fbs: object, uiBaseline: object|null, manifest: object|null }) => boolean} appliesTo
|
|
173
|
+
* @property {object|null} boot
|
|
174
|
+
* @property {Array<{ id: string, severity: 'block'|'warn'|'advisory', description: string, run: (ctx: object) => Promise<{ verdict: 'pass'|'warn'|'fail'|'skipped', detail?: string }>, dependsOn?: string|string[] }>} checks
|
|
175
|
+
* @property {Array<{ id: string, severity: 'block'|'warn'|'advisory', description: string, run: () => Promise<{ verdict: 'pass'|'warn'|'fail', detail?: string }> }>} preChecks
|
|
176
|
+
*/
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Pack pass runner (visual round T-0, US-1701 AC-1701-4 / 6). CN-108.
|
|
2
|
+
//
|
|
3
|
+
// Invokes every applicable loaded pack against a FBS and folds the
|
|
4
|
+
// results into a `probePacks[]` array on the browserVerification
|
|
5
|
+
// record. Applicability is enforced by the pack's own `appliesTo`
|
|
6
|
+
// predicate: a false return records `applicable: false` and no
|
|
7
|
+
// check runs. `preChecks[]` (if any) run first; a failing pre-check
|
|
8
|
+
// causes browser checks that name it under `dependsOn` to record
|
|
9
|
+
// `skipped` with detail `skipped-by-pre-check:<preCheckId>`.
|
|
10
|
+
//
|
|
11
|
+
// The runner is browser-driver-agnostic: the caller injects a
|
|
12
|
+
// `browser` seam (the same shape a pack's `run` expects on its `browser`
|
|
13
|
+
// argument) plus `fetch`, `runtimeUrl` and the route/theme cursor.
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Run applicable packs for one FBS.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} args
|
|
19
|
+
* @param {Array<import('./pack-loader.js').LoadedPack>} args.packs
|
|
20
|
+
* @param {object} args.fbs
|
|
21
|
+
* @param {object|null} args.uiBaseline
|
|
22
|
+
* @param {object|null} args.manifest
|
|
23
|
+
* @param {object|null} args.browser injected browser (or null in unit tests)
|
|
24
|
+
* @param {(url: string, init?: object) => Promise<any>|null} args.fetch
|
|
25
|
+
* @param {string} args.runtimeUrl
|
|
26
|
+
* @param {Array<object>} args.routes FBS routes (nav model)
|
|
27
|
+
* @param {Array<'light'|'dark'>} args.themes
|
|
28
|
+
* @param {string} [args.packNameFilter] when set, only packs whose packName === filter are considered
|
|
29
|
+
* @param {(pack: import('./pack-loader.js').LoadedPack) => object} [args.buildContext] optional context override for unit tests
|
|
30
|
+
* @returns {Promise<{ probePacks: Array<object>, packHighestSeverityFail: 'block'|'warn'|null }>}
|
|
31
|
+
*/
|
|
32
|
+
export async function runProbePacksForFbs({
|
|
33
|
+
packs,
|
|
34
|
+
fbs,
|
|
35
|
+
uiBaseline,
|
|
36
|
+
manifest,
|
|
37
|
+
projectRoot = null,
|
|
38
|
+
browser,
|
|
39
|
+
fetch,
|
|
40
|
+
runtimeUrl,
|
|
41
|
+
routes,
|
|
42
|
+
themes,
|
|
43
|
+
packNameFilter,
|
|
44
|
+
buildContext,
|
|
45
|
+
}) {
|
|
46
|
+
const probePacks = [];
|
|
47
|
+
let highest = null;
|
|
48
|
+
const chosen = Array.isArray(packs) ? packs : [];
|
|
49
|
+
const primaryRoute = Array.isArray(routes) && routes.length > 0 ? routes[0] : null;
|
|
50
|
+
const primaryTheme = Array.isArray(themes) && themes.length > 0 ? themes[0] : 'light';
|
|
51
|
+
|
|
52
|
+
for (const pack of chosen) {
|
|
53
|
+
if (typeof packNameFilter === 'string' && packNameFilter.length > 0 && pack.packName !== packNameFilter) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let applicable;
|
|
58
|
+
let applicabilityDetail;
|
|
59
|
+
try {
|
|
60
|
+
applicable = Boolean(pack.appliesTo({ fbs, uiBaseline, manifest }));
|
|
61
|
+
} catch (err) {
|
|
62
|
+
applicable = false;
|
|
63
|
+
applicabilityDetail = `appliesTo threw: ${err.message}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!applicable) {
|
|
67
|
+
probePacks.push({
|
|
68
|
+
packName: pack.packName,
|
|
69
|
+
packVersion: pack.version,
|
|
70
|
+
blueprintSlug: pack.blueprintSlug,
|
|
71
|
+
applicable: false,
|
|
72
|
+
...(applicabilityDetail ? { detail: applicabilityDetail } : { detail: `appliesTo returned false for ${fbs?.fbsId ?? '(no fbs)'}` }),
|
|
73
|
+
checks: [],
|
|
74
|
+
});
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Pre-checks first.
|
|
79
|
+
const preCheckRecords = [];
|
|
80
|
+
const failedPreCheckIds = new Set();
|
|
81
|
+
for (const pre of pack.preChecks) {
|
|
82
|
+
let verdict = 'pass';
|
|
83
|
+
let detail;
|
|
84
|
+
try {
|
|
85
|
+
const outcome = await pre.run({ fbs, uiBaseline, manifest, runtimeUrl, fetch, browser });
|
|
86
|
+
verdict = outcome?.verdict ?? 'pass';
|
|
87
|
+
detail = outcome?.detail;
|
|
88
|
+
} catch (err) {
|
|
89
|
+
verdict = 'fail';
|
|
90
|
+
detail = `pre-check threw: ${err.message}`;
|
|
91
|
+
}
|
|
92
|
+
preCheckRecords.push({
|
|
93
|
+
id: pre.id,
|
|
94
|
+
verdict,
|
|
95
|
+
severity: pre.severity,
|
|
96
|
+
...(typeof detail === 'string' && detail.length > 0 ? { detail } : {}),
|
|
97
|
+
});
|
|
98
|
+
if (verdict === 'fail') failedPreCheckIds.add(pre.id);
|
|
99
|
+
if (verdict === 'fail' && pre.severity === 'block') highest = 'block';
|
|
100
|
+
else if (verdict === 'fail' && pre.severity === 'warn' && highest !== 'block') highest = 'warn';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const checkRecords = [];
|
|
104
|
+
for (const check of pack.checks) {
|
|
105
|
+
const deps = normaliseDependsOn(check.dependsOn);
|
|
106
|
+
const failingDep = deps.find((d) => failedPreCheckIds.has(d));
|
|
107
|
+
if (failingDep) {
|
|
108
|
+
checkRecords.push({
|
|
109
|
+
id: check.id,
|
|
110
|
+
verdict: 'skipped',
|
|
111
|
+
severity: check.severity,
|
|
112
|
+
detail: `skipped-by-pre-check:${failingDep}`,
|
|
113
|
+
});
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// Per-check applicability (visual round T-5 spec section 5.5).
|
|
117
|
+
// A pack check may declare its own `appliesTo` predicate that
|
|
118
|
+
// reads the applied capability set (from projectRoot) so an
|
|
119
|
+
// absent capability records `applicable: false` and the
|
|
120
|
+
// aggregate verdict treats the check as neither pass nor fail.
|
|
121
|
+
// A check without a predicate always runs (backward compatible
|
|
122
|
+
// with T-0 through T-4 packs).
|
|
123
|
+
if (typeof check.appliesTo === 'function') {
|
|
124
|
+
let checkApplicable;
|
|
125
|
+
let checkAppliesDetail;
|
|
126
|
+
try {
|
|
127
|
+
checkApplicable = Boolean(await check.appliesTo({ fbs, uiBaseline, manifest, projectRoot }));
|
|
128
|
+
} catch (err) {
|
|
129
|
+
checkApplicable = false;
|
|
130
|
+
checkAppliesDetail = `appliesTo threw: ${err.message}`;
|
|
131
|
+
}
|
|
132
|
+
if (!checkApplicable) {
|
|
133
|
+
// Spec section 5.5 says the residual cure records
|
|
134
|
+
// applicable: false at the check level. rcf-schemas 0.6.1
|
|
135
|
+
// (browserVerificationProbePackCheck) closes the schema and
|
|
136
|
+
// still requires a verdict enum, so the manifest write path
|
|
137
|
+
// refuses a check that carries `applicable: false` alone.
|
|
138
|
+
// Until the schema minor promotes an `applicable` field at
|
|
139
|
+
// the check level (follow-up rcf-schemas bump), emit
|
|
140
|
+
// `verdict: 'skipped'` with a detail naming the applicability
|
|
141
|
+
// gate. Semantically the aggregate verdict still treats this
|
|
142
|
+
// as neither pass nor fail (skipped does not raise `highest`).
|
|
143
|
+
checkRecords.push({
|
|
144
|
+
id: check.id,
|
|
145
|
+
verdict: 'skipped',
|
|
146
|
+
severity: check.severity,
|
|
147
|
+
detail: checkAppliesDetail ?? 'check appliesTo returned false (required capability not applied)',
|
|
148
|
+
});
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const context = typeof buildContext === 'function'
|
|
153
|
+
? buildContext(pack)
|
|
154
|
+
: { browser, fetch, runtimeUrl, route: primaryRoute, theme: primaryTheme, projectRoot };
|
|
155
|
+
let verdict = 'pass';
|
|
156
|
+
let detail;
|
|
157
|
+
try {
|
|
158
|
+
const outcome = await check.run(context);
|
|
159
|
+
verdict = outcome?.verdict ?? 'pass';
|
|
160
|
+
detail = outcome?.detail;
|
|
161
|
+
} catch (err) {
|
|
162
|
+
verdict = 'fail';
|
|
163
|
+
detail = `check threw: ${err.message}`;
|
|
164
|
+
}
|
|
165
|
+
checkRecords.push({
|
|
166
|
+
id: check.id,
|
|
167
|
+
verdict,
|
|
168
|
+
severity: check.severity,
|
|
169
|
+
...(typeof detail === 'string' && detail.length > 0 ? { detail } : {}),
|
|
170
|
+
});
|
|
171
|
+
if (verdict === 'fail' && check.severity === 'block') highest = 'block';
|
|
172
|
+
else if (verdict === 'fail' && check.severity === 'warn' && highest !== 'block') highest = 'warn';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const record = {
|
|
176
|
+
packName: pack.packName,
|
|
177
|
+
packVersion: pack.version,
|
|
178
|
+
blueprintSlug: pack.blueprintSlug,
|
|
179
|
+
applicable: true,
|
|
180
|
+
checks: checkRecords,
|
|
181
|
+
};
|
|
182
|
+
if (preCheckRecords.length > 0) record.preChecks = preCheckRecords;
|
|
183
|
+
probePacks.push(record);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { probePacks, packHighestSeverityFail: highest };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function normaliseDependsOn(dependsOn) {
|
|
190
|
+
if (typeof dependsOn === 'string' && dependsOn.length > 0) return [dependsOn];
|
|
191
|
+
if (Array.isArray(dependsOn)) return dependsOn.filter((d) => typeof d === 'string' && d.length > 0);
|
|
192
|
+
return [];
|
|
193
|
+
}
|