@vosjs/cli 0.41.1 → 0.42.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/README.md +13 -0
- package/dist/{chunk-4QVEV3HX.js → chunk-7RMULTDM.js} +270 -11
- package/dist/chunk-7RMULTDM.js.map +1 -0
- package/dist/cli.js +3 -3
- package/dist/index.d.ts +43 -0
- package/dist/index.js +1 -1
- package/dist/{run-ZIOIU3SD.js → run-WN2IO3WL.js} +2 -2
- package/package.json +5 -5
- package/schema/actions.schema.json +22 -0
- package/schema/digest.schema.json +22 -0
- package/dist/chunk-4QVEV3HX.js.map +0 -1
- /package/dist/{run-ZIOIU3SD.js.map → run-WN2IO3WL.js.map} +0 -0
package/README.md
CHANGED
|
@@ -90,6 +90,19 @@ vos plan take --reuse # re-time that cut onto
|
|
|
90
90
|
|
|
91
91
|
**The wall check.** Once the first navigation settles, and before a frame is captured, `record`, `create` and `--dry-run` ask whether the recorder landed where it was sent. A take that met a sign-in instead is refused with exit 4 and a sentence (`asked for /dashboard, landed on /login: no session for app.acme.com`): the asked URL answered 401 or 403, the recorder was sent to an identity provider or a sign-in path, or the page is a sign-in form (one password field, or a one-time-code field) rendered in place. A redirect somewhere else with no sign-in in sight, which is what a site that shows strangers a public page looks like, is refused under `--strict` and in a rehearsal, and said as a warning otherwise. The check runs before a re-record clears anything, so a session that expired since the last take never costs the footage it failed to replace. The way past a wall is a session: `--storage-state <file>`, minted from the test auth the project already has wherever that exists. `--allow-wall` records the page anyway (a video OF a sign-in page is a legitimate take), and the take's `meta.wall` and its digest then say so.
|
|
92
92
|
|
|
93
|
+
**What the frame shows.** Getting past a login puts the account's own data in the picture, and asking for a demo account does not hold that line: a person asked to sign in signs in as themselves. So the recorder looks. After the page opens and after every step it reads the text visible in the viewport and reports the KIND of thing it saw and where, never the string: an email address (one on `example.com` or a `.test`, `.example`, `.invalid` or `.localhost` domain is demo data and is not reported), something shaped like an API key or a JWT, a card number that passes the Luhn check, a masked card's visible tail. They land in `meta.exposures[]` (`step`, `kind`, `selector`, `rect`, `seen`), in the `record` and `create` done events, at the end of a rehearsal (before anything is recorded), as warnings in `vos validate <take>`, and in the digest's `take.exposures`. They warn; they do not fail a take, because a product may legitimately show addresses. The `selector` reaches that element and no other, so it can be pasted into a mask.
|
|
94
|
+
|
|
95
|
+
**`mask`** in `actions.json` hides a selector BEFORE the first frame is captured and keeps it hidden across navigations and re-renders, so the real value is never in a frame, never in the recording, never pushed:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
"mask": [
|
|
99
|
+
{ "selector": "nav > span", "as": "text", "text": "jane@acme.test" },
|
|
100
|
+
{ "selector": ".card-number" }
|
|
101
|
+
]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`as: "blur"` (the default) blurs the element; `as: "text"` swaps its words, which reads as a product where a blur reads as a redaction. Use `text` for IDENTIFIERS (an email, a name, an account id), never for product copy or numbers: the video stays true to the product. A form control is always blurred, since writing into an input would change what the app submits. A mask whose selector reached no element hid nothing, so it fails `--strict` and a rehearsal (exit 2) and is named; `meta.masks[]` records each mask's `hits`.
|
|
105
|
+
|
|
93
106
|
**Rehearse before you record.** `vos record … --dry-run` runs every step against the real page, in order, because a later selector usually exists only after an earlier click. Nothing is captured and nothing is written: the pointer lands instead of travelling, every pause is cut to a beat, and the take directory beside it keeps its footage, its cut and its script exactly as they were (a real re-record moves `doc.json` aside; a rehearsal does not). Selector lookups keep their whole timeout, so a miss here is a miss in the take. It prints each step with the rect it resolved, in capture px, which are the rects a pin or `vos callout --step` reads, and exits 2 on any miss or a first load that never settled. Add `--dry-run` to the exact command you were about to run; `--storage-state` and `--browser-arg=` apply to it too, and the `Next:` line it prints carries them, so the command it hands you records what it rehearsed.
|
|
94
107
|
|
|
95
108
|
**Digest first.** `vos digest <take>` is how an agent sees a recording without reading the video. It writes `digest/digest.json`: one moment per thing the cursor track says mattered (click clusters, typing sessions, scroll runs, dwells, idle gaps, head, tail, and frame-diff scene changes), each with source and output extents, a normalized `focus` and `rect` you can copy into a zoom span, per-second `activity`, and the planners' `proposed` span ids; plus one footage frame and a crop around the target per moment, and `sheet.png`, the contact sheet. Read the JSON, then the sheet, then a crop only where you must decide. `vos validate` then warns when a zoom does not contain what was clicked under it, and `vos frames --at-moments` renders the composed output at every moment so a still and its footage crop share an id.
|
|
@@ -129,6 +129,24 @@ function validateActions(value) {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
+
if (obj.mask !== void 0) {
|
|
133
|
+
if (!Array.isArray(obj.mask)) {
|
|
134
|
+
errors.push("mask must be an array of { selector, as?, text? }");
|
|
135
|
+
} else {
|
|
136
|
+
obj.mask.forEach((raw, i) => {
|
|
137
|
+
const at2 = `mask[${i}]`;
|
|
138
|
+
const m = raw ?? {};
|
|
139
|
+
if (typeof m.selector !== "string" || !m.selector.trim())
|
|
140
|
+
errors.push(`${at2}: needs a selector`);
|
|
141
|
+
if (m.as !== void 0 && m.as !== "blur" && m.as !== "text")
|
|
142
|
+
errors.push(`${at2}: as must be "blur" or "text"`);
|
|
143
|
+
if (m.as === "text" && (typeof m.text !== "string" || !m.text))
|
|
144
|
+
errors.push(`${at2}: as "text" needs the text to show instead`);
|
|
145
|
+
if (m.as !== "text" && m.text !== void 0)
|
|
146
|
+
errors.push(`${at2}: text is only read with as "text"`);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
132
150
|
if (!Array.isArray(obj.steps) || obj.steps.length === 0) {
|
|
133
151
|
errors.push("steps must be a non-empty array");
|
|
134
152
|
return errors;
|
|
@@ -6758,6 +6776,199 @@ var WALL_PROBE = `(() => {
|
|
|
6758
6776
|
}
|
|
6759
6777
|
})()`;
|
|
6760
6778
|
|
|
6779
|
+
// src/plugin/exposure.ts
|
|
6780
|
+
var EXPOSURE_MATCHERS_SRC = `(() => {
|
|
6781
|
+
const EMAIL = /[A-Za-z0-9._%+-]+@((?:[A-Za-z0-9-]+\\.)+[A-Za-z]{2,})/g
|
|
6782
|
+
const SAFE_HOST = /(^|\\.)example\\.(com|org|net)$|\\.(test|example|invalid|localhost)$/i
|
|
6783
|
+
const KEY = new RegExp([
|
|
6784
|
+
'\\\\b[srp]k_(?:live|test)_[A-Za-z0-9]{8,}',
|
|
6785
|
+
'\\\\bgh[pousr]_[A-Za-z0-9]{20,}',
|
|
6786
|
+
'\\\\bgithub_pat_[A-Za-z0-9_]{20,}',
|
|
6787
|
+
'\\\\bxox[abprs]-[A-Za-z0-9-]{10,}',
|
|
6788
|
+
'\\\\bAKIA[0-9A-Z]{16}\\\\b',
|
|
6789
|
+
'\\\\bAIza[0-9A-Za-z_-]{30,}',
|
|
6790
|
+
'\\\\bvos_(?:sk|rg|ho)_[A-Za-z0-9]{8,}',
|
|
6791
|
+
'\\\\beyJ[A-Za-z0-9_-]{8,}\\\\.eyJ[A-Za-z0-9_-]{8,}\\\\.[A-Za-z0-9_-]{8,}',
|
|
6792
|
+
].join('|'))
|
|
6793
|
+
const CARD = /(?:^|[^0-9])((?:[0-9][ -]?){12,18}[0-9])(?![0-9])/g
|
|
6794
|
+
const TAIL = /[\\u2022\\u00b7*xX]{2,}[ -]?[0-9]{4}(?![0-9])/
|
|
6795
|
+
const luhn = (digits) => {
|
|
6796
|
+
let sum = 0, alt = false
|
|
6797
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
6798
|
+
let d = digits.charCodeAt(i) - 48
|
|
6799
|
+
if (alt) { d *= 2; if (d > 9) d -= 9 }
|
|
6800
|
+
sum += d; alt = !alt
|
|
6801
|
+
}
|
|
6802
|
+
return sum % 10 === 0
|
|
6803
|
+
}
|
|
6804
|
+
const kindsIn = (text) => {
|
|
6805
|
+
const kinds = []
|
|
6806
|
+
if (!text || text.length < 6) return kinds
|
|
6807
|
+
EMAIL.lastIndex = 0
|
|
6808
|
+
for (let m; (m = EMAIL.exec(text)); ) {
|
|
6809
|
+
if (!SAFE_HOST.test(m[1])) { kinds.push('email'); break }
|
|
6810
|
+
}
|
|
6811
|
+
if (KEY.test(text)) kinds.push('key')
|
|
6812
|
+
CARD.lastIndex = 0
|
|
6813
|
+
for (let m; (m = CARD.exec(text)); ) {
|
|
6814
|
+
const digits = m[1].replace(/[ -]/g, '')
|
|
6815
|
+
if (digits.length >= 13 && digits.length <= 19 && !/^0+$/.test(digits) && luhn(digits)) { kinds.push('card'); break }
|
|
6816
|
+
}
|
|
6817
|
+
if (TAIL.test(text)) kinds.push('card-tail')
|
|
6818
|
+
return kinds
|
|
6819
|
+
}
|
|
6820
|
+
return { kindsIn }
|
|
6821
|
+
})()`;
|
|
6822
|
+
var EXPOSURE_PROBE = `(() => {
|
|
6823
|
+
const { kindsIn } = ${EXPOSURE_MATCHERS_SRC}
|
|
6824
|
+
const vw = innerWidth, vh = innerHeight
|
|
6825
|
+
// A selector that reaches THIS element and no other, so it can be pasted
|
|
6826
|
+
// into "mask" as it is: climb until the path is unique, and say which
|
|
6827
|
+
// sibling wherever a tag-and-class alone would match several.
|
|
6828
|
+
const segment = (el) => {
|
|
6829
|
+
const tid = el.getAttribute('data-testid')
|
|
6830
|
+
if (tid) return el.tagName.toLowerCase() + '[data-testid="' + tid + '"]'
|
|
6831
|
+
if (el.id && /^[A-Za-z_][\\w-]*$/.test(el.id)) return el.tagName.toLowerCase() + '#' + el.id
|
|
6832
|
+
const cls = [...el.classList].filter((c) => /^[A-Za-z_-][\\w-]*$/.test(c)).slice(0, 2)
|
|
6833
|
+
let seg = el.tagName.toLowerCase() + (cls.length ? '.' + cls.join('.') : '')
|
|
6834
|
+
const p = el.parentElement
|
|
6835
|
+
if (p && [...p.children].filter((c) => c.matches(seg)).length > 1)
|
|
6836
|
+
seg += ':nth-child(' + ([...p.children].indexOf(el) + 1) + ')'
|
|
6837
|
+
return seg
|
|
6838
|
+
}
|
|
6839
|
+
const name = (el) => {
|
|
6840
|
+
const parts = []
|
|
6841
|
+
for (let cur = el; cur && cur !== document.body && cur !== document.documentElement; cur = cur.parentElement) {
|
|
6842
|
+
parts.unshift(segment(cur))
|
|
6843
|
+
let n = 2
|
|
6844
|
+
try { n = document.querySelectorAll(parts.join(' > ')).length } catch {}
|
|
6845
|
+
// a bare tag is unique HERE and nowhere else: give it its parent too
|
|
6846
|
+
const bare = parts.length === 1 && !/[#.\\[]/.test(parts[0])
|
|
6847
|
+
if ((n === 1 && !bare) || parts.length >= 5) break
|
|
6848
|
+
}
|
|
6849
|
+
return parts.join(' > ')
|
|
6850
|
+
}
|
|
6851
|
+
const seen = (el) => {
|
|
6852
|
+
if (el.closest('[data-vos-masked]')) return null
|
|
6853
|
+
const r = el.getBoundingClientRect()
|
|
6854
|
+
if (r.width < 2 || r.height < 2) return null
|
|
6855
|
+
if (r.bottom <= 0 || r.right <= 0 || r.top >= vh || r.left >= vw) return null
|
|
6856
|
+
const s = getComputedStyle(el)
|
|
6857
|
+
if (s.visibility === 'hidden' || s.display === 'none' || +s.opacity === 0) return null
|
|
6858
|
+
return r
|
|
6859
|
+
}
|
|
6860
|
+
const out = [], keys = new Set()
|
|
6861
|
+
const add = (el, text) => {
|
|
6862
|
+
const kinds = kindsIn(text)
|
|
6863
|
+
if (!kinds.length) return
|
|
6864
|
+
const r = seen(el)
|
|
6865
|
+
if (!r) return
|
|
6866
|
+
const selector = name(el)
|
|
6867
|
+
for (const kind of kinds) {
|
|
6868
|
+
const k = kind + '|' + selector
|
|
6869
|
+
if (keys.has(k) || out.length >= 40) continue
|
|
6870
|
+
keys.add(k)
|
|
6871
|
+
out.push({ kind, selector, rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) } })
|
|
6872
|
+
}
|
|
6873
|
+
}
|
|
6874
|
+
const walker = document.createTreeWalker(document.body || document.documentElement, NodeFilter.SHOW_TEXT)
|
|
6875
|
+
for (let n; (n = walker.nextNode()); ) {
|
|
6876
|
+
const el = n.parentElement
|
|
6877
|
+
if (!el || /^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE)$/.test(el.tagName)) continue
|
|
6878
|
+
add(el, n.nodeValue)
|
|
6879
|
+
}
|
|
6880
|
+
for (const el of document.querySelectorAll('input, textarea')) {
|
|
6881
|
+
if (/^(password|hidden|file|checkbox|radio)$/.test(el.type)) continue
|
|
6882
|
+
add(el, el.value)
|
|
6883
|
+
}
|
|
6884
|
+
const hits = {}
|
|
6885
|
+
for (const [sel, n] of Object.entries(window.__vosMaskHits || {})) hits[sel] = n
|
|
6886
|
+
return { exposures: out, maskHits: hits }
|
|
6887
|
+
})()`;
|
|
6888
|
+
function maskInitScript(masks) {
|
|
6889
|
+
const rules = masks.map((m) => ({
|
|
6890
|
+
selector: m.selector,
|
|
6891
|
+
as: m.as === "text" ? "text" : "blur",
|
|
6892
|
+
text: m.text ?? ""
|
|
6893
|
+
}));
|
|
6894
|
+
return `(() => {
|
|
6895
|
+
const RULES = ${JSON.stringify(rules)}
|
|
6896
|
+
const hits = (window.__vosMaskHits = window.__vosMaskHits || {})
|
|
6897
|
+
for (const r of RULES) hits[r.selector] = hits[r.selector] || 0
|
|
6898
|
+
const css = RULES.map((r) => r.selector + '[data-vos-masked="blur"]{filter:blur(7px) !important;user-select:none !important}').join('\\n')
|
|
6899
|
+
const paint = () => {
|
|
6900
|
+
const root = document.head || document.documentElement
|
|
6901
|
+
if (!root || document.getElementById('__vos-mask-style')) return
|
|
6902
|
+
const style = document.createElement('style')
|
|
6903
|
+
style.id = '__vos-mask-style'
|
|
6904
|
+
style.textContent = css
|
|
6905
|
+
root.appendChild(style)
|
|
6906
|
+
}
|
|
6907
|
+
const apply = () => {
|
|
6908
|
+
paint()
|
|
6909
|
+
for (const r of RULES) {
|
|
6910
|
+
let found
|
|
6911
|
+
try { found = document.querySelectorAll(r.selector) } catch { continue }
|
|
6912
|
+
if (found.length > hits[r.selector]) hits[r.selector] = found.length
|
|
6913
|
+
for (const el of found) {
|
|
6914
|
+
const form = /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName)
|
|
6915
|
+
const mode = r.as === 'text' && !form ? 'text' : 'blur'
|
|
6916
|
+
if (el.getAttribute('data-vos-masked') !== mode) el.setAttribute('data-vos-masked', mode)
|
|
6917
|
+
if (mode !== 'text') continue
|
|
6918
|
+
const texts = []
|
|
6919
|
+
const w = document.createTreeWalker(el, NodeFilter.SHOW_TEXT)
|
|
6920
|
+
for (let n; (n = w.nextNode()); ) texts.push(n)
|
|
6921
|
+
if (!texts.length) { if (el.textContent !== r.text) el.textContent = r.text; continue }
|
|
6922
|
+
texts.forEach((n, i) => { const want = i === 0 ? r.text : ''; if (n.nodeValue !== want) n.nodeValue = want })
|
|
6923
|
+
}
|
|
6924
|
+
}
|
|
6925
|
+
}
|
|
6926
|
+
new MutationObserver(apply).observe(document, { childList: true, subtree: true, characterData: true })
|
|
6927
|
+
document.addEventListener('DOMContentLoaded', apply)
|
|
6928
|
+
apply()
|
|
6929
|
+
})()`;
|
|
6930
|
+
}
|
|
6931
|
+
var ExposureLog = class {
|
|
6932
|
+
byKey = /* @__PURE__ */ new Map();
|
|
6933
|
+
maskHits = /* @__PURE__ */ new Map();
|
|
6934
|
+
constructor(masks = []) {
|
|
6935
|
+
for (const m of masks) this.maskHits.set(m.selector, 0);
|
|
6936
|
+
}
|
|
6937
|
+
add(step, scan) {
|
|
6938
|
+
if (!scan) return;
|
|
6939
|
+
for (const e of scan.exposures ?? []) {
|
|
6940
|
+
const key = `${e.kind}|${e.selector}`;
|
|
6941
|
+
const had = this.byKey.get(key);
|
|
6942
|
+
if (had) had.seen += 1;
|
|
6943
|
+
else this.byKey.set(key, { step, ...e, seen: 1 });
|
|
6944
|
+
}
|
|
6945
|
+
for (const [sel, n] of Object.entries(scan.maskHits ?? {})) {
|
|
6946
|
+
if (n > (this.maskHits.get(sel) ?? 0)) this.maskHits.set(sel, n);
|
|
6947
|
+
}
|
|
6948
|
+
}
|
|
6949
|
+
exposures() {
|
|
6950
|
+
return [...this.byKey.values()];
|
|
6951
|
+
}
|
|
6952
|
+
masks(rules) {
|
|
6953
|
+
return rules.map((m) => ({
|
|
6954
|
+
selector: m.selector,
|
|
6955
|
+
as: m.as === "text" ? "text" : "blur",
|
|
6956
|
+
hits: this.maskHits.get(m.selector) ?? 0
|
|
6957
|
+
}));
|
|
6958
|
+
}
|
|
6959
|
+
};
|
|
6960
|
+
var KIND_WORDS = {
|
|
6961
|
+
email: "an email address",
|
|
6962
|
+
key: "something shaped like an API key or token",
|
|
6963
|
+
card: "a card number",
|
|
6964
|
+
"card-tail": "a card's last digits"
|
|
6965
|
+
};
|
|
6966
|
+
function exposureLine(e) {
|
|
6967
|
+
const when = e.step < 0 ? "as the page opened" : `at step #${e.step}`;
|
|
6968
|
+
return `${KIND_WORDS[e.kind]} in ${e.selector} (${when}, ${e.rect.x},${e.rect.y} ${e.rect.w}\xD7${e.rect.h})`;
|
|
6969
|
+
}
|
|
6970
|
+
var EXPOSURE_ADVICE = 'The recording shows these. Record from a demo account, or hide them before the camera rolls with "mask" in actions.json: { "selector": "\u2026", "as": "text", "text": "demo@acme.test" } swaps the words, "as": "blur" blurs. Then re-record.';
|
|
6971
|
+
|
|
6761
6972
|
// src/plugin/recorder.ts
|
|
6762
6973
|
var realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6763
6974
|
var DRY_PAUSE_MS = 120;
|
|
@@ -6785,7 +6996,14 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6785
6996
|
deviceScaleFactor: 1,
|
|
6786
6997
|
...opts.storageState ? { storageState: opts.storageState } : {}
|
|
6787
6998
|
});
|
|
6999
|
+
const masks = actions.mask ?? [];
|
|
7000
|
+
if (masks.length) await context.addInitScript(maskInitScript(masks));
|
|
7001
|
+
const exposureLog = new ExposureLog(masks);
|
|
6788
7002
|
const page = await context.newPage();
|
|
7003
|
+
const scanExposures = async (step) => {
|
|
7004
|
+
const scan = await page.evaluate(EXPOSURE_PROBE).catch(() => null);
|
|
7005
|
+
exposureLog.add(step, scan);
|
|
7006
|
+
};
|
|
6789
7007
|
page.on("console", (m) => {
|
|
6790
7008
|
if (m.type() === "error") log(` [page error] ${m.text()}`);
|
|
6791
7009
|
});
|
|
@@ -6815,6 +7033,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6815
7033
|
throw e;
|
|
6816
7034
|
}
|
|
6817
7035
|
}
|
|
7036
|
+
await scanExposures(-1);
|
|
6818
7037
|
const cdp = await context.newCDPSession(page);
|
|
6819
7038
|
const frames = [];
|
|
6820
7039
|
let frameIdx = 0;
|
|
@@ -7074,6 +7293,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
7074
7293
|
gestureMs,
|
|
7075
7294
|
wallMs: now() - stepStart
|
|
7076
7295
|
});
|
|
7296
|
+
await scanExposures(stepIdx);
|
|
7077
7297
|
}
|
|
7078
7298
|
if (!capped) await sleep(TRAILING_HOLD_MS);
|
|
7079
7299
|
if (capped || capReached(now(), maxSeconds)) {
|
|
@@ -7104,7 +7324,9 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
7104
7324
|
platform: process.platform === "darwin" ? "mac" : process.platform === "win32" ? "windows" : "linux",
|
|
7105
7325
|
producer: "cli",
|
|
7106
7326
|
steps: stepSpans,
|
|
7107
|
-
...wall ? { wall: { kind: wall.kind, asked: wall.asked, landed: wall.landed } } : {}
|
|
7327
|
+
...wall ? { wall: { kind: wall.kind, asked: wall.asked, landed: wall.landed } } : {},
|
|
7328
|
+
...exposureLog.exposures().length ? { exposures: exposureLog.exposures() } : {},
|
|
7329
|
+
...masks.length ? { masks: exposureLog.masks(masks) } : {}
|
|
7108
7330
|
};
|
|
7109
7331
|
const freezes = [];
|
|
7110
7332
|
for (let i = 1; i < frames.length; i++) {
|
|
@@ -7140,6 +7362,8 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
7140
7362
|
skipped,
|
|
7141
7363
|
navTimeout,
|
|
7142
7364
|
wall,
|
|
7365
|
+
exposures: exposureLog.exposures(),
|
|
7366
|
+
masks: exposureLog.masks(masks),
|
|
7143
7367
|
freezes,
|
|
7144
7368
|
freezePct,
|
|
7145
7369
|
capped
|
|
@@ -10575,8 +10799,23 @@ function strictReason(rec) {
|
|
|
10575
10799
|
const parts = [`${rec.skipped.length} skipped step(s)`];
|
|
10576
10800
|
if (rec.navTimeout) parts.push("networkidle timeout");
|
|
10577
10801
|
if (rec.capped) parts.push("stopped at --max-duration");
|
|
10802
|
+
const missed = maskMisses(rec);
|
|
10803
|
+
if (missed.length)
|
|
10804
|
+
parts.push(
|
|
10805
|
+
`${missed.length} mask(s) that hid nothing (${missed.join(", ")}): whatever they were for may be showing`
|
|
10806
|
+
);
|
|
10578
10807
|
return parts.join(" + ");
|
|
10579
10808
|
}
|
|
10809
|
+
function maskMisses(rec) {
|
|
10810
|
+
return (rec.masks ?? []).filter((m) => m.hits === 0).map((m) => m.selector);
|
|
10811
|
+
}
|
|
10812
|
+
function exposureNote(rec) {
|
|
10813
|
+
if (!rec.exposures.length) return "";
|
|
10814
|
+
return `
|
|
10815
|
+
EXPOSED in the frame (${rec.exposures.length}):
|
|
10816
|
+
${rec.exposures.map((e) => ` - ${exposureLine(e)}`).join("\n")}
|
|
10817
|
+
${EXPOSURE_ADVICE}`;
|
|
10818
|
+
}
|
|
10580
10819
|
function takeBrowserArgs(multi) {
|
|
10581
10820
|
const given = multi["browser-arg"];
|
|
10582
10821
|
return (given ?? []).filter(Boolean);
|
|
@@ -10674,7 +10913,7 @@ async function cmdRecord(argv) {
|
|
|
10674
10913
|
const what = s.selector ? ` ${s.selector}` : "";
|
|
10675
10914
|
return ` ${s.skipped ? "\u2717" : "\u2713"} #${s.step}${s.id ? ` (${s.id})` : ""} ${s.do}${what}${s.skipped ? " NOT FOUND" : rect}${s.navigated ? " \u2192 navigated" : ""}`;
|
|
10676
10915
|
});
|
|
10677
|
-
const failed2 = rec.skipped.length > 0 || rec.navTimeout;
|
|
10916
|
+
const failed2 = rec.skipped.length > 0 || rec.navTimeout || maskMisses(rec).length > 0;
|
|
10678
10917
|
const carried = [
|
|
10679
10918
|
...strFlag(flags, "storage-state") ? [`--storage-state ${strFlag(flags, "storage-state")}`] : [],
|
|
10680
10919
|
...browserArgs.map((a) => `--browser-arg=${a}`),
|
|
@@ -10688,11 +10927,13 @@ async function cmdRecord(argv) {
|
|
|
10688
10927
|
seconds: +((Date.now() - started) / 1e3).toFixed(1),
|
|
10689
10928
|
steps,
|
|
10690
10929
|
skipped: rec.skipped,
|
|
10691
|
-
navTimeout: rec.navTimeout
|
|
10930
|
+
navTimeout: rec.navTimeout,
|
|
10931
|
+
exposures: rec.exposures,
|
|
10932
|
+
masks: rec.masks
|
|
10692
10933
|
},
|
|
10693
10934
|
`${failed2 ? "REHEARSAL FAILED" : "Rehearsal passed"}: ${steps.length - rec.skipped.length}/${steps.length} steps resolved in ${((Date.now() - started) / 1e3).toFixed(1)}s${rec.navTimeout ? " (the first load never reached networkidle)" : ""}
|
|
10694
10935
|
${lines.join("\n")}
|
|
10695
|
-
${failed2 ? "Fix the script, rehearse again, then record." : `Rects are capture px (the step rects a pin or a callout reads). Next: vos record --actions ${actionsPath} --out ${nextOut}${carried ? ` ${carried}` : ""} --strict`}`
|
|
10936
|
+
${maskMisses(rec).length ? `MASK hid nothing: ${maskMisses(rec).join(", ")} (the selector reached no element). ` : ""}${failed2 ? "Fix the script, rehearse again, then record." : `Rects are capture px (the step rects a pin or a callout reads). Next: vos record --actions ${actionsPath} --out ${nextOut}${carried ? ` ${carried}` : ""} --strict`}${exposureNote(rec)}`
|
|
10696
10937
|
);
|
|
10697
10938
|
return failed2 ? EXIT_USAGE : EXIT_OK;
|
|
10698
10939
|
} finally {
|
|
@@ -10734,7 +10975,7 @@ ${lines.join("\n")}
|
|
|
10734
10975
|
const plan = await planTake(outDir, { backdrop });
|
|
10735
10976
|
const clicks = rec.events.filter((e) => e.type === "down").length;
|
|
10736
10977
|
const strict = flags.strict === true;
|
|
10737
|
-
const strictFail = strict && (rec.skipped.length > 0 || rec.navTimeout || rec.capped);
|
|
10978
|
+
const strictFail = strict && (rec.skipped.length > 0 || rec.navTimeout || rec.capped || maskMisses(rec).length > 0);
|
|
10738
10979
|
const skippedNote = rec.skipped.length ? `
|
|
10739
10980
|
SKIPPED ${rec.skipped.length} step(s): ${rec.skipped.map((s) => `#${s.step} ${s.do} ${s.selector}`).join(", ")}` : "";
|
|
10740
10981
|
r.done(
|
|
@@ -10753,11 +10994,13 @@ ${lines.join("\n")}
|
|
|
10753
10994
|
capped: rec.capped,
|
|
10754
10995
|
pace: rec.pace,
|
|
10755
10996
|
...rec.wall ? { wall: rec.wall } : {},
|
|
10997
|
+
exposures: rec.exposures,
|
|
10998
|
+
...rec.masks.length ? { masks: rec.masks } : {},
|
|
10756
10999
|
...strictFail ? { strictFailed: true } : {}
|
|
10757
11000
|
},
|
|
10758
|
-
strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}` : `Take ready: ${outDir}
|
|
11001
|
+
strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${exposureNote(rec)}` : `Take ready: ${outDir}
|
|
10759
11002
|
${(rec.meta.durationMs / 1e3).toFixed(1)}s \xB7 ${rec.frames.length} frames \xB7 ${rec.events.length} cursor events \xB7 ${clicks} clicks \xB7 ${plan.doc.zoom.length} zoom spans planned \xB7 ${rec.freezePct}% frozen${rec.freezePct >= 25 ? " \u26A0 keep motion in frame or trim" : ""}${skippedNote}
|
|
10760
|
-
Next: edit ${join18(outDir, "doc.json")} (optional), then: vos render ${outDir}`
|
|
11003
|
+
Next: edit ${join18(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
|
|
10761
11004
|
);
|
|
10762
11005
|
return strictFail ? EXIT_USAGE : EXIT_OK;
|
|
10763
11006
|
} finally {
|
|
@@ -10828,7 +11071,7 @@ async function cmdCreate2(argv) {
|
|
|
10828
11071
|
await releaseFrames(paths, flags, r.log);
|
|
10829
11072
|
r.event({ event: "phase", phase: "plan" });
|
|
10830
11073
|
await planTake(outDir, { backdrop });
|
|
10831
|
-
if (flags.strict === true && (rec.skipped.length > 0 || rec.navTimeout || rec.capped)) {
|
|
11074
|
+
if (flags.strict === true && (rec.skipped.length > 0 || rec.navTimeout || rec.capped || maskMisses(rec).length > 0)) {
|
|
10832
11075
|
r.done(
|
|
10833
11076
|
{
|
|
10834
11077
|
take: outDir,
|
|
@@ -10872,10 +11115,12 @@ async function cmdCreate2(argv) {
|
|
|
10872
11115
|
navTimeout: rec.navTimeout,
|
|
10873
11116
|
freezes: rec.freezes,
|
|
10874
11117
|
freezePct: rec.freezePct,
|
|
11118
|
+
exposures: rec.exposures,
|
|
11119
|
+
...rec.masks.length ? { masks: rec.masks } : {},
|
|
10875
11120
|
...flags.draft === true ? { draft: true } : {}
|
|
10876
11121
|
},
|
|
10877
11122
|
`Created ${result.out} (${(result.bytes / 1024).toFixed(0)} KB, ${result.width}x${result.height}@${result.fps}, ${result.duration.toFixed(1)}s, ${result.zoomSpans} zoom spans, ${result.clicks} click effects${result.audio ? ", audio" : ""}${flags.draft === true ? ", DRAFT" : ""})
|
|
10878
|
-
take: ${outDir} \u2014 inspect with: vos frames ${outDir} --at-zooms; edit doc.json and re-render with: vos render ${outDir}`
|
|
11123
|
+
take: ${outDir} \u2014 inspect with: vos frames ${outDir} --at-zooms; edit doc.json and re-render with: vos render ${outDir}${exposureNote(rec)}`
|
|
10879
11124
|
);
|
|
10880
11125
|
return EXIT_OK;
|
|
10881
11126
|
} finally {
|
|
@@ -11440,12 +11685,14 @@ async function cmdDigest(argv) {
|
|
|
11440
11685
|
outputDuration: d.take.outputDuration,
|
|
11441
11686
|
hasCursor: d.take.hasCursor,
|
|
11442
11687
|
...d.take.wall ? { wall: d.take.wall } : {},
|
|
11688
|
+
...d.take.exposures.length ? { exposures: d.take.exposures } : {},
|
|
11443
11689
|
tokensEstimate: d.images.tokensEstimate,
|
|
11444
11690
|
bytes: result.bytes
|
|
11445
11691
|
},
|
|
11446
11692
|
`Wrote ${result.file} \u2014 ${d.moments.length} moments (${Object.entries(
|
|
11447
11693
|
kinds
|
|
11448
|
-
).map(([k, n]) => `${n} ${k}`).join(", ")}), ${d.images.full} frames + ${d.images.crop} crops` + (d.images.sheet ? `, sheet.png` : "") + ` \u2014 ~${d.images.tokensEstimate} image tokens to read them all` + (d.take.hasCursor ? "" : "\n no cursor track: moments are head/tail/scenes only \u2014 pace by activity, zoom only where the ask names a place") + (d.take.
|
|
11694
|
+
).map(([k, n]) => `${n} ${k}`).join(", ")}), ${d.images.full} frames + ${d.images.crop} crops` + (d.images.sheet ? `, sheet.png` : "") + ` \u2014 ~${d.images.tokensEstimate} image tokens to read them all` + (d.take.hasCursor ? "" : "\n no cursor track: moments are head/tail/scenes only \u2014 pace by activity, zoom only where the ask names a place") + (d.take.exposures.length ? `
|
|
11695
|
+
EXPOSED in the footage: ${d.take.exposures.map((e) => `${e.kind} in ${e.selector}`).join("; ")} \u2014 look at those frames before this take goes anywhere` : "") + (d.take.wall ? `
|
|
11449
11696
|
WALL: asked for ${d.take.wall.asked}, recorded ${d.take.wall.landed} \u2014 this footage may be the wrong page; re-record with a session before cutting it` : "") + `
|
|
11450
11697
|
Read digest.json, then the sheet, then a crop only where you must decide.`
|
|
11451
11698
|
);
|
|
@@ -11709,6 +11956,18 @@ async function cmdValidate(argv) {
|
|
|
11709
11956
|
);
|
|
11710
11957
|
if (mismatch) warnings.push(mismatch);
|
|
11711
11958
|
}
|
|
11959
|
+
for (const e of take.meta.exposures ?? [])
|
|
11960
|
+
warnings.push(`the recording shows ${exposureLine(e)}`);
|
|
11961
|
+
if (take.meta.exposures?.length) warnings.push(EXPOSURE_ADVICE);
|
|
11962
|
+
for (const m of take.meta.masks ?? [])
|
|
11963
|
+
if (m.hits === 0)
|
|
11964
|
+
warnings.push(
|
|
11965
|
+
`mask "${m.selector}" reached no element, so it hid nothing: whatever it was for may be showing`
|
|
11966
|
+
);
|
|
11967
|
+
if (take.meta.wall)
|
|
11968
|
+
warnings.push(
|
|
11969
|
+
`recorded past a wall: asked for ${take.meta.wall.asked}, landed on ${take.meta.wall.landed}. This footage may be the wrong page`
|
|
11970
|
+
);
|
|
11712
11971
|
if (!take.doc) problems.push("missing doc.json (run plan)");
|
|
11713
11972
|
if (take.doc) {
|
|
11714
11973
|
const lint = lintDoc(take.doc);
|
|
@@ -11989,4 +12248,4 @@ export {
|
|
|
11989
12248
|
verbHelp,
|
|
11990
12249
|
run
|
|
11991
12250
|
};
|
|
11992
|
-
//# sourceMappingURL=chunk-
|
|
12251
|
+
//# sourceMappingURL=chunk-7RMULTDM.js.map
|