diff --git a/.planning/milestones/v0.11.0-phases/20-integration-cap-ui-docs-edge-cases/20-PATTERNS.md b/.planning/milestones/v0.11.0-phases/20-integration-cap-ui-docs-edge-cases/20-PATTERNS.md index 7bb889d77..529fdd1ea 100644 --- a/.planning/milestones/v0.11.0-phases/20-integration-cap-ui-docs-edge-cases/20-PATTERNS.md +++ b/.planning/milestones/v0.11.0-phases/20-integration-cap-ui-docs-edge-cases/20-PATTERNS.md @@ -787,7 +787,7 @@ check(/fsbTriggerHandleRefreshPollForTest/.test(src), 'test-only refresh-poll al "": { "name": "fsb-mcp-server", "version": "0.8.0", - "license": "BUSL-1.1" + "license": "MIT" } } } diff --git a/.planning/milestones/v0.9.62-phases/258-removal-migration-errors-package-0.9.0/258-02-PLAN.md b/.planning/milestones/v0.9.62-phases/258-removal-migration-errors-package-0.9.0/258-02-PLAN.md index ddf4c0319..496025922 100644 --- a/.planning/milestones/v0.9.62-phases/258-removal-migration-errors-package-0.9.0/258-02-PLAN.md +++ b/.planning/milestones/v0.9.62-phases/258-removal-migration-errors-package-0.9.0/258-02-PLAN.md @@ -129,7 +129,7 @@ From mcp/package.json (lines 1-6, the relevant top of file -- only line 3 `versi "name": "fsb-mcp-server", "version": "0.8.0", "description": "FSB Browser Automation MCP Server", - "license": "BUSL-1.1", + "license": "MIT", "author": "Lakshman Turlapati", ``` @@ -355,7 +355,7 @@ The build command runs `tsc` (compiles all .ts under mcp/src/ to mcp/build/) AND 6. Non-version content in each edited file is unchanged. Verified by: - mcp/src/version.ts: `grep -c "FSB_SERVER_NAME\\|FSB_EXTENSION_BRIDGE_PORT\\|FSB_EXTENSION_BRIDGE_URL\\|DEFAULT_HTTP_HOST\\|DEFAULT_HTTP_PORT\\|FSB_REGISTRY_NAME" mcp/src/version.ts` returns at least 6 (one per other constant). - - mcp/package.json: `node -e "const p = require('./mcp/package.json'); console.log(p.name, p.license, p.scripts.build.length > 0)"` prints `fsb-mcp-server BUSL-1.1 true`. + - mcp/package.json: `node -e "const p = require('./mcp/package.json'); console.log(p.name, p.license, p.scripts.build.length > 0)"` prints `fsb-mcp-server MIT true`. - mcp/server.json: `node -e "const s = require('./mcp/server.json'); console.log(s.name, s.title, s.packages.length, s.packages[0].registryType)"` prints `io.github.lakshmanturlapati/fsb-mcp-server FSB MCP Server 1 npm`. - tests/mcp-version-parity.test.js: `grep -c "extractRuntimeVersion\\|collectExplicitVersions\\|runCommand" tests/mcp-version-parity.test.js` returns at least 3 (one per helper function). diff --git a/.planning/milestones/v1.0.0-phases/36-codegen-pipeline-no-dead-entry-resolution/36-REVIEW.md b/.planning/milestones/v1.0.0-phases/36-codegen-pipeline-no-dead-entry-resolution/36-REVIEW.md index edb8d8333..b2a7bbed8 100644 --- a/.planning/milestones/v1.0.0-phases/36-codegen-pipeline-no-dead-entry-resolution/36-REVIEW.md +++ b/.planning/milestones/v1.0.0-phases/36-codegen-pipeline-no-dead-entry-resolution/36-REVIEW.md @@ -275,13 +275,13 @@ The override only works because the slug-suffix branch (lines 179-188) saves it. --- -#### LO-03: `package.json` version/license/badge metadata is internally inconsistent (pre-existing, surfaced by this phase's audit) +#### LO-03: `package.json` version/package filename metadata is internally inconsistent (pre-existing, surfaced by this phase's audit) -**File:** `package.json:3` (`"version": "0.9.90"`), `:71` (`"license": "BUSL-1.1"`), `:133` (badge `license-MIT`), `:24` (`package` script hardcodes `fsb-v0.9.90.zip`) +**File:** `package.json:3` (`"version": "0.9.90"`), `:71` (`"license": "MIT"`), `:133` (badge `license-MIT`), `:24` (`package` script hardcodes `fsb-v0.9.90.zip`) -**Issue:** The package declares `version: 0.9.90` while the milestone/requirements target v1.0.0; the SPDX `license` is `BUSL-1.1` but the README badge advertises `license-MIT`; the `package` zip name is hardcoded to the version string (drifts silently on bump). None is a Phase-36 regression, but the OpenTabs descriptors this phase vendors are stamped `license: 'MIT'` in-descriptor while the package is BUSL-1.1, so the license story now spans two values and deserves a deliberate note. Not a code defect. +**Issue:** The package declares `version: 0.9.90` while the milestone/requirements target v1.0.0, and the `package` zip name is hardcoded to the version string (drifts silently on bump). The SPDX field, README badge, and vendored OpenTabs descriptors now consistently advertise MIT. The remaining version drift is not a Phase-36 regression or a code defect. -**Fix:** Out of Phase-36 scope; recommend a metadata-reconciliation pass (align the SPDX license with the badge, parameterize the zip name) tracked separately. +**Fix:** Out of Phase-36 scope; recommend a version-metadata reconciliation pass that parameterizes the zip name. --- diff --git a/.planning/phases/36-codegen-pipeline-no-dead-entry-resolution/36-REVIEW.md b/.planning/phases/36-codegen-pipeline-no-dead-entry-resolution/36-REVIEW.md index edb8d8333..b2a7bbed8 100644 --- a/.planning/phases/36-codegen-pipeline-no-dead-entry-resolution/36-REVIEW.md +++ b/.planning/phases/36-codegen-pipeline-no-dead-entry-resolution/36-REVIEW.md @@ -275,13 +275,13 @@ The override only works because the slug-suffix branch (lines 179-188) saves it. --- -#### LO-03: `package.json` version/license/badge metadata is internally inconsistent (pre-existing, surfaced by this phase's audit) +#### LO-03: `package.json` version/package filename metadata is internally inconsistent (pre-existing, surfaced by this phase's audit) -**File:** `package.json:3` (`"version": "0.9.90"`), `:71` (`"license": "BUSL-1.1"`), `:133` (badge `license-MIT`), `:24` (`package` script hardcodes `fsb-v0.9.90.zip`) +**File:** `package.json:3` (`"version": "0.9.90"`), `:71` (`"license": "MIT"`), `:133` (badge `license-MIT`), `:24` (`package` script hardcodes `fsb-v0.9.90.zip`) -**Issue:** The package declares `version: 0.9.90` while the milestone/requirements target v1.0.0; the SPDX `license` is `BUSL-1.1` but the README badge advertises `license-MIT`; the `package` zip name is hardcoded to the version string (drifts silently on bump). None is a Phase-36 regression, but the OpenTabs descriptors this phase vendors are stamped `license: 'MIT'` in-descriptor while the package is BUSL-1.1, so the license story now spans two values and deserves a deliberate note. Not a code defect. +**Issue:** The package declares `version: 0.9.90` while the milestone/requirements target v1.0.0, and the `package` zip name is hardcoded to the version string (drifts silently on bump). The SPDX field, README badge, and vendored OpenTabs descriptors now consistently advertise MIT. The remaining version drift is not a Phase-36 regression or a code defect. -**Fix:** Out of Phase-36 scope; recommend a metadata-reconciliation pass (align the SPDX license with the badge, parameterize the zip name) tracked separately. +**Fix:** Out of Phase-36 scope; recommend a version-metadata reconciliation pass that parameterizes the zip name. --- diff --git a/.planning/phases/53-trans-unit-resync-stats-translation-transcreation-review/53-TRANSCREATION.md b/.planning/phases/53-trans-unit-resync-stats-translation-transcreation-review/53-TRANSCREATION.md index f8fee1a5a..ce23dc1ea 100644 --- a/.planning/phases/53-trans-unit-resync-stats-translation-transcreation-review/53-TRANSCREATION.md +++ b/.planning/phases/53-trans-unit-resync-stats-translation-transcreation-review/53-TRANSCREATION.md @@ -12,7 +12,7 @@ | `home.hero.subtitle` | Shorter, spoken marketing voice | | `home.hero.cta.getStarted` | CTA punch-up (e.g. “Empezar ahora” / “Jetzt starten”) | | `home.hero.cta.seeAction` | Natural “see it work” CTA | -| `home.cta.openSource.title` | Open-source + BSL 1.1 line, brand span intact | +| `home.cta.openSource.title` | Open-source + MIT line, brand span intact | | `home.cta.openSource.desc` | Fork/contribute framing | | `home.cta.openSource.github` | “View on GitHub” localized | | `about.hero.title` | “See FSB in Action” with orange span | diff --git a/.planning/quick/260514-1nv-showcase-stats-page-footer-only-easter-e/260514-1nv-PLAN.md b/.planning/quick/260514-1nv-showcase-stats-page-footer-only-easter-e/260514-1nv-PLAN.md index aaf9fdd76..8d740bd9b 100644 --- a/.planning/quick/260514-1nv-showcase-stats-page-footer-only-easter-e/260514-1nv-PLAN.md +++ b/.planning/quick/260514-1nv-showcase-stats-page-footer-only-easter-e/260514-1nv-PLAN.md @@ -143,10 +143,10 @@ From showcase/angular/src/app/layout/showcase-shell/showcase-shell.component.htm

