my-frontend-observer 0.1.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 +22 -0
- package/README.md +69 -0
- package/dist/application/browserCaptureService.d.ts +11 -0
- package/dist/application/browserCaptureService.js +12 -0
- package/dist/application/browserCaptureService.js.map +1 -0
- package/dist/application/observationPersistence.d.ts +59 -0
- package/dist/application/observationPersistence.js +78 -0
- package/dist/application/observationPersistence.js.map +1 -0
- package/dist/artifacts/artifactWriter.d.ts +25 -0
- package/dist/artifacts/artifactWriter.js +68 -0
- package/dist/artifacts/artifactWriter.js.map +1 -0
- package/dist/artifacts/types.d.ts +17 -0
- package/dist/artifacts/types.js +2 -0
- package/dist/artifacts/types.js.map +1 -0
- package/dist/browser/chromiumAdapter.d.ts +21 -0
- package/dist/browser/chromiumAdapter.js +150 -0
- package/dist/browser/chromiumAdapter.js.map +1 -0
- package/dist/browser/evidenceCapture.d.ts +19 -0
- package/dist/browser/evidenceCapture.js +201 -0
- package/dist/browser/evidenceCapture.js.map +1 -0
- package/dist/browser/types.d.ts +22 -0
- package/dist/browser/types.js +2 -0
- package/dist/browser/types.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +216 -0
- package/dist/cli.js.map +1 -0
- package/dist/domain/completion.d.ts +30 -0
- package/dist/domain/completion.js +22 -0
- package/dist/domain/completion.js.map +1 -0
- package/dist/domain/diagnostics.d.ts +17 -0
- package/dist/domain/diagnostics.js +55 -0
- package/dist/domain/diagnostics.js.map +1 -0
- package/dist/domain/evidence.d.ts +27 -0
- package/dist/domain/evidence.js +55 -0
- package/dist/domain/evidence.js.map +1 -0
- package/dist/domain/identity.d.ts +13 -0
- package/dist/domain/identity.js +37 -0
- package/dist/domain/identity.js.map +1 -0
- package/dist/domain/schema.d.ts +111 -0
- package/dist/domain/schema.js +126 -0
- package/dist/domain/schema.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/request/paths.d.ts +14 -0
- package/dist/request/paths.js +33 -0
- package/dist/request/paths.js.map +1 -0
- package/dist/request/request.d.ts +43 -0
- package/dist/request/request.js +174 -0
- package/dist/request/request.js.map +1 -0
- package/dist/safety/policy.d.ts +14 -0
- package/dist/safety/policy.js +81 -0
- package/dist/safety/policy.js.map +1 -0
- package/docs/ARCHITECTURE.md +85 -0
- package/docs/CI_CD.md +28 -0
- package/docs/COMMANDS.md +82 -0
- package/docs/CONTRACTS.md +54 -0
- package/docs/CURRENT_STATE.md +113 -0
- package/docs/DEVELOPMENT.md +65 -0
- package/docs/DOCUMENTATION_PRESERVATION_POLICY.md +33 -0
- package/docs/PROJECT_DESCRIPTION.md +1770 -0
- package/docs/PROJECT_MILESTONES.md +2073 -0
- package/docs/PROJECT_OVERVIEW.md +53 -0
- package/docs/QUICKSTART.md +35 -0
- package/docs/RELEASE.md +9 -0
- package/docs/ROADMAP.md +352 -0
- package/docs/SECURITY.md +33 -0
- package/docs/WORKFLOWS.md +51 -0
- package/package.json +46 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { orderDiagnostics } from '../domain/diagnostics.js';
|
|
2
|
+
import { classifyUrl } from '../safety/policy.js';
|
|
3
|
+
import { normalizeOutputLocation } from './paths.js';
|
|
4
|
+
const DEFAULT_VIEWPORT = { width: 1280, height: 720 };
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
6
|
+
const DEFAULT_READINESS_CONDITION = 'load';
|
|
7
|
+
const DEFAULT_READINESS_TIMEOUT_MS = 10000;
|
|
8
|
+
const DEFAULT_OUTPUT_LOCATION = 'observations';
|
|
9
|
+
const VIEWPORT_MIN = 200;
|
|
10
|
+
const VIEWPORT_MAX = 3840;
|
|
11
|
+
const TIMEOUT_MIN_MS = 1000;
|
|
12
|
+
const TIMEOUT_MAX_MS = 120000;
|
|
13
|
+
const READINESS_TIMEOUT_MIN_MS = 500;
|
|
14
|
+
const MAX_TARGETS = 20;
|
|
15
|
+
const TARGET_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
16
|
+
const MAX_SELECTOR_LENGTH = 500;
|
|
17
|
+
function isPlainObject(value) {
|
|
18
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
function isInRange(value, min, max) {
|
|
21
|
+
return Number.isInteger(value) && value >= min && value <= max;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Total, never throws. Collects every applicable diagnostic before returning
|
|
25
|
+
* (does not short-circuit on the first violation) so callers see every
|
|
26
|
+
* problem at once, in deterministic order.
|
|
27
|
+
*/
|
|
28
|
+
export function normalizeRequest(raw) {
|
|
29
|
+
const diagnostics = [];
|
|
30
|
+
let targetUrl;
|
|
31
|
+
if (typeof raw.targetUrl !== 'string' || raw.targetUrl.length === 0) {
|
|
32
|
+
diagnostics.push({ code: 'invalid-request', severity: 'error', message: 'targetUrl is required and must be a non-empty string' });
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
targetUrl = raw.targetUrl;
|
|
36
|
+
const decision = classifyUrl(targetUrl);
|
|
37
|
+
if (!decision.allowed)
|
|
38
|
+
diagnostics.push(decision.diagnostic);
|
|
39
|
+
}
|
|
40
|
+
let viewport = DEFAULT_VIEWPORT;
|
|
41
|
+
if (raw.viewport !== undefined) {
|
|
42
|
+
const candidate = raw.viewport;
|
|
43
|
+
if (!isPlainObject(candidate) ||
|
|
44
|
+
typeof candidate.width !== 'number' ||
|
|
45
|
+
typeof candidate.height !== 'number' ||
|
|
46
|
+
!isInRange(candidate.width, VIEWPORT_MIN, VIEWPORT_MAX) ||
|
|
47
|
+
!isInRange(candidate.height, VIEWPORT_MIN, VIEWPORT_MAX)) {
|
|
48
|
+
diagnostics.push({
|
|
49
|
+
code: 'invalid-request',
|
|
50
|
+
severity: 'error',
|
|
51
|
+
message: `viewport width/height must be integers in [${VIEWPORT_MIN}, ${VIEWPORT_MAX}]`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
viewport = { width: candidate.width, height: candidate.height };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const targets = [];
|
|
59
|
+
if (raw.targets !== undefined) {
|
|
60
|
+
if (!Array.isArray(raw.targets)) {
|
|
61
|
+
diagnostics.push({ code: 'invalid-request', severity: 'error', message: 'targets must be an array' });
|
|
62
|
+
}
|
|
63
|
+
else if (raw.targets.length > MAX_TARGETS) {
|
|
64
|
+
diagnostics.push({ code: 'invalid-request', severity: 'error', message: `targets must contain at most ${MAX_TARGETS} entries` });
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const seenNames = new Set();
|
|
68
|
+
for (const rawTarget of raw.targets) {
|
|
69
|
+
if (!isPlainObject(rawTarget) || typeof rawTarget.name !== 'string' || typeof rawTarget.selector !== 'string') {
|
|
70
|
+
diagnostics.push({ code: 'invalid-request', severity: 'error', message: 'each target requires a string name and string selector' });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const name = rawTarget.name;
|
|
74
|
+
const selector = rawTarget.selector;
|
|
75
|
+
if (!TARGET_NAME_RE.test(name)) {
|
|
76
|
+
diagnostics.push({ code: 'invalid-request', severity: 'error', message: 'target name must match ^[A-Za-z0-9_-]{1,64}$', targetName: name });
|
|
77
|
+
}
|
|
78
|
+
if (selector.length === 0 || selector.length > MAX_SELECTOR_LENGTH) {
|
|
79
|
+
diagnostics.push({
|
|
80
|
+
code: 'invalid-request',
|
|
81
|
+
severity: 'error',
|
|
82
|
+
message: `target selector must be 1-${MAX_SELECTOR_LENGTH} characters`,
|
|
83
|
+
targetName: name,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const normalizedName = name.toLowerCase();
|
|
87
|
+
if (seenNames.has(normalizedName)) {
|
|
88
|
+
diagnostics.push({ code: 'invalid-request', severity: 'error', message: 'duplicate target name', targetName: name });
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
seenNames.add(normalizedName);
|
|
92
|
+
}
|
|
93
|
+
targets.push({ name, selector });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
98
|
+
if (raw.timeoutMs !== undefined) {
|
|
99
|
+
if (typeof raw.timeoutMs !== 'number' || !isInRange(raw.timeoutMs, TIMEOUT_MIN_MS, TIMEOUT_MAX_MS)) {
|
|
100
|
+
diagnostics.push({
|
|
101
|
+
code: 'invalid-request',
|
|
102
|
+
severity: 'error',
|
|
103
|
+
message: `timeoutMs must be an integer in [${TIMEOUT_MIN_MS}, ${TIMEOUT_MAX_MS}]`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
timeoutMs = raw.timeoutMs;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
let readinessCondition = DEFAULT_READINESS_CONDITION;
|
|
111
|
+
let readinessTimeoutMs = Math.min(DEFAULT_READINESS_TIMEOUT_MS, timeoutMs);
|
|
112
|
+
if (raw.readiness !== undefined) {
|
|
113
|
+
if (!isPlainObject(raw.readiness)) {
|
|
114
|
+
diagnostics.push({ code: 'unsupported-configuration', severity: 'error', message: 'readiness must be an object' });
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const readinessRaw = raw.readiness;
|
|
118
|
+
if (readinessRaw.condition !== undefined) {
|
|
119
|
+
if (readinessRaw.condition === 'load' || readinessRaw.condition === 'domcontentloaded') {
|
|
120
|
+
readinessCondition = readinessRaw.condition;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
diagnostics.push({
|
|
124
|
+
code: 'unsupported-configuration',
|
|
125
|
+
severity: 'error',
|
|
126
|
+
message: 'readiness.condition must be "load" or "domcontentloaded"',
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (readinessRaw.timeoutMs !== undefined) {
|
|
131
|
+
if (typeof readinessRaw.timeoutMs !== 'number' || !isInRange(readinessRaw.timeoutMs, READINESS_TIMEOUT_MIN_MS, timeoutMs)) {
|
|
132
|
+
diagnostics.push({
|
|
133
|
+
code: 'unsupported-configuration',
|
|
134
|
+
severity: 'error',
|
|
135
|
+
message: `readiness.timeoutMs must be an integer in [${READINESS_TIMEOUT_MIN_MS}, timeoutMs]`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
readinessTimeoutMs = readinessRaw.timeoutMs;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
let outputLocation = DEFAULT_OUTPUT_LOCATION;
|
|
145
|
+
if (raw.outputLocation !== undefined) {
|
|
146
|
+
if (typeof raw.outputLocation !== 'string') {
|
|
147
|
+
diagnostics.push({ code: 'invalid-request', severity: 'error', message: 'outputLocation must be a string' });
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const result = normalizeOutputLocation(raw.outputLocation);
|
|
151
|
+
if (result.ok) {
|
|
152
|
+
outputLocation = result.value;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
diagnostics.push(result.diagnostic);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (diagnostics.length > 0 || targetUrl === undefined) {
|
|
160
|
+
return { ok: false, diagnostics: orderDiagnostics(diagnostics) };
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
ok: true,
|
|
164
|
+
request: {
|
|
165
|
+
targetUrl,
|
|
166
|
+
viewport,
|
|
167
|
+
targets,
|
|
168
|
+
outputLocation,
|
|
169
|
+
timeoutMs,
|
|
170
|
+
readiness: { condition: readinessCondition, timeoutMs: readinessTimeoutMs },
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=request.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request.js","sourceRoot":"","sources":["../../src/request/request.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Diagnostic } from '../domain/diagnostics.js';
|
|
2
|
+
export type SafetyDecision = {
|
|
3
|
+
allowed: true;
|
|
4
|
+
} | {
|
|
5
|
+
allowed: false;
|
|
6
|
+
diagnostic: Diagnostic;
|
|
7
|
+
};
|
|
8
|
+
/** Pure classification: scheme + loopback rules only. Never performs DNS resolution or network I/O. */
|
|
9
|
+
export declare function classifyUrl(url: string): SafetyDecision;
|
|
10
|
+
export declare function classifyRedirect(fromUrl: string, toUrl: string): SafetyDecision;
|
|
11
|
+
export declare function classifySubresource(url: string): SafetyDecision;
|
|
12
|
+
/** Popups and downloads are never followed/saved in v0.1; both are non-fatal (warning) notices, not errors. */
|
|
13
|
+
export declare function classifyPopup(): Diagnostic;
|
|
14
|
+
export declare function classifyDownload(): Diagnostic;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const ALLOWED_SCHEMES = ['http', 'https'];
|
|
2
|
+
/**
|
|
3
|
+
* v0.1 loopback allowlist: literal localhost/127.0.0.1/::1 and any 127.x.x.x
|
|
4
|
+
* form only. No DNS resolution, no arbitrary named "local dev host" support -
|
|
5
|
+
* an explicit conservative reading recorded in behavior-model.txt.
|
|
6
|
+
*/
|
|
7
|
+
function isLoopbackHost(hostname) {
|
|
8
|
+
const normalized = hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
9
|
+
if (normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1')
|
|
10
|
+
return true;
|
|
11
|
+
return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(normalized);
|
|
12
|
+
}
|
|
13
|
+
/** Pure classification: scheme + loopback rules only. Never performs DNS resolution or network I/O. */
|
|
14
|
+
export function classifyUrl(url) {
|
|
15
|
+
let parsed;
|
|
16
|
+
try {
|
|
17
|
+
parsed = new URL(url);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return {
|
|
21
|
+
allowed: false,
|
|
22
|
+
diagnostic: { code: 'invalid-request', severity: 'error', message: `malformed URL: ${url}` },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const scheme = parsed.protocol.replace(/:$/, '');
|
|
26
|
+
if (!ALLOWED_SCHEMES.includes(scheme)) {
|
|
27
|
+
return {
|
|
28
|
+
allowed: false,
|
|
29
|
+
diagnostic: { code: 'unsafe-url', severity: 'error', message: `scheme "${scheme}" is not allowed`, details: { url } },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (parsed.username.length > 0 || parsed.password.length > 0) {
|
|
33
|
+
return {
|
|
34
|
+
allowed: false,
|
|
35
|
+
diagnostic: { code: 'unsafe-url', severity: 'error', message: 'credential-bearing URLs are rejected', details: { url } },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (!isLoopbackHost(parsed.hostname)) {
|
|
39
|
+
return {
|
|
40
|
+
allowed: false,
|
|
41
|
+
diagnostic: { code: 'unsafe-url', severity: 'error', message: `host "${parsed.hostname}" is not a loopback host`, details: { url } },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return { allowed: true };
|
|
45
|
+
}
|
|
46
|
+
export function classifyRedirect(fromUrl, toUrl) {
|
|
47
|
+
const decision = classifyUrl(toUrl);
|
|
48
|
+
if (decision.allowed)
|
|
49
|
+
return decision;
|
|
50
|
+
return {
|
|
51
|
+
allowed: false,
|
|
52
|
+
diagnostic: {
|
|
53
|
+
code: 'prohibited-redirect',
|
|
54
|
+
severity: 'error',
|
|
55
|
+
message: 'redirect target failed safety policy',
|
|
56
|
+
details: { from: fromUrl, to: toUrl },
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function classifySubresource(url) {
|
|
61
|
+
const decision = classifyUrl(url);
|
|
62
|
+
if (decision.allowed)
|
|
63
|
+
return decision;
|
|
64
|
+
return {
|
|
65
|
+
allowed: false,
|
|
66
|
+
diagnostic: {
|
|
67
|
+
code: 'prohibited-subresource-request',
|
|
68
|
+
severity: 'error',
|
|
69
|
+
message: 'subresource target failed safety policy',
|
|
70
|
+
details: { url },
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Popups and downloads are never followed/saved in v0.1; both are non-fatal (warning) notices, not errors. */
|
|
75
|
+
export function classifyPopup() {
|
|
76
|
+
return { code: 'unsupported-configuration', severity: 'warning', message: 'popups are not followed in v0.1' };
|
|
77
|
+
}
|
|
78
|
+
export function classifyDownload() {
|
|
79
|
+
return { code: 'unsupported-configuration', severity: 'warning', message: 'downloads are not followed in v0.1' };
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/safety/policy.ts"],"names":[],"mappings":"AAIA,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,OAAO,CAAU,CAAC;AAEnD;;;;GAIG;AACH,SAAS,cAAc,CAAC,QAAgB;IACtC,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IAClE,IAAI,UAAU,KAAK,WAAW,IAAI,UAAU,KAAK,WAAW,IAAI,UAAU,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAClG,OAAO,kCAAkC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AAC7D,CAAC;AAED,uGAAuG;AACvG,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;YACL,OAAO,EAAE,KAAK;YACd,UAAU,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,kBAAkB,GAAG,EAAE,EAAE;SAC7F,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACjD,IAAI,CAAE,eAAqC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7D,OAAO;YACL,OAAO,EAAE,KAAK;YACd,UAAU,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,MAAM,kBAAkB,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,EAAE;SACtH,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7D,OAAO;YACL,OAAO,EAAE,KAAK;YACd,UAAU,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,sCAAsC,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,EAAE;SACzH,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QACrC,OAAO;YACL,OAAO,EAAE,KAAK;YACd,UAAU,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,MAAM,CAAC,QAAQ,0BAA0B,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,EAAE;SACrI,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,KAAa;IAC7D,MAAM,QAAQ,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IACpC,IAAI,QAAQ,CAAC,OAAO;QAAE,OAAO,QAAQ,CAAC;IACtC,OAAO;QACL,OAAO,EAAE,KAAK;QACd,UAAU,EAAE;YACV,IAAI,EAAE,qBAAqB;YAC3B,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,sCAAsC;YAC/C,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE;SACtC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,QAAQ,CAAC,OAAO;QAAE,OAAO,QAAQ,CAAC;IACtC,OAAO;QACL,OAAO,EAAE,KAAK;QACd,UAAU,EAAE;YACV,IAAI,EAAE,gCAAgC;YACtC,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,yCAAyC;YAClD,OAAO,EAAE,EAAE,GAAG,EAAE;SACjB;KACF,CAAC;AACJ,CAAC;AAED,+GAA+G;AAC/G,MAAM,UAAU,aAAa;IAC3B,OAAO,EAAE,IAAI,EAAE,2BAA2B,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,iCAAiC,EAAE,CAAC;AAChH,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,EAAE,IAAI,EAAE,2BAA2B,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,oCAAoC,EAAE,CAAC;AACnH,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Current scaffold architecture
|
|
4
|
+
|
|
5
|
+
The current repository is one private TypeScript ESM package:
|
|
6
|
+
|
|
7
|
+
- `src/cli.ts` is the real, thin `observe` command entry point (argument
|
|
8
|
+
parsing/output only).
|
|
9
|
+
- `src/index.ts` is the library entry point re-exporting the observer-owned
|
|
10
|
+
contracts/functions from every layer below.
|
|
11
|
+
- `scripts/clean.mjs` safely removes only the project `dist/` directory.
|
|
12
|
+
- `scripts/check-docs.mjs` validates the canonical documentation foundation,
|
|
13
|
+
roadmap version presence, and the no-batches rule.
|
|
14
|
+
- TypeScript, ESLint, Vitest, and package configuration provide foundation
|
|
15
|
+
validation, now exercised by real product tests (`tests/unit/`,
|
|
16
|
+
`tests/browser/`).
|
|
17
|
+
|
|
18
|
+
Batch 1 added the observation domain/schema and safety-policy layer
|
|
19
|
+
(`src/domain/`, `src/request/`, `src/safety/`). Batch 2 added a real
|
|
20
|
+
Playwright Chromium browser adapter (`src/browser/`), a minimal application
|
|
21
|
+
seam invoking it (`src/application/`), and a deterministic browser
|
|
22
|
+
fixture/test boundary (`tests/fixtures/`, `tests/browser/`, run via
|
|
23
|
+
`npm run test:browser`). Batch 3 extended that single browser adapter with an
|
|
24
|
+
internal page/target measurement module (`src/browser/evidenceCapture.ts`)
|
|
25
|
+
that reads page and explicit-CSS-target evidence from the same live,
|
|
26
|
+
already-ready page used for the screenshot - no second browser/page is ever
|
|
27
|
+
opened, and Playwright objects still never leave `src/browser/`. Batch 4
|
|
28
|
+
added the artifact ownership boundary itself: `src/artifacts/artifactWriter.ts`
|
|
29
|
+
is the one canonical place that writes an observation to disk (temp
|
|
30
|
+
directory, then one atomic rename into `<outputLocation>/<observationId>/`),
|
|
31
|
+
and `src/application/observationPersistence.ts` assembles the frozen
|
|
32
|
+
`ObservationArtifact` from a browser-capture result before handing it to the
|
|
33
|
+
writer. The artifact layer has no Playwright dependency and is testable
|
|
34
|
+
without launching Chromium. Batch 5 completed the boundary chain: `src/cli.ts`
|
|
35
|
+
parses `observe` arguments (CLI-syntax errors only - e.g. malformed
|
|
36
|
+
`WIDTHxHEIGHT`), constructs a raw request, and hands it to the existing
|
|
37
|
+
Batch 1 `normalizeRequest`; on success it calls one new application-level use
|
|
38
|
+
case, `observe()` in `src/application/observationPersistence.ts`, which runs
|
|
39
|
+
the existing `runBrowserCapture` exactly once and, only on success, the
|
|
40
|
+
existing artifact writer exactly once, then returns a small observer-owned
|
|
41
|
+
`ApplicationObservationResult` (observation id, completion state, artifact
|
|
42
|
+
path, target/diagnostic counts) for the CLI to print. The CLI never imports
|
|
43
|
+
Playwright or the filesystem-write path directly. Batch 6 closed the
|
|
44
|
+
remaining real-Chromium coverage gap (a genuine navigation failure, distinct
|
|
45
|
+
from a readiness timeout or a pre-launch safety rejection) and validated the
|
|
46
|
+
packed npm tarball end to end in a clean consumer environment, independent
|
|
47
|
+
of the source checkout. There is still no controlled-scroll/comparison
|
|
48
|
+
behavior.
|
|
49
|
+
|
|
50
|
+
## Planned v0.1 architecture constraints
|
|
51
|
+
|
|
52
|
+
v0.1 planning must preserve these approved boundaries without treating module
|
|
53
|
+
names from the historical run as mandatory:
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
thin command-line boundary
|
|
57
|
+
↓
|
|
58
|
+
reusable observation engine/application layer
|
|
59
|
+
↓
|
|
60
|
+
browser automation boundary
|
|
61
|
+
↓
|
|
62
|
+
observer-owned runtime evidence
|
|
63
|
+
|
|
64
|
+
observer-owned domain/schema
|
|
65
|
+
↓
|
|
66
|
+
artifact ownership boundary
|
|
67
|
+
|
|
68
|
+
deterministic fixture/test boundary
|
|
69
|
+
↓
|
|
70
|
+
browser-level validation
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Use one browser engine implementation, keep browser logic out of presentation,
|
|
74
|
+
avoid speculative plugin/multi-browser abstractions, and keep observed
|
|
75
|
+
applications external. Before v0.6, versions must not add runtime coupling to
|
|
76
|
+
sibling ecosystem projects. v0.6 may add only explicit bounded context,
|
|
77
|
+
correlation/export, orchestrator-consumption, and lab-compatibility contracts
|
|
78
|
+
while preserving independent ownership.
|
|
79
|
+
|
|
80
|
+
The text/config-driven coding-agent workflow must be operational before the
|
|
81
|
+
viewer and annotation layers are added. Those interfaces consume the same
|
|
82
|
+
canonical observation, relationship, comparison, contract, change-scope,
|
|
83
|
+
correlation, and context boundaries rather than creating parallel engines.
|
|
84
|
+
The concrete implementation plan and module layout must be designed only after
|
|
85
|
+
the relevant version planning workflow inspects the current repositories.
|
package/docs/CI_CD.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# CI/CD
|
|
2
|
+
|
|
3
|
+
A GitHub Actions pre-release readiness workflow exists at
|
|
4
|
+
`.github/workflows/pre-release-readiness.yml` (triggered manually via
|
|
5
|
+
`workflow_dispatch` or by pushing a `validation/**` branch). It has two
|
|
6
|
+
phases:
|
|
7
|
+
|
|
8
|
+
1. **candidate** (Linux, Node 24): `npm ci`, install Chromium, typecheck,
|
|
9
|
+
lint, `npm test`, `npm run test:browser`, `npm run test:security`,
|
|
10
|
+
build, `npm run check:docs`, then `npm pack` to produce exactly one
|
|
11
|
+
candidate tarball and its SHA-256, uploaded as build artifacts.
|
|
12
|
+
2. **matrix-smoke** (`windows-latest`, `ubuntu-latest`, `macos-latest`, all
|
|
13
|
+
Node 24): each job downloads the *same* candidate tarball produced by the
|
|
14
|
+
candidate job, independently recomputes and verifies its SHA-256 against
|
|
15
|
+
the candidate job's hash (failing immediately on any mismatch - no job
|
|
16
|
+
ever builds its own tarball), installs Chromium via the installed
|
|
17
|
+
package's own Playwright dependency, and runs
|
|
18
|
+
`scripts/ci/runPackedObservationSmoke.mjs` against the installed
|
|
19
|
+
tarball: a real Chromium observation against a disposable local HTTP
|
|
20
|
+
target, with artifact/schema/screenshot/target-immutability assertions.
|
|
21
|
+
|
|
22
|
+
This proves the same packaged candidate installs and performs a real
|
|
23
|
+
observation on Windows, Linux, and macOS, not just in the source checkout.
|
|
24
|
+
|
|
25
|
+
There is no automated npm publication and no automated GitHub Release
|
|
26
|
+
creation - this workflow is readiness validation only, run from a
|
|
27
|
+
`validation/**` branch, never from a release branch or tag. Package
|
|
28
|
+
publication remains a separate, later, explicit release decision.
|
package/docs/COMMANDS.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Commands
|
|
2
|
+
|
|
3
|
+
## Product command surface
|
|
4
|
+
|
|
5
|
+
`node dist/cli.js observe` (or `my-frontend-observer observe` once installed
|
|
6
|
+
as a bin) captures one bounded, loopback-only browser observation and
|
|
7
|
+
persists it as a portable artifact.
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
my-frontend-observer observe --url <loopback-url> [options]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Required:
|
|
14
|
+
|
|
15
|
+
- `--url <url>` — loopback target URL (`http`/`https`; `localhost`,
|
|
16
|
+
`127.x.x.x`, or `::1` only - enforced by the existing request/safety
|
|
17
|
+
contracts, not by CLI-local logic).
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
|
|
21
|
+
- `--viewport <WIDTHxHEIGHT>` — e.g. `1280x720`. Malformed syntax (missing
|
|
22
|
+
`x`, non-numeric, empty side) is rejected before any browser launches;
|
|
23
|
+
in-range bounds are enforced by the existing request validator.
|
|
24
|
+
- `--target <id=css-selector>` — an explicit observation target. Repeatable;
|
|
25
|
+
order is preserved. Parsed on the *first* `=` only, so a selector
|
|
26
|
+
containing `=` survives intact, e.g.
|
|
27
|
+
`--target action=button[data-state="active"]`.
|
|
28
|
+
- `--output <directory>` — portable, relative output location for the
|
|
29
|
+
observation artifact (same contract as the request's `outputLocation`; no
|
|
30
|
+
drive letter, no leading `/`, no `..` segments).
|
|
31
|
+
- `--timeout <ms>` — overall request timeout in milliseconds.
|
|
32
|
+
- `--help` — show `observe` usage.
|
|
33
|
+
|
|
34
|
+
Also available: `--help` / `-h` (top-level usage) and `--version` (prints the
|
|
35
|
+
actual package version).
|
|
36
|
+
|
|
37
|
+
On success the command prints exactly:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
Observation: <observation-id>
|
|
41
|
+
State: <complete|partial|warning|fatal|invalid-request>
|
|
42
|
+
Artifact: <artifact-root-path>
|
|
43
|
+
Targets: <configured-target-count>
|
|
44
|
+
Diagnostics: <diagnostic-count>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
and exits `0` for a validly persisted observation - including one whose
|
|
48
|
+
`State` truthfully reports `partial` (e.g. a missing or ambiguous target)
|
|
49
|
+
- or exits nonzero for invalid CLI syntax, a request the existing validator
|
|
50
|
+
rejects, an unsafe/failed navigation with no persistable artifact, or a
|
|
51
|
+
failed artifact write. No progress output is printed during a normal
|
|
52
|
+
capture. CLI-syntax errors (e.g. a missing `--url`) print as `error:
|
|
53
|
+
<message>` followed by `observe` usage; request/capture/persistence
|
|
54
|
+
diagnostics print one per line as `[code] message`.
|
|
55
|
+
|
|
56
|
+
## Foundation commands
|
|
57
|
+
|
|
58
|
+
- `npm install` — install dependencies (includes the `playwright` runtime
|
|
59
|
+
dependency since Batch 2).
|
|
60
|
+
- `npx playwright install chromium` — install the Chromium binary once per
|
|
61
|
+
machine (see `docs/DEVELOPMENT.md`).
|
|
62
|
+
- `npm run typecheck` — run TypeScript no-emit checking.
|
|
63
|
+
- `npm run lint` — lint the repository and scripts.
|
|
64
|
+
- `npm test` — run the fast unit suite (`tests/unit/`).
|
|
65
|
+
- `npm run test:browser` — run the real-Chromium integration suite
|
|
66
|
+
(`tests/browser/`), including a real `observe` end-to-end test against the
|
|
67
|
+
deterministic local fixture.
|
|
68
|
+
- `npm run test:security` — run only the safety-relevant subset of the suite
|
|
69
|
+
(`tests/unit/policy.test.ts` plus the real-Chromium enforcement cases in
|
|
70
|
+
`tests/browser/chromiumAdapter.test.ts`: unsafe initial target, prohibited
|
|
71
|
+
redirect, prohibited subresource request, and browser cleanup around
|
|
72
|
+
safety/navigation failure) — a discoverable entry point for security
|
|
73
|
+
review tooling; it is a subset of, not a replacement for, `npm test` and
|
|
74
|
+
`npm run test:browser`.
|
|
75
|
+
- `npm run build` — clean and compile `src/` (including `src/cli.ts`) to
|
|
76
|
+
`dist/`.
|
|
77
|
+
- `npm run check:docs` — validate canonical documents and roadmap structure.
|
|
78
|
+
- `npm pack --dry-run` — inspect the private package inventory without
|
|
79
|
+
publishing. The real tarball has been installed and exercised in a clean
|
|
80
|
+
temporary consumer directory (real Chromium install, real `observe` run,
|
|
81
|
+
real artifact) as part of v0.1 validation; this is local package
|
|
82
|
+
validation, not a release/publication step.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Contracts
|
|
2
|
+
|
|
3
|
+
## Current contracts
|
|
4
|
+
|
|
5
|
+
The v0.1 observation artifact contract is implemented (`src/domain/schema.ts`)
|
|
6
|
+
and proven both from the source checkout and from the packed npm tarball:
|
|
7
|
+
|
|
8
|
+
- artifact kind `my-frontend-observer/observation`, schema version `1.0.0`
|
|
9
|
+
(independent of the package version, currently `0.1.0`);
|
|
10
|
+
- one artifact root per observation, `<outputLocation>/<observationId>/`,
|
|
11
|
+
containing exactly `manifest.json` (the full `ObservationArtifact`, with
|
|
12
|
+
page/target evidence embedded inline) and `screenshot.png` - there is no
|
|
13
|
+
separate `evidence.json`;
|
|
14
|
+
- `manifest.json` is written last, after `screenshot.png`, via one atomic
|
|
15
|
+
directory rename, so a consumer never observes a partially-written
|
|
16
|
+
artifact; a filesystem failure anywhere in that sequence reports the
|
|
17
|
+
`artifact-write-failure` diagnostic and leaves no completed artifact;
|
|
18
|
+
- internal artifact references (e.g. `screenshot.png`) are relative to the
|
|
19
|
+
artifact root, never an absolute machine path; the observation's logical
|
|
20
|
+
identity is its `observationId`, not its filesystem location;
|
|
21
|
+
- evidence states `available`, `unavailable`, `not-applicable`, `partial`;
|
|
22
|
+
evidence sources `browser`, `computed-browser`, `derived`;
|
|
23
|
+
- a stable diagnostic vocabulary (`src/domain/diagnostics.ts`) and completion
|
|
24
|
+
states `complete`, `partial`, `warning`, `invalid-request`, `fatal`
|
|
25
|
+
(`src/domain/completion.ts`);
|
|
26
|
+
- observation/request identity, producer/package identity, and browser
|
|
27
|
+
provenance are all present in every persisted manifest.
|
|
28
|
+
|
|
29
|
+
This contract is implemented; it is not yet published as a package, and no
|
|
30
|
+
public programmatic-API compatibility promise has been made.
|
|
31
|
+
|
|
32
|
+
## Approved v0.1 design inputs
|
|
33
|
+
|
|
34
|
+
The historical greenfield scaffold plan recorded these v0.1 design decisions:
|
|
35
|
+
|
|
36
|
+
- artifact kind `my-frontend-observer/observation`;
|
|
37
|
+
- schema version `1.0.0`, independent of package version;
|
|
38
|
+
- one portable directory containing `manifest.json`, `evidence.json`, and
|
|
39
|
+
`screenshot.png`;
|
|
40
|
+
- evidence states `available`, `unavailable`, `not-applicable`, and `partial`;
|
|
41
|
+
- evidence sources `browser`, `computed-browser`, and `derived`;
|
|
42
|
+
- bounded explicitly requested targets, provenance, diagnostics, completion
|
|
43
|
+
state, limits, and relative artifact references.
|
|
44
|
+
|
|
45
|
+
These were planning inputs only at the time they were recorded. As shown in
|
|
46
|
+
"Current contracts" above, the implemented contract matches them except for
|
|
47
|
+
the file layout: there is no separate `evidence.json` - page/target evidence
|
|
48
|
+
is embedded directly inside `manifest.json`.
|
|
49
|
+
|
|
50
|
+
Comparison and relationship contracts belong to v0.4, and canonical
|
|
51
|
+
change-scope contracts belong to v0.5. Bounded agent-context plus ecosystem
|
|
52
|
+
integration contracts move to v0.6, followed by the text/config-driven
|
|
53
|
+
coding-agent review contract in v0.7. Viewer and annotation contracts follow in
|
|
54
|
+
v0.8 and v0.9 and converge with the existing workflow in v0.10.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Current State
|
|
2
|
+
|
|
3
|
+
The project is published at package version `0.1.0` (roadmap v0.1, Runtime
|
|
4
|
+
Observation Foundation).
|
|
5
|
+
|
|
6
|
+
## Greenfield foundation established
|
|
7
|
+
|
|
8
|
+
The retained repository contains:
|
|
9
|
+
|
|
10
|
+
- Node.js 24+ and TypeScript ESM package configuration;
|
|
11
|
+
- TypeScript build and typecheck configuration;
|
|
12
|
+
- ESLint configuration;
|
|
13
|
+
- a Vitest runner configured to report honestly when no tests exist;
|
|
14
|
+
- a safe `dist/` clean script;
|
|
15
|
+
- documentation validation;
|
|
16
|
+
- package allowlisting;
|
|
17
|
+
- minimal `src/cli.ts` and `src/index.ts` placeholders required by the selected
|
|
18
|
+
TypeScript CLI starter profile;
|
|
19
|
+
- complete repository-local Project Description, Project Milestones, ROADMAP,
|
|
20
|
+
and standardized documentation.
|
|
21
|
+
|
|
22
|
+
The package bin (`src/cli.ts`) now implements the real `observe` command
|
|
23
|
+
described below; it is no longer the not-implemented placeholder.
|
|
24
|
+
|
|
25
|
+
## v0.1 progress (Batch 1–6; implemented and released as 0.1.0)
|
|
26
|
+
|
|
27
|
+
- Batch 1 froze and implemented the observation request contract, evidence
|
|
28
|
+
states/sources, schema 1.0.0, observation/request identity, bounded
|
|
29
|
+
readiness semantics, diagnostic/completion semantics, browser/network
|
|
30
|
+
safety policy, and portable path normalization, with 40 passing unit tests.
|
|
31
|
+
- Batch 2 added a Playwright Chromium browser boundary (`src/browser/`) and a
|
|
32
|
+
minimal application seam (`src/application/`) that launches a real
|
|
33
|
+
Chromium browser, enforces the Batch 1 loopback/redirect/subresource
|
|
34
|
+
safety policy at runtime, applies the requested viewport, waits for the
|
|
35
|
+
approved bounded readiness condition, captures a real viewport PNG
|
|
36
|
+
screenshot, returns observer-owned browser provenance, and reliably closes
|
|
37
|
+
the browser on every exit path. Deterministic local HTTP fixtures live
|
|
38
|
+
under `tests/fixtures/`; the real-Chromium integration tests live under
|
|
39
|
+
`tests/browser/` and run via `npm run test:browser` (kept separate from
|
|
40
|
+
`npm test`, which continues to run only the fast unit suite).
|
|
41
|
+
- Batch 3 extended the same single browser observation (no second Chromium
|
|
42
|
+
lifecycle) to also capture the v0.1 minimum page evidence (requested/final
|
|
43
|
+
URL, title, viewport, device pixel ratio, document scroll/client
|
|
44
|
+
dimensions plus a derived overall document width/height, window scroll
|
|
45
|
+
position) and explicit-target evidence (tag, geometry, computed
|
|
46
|
+
display/position/overflow, scroll/client metrics, initial visibility, and
|
|
47
|
+
role/name where the browser reliably exposes them) for every configured
|
|
48
|
+
CSS target, honoring missing/ambiguous-target semantics honestly. This
|
|
49
|
+
additively extended `src/domain/schema.ts`'s `TargetEvidenceRecord` (new
|
|
50
|
+
`tag`/`layout`/`visibility`/`semantics` categories, and concrete shapes for
|
|
51
|
+
`geometry`/`style`) and `BrowserCaptureResult`; schema version stays
|
|
52
|
+
`1.0.0`.
|
|
53
|
+
- Batch 4 added a portable, atomic observation-artifact writer
|
|
54
|
+
(`src/artifacts/artifactWriter.ts`) and a minimal application persistence
|
|
55
|
+
seam (`src/application/observationPersistence.ts`) that assembles the
|
|
56
|
+
frozen `ObservationArtifact` shape from a Batch 2/3 browser capture (using
|
|
57
|
+
the existing Batch 1 identity/completion functions verbatim, no new logic
|
|
58
|
+
invented) and writes it to `<outputLocation>/<observationId>/manifest.json`
|
|
59
|
+
plus `screenshot.png`. Writing happens in a sibling temporary directory
|
|
60
|
+
first (screenshot before manifest), finalized only via one atomic
|
|
61
|
+
directory rename, so a consumer can never observe a partially-written
|
|
62
|
+
artifact under its real name; a filesystem failure anywhere in that
|
|
63
|
+
sequence reports the existing `artifact-write-failure` diagnostic and
|
|
64
|
+
leaves no completed artifact behind. Internal artifact references
|
|
65
|
+
(`screenshot.png`) are relative/portable; the observation's logical
|
|
66
|
+
identity is the existing Batch 1 `observationId`, not its filesystem
|
|
67
|
+
location. The writer has no Playwright dependency and does not modify the
|
|
68
|
+
observed target. Schema stays `1.0.0`.
|
|
69
|
+
- Batch 5 wired the existing owners into the real user-facing workflow:
|
|
70
|
+
`src/cli.ts` implements a real `observe` command (thin argument
|
|
71
|
+
parsing/output only - no Chromium, safety, evidence, or filesystem logic
|
|
72
|
+
of its own), and `src/application/observationPersistence.ts` gained one
|
|
73
|
+
`observe()` use case that runs the existing browser capture exactly once
|
|
74
|
+
and, only on success, persists it exactly once through the existing
|
|
75
|
+
artifact writer. CLI syntax errors (malformed `WIDTHxHEIGHT`, malformed
|
|
76
|
+
`id=selector`) are rejected before any browser launches; all domain bounds
|
|
77
|
+
and safety decisions still come from the existing Batch 1 request
|
|
78
|
+
validator and safety policy, not CLI-local logic. A successfully
|
|
79
|
+
persisted observation - including one whose completion state honestly
|
|
80
|
+
reports `partial` - exits `0`; invalid syntax/request, an unpersistable
|
|
81
|
+
browser failure, or a failed artifact write exits nonzero. Package version
|
|
82
|
+
is `0.1.0`; schema stays `1.0.0`.
|
|
83
|
+
|
|
84
|
+
So: `my-frontend-observer observe --url ... --viewport ... --target ...
|
|
85
|
+
--output ...` is a real, working, source-checkout command that launches
|
|
86
|
+
Chromium, produces bounded runtime evidence, and writes a portable local
|
|
87
|
+
artifact - proven both via `runCli()`-level tests and a built
|
|
88
|
+
`node dist/cli.js observe ...` smoke run against the deterministic fixture.
|
|
89
|
+
|
|
90
|
+
Batch 6 closed the remaining v0.1 coverage gap (a genuine real-Chromium
|
|
91
|
+
navigation failure - connection reset mid-navigation - distinct from a
|
|
92
|
+
readiness timeout or a pre-launch safety rejection) and proved the packaged
|
|
93
|
+
form of the implementation works independent of the source checkout: the
|
|
94
|
+
real `npm pack` tarball, installed fresh in a clean temporary consumer
|
|
95
|
+
directory outside the repository, exposes its `my-frontend-observer` bin,
|
|
96
|
+
reports the correct version/help text, installs its own Chromium binary via
|
|
97
|
+
the consumer-local Playwright toolchain, and performs a real observation
|
|
98
|
+
against a disposable local HTTP target - producing a `manifest.json` +
|
|
99
|
+
`screenshot.png` artifact identical in shape to the source-checkout result,
|
|
100
|
+
without modifying the observed target, and with the temporary consumer/
|
|
101
|
+
tarball/output fully cleaned up afterward. Documentation across the
|
|
102
|
+
repository was reconciled to this implemented state as part of the same
|
|
103
|
+
batch.
|
|
104
|
+
|
|
105
|
+
## Not implemented
|
|
106
|
+
|
|
107
|
+
- There is no controlled-scroll behavior, target-source correlation, or
|
|
108
|
+
capability beyond the bounded page/target evidence Batches 3-5 established.
|
|
109
|
+
- No v0.2–v0.10 capability is implemented.
|
|
110
|
+
|
|
111
|
+
## Next target
|
|
112
|
+
|
|
113
|
+
v0.1.0 is released. The next allowed workflow is v0.2 planning.
|