@typeroll/mcp-server 0.45.76 → 0.45.79
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/AGENTS.md +8 -2
- package/README.md +18 -0
- package/dist/bundled-content.js +35 -10
- package/dist/init.js +1 -1
- package/dist/tools/deploy.js +2 -1
- package/dist/tools/media.js +1 -1
- package/dist/tools/migration.js +4 -2
- package/dist/tools/pages.js +1 -1
- package/dist/tools/preview.js +1 -1
- package/dist/tools/working-copy.js +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace.js +34 -10
- package/package.json +3 -2
- package/skills/tr-header-footer.md +95 -41
- package/skills/tr-import-url.md +6 -6
- package/skills/tr-migrate-astro.md +3 -3
- package/skills/tr-migrate-multisite.md +4 -4
- package/skills/tr-migrate-wp.md +28 -12
- package/skills/tr-migration-evidence.md +35 -23
- package/skills/tr-new-site.md +5 -7
- package/workspace-templates/brief/interview.md +18 -0
- package/workspace-templates/brief/migration.md +42 -0
- package/workspace-templates/qa/checklist.md +35 -0
- package/workspace-templates/qa/typeroll-platform-feedback.md +11 -0
- package/workspace-templates/scripts/README.md +85 -0
- package/workspace-templates/scripts/typeroll/browser.cjs +50 -0
- package/workspace-templates/scripts/typeroll/call.py +12 -0
- package/workspace-templates/scripts/typeroll/capture_text.cjs +17 -0
- package/workspace-templates/scripts/typeroll/card-stacking-check.cjs +27 -0
- package/workspace-templates/scripts/typeroll/check_assets.py +72 -0
- package/workspace-templates/scripts/typeroll/check_unsaved.py +13 -0
- package/workspace-templates/scripts/typeroll/check_url_parity.py +74 -0
- package/workspace-templates/scripts/typeroll/compare_text.py +59 -0
- package/workspace-templates/scripts/typeroll/contact_sheet.py +20 -0
- package/workspace-templates/scripts/typeroll/layout_trace.cjs +33 -0
- package/workspace-templates/scripts/typeroll/shoot_hosted.cjs +26 -0
- package/workspace-templates/scripts/typeroll/shootframe.cjs +16 -0
- package/workspace-templates/scripts/typeroll/sweep_blocks.example.py +23 -0
- package/workspace-templates/scripts/typeroll/trlib.py +212 -0
- package/workspace-templates/workflows/design.md +30 -0
- package/workspace-templates/workflows/interview.md +72 -0
- package/workspace-templates/workflows/migration.md +84 -0
- package/workspace-templates/workflows/new-site.md +17 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tr-migration-evidence
|
|
3
|
-
description: Choose
|
|
3
|
+
description: Choose text and design/image preservation independently, map standard and custom blocks, preserve old URLs, and verify content, design and behavior before migration acceptance.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Migration evidence contract
|
|
@@ -12,22 +12,33 @@ AI assistant has no migration tools: it does not import, convert or verify sites
|
|
|
12
12
|
Use this shared contract with WordPress, Astro, URL and multisite recipes.
|
|
13
13
|
## Choose the migration intent before writes
|
|
14
14
|
|
|
15
|
-
Record
|
|
16
|
-
|
|
17
|
-
These are planning labels, not
|
|
18
|
-
the
|
|
15
|
+
Record two independent choices in `brief/migration.md` (or the project's
|
|
16
|
+
corresponding brief): `text_intent` and `design_intent`, each `preserve` or `renew`.
|
|
17
|
+
Design includes images. These are planning labels, not migration API parameters.
|
|
18
|
+
Use the starter's `workflows/interview.md` when guidance or decisions are missing;
|
|
19
|
+
resume existing answers. Do not infer either choice from a vague "migrate" or
|
|
20
|
+
"renew", or silently apply an old single-choice brief to both dimensions.
|
|
19
21
|
|
|
20
|
-
|
|
|
22
|
+
| Text | Design/images | Acceptance |
|
|
21
23
|
| --- | --- | --- |
|
|
22
|
-
| Preserve
|
|
23
|
-
|
|
|
24
|
+
| Preserve | Preserve | Compare wording and appearance separately with the source. |
|
|
25
|
+
| Preserve | Renew | Keep wording; compare appearance with approved new prototypes. |
|
|
26
|
+
| Renew | Preserve | Review copy against the editorial brief; keep source appearance, allowing documented layout adjustments for new text length. |
|
|
27
|
+
| Renew | Renew | Review copy and appearance against their separately approved briefs. |
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
Preserved text includes headings, body copy, CTA labels and captions. Record
|
|
30
|
+
metadata, alt text, legal text and translation rules explicitly; SEO is not an
|
|
31
|
+
exception permitting silent rewrites. With renewed design, list required assets
|
|
32
|
+
and which images may be replaced or generated, including cropping, credits and
|
|
33
|
+
rights. Format/size optimization must not change a preserved image's motif.
|
|
29
34
|
|
|
30
|
-
|
|
35
|
+
Record source URLs, dates, approved scope, per-Page exceptions and acceptance
|
|
36
|
+
references separately. Both choices preserve facts, ownership, required behavior
|
|
37
|
+
and historical URL continuity. Renewing design does not authorize text rewrites;
|
|
38
|
+
renewing text does not authorize new imagery. Neither authorizes invented claims,
|
|
39
|
+
unreviewed content removal or domain cutover. Protect existing target edits.
|
|
40
|
+
|
|
41
|
+
## Use editable blocks for all four combinations
|
|
31
42
|
|
|
32
43
|
1. Inspect standard block schemas, composition starters, named styles and Page
|
|
33
44
|
templates. Prefer these when they fit.
|
|
@@ -60,10 +71,10 @@ still needs a platform fix, not a disguised HTML/CSS workaround.
|
|
|
60
71
|
|
|
61
72
|
The managed WordPress importer deterministically stores cleaned HTML and shared
|
|
62
73
|
references, without AI rewriting. That is a source-preserving intermediate step.
|
|
63
|
-
The external agent rebuilds it into reviewed blocks/templates for
|
|
74
|
+
The external agent rebuilds it into reviewed blocks/templates for all four combinations;
|
|
64
75
|
`set_page_mode` does not perform conversion. Keep the complete source, map every
|
|
65
|
-
in-scope content element, and do not silently truncate long pages.
|
|
66
|
-
|
|
76
|
+
in-scope content element, and do not silently truncate long pages. When text_intent is preserve retain wording; when it is renew track approved
|
|
77
|
+
rewrites and source facts, independently of design_intent.
|
|
67
78
|
An import finishing does not mean design, block reconstruction or launch is done.
|
|
68
79
|
|
|
69
80
|
## Old URLs must continue to work
|
|
@@ -86,10 +97,10 @@ Do not switch traffic until these checks pass and cutover is authorized.
|
|
|
86
97
|
|
|
87
98
|
## Gate 1: source baseline, before conversion
|
|
88
99
|
|
|
89
|
-
Record
|
|
100
|
+
Record both chosen intents, authoritative source URL/date, scope, relevant Core version, active
|
|
90
101
|
Page templates and overrides. Inventory home, category/archive, long/short
|
|
91
102
|
articles, downloads, empty related lists and each distinct business surface.
|
|
92
|
-
For preserve, capture source and target geometry; for renew, compare the target
|
|
103
|
+
For design_intent=preserve, capture source and target geometry; for design_intent=renew, compare the target
|
|
93
104
|
with the approved design while retaining source coverage evidence. Capture
|
|
94
105
|
screenshots and computed geometry at matching actual `innerWidth`
|
|
95
106
|
values (320/390/768/1024/1280/1536 where relevant, plus both sides of actual
|
|
@@ -249,14 +260,15 @@ pre-cutover scope honestly when DNS or integrations remain out of scope.
|
|
|
249
260
|
|
|
250
261
|
The initial route census must combine source sitemaps, internal links, archive
|
|
251
262
|
pagination, CMS inventory and known utility routes. Include homepage, archives,
|
|
252
|
-
404/search behavior and routes outside the sitemap where present.
|
|
253
|
-
features absent from the source. In renew mode, include additions in the agreed brief. Assign every route a recipe, template override,
|
|
263
|
+
404/search behavior and routes outside the sitemap where present. Do not invent features absent from the source; include additions only in the agreed brief. Assign every route a recipe, template override,
|
|
254
264
|
composition or documented exception. Inventory the whole shell: content outside
|
|
255
265
|
`main`, header, footer, service navigation and archive heroes are not optional.
|
|
256
266
|
|
|
257
|
-
Derive required evidence from that census. For preserve, pair every
|
|
258
|
-
at mobile and desktop; for renew, compare
|
|
259
|
-
|
|
267
|
+
Derive required evidence from that census. For design_intent=preserve, pair every
|
|
268
|
+
in-scope source/target route at mobile and desktop; for design_intent=renew, compare
|
|
269
|
+
every target route to its approved prototype. Independently compare preserved text
|
|
270
|
+
to the source and renewed text to its editorial brief; account for the source
|
|
271
|
+
content/URL mapping in every combination; add each unique component, breakpoint and interaction
|
|
260
272
|
state. Inspect page-specific media, lists, tables, embeds, CTAs, anchors and related
|
|
261
273
|
content. One representative article cannot attest every article's content.
|
|
262
274
|
|
package/skills/tr-new-site.md
CHANGED
|
@@ -165,7 +165,7 @@ it there.
|
|
|
165
165
|
### 3. Header + footer partials
|
|
166
166
|
|
|
167
167
|
**Start from the native preset — don't hand-roll navigation.** Read
|
|
168
|
-
`tr-header-footer` and use its `template/site_logo` + `core/
|
|
168
|
+
`tr-header-footer` and use its `template/site_logo` + `core/navigation_menu` with `core/navigation_links` slots
|
|
169
169
|
composition. The server-rendered landmark, current-page state, no-JS links,
|
|
170
170
|
mobile disclosure, focus treatment, and responsive behavior are Core
|
|
171
171
|
contracts rather than tenant CSS/JavaScript.
|
|
@@ -269,12 +269,10 @@ passing the whole tree inline (`block={ type: 'core/columns', slots:
|
|
|
269
269
|
slot_index=0|1`. Requires template_capabilities_version ≥ 0.15.2 — on
|
|
270
270
|
older sites use `core/grid` (children flow into columns) instead.
|
|
271
271
|
|
|
272
|
-
Partial last rows
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
don't hand-roll 6-column CSS tricks; both distort content to patch
|
|
277
|
-
layout. On older sites, pick a column count that divides N.
|
|
272
|
+
Partial last rows: keep `last_row: 'start'` on `core/grid`. Incomplete rows
|
|
273
|
+
stay left-aligned, preserving the reading edge. Center only after an explicit
|
|
274
|
+
owner design request, never as an automatic fix. Do not widen one peer card
|
|
275
|
+
or invent six-column CSS to fill a gap.
|
|
278
276
|
|
|
279
277
|
Icons (template_capabilities_version ≥ 0.16.0): `type: 'icon'` fields on
|
|
280
278
|
`core/icon`, `core/icon_box`, and `core/step_card` render inline SVG when
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Interview progress
|
|
2
|
+
|
|
3
|
+
Status: not started
|
|
4
|
+
Next question: goal and scope
|
|
5
|
+
|
|
6
|
+
## Confirmed answers
|
|
7
|
+
|
|
8
|
+
| Topic | Answer | Source/date | Brief location |
|
|
9
|
+
| --- | --- | --- | --- |
|
|
10
|
+
|
|
11
|
+
## Proposals and unknowns
|
|
12
|
+
|
|
13
|
+
Record what is not yet decided, whether it blocks the next step and who can decide.
|
|
14
|
+
|
|
15
|
+
## Recap and approved next step
|
|
16
|
+
|
|
17
|
+
Record the owner's confirmation, exact scope and date. Do not infer deployment,
|
|
18
|
+
DNS or message approval from a completed brief. No secrets or raw private transcripts.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Migration brief (when applicable)
|
|
2
|
+
|
|
3
|
+
## Independent choices
|
|
4
|
+
|
|
5
|
+
- text_intent: undecided (preserve or renew)
|
|
6
|
+
- design_intent: undecided (preserve or renew; includes images)
|
|
7
|
+
|
|
8
|
+
These are planning labels, not API parameters. Choose each separately. If guidance
|
|
9
|
+
is needed, use workflows/interview.md. A single "renew" must not silently authorize
|
|
10
|
+
both. Expand an older single-choice brief with the owner before dependent writes.
|
|
11
|
+
|
|
12
|
+
## Source URLs, date and agreed scope
|
|
13
|
+
|
|
14
|
+
## Text policy and exceptions
|
|
15
|
+
|
|
16
|
+
Preserve wording, headings, CTAs and captions when text_intent is preserve. Record
|
|
17
|
+
SEO metadata/alt text, legal copy, translation and page-specific exceptions explicitly.
|
|
18
|
+
For renewal, record the approved editorial brief and facts that must remain true.
|
|
19
|
+
|
|
20
|
+
## Design and image policy and exceptions
|
|
21
|
+
|
|
22
|
+
Record layout/brand references, required logos/assets, permitted replacements or
|
|
23
|
+
generation, cropping, rights and credits. Design renewal alone never permits text
|
|
24
|
+
rewriting. Preserve image motifs unless a replacement is agreed; format/size
|
|
25
|
+
optimization does not mean changing the image's content.
|
|
26
|
+
|
|
27
|
+
## Acceptance references
|
|
28
|
+
|
|
29
|
+
Text: original source for preserve; approved editorial brief for renew.
|
|
30
|
+
Design/images: original source for preserve; approved design/prototypes for renew.
|
|
31
|
+
Record these separately per layout or page when exceptions apply.
|
|
32
|
+
|
|
33
|
+
## Standard blocks, custom types and HTML exceptions
|
|
34
|
+
|
|
35
|
+
## Old URL to preserved path or relevant redirect
|
|
36
|
+
|
|
37
|
+
Both choices retain URL continuity, source facts and required business behavior.
|
|
38
|
+
|
|
39
|
+
## Prototype and verification evidence
|
|
40
|
+
|
|
41
|
+
Record text, design/images, links and behavior results separately. Keep private
|
|
42
|
+
extracts in sources/private/ and screenshots in qa/reports/.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Verification
|
|
2
|
+
|
|
3
|
+
- Confirm Organization, Site, Version and deployed/source identity.
|
|
4
|
+
- Run `python3 scripts/typeroll/check_unsaved.py` before trigger_deploy. Review
|
|
5
|
+
and explicitly save authorized Pages and partials; templates save immediately.
|
|
6
|
+
- Compare preserved text with capture_text.cjs + compare_text.py and an explicit
|
|
7
|
+
selector. Check intentional additions, source comments and images separately.
|
|
8
|
+
- Inspect desktop/mobile previews and actual hosted output around breakpoints.
|
|
9
|
+
Use shootframe.cjs / shoot_hosted.cjs and inspect full screenshots, not only tiles.
|
|
10
|
+
- Exercise menus, keyboard focus, current links, forms and enabled app states.
|
|
11
|
+
Use card-stacking-check.cjs for whole-card excerpt/media/whitespace targets.
|
|
12
|
+
- Check actual contrast on light/dark/photo backgrounds, image subjects and
|
|
13
|
+
sharpness, footer balance, left-aligned last rows and heading hierarchy.
|
|
14
|
+
- Check home title/suffix, metadata, canonical/indexing policy and JSON-LD.
|
|
15
|
+
- Run check_url_parity.py and check_assets.py on the deployed candidate; compare
|
|
16
|
+
final targets with the approved URL map and flatten accidental redirect chains.
|
|
17
|
+
- Treat 403/network/DNS failures as unverified. Do not claim search-bot access
|
|
18
|
+
from a user-agent string or turn off customer protection without approval.
|
|
19
|
+
- Record tested pages, viewports, revision, results and unresolved issues per QA
|
|
20
|
+
round. Record product defects in qa/typeroll-platform-feedback.md.
|
|
21
|
+
- Keep screenshots and private runtime reports in ignored qa/reports/.
|
|
22
|
+
- Check publishing readiness and migration launch report. Passing technical
|
|
23
|
+
validation does not prove visual/editorial completeness or grant launch approval.
|
|
24
|
+
|
|
25
|
+
Commands/dependencies/limits: scripts/README.md. Design: workflows/design.md.
|
|
26
|
+
Follow existing owner authorization; test publication, traffic cutover and
|
|
27
|
+
indexing have distinct effects but do not require repeated approval if covered.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
- [ ] Check heading role spacing, slot gap and responsive vertical padding in the browser.
|
|
31
|
+
- [ ] Review global/current navigation colors against the real parent background.
|
|
32
|
+
- [ ] Check intrinsic-width image and full-image hero framing at breakpoint boundaries.
|
|
33
|
+
- [ ] Test click-to-load video with keyboard; no provider request before activation.
|
|
34
|
+
- [ ] Review matched drafts, unpublished targets and redirect chains before launch.
|
|
35
|
+
- [ ] Explicitly save working copies, or obtain approval to publish saved content only.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Platform feedback
|
|
2
|
+
|
|
3
|
+
Record site-specific observations here, without credentials or private exports.
|
|
4
|
+
|
|
5
|
+
| ID | Priority | Surface / version | Observation | Evidence | Workaround | Status |
|
|
6
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
7
|
+
|
|
8
|
+
For each finding record expected versus actual behavior, minimal reproduction,
|
|
9
|
+
Site/Version, page/block locator, relevant schema, tested widths, source revision
|
|
10
|
+
and whether it blocks launch. Distinguish bugs, capabilities, instructions and
|
|
11
|
+
project choices. Link upstream fixes; verify deployed output before closing them.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Repeatable site QA
|
|
2
|
+
|
|
3
|
+
These optional tools supplement MCP and the platform's publication validator.
|
|
4
|
+
They do not publish, change DNS, approve a launch or replace visual review.
|
|
5
|
+
Python tools use the standard library except contact_sheet.py (Pillow). Browser
|
|
6
|
+
tools require Node.js 22+, Playwright and its Chromium browser. From scripts/typeroll:
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install --no-save --package-lock=false playwright
|
|
10
|
+
npx playwright install chromium
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Run the commands below from the **workspace root**. Inject TYPEROLL_API_KEY through
|
|
14
|
+
your private credential manager. Never put it in a file or command argument.
|
|
15
|
+
trlib.py reads portal, organization_id, site_id and version from typeroll.json,
|
|
16
|
+
checks the connected Site/Organization, discovers the live tool schema, and
|
|
17
|
+
passes the Version where supported. It refuses unversioned operations on a
|
|
18
|
+
non-main binding; use the explicit interactive API workflow for organization or
|
|
19
|
+
shared media operations instead. It is a small hosted MCP helper for API keys,
|
|
20
|
+
not a replacement OAuth client or a new general CRUD CLI. REST is also supported
|
|
21
|
+
by Typeroll if your integration already uses it.
|
|
22
|
+
|
|
23
|
+
## Before publication
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
python3 scripts/typeroll/check_unsaved.py
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Exit 1 lists unsaved Pages **and partials**. Review those changes; save only the
|
|
30
|
+
work authorized for this publication, then run again. Templates save immediately.
|
|
31
|
+
A working-copy preview is not proof of saved output. Never auto-save other work.
|
|
32
|
+
|
|
33
|
+
## After an authorized publication
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
python3 scripts/typeroll/check_url_parity.py https://YOUR-TEST-HOST
|
|
37
|
+
python3 scripts/typeroll/check_assets.py https://YOUR-TEST-HOST
|
|
38
|
+
node scripts/typeroll/shoot_hosted.cjs https://YOUR-TEST-HOST qa/reports/hosted /,/about/ 390 768 1023 1024 1280
|
|
39
|
+
node scripts/typeroll/card-stacking-check.cjs https://YOUR-TEST-HOST/listing/ 390
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
URL parity reads every migration inventory page and observed spelling, skips
|
|
43
|
+
explicit exclusions unless --show-excluded, keeps origins/query parameters,
|
|
44
|
+
detects loops and flags chains beyond a single redirect (one initial slash
|
|
45
|
+
normalization is allowed). This does **not** prove the final destination is the
|
|
46
|
+
editorially correct page: compare with the approved URL map too. No inventory
|
|
47
|
+
is a failure, not a passing zero-row check. Asset QA uses resolved routes,
|
|
48
|
+
checks published/unlisted Pages, relative URLs, srcset and downloads, uses four
|
|
49
|
+
concurrent HEAD requests and falls back to GET when HEAD is unsupported. CSS
|
|
50
|
+
backgrounds, authenticated assets and dynamically loaded resources need browser QA.
|
|
51
|
+
Neither tool spoofs a search crawler. A 403/DNS/network failure is unverified;
|
|
52
|
+
inspect access policy rather than treating it as a broken migration or a pass.
|
|
53
|
+
|
|
54
|
+
## Other helpers
|
|
55
|
+
|
|
56
|
+
- `call.py TOOL '{"argument":"value"}'`: raw MCP result; use `-` to read JSON
|
|
57
|
+
from stdin. Mutating tools require the same owner authorization as interactive
|
|
58
|
+
calls. Network failures are not automatically retried for mutations.
|
|
59
|
+
- `trlib.upload(path, content_type, alt)`: direct signed upload and mandatory
|
|
60
|
+
finalize SHA-256 verification. No image bytes pass through the model or Git.
|
|
61
|
+
- `sweep_blocks.example.py`: dry-run grid normalization across Pages, templates
|
|
62
|
+
and partials. Review before --apply; refuses to commit existing unsaved work.
|
|
63
|
+
It is not transactional: a failure can leave a partial draft; inspect before retry.
|
|
64
|
+
- `shootframe.cjs OUT-PREFIX SIGNED-PREVIEW-URL [widths...]`: selects only the
|
|
65
|
+
Typeroll preview iframe, reports fonts/hosts/overflow. Signed URLs are secrets;
|
|
66
|
+
pass privately, never paste into committed reports or shell history. Direct-frame
|
|
67
|
+
capture checks rendering only; test Extension storage/navigation/handoffs in
|
|
68
|
+
the original wrapper because those features depend on its parent bridge.
|
|
69
|
+
- `layout_trace.cjs URL [width] [selector]`: computed spacing of ancestors and
|
|
70
|
+
next section. Works with a direct page or Typeroll preview.
|
|
71
|
+
- `contact_sheet.py OUT-PREFIX COLS TILE-WIDTH PNG...`: Pillow contact sheets.
|
|
72
|
+
It crops tall screenshots; inspect full screenshots too.
|
|
73
|
+
- `capture_text.cjs URL qa/reports/text.json 'main .article-content'` followed by
|
|
74
|
+
`compare_text.py sources/private/page.html qa/reports/text.json --source-title 'Title'`:
|
|
75
|
+
word/punctuation diff. A saved single WP REST record (.json) is another source
|
|
76
|
+
adapter. Select exactly the preserved region, including its template-owned
|
|
77
|
+
title once. Deliberate shared additions need separate acceptance. Comments,
|
|
78
|
+
scripts and styles are excluded from source text. It never edits content.
|
|
79
|
+
|
|
80
|
+
Browser reports exit nonzero for missing images, overflow, unexpected H1 count,
|
|
81
|
+
failed requests or incomplete scrolling. They keep third-party failures visible.
|
|
82
|
+
Network checks retry only a bounded read. All reports, screenshots and source
|
|
83
|
+
exports stay under ignored qa/reports/ or sources/private/. Do not commit signed
|
|
84
|
+
URLs, personal data or customer assets. Adapt scripts deliberately and run tests
|
|
85
|
+
before applying broad changes. The workspace updater preserves your edits.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Isolated QA browser only. Never persist storage state, cookies or signed URLs.
|
|
2
|
+
const { chromium } = require('playwright');
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
async function open(page, url) {
|
|
6
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
7
|
+
// Select only Typeroll's own preview, never an arbitrary ad/video iframe.
|
|
8
|
+
const frame = page.locator('iframe[title="Preview"], iframe[title="Site preview"]');
|
|
9
|
+
if (await frame.count() === 1) {
|
|
10
|
+
await page.waitForFunction(() => document.querySelector('iframe[title="Preview"], iframe[title="Site preview"]')?.getAttribute('src'), null, { timeout: 15000 });
|
|
11
|
+
const src = await frame.getAttribute('src');
|
|
12
|
+
if (!src) throw Error('Preview frame has no source');
|
|
13
|
+
const target = new URL(src, page.url());
|
|
14
|
+
if (target.origin !== new URL(page.url()).origin) throw Error('Unexpected preview frame origin');
|
|
15
|
+
await page.goto(target.href, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
16
|
+
}
|
|
17
|
+
await page.evaluate(() => document.fonts.ready);
|
|
18
|
+
}
|
|
19
|
+
async function settle(page) {
|
|
20
|
+
// Bounded for infinite-scroll pages; report coverage rather than hanging.
|
|
21
|
+
for (let y = 0; y < 30000; y += 600) {
|
|
22
|
+
await page.evaluate(y => scrollTo(0, y), y);
|
|
23
|
+
await page.waitForTimeout(100);
|
|
24
|
+
if (y + 900 >= await page.evaluate(() => document.documentElement.scrollHeight)) break;
|
|
25
|
+
}
|
|
26
|
+
await page.evaluate(() => scrollTo(0, 0));
|
|
27
|
+
await page.waitForTimeout(300);
|
|
28
|
+
}
|
|
29
|
+
async function inspect(page) {
|
|
30
|
+
return page.evaluate(() => ({
|
|
31
|
+
overflow: document.documentElement.scrollWidth > innerWidth,
|
|
32
|
+
h1: document.querySelectorAll('h1').length,
|
|
33
|
+
title: document.title,
|
|
34
|
+
robots: document.querySelector('meta[name="robots"]')?.content ?? null,
|
|
35
|
+
fonts: { body: getComputedStyle(document.body).fontFamily, heading: getComputedStyle(document.querySelector('h1') || document.body).fontFamily },
|
|
36
|
+
incompleteImages: [...document.images].filter(i => !i.complete || !i.naturalWidth).map(i => new URL(i.currentSrc || i.src, location.href).pathname),
|
|
37
|
+
scrollCoverageComplete: document.documentElement.scrollHeight <= 30000,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
async function screenshot(page, file) {
|
|
41
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
42
|
+
await page.screenshot({ path: file, fullPage: true, animations: 'disabled' });
|
|
43
|
+
}
|
|
44
|
+
function widths(args) {
|
|
45
|
+
const values = (args.length ? args : ['390', '768', '1023', '1024', '1280']).map(Number);
|
|
46
|
+
if (values.some(w => !Number.isInteger(w) || w < 240 || w > 4000)) throw Error('Invalid viewport width');
|
|
47
|
+
return values;
|
|
48
|
+
}
|
|
49
|
+
function fail() { console.error('QA failed; inspect the target privately. URLs and credentials are withheld.'); process.exitCode = 2; }
|
|
50
|
+
module.exports = { chromium, open, settle, inspect, screenshot, widths, fail };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""One MCP call; output can contain private data. Do not commit it."""
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import trlib
|
|
6
|
+
|
|
7
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
8
|
+
parser.add_argument('tool')
|
|
9
|
+
parser.add_argument('arguments', nargs='?', default='{}', help='JSON or - for stdin')
|
|
10
|
+
args = parser.parse_args()
|
|
11
|
+
value = json.loads(sys.stdin.read() if args.arguments == '-' else args.arguments)
|
|
12
|
+
print(json.dumps(trlib.call(args.tool, value), ensure_ascii=False, indent=2))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Explicit selector avoids silently excluding the wrong column/sidebar.
|
|
2
|
+
const qa = require('./browser.cjs');
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const [url, out, selector = 'main'] = process.argv.slice(2);
|
|
5
|
+
(async () => {
|
|
6
|
+
const browser = await qa.chromium.launch();
|
|
7
|
+
try {
|
|
8
|
+
const page = await browser.newPage();
|
|
9
|
+
await qa.open(page, url); await qa.settle(page);
|
|
10
|
+
const target = page.locator(selector);
|
|
11
|
+
if (await target.count() !== 1) throw Error('Select exactly one content region');
|
|
12
|
+
const value = await target.evaluate(el => ({ text: el.innerText,
|
|
13
|
+
images: [...el.querySelectorAll('img')].map(i => ({ alt: i.alt, path: new URL(i.currentSrc || i.src, location.href).pathname })) }));
|
|
14
|
+
await fs.mkdir(require('node:path').dirname(out), { recursive: true });
|
|
15
|
+
await fs.writeFile(out, JSON.stringify(value, null, 2));
|
|
16
|
+
} finally { await browser.close(); }
|
|
17
|
+
})().catch(qa.fail);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Check every whole-card target, including excerpt/media/meta, without navigating.
|
|
2
|
+
const qa = require('./browser.cjs');
|
|
3
|
+
const [url, width = '390'] = process.argv.slice(2);
|
|
4
|
+
(async () => {
|
|
5
|
+
const browser = await qa.chromium.launch();
|
|
6
|
+
try {
|
|
7
|
+
const page = await browser.newPage({ viewport: { width: qa.widths([width])[0], height: 900 } });
|
|
8
|
+
await qa.open(page, url);
|
|
9
|
+
const cards = page.locator('[data-block="post_card"][data-whole="true"]');
|
|
10
|
+
const report = [];
|
|
11
|
+
for (let index = 0; index < await cards.count(); index++) {
|
|
12
|
+
const card = cards.nth(index);
|
|
13
|
+
await card.scrollIntoViewIfNeeded();
|
|
14
|
+
report.push(await card.evaluate(card => {
|
|
15
|
+
const link = card.querySelector('.block-postcard-title > a');
|
|
16
|
+
const targets = [card, ...card.querySelectorAll('.block-postcard-excerpt,.block-postcard-meta,.block-postcard-media')];
|
|
17
|
+
const hits = targets.filter(el => el.getBoundingClientRect().height > 0).map(el => {
|
|
18
|
+
const r = el.getBoundingClientRect(), x = r.left + r.width / 2, y = r.top + r.height / 2;
|
|
19
|
+
return { part: el.className || 'card', inViewport: y >= 0 && y < innerHeight, linked: document.elementFromPoint(x, y)?.closest('a') === link };
|
|
20
|
+
});
|
|
21
|
+
return { block: card.dataset.blockId || card.dataset.bid || null, title: link?.textContent, hits };
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
console.log(JSON.stringify(report, null, 2));
|
|
25
|
+
if (!report.length || report.some(r => r.hits.some(h => !h.inViewport || !h.linked))) process.exitCode = 1;
|
|
26
|
+
} finally { await browser.close(); }
|
|
27
|
+
})().catch(qa.fail);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Check published page assets, including relative URLs and srcset. CSS backgrounds need visual QA."""
|
|
2
|
+
import argparse
|
|
3
|
+
import concurrent.futures
|
|
4
|
+
from html.parser import HTMLParser
|
|
5
|
+
import json
|
|
6
|
+
import urllib.parse
|
|
7
|
+
import trlib
|
|
8
|
+
from check_url_parity import origin, fetch, probe
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Assets(HTMLParser):
|
|
12
|
+
def __init__(self, base):
|
|
13
|
+
super().__init__(convert_charrefs=True)
|
|
14
|
+
self.base, self.urls = base, set()
|
|
15
|
+
|
|
16
|
+
def handle_starttag(self, tag, attrs):
|
|
17
|
+
data = dict(attrs)
|
|
18
|
+
values = []
|
|
19
|
+
if tag in ('img', 'source', 'video', 'audio', 'script'):
|
|
20
|
+
values.extend(data[k] for k in ('src', 'poster') if data.get(k))
|
|
21
|
+
if tag == 'link' and data.get('rel') == 'stylesheet':
|
|
22
|
+
values.append(data.get('href', ''))
|
|
23
|
+
href = data.get('href', '')
|
|
24
|
+
if tag == 'a' and ('download' in data or urllib.parse.urlsplit(href).path.lower().endswith(('.pdf', '.jpg', '.jpeg', '.png', '.webp', '.avif', '.svg', '.zip'))):
|
|
25
|
+
values.append(href)
|
|
26
|
+
if data.get('srcset') and not data['srcset'].strip().startswith('data:'):
|
|
27
|
+
values.extend(item.strip().split()[0] for item in data['srcset'].split(',') if item.strip())
|
|
28
|
+
for value in values:
|
|
29
|
+
url = urllib.parse.urljoin(self.base, value)
|
|
30
|
+
if urllib.parse.urlsplit(url).scheme in ('http', 'https'):
|
|
31
|
+
self.urls.add(url)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def check(url):
|
|
35
|
+
status, location, _ = fetch(url, 'HEAD')
|
|
36
|
+
# HEAD is not supported everywhere; redirects must resolve too.
|
|
37
|
+
if status in (405, 501) or location:
|
|
38
|
+
status = probe(url).get('status', 0)
|
|
39
|
+
return {'url': url, 'status': status, 'ok': status == 200}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main():
|
|
43
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
44
|
+
parser.add_argument('origin', type=origin)
|
|
45
|
+
args = parser.parse_args()
|
|
46
|
+
pages = [p for p in trlib.all_pages() if p.get('status') in ('published', 'unlisted')]
|
|
47
|
+
errors, assets = [], set()
|
|
48
|
+
for page in pages:
|
|
49
|
+
if not page.get('url'):
|
|
50
|
+
errors.append({'page_id': page['id'], 'error': 'missing_resolved_route'})
|
|
51
|
+
continue
|
|
52
|
+
route = urllib.parse.urlsplit(page['url'])
|
|
53
|
+
url = args.origin + (route.path or '/') + ('?' + route.query if route.query else '')
|
|
54
|
+
evidence = probe(url, expected_page=True)
|
|
55
|
+
if not evidence.get('ok'):
|
|
56
|
+
errors.append({'page_id': page['id'], **evidence})
|
|
57
|
+
continue
|
|
58
|
+
status, _, body = fetch(evidence['final_url'])
|
|
59
|
+
if status != 200:
|
|
60
|
+
errors.append({'page_id': page['id'], 'status': status})
|
|
61
|
+
continue
|
|
62
|
+
parser = Assets(evidence['final_url'])
|
|
63
|
+
parser.feed(body.decode('utf8', 'replace'))
|
|
64
|
+
assets.update(parser.urls)
|
|
65
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
|
|
66
|
+
results = list(pool.map(check, sorted(assets)))
|
|
67
|
+
print(json.dumps({'pages': len(pages), 'page_errors': errors, 'assets': results}, indent=2))
|
|
68
|
+
return 1 if not pages or errors or any(not item['ok'] for item in results) else 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == '__main__':
|
|
72
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Pre-deploy gate: list every Page and partial with an unsaved working copy (Page templates save immediately).
|
|
2
|
+
Deploys build saved content only, and previews with include_working_copy hide the difference.
|
|
3
|
+
usage: python3 scripts/typeroll/check_unsaved.py (exit 1 when something is unsaved)"""
|
|
4
|
+
import os, sys
|
|
5
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
6
|
+
import trlib
|
|
7
|
+
SITE = trlib.site_id()
|
|
8
|
+
bad = [t for t in trlib.containers() if t["kind"] != "template"
|
|
9
|
+
and trlib.call("read_working_copy", {"site_id": SITE, "target": t}).get("working_copy")]
|
|
10
|
+
for t in bad:
|
|
11
|
+
print("unsaved:", t["kind"], t["id"])
|
|
12
|
+
print("unsaved total:", len(bad))
|
|
13
|
+
sys.exit(1 if bad else 0)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Read-only URL continuity check on an explicitly supplied deployed origin."""
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
import urllib.error
|
|
5
|
+
import urllib.parse
|
|
6
|
+
import urllib.request
|
|
7
|
+
import trlib
|
|
8
|
+
|
|
9
|
+
UA = 'Typeroll-Starter-QA/1.0 (+https://typeroll.com/docs/)'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def fetch(url, method='GET'):
|
|
13
|
+
request = urllib.request.Request(url, method=method, headers={'User-Agent': UA})
|
|
14
|
+
for attempt in range(2):
|
|
15
|
+
try:
|
|
16
|
+
with urllib.request.build_opener(trlib.NoRedirect).open(request, timeout=30) as response:
|
|
17
|
+
return response.status, response.headers.get('Location'), response.read() if method == 'GET' else b''
|
|
18
|
+
except urllib.error.HTTPError as error:
|
|
19
|
+
if error.code in (502, 503, 504) and attempt == 0:
|
|
20
|
+
continue
|
|
21
|
+
return error.code, error.headers.get('Location'), b''
|
|
22
|
+
except (OSError, TimeoutError):
|
|
23
|
+
if attempt:
|
|
24
|
+
return 0, None, b''
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def origin(value):
|
|
28
|
+
parsed = urllib.parse.urlsplit(value)
|
|
29
|
+
if parsed.scheme not in ('http', 'https') or not parsed.netloc or parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path not in ('', '/'):
|
|
30
|
+
raise ValueError('Supply a plain deployed HTTP(S) origin')
|
|
31
|
+
return value.rstrip('/')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def probe(url, expected_page=False):
|
|
35
|
+
initial, seen, hops = url, set(), []
|
|
36
|
+
for _ in range(6):
|
|
37
|
+
if url in seen:
|
|
38
|
+
return {'error': 'redirect_loop', 'hops': hops}
|
|
39
|
+
seen.add(url)
|
|
40
|
+
status, location, _ = fetch(url)
|
|
41
|
+
if status not in (301, 302, 303, 307, 308) or not location:
|
|
42
|
+
slash = bool(hops) and urllib.parse.urlsplit(initial)._replace(path=urllib.parse.urlsplit(initial).path.rstrip('/')) == urllib.parse.urlsplit(hops[0])._replace(path=urllib.parse.urlsplit(hops[0]).path.rstrip('/'))
|
|
43
|
+
maximum = 1 if expected_page else 2 if slash else 1
|
|
44
|
+
acceptable_page_hop = not expected_page or not hops or (len(hops) == 1 and slash)
|
|
45
|
+
ok = status == 200 and len(hops) <= maximum and acceptable_page_hop
|
|
46
|
+
return {'ok': ok, 'status': status, 'hops': hops, 'final_url': url}
|
|
47
|
+
url = urllib.parse.urljoin(url, location)
|
|
48
|
+
if urllib.parse.urlsplit(url).scheme not in ('http', 'https'):
|
|
49
|
+
return {'error': 'unsupported_redirect_scheme', 'hops': hops}
|
|
50
|
+
hops.append(url)
|
|
51
|
+
return {'error': 'too_many_redirects', 'hops': hops}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
56
|
+
parser.add_argument('origin', type=origin)
|
|
57
|
+
parser.add_argument('--show-excluded', action='store_true')
|
|
58
|
+
args = parser.parse_args()
|
|
59
|
+
report = []
|
|
60
|
+
for row in trlib.all_migration_urls():
|
|
61
|
+
if row.get('status') == 'excluded' and not args.show_excluded:
|
|
62
|
+
continue
|
|
63
|
+
for path in sorted(set([row['path']] + (row.get('observed_paths') or []))):
|
|
64
|
+
# Inventory spellings must be paths, not a silent change of tested host.
|
|
65
|
+
if not path.startswith('/') or path.startswith('//'):
|
|
66
|
+
raise ValueError('Inventory contains a non-local path')
|
|
67
|
+
url = args.origin + urllib.parse.quote(path, safe='/%?=&:+')
|
|
68
|
+
report.append({'path': path, **probe(url, row.get('status') == 'migrated')})
|
|
69
|
+
print(json.dumps(report, indent=2))
|
|
70
|
+
return 1 if not report or any(not row.get('ok') for row in report) else 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == '__main__':
|
|
74
|
+
raise SystemExit(main())
|