Project

GitHub Issues - BSL 1.1 License + MIT License ``` -The new "Stats" link is added inside this same ` diff --git a/showcase/angular/public/llms-full.txt b/showcase/angular/public/llms-full.txt index 5b85b2f5a..3335b6eb5 100644 --- a/showcase/angular/public/llms-full.txt +++ b/showcase/angular/public/llms-full.txt @@ -3,7 +3,9 @@ ## 1. Project Description -FSB (Full Self-Browsing) is an open-source Chrome extension that automates the browser through natural language. You describe a task in plain English; FSB plans the clicks, types, and navigation to complete it. The extension runs in your browser with your own API keys, encrypted local storage, and opt-out anonymous usage telemetry for aggregate public stats only. Multi-model AI (xAI Grok, OpenAI, Anthropic Claude, Google Gemini, OpenRouter, LM Studio, custom OpenAI-compatible endpoints), 56 canonical browser tools, a 66-tool MCP surface, 142+ site-specific guides, reactive trigger watchers, real file uploads, and a 128-app native capability catalog for verified first-party API execution. Current release: FSB v0.9.90 (Chrome extension) with fsb-mcp-server 0.10.0 (npm MCP package). BSL 1.1 licensed. +FSB (Full Self-Browsing) is an open-source Chrome extension that automates the browser through natural language. You describe a task in plain English; FSB plans the clicks, types, and navigation to complete it. The extension runs in your browser with your own API keys, encrypted local storage, and opt-out anonymous usage telemetry for aggregate public stats only. Multi-model AI (xAI Grok, OpenAI, Anthropic Claude, Google Gemini, OpenRouter, LM Studio, custom OpenAI-compatible endpoints), 56 canonical browser tools, a 66-tool MCP surface, 142+ site-specific guides, reactive trigger watchers, real file uploads, and a 128-app native capability catalog for verified first-party API execution. Current release: FSB v0.9.90 (Chrome extension) with fsb-mcp-server 0.10.0 (npm MCP package). + +License: [MIT License](https://github.com/fullselfbrowsing/FSB/blob/main/LICENSE). FSB also works especially well as an MCP browser layer for AI agents. Claude Code, Codex, Cursor, VS Code, Windsurf, OpenClaw, Hermes, and other MCP clients can use FSB to operate a real Chrome browser, inspect page state, verify outcomes, and feed observations back into their own coding or autonomy loop. diff --git a/showcase/angular/public/llms.txt b/showcase/angular/public/llms.txt index 6a1a12258..fd25611ef 100644 --- a/showcase/angular/public/llms.txt +++ b/showcase/angular/public/llms.txt @@ -3,6 +3,8 @@ FSB (Full Self-Browsing) is an open-source Chrome extension that automates the browser through natural language. You describe a task; FSB plans the clicks, types, and navigation to complete it. Multi-model AI (xAI, OpenAI, Anthropic, Gemini, OpenRouter, LM Studio, local), 56 browser tools, a 66-tool MCP surface, 142+ site guides, local-first execution, and BYO API keys. Current release: FSB v0.9.90 (extension) with fsb-mcp-server 0.10.0 (npm). +License: [MIT License](https://github.com/fullselfbrowsing/FSB/blob/main/LICENSE). + FSB also works as an MCP browser layer for AI agents. Claude Code, Codex, Cursor, VS Code, Windsurf, OpenClaw, Hermes, and other MCP clients can use FSB to operate a real Chrome browser, inspect page state, verify outcomes, and keep the calling agent in the loop. The MCP surface includes trigger watchers (`trigger`, `stop_trigger`, `get_trigger_status`, `list_triggers`) for reactive DOM monitoring, real local file uploads (`upload_file`) and synthetic dropzone support (`drop_file`), and a 128-app native capability catalog where `search_capabilities` finds verified first-party API actions and `invoke_capability` runs them against the user's logged-in sessions with readiness labels such as `t1-ready`, `t1-guarded-fail-closed`, `learn-pending`, and `discovery-pending`, plus audit records. The vault boundary is explicit: credentials and payment methods resolve inside the encrypted Chrome extension store through tools such as `fill_credential` and `use_payment_method`; raw passwords, card numbers, and secrets do not cross the MCP bridge. One-command install: `npx -y fsb-mcp-server install --claude-code` (also `--claude-desktop`, `--cursor`, `--vscode`, `--windsurf`, `--codex`, `--all`). @@ -36,7 +38,7 @@ Unlike cloud browser agents that run in a headless container without your cookie - [Site Maps](https://full-selfbrowsing.com/sitemaps): Community site maps hub (under development) -- contribute well-tested site schemas that can become built-in browser knowledge. - [Legal](https://full-selfbrowsing.com/legal): Public non-indexed legal reference for automation posture, consent model, audit-log retention, and service-denylist rationale. It is intentionally excluded from sitemap and indexed SEO surfaces. - [Chrome Web Store](https://chromewebstore.google.com/detail/badgafnfchcihdfnjneklogedcdkmjfk): One-click extension install. -- [GitHub repository](https://github.com/fullselfbrowsing/FSB): BSL 1.1 source code, install instructions, issue tracker. +- [GitHub repository](https://github.com/fullselfbrowsing/FSB): MIT source code, install instructions, issue tracker. - [llms-full.txt](https://full-selfbrowsing.com/llms-full.txt): Long-form project dump for LLM ingestion -- capabilities, install, comparison framing. ## Demo Videos diff --git a/showcase/angular/scripts/llms-full.source.md b/showcase/angular/scripts/llms-full.source.md index b691a46c2..c0f30a6a5 100644 --- a/showcase/angular/scripts/llms-full.source.md +++ b/showcase/angular/scripts/llms-full.source.md @@ -2,7 +2,9 @@ ## 1. Project Description -FSB (Full Self-Browsing) is an open-source Chrome extension that automates the browser through natural language. You describe a task in plain English; FSB plans the clicks, types, and navigation to complete it. The extension runs in your browser with your own API keys, encrypted local storage, and opt-out anonymous usage telemetry for aggregate public stats only. Multi-model AI (xAI Grok, OpenAI, Anthropic Claude, Google Gemini, OpenRouter, LM Studio, custom OpenAI-compatible endpoints), 56 canonical browser tools, a 66-tool MCP surface, 142+ site-specific guides, reactive trigger watchers, real file uploads, and a 128-app native capability catalog for verified first-party API execution. Current release: FSB v0.9.90 (Chrome extension) with fsb-mcp-server 0.10.0 (npm MCP package). BSL 1.1 licensed. +FSB (Full Self-Browsing) is an open-source Chrome extension that automates the browser through natural language. You describe a task in plain English; FSB plans the clicks, types, and navigation to complete it. The extension runs in your browser with your own API keys, encrypted local storage, and opt-out anonymous usage telemetry for aggregate public stats only. Multi-model AI (xAI Grok, OpenAI, Anthropic Claude, Google Gemini, OpenRouter, LM Studio, custom OpenAI-compatible endpoints), 56 canonical browser tools, a 66-tool MCP surface, 142+ site-specific guides, reactive trigger watchers, real file uploads, and a 128-app native capability catalog for verified first-party API execution. Current release: FSB v0.9.90 (Chrome extension) with fsb-mcp-server 0.10.0 (npm MCP package). + +License: [MIT License](https://github.com/fullselfbrowsing/FSB/blob/main/LICENSE). FSB also works especially well as an MCP browser layer for AI agents. Claude Code, Codex, Cursor, VS Code, Windsurf, OpenClaw, Hermes, and other MCP clients can use FSB to operate a real Chrome browser, inspect page state, verify outcomes, and feed observations back into their own coding or autonomy loop. diff --git a/showcase/angular/scripts/llms.source.md b/showcase/angular/scripts/llms.source.md index 845a30201..69cecb7a8 100644 --- a/showcase/angular/scripts/llms.source.md +++ b/showcase/angular/scripts/llms.source.md @@ -2,6 +2,8 @@ FSB (Full Self-Browsing) is an open-source Chrome extension that automates the browser through natural language. You describe a task; FSB plans the clicks, types, and navigation to complete it. Multi-model AI (xAI, OpenAI, Anthropic, Gemini, OpenRouter, LM Studio, local), 56 browser tools, a 66-tool MCP surface, 142+ site guides, local-first execution, and BYO API keys. Current release: FSB v0.9.90 (extension) with fsb-mcp-server 0.10.0 (npm). +License: [MIT License](https://github.com/fullselfbrowsing/FSB/blob/main/LICENSE). + FSB also works as an MCP browser layer for AI agents. Claude Code, Codex, Cursor, VS Code, Windsurf, OpenClaw, Hermes, and other MCP clients can use FSB to operate a real Chrome browser, inspect page state, verify outcomes, and keep the calling agent in the loop. The MCP surface includes trigger watchers (`trigger`, `stop_trigger`, `get_trigger_status`, `list_triggers`) for reactive DOM monitoring, real local file uploads (`upload_file`) and synthetic dropzone support (`drop_file`), and a 128-app native capability catalog where `search_capabilities` finds verified first-party API actions and `invoke_capability` runs them against the user's logged-in sessions with readiness labels such as `t1-ready`, `t1-guarded-fail-closed`, `learn-pending`, and `discovery-pending`, plus audit records. The vault boundary is explicit: credentials and payment methods resolve inside the encrypted Chrome extension store through tools such as `fill_credential` and `use_payment_method`; raw passwords, card numbers, and secrets do not cross the MCP bridge. One-command install: `npx -y fsb-mcp-server install --claude-code` (also `--claude-desktop`, `--cursor`, `--vscode`, `--windsurf`, `--codex`, `--all`). @@ -35,7 +37,7 @@ Unlike cloud browser agents that run in a headless container without your cookie - [Site Maps](https://full-selfbrowsing.com/sitemaps): Community site maps hub (under development) -- contribute well-tested site schemas that can become built-in browser knowledge. - [Legal](https://full-selfbrowsing.com/legal): Public non-indexed legal reference for automation posture, consent model, audit-log retention, and service-denylist rationale. It is intentionally excluded from sitemap and indexed SEO surfaces. - [Chrome Web Store](https://chromewebstore.google.com/detail/badgafnfchcihdfnjneklogedcdkmjfk): One-click extension install. -- [GitHub repository](https://github.com/fullselfbrowsing/FSB): BSL 1.1 source code, install instructions, issue tracker. +- [GitHub repository](https://github.com/fullselfbrowsing/FSB): MIT source code, install instructions, issue tracker. - [llms-full.txt](https://full-selfbrowsing.com/llms-full.txt): Long-form project dump for LLM ingestion -- capabilities, install, comparison framing. ## Demo Videos diff --git a/showcase/angular/scripts/smoke-crawler.mjs b/showcase/angular/scripts/smoke-crawler.mjs index 788ea2b78..eb2715180 100644 --- a/showcase/angular/scripts/smoke-crawler.mjs +++ b/showcase/angular/scripts/smoke-crawler.mjs @@ -1,8 +1,8 @@ #!/usr/bin/env node // Phase 216 SMOKE-01..03: production-or-local crawler smoke. // Curls marketing routes under GPTBot UA; curls crawler files; resolves every -// sitemap . Asserts route-specific titles, canonicals, JSON-LD presence -// (home), Content-Type headers, and llms-full.txt size budget. +// sitemap . Asserts route-specific titles, canonicals, licensed JSON-LD +// software metadata, Content-Type headers, and llms-full.txt size budget. // // Run: // npm --prefix showcase/angular run smoke:crawler:local (local static build) @@ -15,6 +15,7 @@ const BASE_URL = (process.env.BASE_URL || 'https://full-selfbrowsing.com').replace(/\/$/, ''); const PROD_HOST = 'https://full-selfbrowsing.com'; +const LICENSE_URL = 'https://github.com/fullselfbrowsing/FSB/blob/main/LICENSE'; const UA = 'GPTBot'; const failures = []; @@ -39,14 +40,21 @@ async function fetchText(url, opts = {}) { return { status: res.status, contentType: res.headers.get('content-type') || '', body }; } +// licensedSoftware, where present, selects the SoftwareApplication node whose +// `license` must be the canonical MIT URL. Routes without one skip the JSON-LD asserts. const MARKETING_ASSERTIONS = [ - { path: '/', titleSubstr: 'Full Self-Browsing', canonical: `${PROD_HOST}` }, + { path: '/', titleSubstr: 'Full Self-Browsing', canonical: `${PROD_HOST}`, + licensedSoftware: (node) => node['@type'] === 'SoftwareApplication' && node.name === 'FSB' }, { path: '/about', titleSubstr: 'About', canonical: `${PROD_HOST}/about` }, - { path: '/agents', titleSubstr: 'Agents', canonical: `${PROD_HOST}/agents` }, + { path: '/agents', titleSubstr: 'Agents', canonical: `${PROD_HOST}/agents`, + licensedSoftware: (node) => node['@type'] === 'SoftwareApplication' + && node['@id'] === `${PROD_HOST}/agents#fsb-skill` }, { path: '/support', titleSubstr: 'Support', canonical: `${PROD_HOST}/support` }, { path: '/privacy', titleSubstr: 'Privacy', canonical: `${PROD_HOST}/privacy` }, { path: '/lattice', titleSubstr: 'Lattice', canonical: `${PROD_HOST}/lattice` }, - { path: '/concierge', titleSubstr: 'Concierge', canonical: `${PROD_HOST}/concierge` }, + { path: '/concierge', titleSubstr: 'Concierge', canonical: `${PROD_HOST}/concierge`, + licensedSoftware: (node) => node['@type'] === 'SoftwareApplication' + && node['@id'] === `${PROD_HOST}/concierge#concierge-sdk` }, { path: '/phantom-stream', titleSubstr: 'PhantomStream', canonical: `${PROD_HOST}/phantom-stream` }, { path: '/prometheus', titleSubstr: 'Prometheus', canonical: `${PROD_HOST}/prometheus` }, { path: '/sitemaps', titleSubstr: 'Site Maps', canonical: `${PROD_HOST}/sitemaps` }, @@ -75,14 +83,43 @@ const AEO_MUST_CONTAIN = [ 'Flight Booking: Powered by Codex MCP', 'OpenClaw Monitoring Doge Price', 'An Aha Moment by Claude Opus 4.6', + 'MIT License', + LICENSE_URL, ]; const AEO_MUST_NOT_CONTAIN = [ 'Watch FSB drive Google, search Amazon, and book travel autonomously.', 'Claude Opus 4.7', ]; +function extractJsonLdNodes(html) { + const nodes = []; + const parseErrors = []; + const scriptRe = /]*\btype=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi; + let match; + + function visit(value) { + if (Array.isArray(value)) { + value.forEach(visit); + return; + } + if (!value || typeof value !== 'object') return; + nodes.push(value); + if (value['@graph']) visit(value['@graph']); + } + + while ((match = scriptRe.exec(html)) !== null) { + try { + visit(JSON.parse(match[1])); + } catch (err) { + parseErrors.push(err.message); + } + } + + return { nodes, parseErrors }; +} + async function checkMarketingRoutes() { - for (const { path, titleSubstr, canonical } of MARKETING_ASSERTIONS) { + for (const { path, titleSubstr, canonical, licensedSoftware } of MARKETING_ASSERTIONS) { const url = `${BASE_URL}${path}`; let r; try { @@ -96,12 +133,24 @@ async function checkMarketingRoutes() { record(r.body.includes(titleSubstr), `GET ${path} body contains title substring "${titleSubstr}"`, ''); record(r.body.includes(`href="${canonical}"`), `GET ${path} canonical href="${canonical}"`, ''); record(r.body.includes('`, ''); - if (path === '/') { + if (licensedSoftware) { + const { nodes, parseErrors } = extractJsonLdNodes(r.body); record( r.body.includes('type="application/ld+json"'), - 'GET / contains JSON-LD