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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.1.0 - 2026-08-11
|
|
6
|
+
|
|
7
|
+
Runtime Observation Foundation. First public release.
|
|
8
|
+
|
|
9
|
+
- Local-first browser runtime evidence producer: a real `observe` CLI command
|
|
10
|
+
that launches Chromium under a loopback-only network safety policy.
|
|
11
|
+
- Explicit CSS-selector observation targets (`--target id=selector`,
|
|
12
|
+
repeatable).
|
|
13
|
+
- Viewport screenshot capture (`screenshot.png`).
|
|
14
|
+
- Bounded page evidence and bounded target evidence, with honest
|
|
15
|
+
unavailable/not-applicable/partial states when evidence cannot be
|
|
16
|
+
determined rather than guessing.
|
|
17
|
+
- Loopback/network safety enforcement (`http`/`https`, `localhost`/`127.x.x.x`/
|
|
18
|
+
`::1` only).
|
|
19
|
+
- Versioned, portable observation artifact: `manifest.json` + `screenshot.png`
|
|
20
|
+
written atomically per observation, artifact schema `1.0.0`.
|
|
21
|
+
- Validated as a packed npm tarball with a clean-consumer install-and-observe
|
|
22
|
+
smoke on Windows, Linux, and macOS.
|
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# my-frontend-observer
|
|
2
|
+
|
|
3
|
+
`my-frontend-observer` is the local-first rendered browser/runtime evidence
|
|
4
|
+
producer in the my-dev-kit ecosystem. Its durable product purpose is defined
|
|
5
|
+
in [docs/PROJECT_DESCRIPTION.md](docs/PROJECT_DESCRIPTION.md).
|
|
6
|
+
|
|
7
|
+
## Current status
|
|
8
|
+
|
|
9
|
+
`v0.1.0`, Runtime Observation Foundation, is the current published release:
|
|
10
|
+
`my-frontend-observer observe` launches a real, sandboxed Chromium browser,
|
|
11
|
+
enforces a loopback-only safety policy, captures a viewport screenshot plus
|
|
12
|
+
bounded page/target evidence, and persists it as one portable
|
|
13
|
+
`manifest.json` + `screenshot.png` artifact.
|
|
14
|
+
|
|
15
|
+
Install:
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
npm install my-frontend-observer
|
|
19
|
+
npx playwright install chromium
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Setup for working from a source checkout instead:
|
|
23
|
+
|
|
24
|
+
```powershell
|
|
25
|
+
npm install
|
|
26
|
+
npx playwright install chromium
|
|
27
|
+
npm run build
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Example use, against your own locally running frontend:
|
|
31
|
+
|
|
32
|
+
```powershell
|
|
33
|
+
my-frontend-observer observe `
|
|
34
|
+
--url http://localhost:3000/ `
|
|
35
|
+
--viewport 1280x720 `
|
|
36
|
+
--target header=header `
|
|
37
|
+
--target main-content=main `
|
|
38
|
+
--output observations
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
(From a source checkout, use `node dist/cli.js observe ...` instead.)
|
|
42
|
+
|
|
43
|
+
This prints a concise result (`Observation:`/`State:`/`Artifact:`/`Targets:`/
|
|
44
|
+
`Diagnostics:`) and exits `0` on a successfully persisted observation. See
|
|
45
|
+
[docs/COMMANDS.md](docs/COMMANDS.md) for the full flag reference.
|
|
46
|
+
|
|
47
|
+
Validation:
|
|
48
|
+
|
|
49
|
+
```powershell
|
|
50
|
+
npm run typecheck
|
|
51
|
+
npm run lint
|
|
52
|
+
npm test
|
|
53
|
+
npm run test:browser
|
|
54
|
+
npm run build
|
|
55
|
+
npm run check:docs
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Planning authorities:
|
|
59
|
+
|
|
60
|
+
- [Project Description](docs/PROJECT_DESCRIPTION.md): complete durable product
|
|
61
|
+
intent and responsibility boundaries.
|
|
62
|
+
- [Project Milestones](docs/PROJECT_MILESTONES.md): complete ordered capability
|
|
63
|
+
design and cross-milestone rules.
|
|
64
|
+
- [ROADMAP](docs/ROADMAP.md): version-level requirements; v0.1 is released,
|
|
65
|
+
v0.2 is next.
|
|
66
|
+
- [Current State](docs/CURRENT_STATE.md): retained scaffold and release state.
|
|
67
|
+
|
|
68
|
+
No sibling ecosystem repository is a runtime dependency of the retained
|
|
69
|
+
foundation.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { NormalizedObservationRequest } from '../request/request.js';
|
|
2
|
+
import type { BrowserCaptureResult } from '../browser/types.js';
|
|
3
|
+
export type { BrowserCaptureResult, BrowserProvenance } from '../browser/types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Minimum application-facing seam over the browser adapter: takes an
|
|
6
|
+
* already-validated request and returns the observer-owned browser-boundary
|
|
7
|
+
* result. Coordinates nothing beyond that single invocation - no page/target
|
|
8
|
+
* evidence, no artifact writing, no CLI reporting. Later batches extend this
|
|
9
|
+
* seam rather than calling the browser adapter directly.
|
|
10
|
+
*/
|
|
11
|
+
export declare function runBrowserCapture(request: NormalizedObservationRequest): Promise<BrowserCaptureResult>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { captureViewport } from '../browser/chromiumAdapter.js';
|
|
2
|
+
/**
|
|
3
|
+
* Minimum application-facing seam over the browser adapter: takes an
|
|
4
|
+
* already-validated request and returns the observer-owned browser-boundary
|
|
5
|
+
* result. Coordinates nothing beyond that single invocation - no page/target
|
|
6
|
+
* evidence, no artifact writing, no CLI reporting. Later batches extend this
|
|
7
|
+
* seam rather than calling the browser adapter directly.
|
|
8
|
+
*/
|
|
9
|
+
export async function runBrowserCapture(request) {
|
|
10
|
+
return captureViewport(request);
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=browserCaptureService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browserCaptureService.js","sourceRoot":"","sources":["../../src/application/browserCaptureService.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAKhE;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,OAAqC;IAC3E,OAAO,eAAe,CAAC,OAAO,CAAC,CAAC;AAClC,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { NormalizedObservationRequest } from '../request/request.js';
|
|
2
|
+
import type { BrowserCaptureResult } from '../browser/types.js';
|
|
3
|
+
import type { ObservationArtifact } from '../domain/schema.js';
|
|
4
|
+
import type { CompletionState } from '../domain/completion.js';
|
|
5
|
+
import type { Diagnostic } from '../domain/diagnostics.js';
|
|
6
|
+
import type { PersistedObservationResult } from '../artifacts/types.js';
|
|
7
|
+
import type { WriteObservationArtifactOptions } from '../artifacts/artifactWriter.js';
|
|
8
|
+
export type { PersistedObservationResult } from '../artifacts/types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Assembles the frozen ObservationArtifact shape from one already-successful
|
|
11
|
+
* Batch 2/3 browser capture. Pure - no filesystem access, no Playwright
|
|
12
|
+
* dependency - so it is testable independently of both Chromium and disk
|
|
13
|
+
* I/O. `requestId`/`observationId` reuse the existing Batch 1 identity
|
|
14
|
+
* functions verbatim; `completion` reuses the existing Batch 1
|
|
15
|
+
* post-capture completion rule over the capture's own diagnostics.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildObservationArtifact(capture: Extract<BrowserCaptureResult, {
|
|
18
|
+
ok: true;
|
|
19
|
+
}>, request: NormalizedObservationRequest): ObservationArtifact;
|
|
20
|
+
/**
|
|
21
|
+
* Minimum application-facing persistence seam for Batch 5: combines an
|
|
22
|
+
* existing successful browser capture with the artifact writer. Not wired to
|
|
23
|
+
* the CLI in this batch.
|
|
24
|
+
*/
|
|
25
|
+
export declare function persistBrowserCapture(capture: Extract<BrowserCaptureResult, {
|
|
26
|
+
ok: true;
|
|
27
|
+
}>, request: NormalizedObservationRequest, options?: WriteObservationArtifactOptions): Promise<PersistedObservationResult>;
|
|
28
|
+
/**
|
|
29
|
+
* Observer-owned summary of one full observation attempt, with just enough
|
|
30
|
+
* information for a presentation layer (the Batch 5 CLI) to report a result
|
|
31
|
+
* without ever seeing a Playwright object, a raw Node filesystem exception,
|
|
32
|
+
* or persistence implementation detail.
|
|
33
|
+
*/
|
|
34
|
+
export type ApplicationObservationResult = {
|
|
35
|
+
ok: true;
|
|
36
|
+
observationId: string;
|
|
37
|
+
requestId: string;
|
|
38
|
+
completion: CompletionState;
|
|
39
|
+
artifactRoot: string;
|
|
40
|
+
manifestPath: string;
|
|
41
|
+
screenshotPath: string;
|
|
42
|
+
targetCount: number;
|
|
43
|
+
diagnostics: Diagnostic[];
|
|
44
|
+
} | {
|
|
45
|
+
ok: false;
|
|
46
|
+
diagnostics: Diagnostic[];
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The one canonical application-level observation use case: takes an
|
|
50
|
+
* already-normalized request, runs the existing browser capture exactly
|
|
51
|
+
* once, and - only for a successful capture - persists it exactly once
|
|
52
|
+
* through the existing artifact writer. Reuses `runBrowserCapture`,
|
|
53
|
+
* `buildObservationArtifact`, and `writeObservationArtifact` verbatim; it
|
|
54
|
+
* does not go through `persistBrowserCapture` because that helper re-derives
|
|
55
|
+
* a fresh `ObservationArtifact` (and therefore a fresh, different
|
|
56
|
+
* `observationId`) internally, which would desync the identity a caller
|
|
57
|
+
* inspects here from the identity actually written to disk.
|
|
58
|
+
*/
|
|
59
|
+
export declare function observe(request: NormalizedObservationRequest, options?: WriteObservationArtifactOptions): Promise<ApplicationObservationResult>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { runBrowserCapture } from './browserCaptureService.js';
|
|
2
|
+
import { ARTIFACT_KIND, SCHEMA_VERSION, getProducerInfo } from '../domain/schema.js';
|
|
3
|
+
import { buildRequestIdentity, buildObservationIdentity } from '../domain/identity.js';
|
|
4
|
+
import { deriveCompletion } from '../domain/completion.js';
|
|
5
|
+
import { writeObservationArtifact, SCREENSHOT_FILENAME } from '../artifacts/artifactWriter.js';
|
|
6
|
+
/**
|
|
7
|
+
* Assembles the frozen ObservationArtifact shape from one already-successful
|
|
8
|
+
* Batch 2/3 browser capture. Pure - no filesystem access, no Playwright
|
|
9
|
+
* dependency - so it is testable independently of both Chromium and disk
|
|
10
|
+
* I/O. `requestId`/`observationId` reuse the existing Batch 1 identity
|
|
11
|
+
* functions verbatim; `completion` reuses the existing Batch 1
|
|
12
|
+
* post-capture completion rule over the capture's own diagnostics.
|
|
13
|
+
*/
|
|
14
|
+
export function buildObservationArtifact(capture, request) {
|
|
15
|
+
const requestId = buildRequestIdentity(request);
|
|
16
|
+
const observationId = buildObservationIdentity(requestId);
|
|
17
|
+
const completion = deriveCompletion(capture.diagnostics, 'post-capture');
|
|
18
|
+
return {
|
|
19
|
+
artifactKind: ARTIFACT_KIND,
|
|
20
|
+
schemaVersion: SCHEMA_VERSION,
|
|
21
|
+
observationId,
|
|
22
|
+
requestId,
|
|
23
|
+
producer: getProducerInfo(),
|
|
24
|
+
browser: { state: 'available', source: 'browser', value: capture.provenance },
|
|
25
|
+
requestConfig: request,
|
|
26
|
+
provenance: { capturedAt: new Date().toISOString(), observationMethod: 'chromium-navigate-and-capture' },
|
|
27
|
+
pageEvidence: capture.pageEvidence,
|
|
28
|
+
targetEvidence: capture.targetEvidence,
|
|
29
|
+
screenshot: { state: 'available', source: 'browser', value: { path: SCREENSHOT_FILENAME } },
|
|
30
|
+
completion,
|
|
31
|
+
diagnostics: capture.diagnostics,
|
|
32
|
+
limits: { truncated: false, omittedFields: [], omittedTargets: [] },
|
|
33
|
+
artifactReferences: [{ path: SCREENSHOT_FILENAME, kind: 'screenshot' }],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Minimum application-facing persistence seam for Batch 5: combines an
|
|
38
|
+
* existing successful browser capture with the artifact writer. Not wired to
|
|
39
|
+
* the CLI in this batch.
|
|
40
|
+
*/
|
|
41
|
+
export async function persistBrowserCapture(capture, request, options = {}) {
|
|
42
|
+
const artifact = buildObservationArtifact(capture, request);
|
|
43
|
+
return writeObservationArtifact(artifact, capture.screenshot, options);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The one canonical application-level observation use case: takes an
|
|
47
|
+
* already-normalized request, runs the existing browser capture exactly
|
|
48
|
+
* once, and - only for a successful capture - persists it exactly once
|
|
49
|
+
* through the existing artifact writer. Reuses `runBrowserCapture`,
|
|
50
|
+
* `buildObservationArtifact`, and `writeObservationArtifact` verbatim; it
|
|
51
|
+
* does not go through `persistBrowserCapture` because that helper re-derives
|
|
52
|
+
* a fresh `ObservationArtifact` (and therefore a fresh, different
|
|
53
|
+
* `observationId`) internally, which would desync the identity a caller
|
|
54
|
+
* inspects here from the identity actually written to disk.
|
|
55
|
+
*/
|
|
56
|
+
export async function observe(request, options = {}) {
|
|
57
|
+
const capture = await runBrowserCapture(request);
|
|
58
|
+
if (!capture.ok) {
|
|
59
|
+
return { ok: false, diagnostics: capture.diagnostics };
|
|
60
|
+
}
|
|
61
|
+
const artifact = buildObservationArtifact(capture, request);
|
|
62
|
+
const persisted = await writeObservationArtifact(artifact, capture.screenshot, options);
|
|
63
|
+
if (!persisted.ok) {
|
|
64
|
+
return { ok: false, diagnostics: persisted.diagnostics };
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
ok: true,
|
|
68
|
+
observationId: artifact.observationId,
|
|
69
|
+
requestId: artifact.requestId,
|
|
70
|
+
completion: artifact.completion,
|
|
71
|
+
artifactRoot: persisted.artifactRoot,
|
|
72
|
+
manifestPath: persisted.manifestPath,
|
|
73
|
+
screenshotPath: persisted.screenshotPath,
|
|
74
|
+
targetCount: Object.keys(artifact.targetEvidence).length,
|
|
75
|
+
diagnostics: artifact.diagnostics,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=observationPersistence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observationPersistence.js","sourceRoot":"","sources":["../../src/application/observationPersistence.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAE/D,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACvF,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAG3D,OAAO,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAM/F;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB,CACtC,OAAoD,EACpD,OAAqC;IAErC,MAAM,SAAS,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAChD,MAAM,aAAa,GAAG,wBAAwB,CAAC,SAAS,CAAC,CAAC;IAC1D,MAAM,UAAU,GAAG,gBAAgB,CAAC,OAAO,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;IAEzE,OAAO;QACL,YAAY,EAAE,aAAa;QAC3B,aAAa,EAAE,cAAc;QAC7B,aAAa;QACb,SAAS;QACT,QAAQ,EAAE,eAAe,EAAE;QAC3B,OAAO,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,CAAC,UAAU,EAAE;QAC7E,aAAa,EAAE,OAAO;QACtB,UAAU,EAAE,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,iBAAiB,EAAE,+BAA+B,EAAE;QACxG,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,cAAc,EAAE,OAAO,CAAC,cAAc;QACtC,UAAU,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,EAAE;QAC3F,UAAU;QACV,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,MAAM,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,EAAE,cAAc,EAAE,EAAE,EAAE;QACnE,kBAAkB,EAAE,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;KACxE,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,OAAoD,EACpD,OAAqC,EACrC,UAA2C,EAAE;IAE7C,MAAM,QAAQ,GAAG,wBAAwB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC5D,OAAO,wBAAwB,CAAC,QAAQ,EAAE,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;AACzE,CAAC;AAyBD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,OAAqC,EACrC,UAA2C,EAAE;IAE7C,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACjD,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;QAChB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC;IACzD,CAAC;IAED,MAAM,QAAQ,GAAG,wBAAwB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC5D,MAAM,SAAS,GAAG,MAAM,wBAAwB,CAAC,QAAQ,EAAE,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACxF,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;QAClB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,CAAC,WAAW,EAAE,CAAC;IAC3D,CAAC;IAED,OAAO;QACL,EAAE,EAAE,IAAI;QACR,aAAa,EAAE,QAAQ,CAAC,aAAa;QACrC,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,YAAY,EAAE,SAAS,CAAC,YAAY;QACpC,YAAY,EAAE,SAAS,CAAC,YAAY;QACpC,cAAc,EAAE,SAAS,CAAC,cAAc;QACxC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,MAAM;QACxD,WAAW,EAAE,QAAQ,CAAC,WAAW;KAClC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ObservationArtifact } from '../domain/schema.js';
|
|
2
|
+
import type { PersistedObservationResult } from './types.js';
|
|
3
|
+
export declare const MANIFEST_FILENAME = "manifest.json";
|
|
4
|
+
export declare const SCREENSHOT_FILENAME = "screenshot.png";
|
|
5
|
+
export interface WriteObservationArtifactOptions {
|
|
6
|
+
/** Base directory that `artifact.requestConfig.outputLocation` is resolved against. Defaults to `process.cwd()`. */
|
|
7
|
+
cwd?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Persists one already-assembled, already-validated ObservationArtifact plus
|
|
11
|
+
* its screenshot bytes as one portable artifact directory:
|
|
12
|
+
* `<outputLocation>/<observationId>/{manifest.json,screenshot.png}`.
|
|
13
|
+
*
|
|
14
|
+
* `observationId` is guaranteed fresh per capture (see
|
|
15
|
+
* domain/identity.ts#buildObservationIdentity's "never collides" contract),
|
|
16
|
+
* so each write targets its own unique subdirectory - no destructive
|
|
17
|
+
* overwrite decision is needed for the normal case; an existing directory at
|
|
18
|
+
* that path is treated as a genuine collision and rejected rather than
|
|
19
|
+
* overwritten. Writing happens in a sibling temporary directory first,
|
|
20
|
+
* `screenshot.png` before `manifest.json` (manifest-last), and the whole
|
|
21
|
+
* temporary directory is only exposed under its real name via one atomic
|
|
22
|
+
* rename - a consumer can never observe a partially-written artifact under
|
|
23
|
+
* its final name.
|
|
24
|
+
*/
|
|
25
|
+
export declare function writeObservationArtifact(artifact: ObservationArtifact, screenshotBytes: Uint8Array, options?: WriteObservationArtifactOptions): Promise<PersistedObservationResult>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { mkdir, writeFile, rename, rm, access } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isValidObservationArtifact } from '../domain/schema.js';
|
|
4
|
+
import { DIAGNOSTIC_SEVERITY } from '../domain/diagnostics.js';
|
|
5
|
+
export const MANIFEST_FILENAME = 'manifest.json';
|
|
6
|
+
export const SCREENSHOT_FILENAME = 'screenshot.png';
|
|
7
|
+
function writeFailure(message, details) {
|
|
8
|
+
const diagnostic = {
|
|
9
|
+
code: 'artifact-write-failure',
|
|
10
|
+
severity: DIAGNOSTIC_SEVERITY['artifact-write-failure'],
|
|
11
|
+
message,
|
|
12
|
+
...(details === undefined ? {} : { details }),
|
|
13
|
+
};
|
|
14
|
+
return { ok: false, diagnostics: [diagnostic] };
|
|
15
|
+
}
|
|
16
|
+
async function pathExists(candidate) {
|
|
17
|
+
return access(candidate).then(() => true, () => false);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Persists one already-assembled, already-validated ObservationArtifact plus
|
|
21
|
+
* its screenshot bytes as one portable artifact directory:
|
|
22
|
+
* `<outputLocation>/<observationId>/{manifest.json,screenshot.png}`.
|
|
23
|
+
*
|
|
24
|
+
* `observationId` is guaranteed fresh per capture (see
|
|
25
|
+
* domain/identity.ts#buildObservationIdentity's "never collides" contract),
|
|
26
|
+
* so each write targets its own unique subdirectory - no destructive
|
|
27
|
+
* overwrite decision is needed for the normal case; an existing directory at
|
|
28
|
+
* that path is treated as a genuine collision and rejected rather than
|
|
29
|
+
* overwritten. Writing happens in a sibling temporary directory first,
|
|
30
|
+
* `screenshot.png` before `manifest.json` (manifest-last), and the whole
|
|
31
|
+
* temporary directory is only exposed under its real name via one atomic
|
|
32
|
+
* rename - a consumer can never observe a partially-written artifact under
|
|
33
|
+
* its final name.
|
|
34
|
+
*/
|
|
35
|
+
export async function writeObservationArtifact(artifact, screenshotBytes, options = {}) {
|
|
36
|
+
const validation = isValidObservationArtifact(artifact);
|
|
37
|
+
if (!validation.valid) {
|
|
38
|
+
return writeFailure(`refusing to persist an invalid ObservationArtifact: ${validation.reason}`);
|
|
39
|
+
}
|
|
40
|
+
const cwd = options.cwd ?? process.cwd();
|
|
41
|
+
const outputRoot = path.resolve(cwd, artifact.requestConfig.outputLocation);
|
|
42
|
+
const finalRoot = path.join(outputRoot, artifact.observationId);
|
|
43
|
+
const tempRoot = path.join(outputRoot, `.tmp-${artifact.observationId}`);
|
|
44
|
+
if (await pathExists(finalRoot)) {
|
|
45
|
+
return writeFailure(`an observation artifact already exists at "${artifact.observationId}"; refusing to overwrite it`, {
|
|
46
|
+
artifactRoot: finalRoot,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
51
|
+
await mkdir(tempRoot, { recursive: true });
|
|
52
|
+
await writeFile(path.join(tempRoot, SCREENSHOT_FILENAME), screenshotBytes);
|
|
53
|
+
await writeFile(path.join(tempRoot, MANIFEST_FILENAME), JSON.stringify(artifact, null, 2), 'utf8');
|
|
54
|
+
await rename(tempRoot, finalRoot);
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
artifactRoot: finalRoot,
|
|
58
|
+
manifestPath: path.join(finalRoot, MANIFEST_FILENAME),
|
|
59
|
+
screenshotPath: path.join(finalRoot, SCREENSHOT_FILENAME),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
await rm(tempRoot, { recursive: true, force: true }).catch(() => undefined);
|
|
64
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
65
|
+
return writeFailure(`failed to persist observation artifact: ${message}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=artifactWriter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifactWriter.js","sourceRoot":"","sources":["../../src/artifacts/artifactWriter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAG/D,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAC;AACjD,MAAM,CAAC,MAAM,mBAAmB,GAAG,gBAAgB,CAAC;AAEpD,SAAS,YAAY,CAAC,OAAe,EAAE,OAAiC;IACtE,MAAM,UAAU,GAAe;QAC7B,IAAI,EAAE,wBAAwB;QAC9B,QAAQ,EAAE,mBAAmB,CAAC,wBAAwB,CAAC;QACvD,OAAO;QACP,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;KAC9C,CAAC;IACF,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC;AAClD,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,SAAiB;IACzC,OAAO,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAC3B,GAAG,EAAE,CAAC,IAAI,EACV,GAAG,EAAE,CAAC,KAAK,CACZ,CAAC;AACJ,CAAC;AAOD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,QAA6B,EAC7B,eAA2B,EAC3B,UAA2C,EAAE;IAE7C,MAAM,UAAU,GAAG,0BAA0B,CAAC,QAAQ,CAAC,CAAC;IACxD,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QACtB,OAAO,YAAY,CAAC,uDAAuD,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;IAClG,CAAC;IAED,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IACzC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC;IAC5E,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,aAAa,CAAC,CAAC;IAChE,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,QAAQ,CAAC,aAAa,EAAE,CAAC,CAAC;IAEzE,IAAI,MAAM,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAChC,OAAO,YAAY,CAAC,8CAA8C,QAAQ,CAAC,aAAa,6BAA6B,EAAE;YACrH,YAAY,EAAE,SAAS;SACxB,CAAC,CAAC;IACL,CAAC;IAED,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,MAAM,KAAK,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAE3C,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,mBAAmB,CAAC,EAAE,eAAe,CAAC,CAAC;QAC3E,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,iBAAiB,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QAEnG,MAAM,MAAM,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;QAElC,OAAO;YACL,EAAE,EAAE,IAAI;YACR,YAAY,EAAE,SAAS;YACvB,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,iBAAiB,CAAC;YACrD,cAAc,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,mBAAmB,CAAC;SAC1D,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC5E,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO,YAAY,CAAC,2CAA2C,OAAO,EAAE,CAAC,CAAC;IAC5E,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Diagnostic } from '../domain/diagnostics.js';
|
|
2
|
+
/**
|
|
3
|
+
* Observer-owned result of persisting one ObservationArtifact. `artifactRoot`
|
|
4
|
+
* and the file paths are absolute filesystem paths for the *caller's* local
|
|
5
|
+
* use only (e.g. to print or open the result); they are never written back
|
|
6
|
+
* into the persisted manifest itself, which stores only portable relative
|
|
7
|
+
* references.
|
|
8
|
+
*/
|
|
9
|
+
export type PersistedObservationResult = {
|
|
10
|
+
ok: true;
|
|
11
|
+
artifactRoot: string;
|
|
12
|
+
manifestPath: string;
|
|
13
|
+
screenshotPath: string;
|
|
14
|
+
} | {
|
|
15
|
+
ok: false;
|
|
16
|
+
diagnostics: Diagnostic[];
|
|
17
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/artifacts/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { NormalizedObservationRequest } from '../request/request.js';
|
|
2
|
+
import type { BrowserCaptureResult } from './types.js';
|
|
3
|
+
export type { BrowserCaptureResult, BrowserProvenance } from './types.js';
|
|
4
|
+
export interface CaptureViewportInternalResult {
|
|
5
|
+
result: BrowserCaptureResult;
|
|
6
|
+
/** True if the launched browser process is still connected after cleanup ran. Always false unless cleanup failed. */
|
|
7
|
+
browserConnected: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Owns the entire launch -> context -> page -> navigate -> ready -> screenshot
|
|
11
|
+
* -> close lifecycle for one capture. Guarantees `browser.close()` runs on
|
|
12
|
+
* every exit path (success, safety rejection, navigation/readiness failure,
|
|
13
|
+
* or an unexpected internal error), so callers never manage cleanup
|
|
14
|
+
* themselves. Exported (not re-exported from src/index.ts or the application
|
|
15
|
+
* seam) only so integration tests can assert `browserConnected` becomes
|
|
16
|
+
* false after every call - the Browser handle itself never leaves this
|
|
17
|
+
* module.
|
|
18
|
+
*/
|
|
19
|
+
export declare function captureViewportInternal(request: NormalizedObservationRequest): Promise<CaptureViewportInternalResult>;
|
|
20
|
+
/** Observer-facing entrypoint: no Playwright object is reachable from its signature or return value. */
|
|
21
|
+
export declare function captureViewport(request: NormalizedObservationRequest): Promise<BrowserCaptureResult>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
import { DIAGNOSTIC_SEVERITY } from '../domain/diagnostics.js';
|
|
3
|
+
import { classifyUrl, classifyRedirect, classifySubresource, classifyPopup, classifyDownload } from '../safety/policy.js';
|
|
4
|
+
import { capturePageEvidence, captureTargetEvidence } from './evidenceCapture.js';
|
|
5
|
+
function diagnostic(code, message) {
|
|
6
|
+
return { code, severity: DIAGNOSTIC_SEVERITY[code], message };
|
|
7
|
+
}
|
|
8
|
+
function classifyNavigationError(error, readinessTimeoutMs) {
|
|
9
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
10
|
+
if (/timeout/i.test(message)) {
|
|
11
|
+
return diagnostic('readiness-timeout', `readiness condition not reached within ${readinessTimeoutMs}ms: ${message}`);
|
|
12
|
+
}
|
|
13
|
+
return diagnostic('navigation-failure', `navigation failed: ${message}`);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Owns the entire launch -> context -> page -> navigate -> ready -> screenshot
|
|
17
|
+
* -> close lifecycle for one capture. Guarantees `browser.close()` runs on
|
|
18
|
+
* every exit path (success, safety rejection, navigation/readiness failure,
|
|
19
|
+
* or an unexpected internal error), so callers never manage cleanup
|
|
20
|
+
* themselves. Exported (not re-exported from src/index.ts or the application
|
|
21
|
+
* seam) only so integration tests can assert `browserConnected` becomes
|
|
22
|
+
* false after every call - the Browser handle itself never leaves this
|
|
23
|
+
* module.
|
|
24
|
+
*/
|
|
25
|
+
export async function captureViewportInternal(request) {
|
|
26
|
+
const initialDecision = classifyUrl(request.targetUrl);
|
|
27
|
+
if (!initialDecision.allowed) {
|
|
28
|
+
return { result: { ok: false, diagnostics: [initialDecision.diagnostic] }, browserConnected: false };
|
|
29
|
+
}
|
|
30
|
+
let browser;
|
|
31
|
+
let result;
|
|
32
|
+
try {
|
|
33
|
+
browser = await chromium.launch({ headless: true });
|
|
34
|
+
const context = await browser.newContext({
|
|
35
|
+
viewport: { width: request.viewport.width, height: request.viewport.height },
|
|
36
|
+
});
|
|
37
|
+
const page = await context.newPage();
|
|
38
|
+
const diagnostics = [];
|
|
39
|
+
page.on('popup', () => diagnostics.push(classifyPopup()));
|
|
40
|
+
page.on('download', () => diagnostics.push(classifyDownload()));
|
|
41
|
+
let lastAllowedMainFrameUrl = request.targetUrl;
|
|
42
|
+
let rejection;
|
|
43
|
+
await page.route('**/*', async (route) => {
|
|
44
|
+
const req = route.request();
|
|
45
|
+
const url = req.url();
|
|
46
|
+
const isMainFrameNavigation = req.isNavigationRequest() && req.frame() === page.mainFrame();
|
|
47
|
+
if (!isMainFrameNavigation) {
|
|
48
|
+
const decision = classifySubresource(url);
|
|
49
|
+
if (!decision.allowed) {
|
|
50
|
+
rejection = rejection ?? decision.diagnostic;
|
|
51
|
+
await route.abort('blockedbyclient').catch(() => undefined);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
await route.continue().catch(() => undefined);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Chromium follows navigation redirects internally without re-emitting a
|
|
58
|
+
// routable request per hop, so each main-frame hop is fetched manually
|
|
59
|
+
// (maxRedirects: 0) and inspected here before any further hop is
|
|
60
|
+
// allowed to proceed - this is what lets a prohibited redirect target
|
|
61
|
+
// be blocked before it is ever contacted.
|
|
62
|
+
const decision = url === lastAllowedMainFrameUrl ? { allowed: true } : classifyRedirect(lastAllowedMainFrameUrl, url);
|
|
63
|
+
if (!decision.allowed) {
|
|
64
|
+
rejection = rejection ?? decision.diagnostic;
|
|
65
|
+
await route.fulfill({ status: 204, body: '' }).catch(() => undefined);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
lastAllowedMainFrameUrl = url;
|
|
69
|
+
let response;
|
|
70
|
+
try {
|
|
71
|
+
response = await route.fetch({ maxRedirects: 0 });
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
await route.abort('failed').catch(() => undefined);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const status = response.status();
|
|
78
|
+
if (status >= 300 && status < 400) {
|
|
79
|
+
const location = response.headers()['location'];
|
|
80
|
+
const resolvedUrl = location ? new URL(location, url).toString() : undefined;
|
|
81
|
+
const redirectDecision = resolvedUrl ? classifyRedirect(lastAllowedMainFrameUrl, resolvedUrl) : { allowed: true };
|
|
82
|
+
if (!redirectDecision.allowed) {
|
|
83
|
+
rejection = rejection ?? redirectDecision.diagnostic;
|
|
84
|
+
await route.fulfill({ status: 204, body: '' }).catch(() => undefined);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
await route.fulfill({ response }).catch(() => undefined);
|
|
89
|
+
});
|
|
90
|
+
let navigationError;
|
|
91
|
+
try {
|
|
92
|
+
await page.goto(request.targetUrl, {
|
|
93
|
+
waitUntil: request.readiness.condition,
|
|
94
|
+
timeout: request.readiness.timeoutMs,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
navigationError = err;
|
|
99
|
+
}
|
|
100
|
+
if (rejection) {
|
|
101
|
+
result = { ok: false, diagnostics: [rejection, ...diagnostics] };
|
|
102
|
+
}
|
|
103
|
+
else if (navigationError) {
|
|
104
|
+
result = { ok: false, diagnostics: [...diagnostics, classifyNavigationError(navigationError, request.readiness.timeoutMs)] };
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Captured from the same live page, after the same readiness point, as
|
|
108
|
+
// required by Batch 3: no second browser/page is ever opened.
|
|
109
|
+
const screenshotBuffer = await page.screenshot({ type: 'png' });
|
|
110
|
+
const provenance = { engine: 'chromium', version: browser.version() };
|
|
111
|
+
let pageEvidence;
|
|
112
|
+
try {
|
|
113
|
+
pageEvidence = await capturePageEvidence(page, request.targetUrl, request.viewport);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
117
|
+
diagnostics.push(diagnostic('browser-evidence-unavailable', `page evidence collection failed: ${message}`));
|
|
118
|
+
pageEvidence = {};
|
|
119
|
+
}
|
|
120
|
+
let targetEvidence;
|
|
121
|
+
try {
|
|
122
|
+
const captured = await captureTargetEvidence(page, request.targets);
|
|
123
|
+
targetEvidence = captured.targetEvidence;
|
|
124
|
+
diagnostics.push(...captured.diagnostics);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
128
|
+
diagnostics.push(diagnostic('browser-evidence-unavailable', `target evidence collection failed: ${message}`));
|
|
129
|
+
targetEvidence = {};
|
|
130
|
+
}
|
|
131
|
+
result = { ok: true, provenance, screenshot: new Uint8Array(screenshotBuffer), pageEvidence, targetEvidence, diagnostics };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
136
|
+
result = { ok: false, diagnostics: [diagnostic('browser-runtime-failure', `unexpected browser failure: ${message}`)] };
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
if (browser) {
|
|
140
|
+
await browser.close().catch(() => undefined);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { result, browserConnected: browser ? browser.isConnected() : false };
|
|
144
|
+
}
|
|
145
|
+
/** Observer-facing entrypoint: no Playwright object is reachable from its signature or return value. */
|
|
146
|
+
export async function captureViewport(request) {
|
|
147
|
+
const { result } = await captureViewportInternal(request);
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=chromiumAdapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chromiumAdapter.js","sourceRoot":"","sources":["../../src/browser/chromiumAdapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
import type { EvidenceField } from '../domain/evidence.js';
|
|
3
|
+
import type { Diagnostic } from '../domain/diagnostics.js';
|
|
4
|
+
import type { NamedTarget, Viewport } from '../request/request.js';
|
|
5
|
+
import type { TargetEvidenceRecord } from '../domain/schema.js';
|
|
6
|
+
/**
|
|
7
|
+
* Captures the v0.1 minimum page evidence (docs/PROJECT_MILESTONES.md
|
|
8
|
+
* "Minimum page evidence") from the already-navigated, already-ready page.
|
|
9
|
+
* `documentWidth`/`documentHeight` are the one explicitly *derived* pair -
|
|
10
|
+
* max(scroll, client) in each axis - everything else is a direct browser
|
|
11
|
+
* read, per the Batch 1 evidence-source model.
|
|
12
|
+
*/
|
|
13
|
+
export declare function capturePageEvidence(page: Page, requestedUrl: string, viewport: Viewport): Promise<Record<string, EvidenceField<unknown>>>;
|
|
14
|
+
export interface TargetEvidenceCaptureResult {
|
|
15
|
+
targetEvidence: Record<string, TargetEvidenceRecord>;
|
|
16
|
+
diagnostics: Diagnostic[];
|
|
17
|
+
}
|
|
18
|
+
/** Observes every explicitly configured Batch 1 target from the same live page, honoring the 0/1/many cardinality contract. */
|
|
19
|
+
export declare function captureTargetEvidence(page: Page, targets: readonly NamedTarget[]): Promise<TargetEvidenceCaptureResult>;
|