@jevitate/cli 0.10.0 → 0.11.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/dist/approved-demo.d.ts +86 -0
- package/dist/approved-demo.js +184 -0
- package/dist/approved-demo.js.map +1 -0
- package/dist/bin.js +3371 -1221
- package/dist/build-info.generated.js +2 -2
- package/dist/campaign-api.d.ts +15 -0
- package/dist/campaign-api.js +67 -6
- package/dist/campaign-api.js.map +1 -1
- package/dist/campaign-cli.js +7 -2
- package/dist/campaign-cli.js.map +1 -1
- package/dist/campaign-preflight.d.ts +76 -0
- package/dist/campaign-preflight.js +117 -0
- package/dist/campaign-preflight.js.map +1 -0
- package/dist/catalog-bundle-api.d.ts +15 -5
- package/dist/catalog-bundle-api.js +117 -14
- package/dist/catalog-bundle-api.js.map +1 -1
- package/dist/catalog-bundle-findings.d.ts +10 -0
- package/dist/catalog-bundle-findings.js +75 -1
- package/dist/catalog-bundle-findings.js.map +1 -1
- package/dist/catalog-bundle.d.ts +111 -7
- package/dist/catalog-bundle.js +208 -9
- package/dist/catalog-bundle.js.map +1 -1
- package/dist/catalog-cli.js +3 -2
- package/dist/catalog-cli.js.map +1 -1
- package/dist/check-api.js +20 -2
- package/dist/check-api.js.map +1 -1
- package/dist/check-cli.d.ts +2 -0
- package/dist/check-cli.js +25 -0
- package/dist/check-cli.js.map +1 -1
- package/dist/check-execute.d.ts +6 -0
- package/dist/check-execute.js +1 -0
- package/dist/check-execute.js.map +1 -1
- package/dist/check-types.d.ts +7 -0
- package/dist/check-types.js.map +1 -1
- package/dist/cli-output.js +15 -1
- package/dist/cli-output.js.map +1 -1
- package/dist/cold-start-report.d.ts +32 -0
- package/dist/cold-start-report.js +31 -0
- package/dist/cold-start-report.js.map +1 -0
- package/dist/defect-evidence.js +10 -5
- package/dist/defect-evidence.js.map +1 -1
- package/dist/demo-aspect-api.d.ts +11 -0
- package/dist/demo-aspect-api.js +27 -2
- package/dist/demo-aspect-api.js.map +1 -1
- package/dist/demo-aspect-cli.js +5 -0
- package/dist/demo-aspect-cli.js.map +1 -1
- package/dist/demo-capture.d.ts +79 -2
- package/dist/demo-capture.js +504 -12
- package/dist/demo-capture.js.map +1 -1
- package/dist/environments.d.ts +8 -1
- package/dist/environments.js +14 -3
- package/dist/environments.js.map +1 -1
- package/dist/exit-codes.js +1 -0
- package/dist/exit-codes.js.map +1 -1
- package/dist/explore-adversarial.js +2 -2
- package/dist/explore-adversarial.js.map +1 -1
- package/dist/explore-shared.d.ts +11 -0
- package/dist/explore-shared.js +9 -0
- package/dist/explore-shared.js.map +1 -1
- package/dist/findings-filing.js +7 -3
- package/dist/findings-filing.js.map +1 -1
- package/dist/github-issue-filer.js +15 -9
- package/dist/github-issue-filer.js.map +1 -1
- package/dist/journeeze-api.d.ts +8 -2
- package/dist/journeeze-api.js +105 -19
- package/dist/journeeze-api.js.map +1 -1
- package/dist/journey-anchor-api.d.ts +37 -0
- package/dist/journey-anchor-api.js +58 -0
- package/dist/journey-anchor-api.js.map +1 -0
- package/dist/journey-anchor-cli.d.ts +17 -0
- package/dist/journey-anchor-cli.js +104 -0
- package/dist/journey-anchor-cli.js.map +1 -0
- package/dist/journey-api.d.ts +29 -1
- package/dist/journey-api.js +82 -7
- package/dist/journey-api.js.map +1 -1
- package/dist/journey-cli.js +36 -5
- package/dist/journey-cli.js.map +1 -1
- package/dist/journey-demo-api.d.ts +29 -0
- package/dist/journey-demo-api.js +29 -7
- package/dist/journey-demo-api.js.map +1 -1
- package/dist/journey-lint-sarif.js +2 -0
- package/dist/journey-lint-sarif.js.map +1 -1
- package/dist/journey-network-checks.d.ts +11 -0
- package/dist/journey-network-checks.js +36 -13
- package/dist/journey-network-checks.js.map +1 -1
- package/dist/load-api.js +4 -2
- package/dist/load-api.js.map +1 -1
- package/dist/mcp-api.d.ts +3 -1
- package/dist/mcp-api.js +21 -2
- package/dist/mcp-api.js.map +1 -1
- package/dist/mcp-cli-tools.d.ts +1 -0
- package/dist/mcp-cli-tools.js +30 -8
- package/dist/mcp-cli-tools.js.map +1 -1
- package/dist/openrouter-call.d.ts +23 -10
- package/dist/openrouter-call.js +14 -11
- package/dist/openrouter-call.js.map +1 -1
- package/dist/png-pixels.d.ts +12 -0
- package/dist/png-pixels.js +96 -0
- package/dist/png-pixels.js.map +1 -0
- package/dist/regression-api.js +2 -1
- package/dist/regression-api.js.map +1 -1
- package/dist/server-log-verify.js +2 -1
- package/dist/server-log-verify.js.map +1 -1
- package/dist/signal-triage.js +7 -3
- package/dist/signal-triage.js.map +1 -1
- package/dist/source-run-api.js +2 -2
- package/dist/source-run-api.js.map +1 -1
- package/dist/verify-fix-api.d.ts +29 -0
- package/dist/verify-fix-api.js +43 -2
- package/dist/verify-fix-api.js.map +1 -1
- package/dist/verify-fix-cli.js +4 -0
- package/dist/verify-fix-cli.js.map +1 -1
- package/dist/zip-store.d.ts +15 -0
- package/dist/zip-store.js +93 -0
- package/dist/zip-store.js.map +1 -0
- package/package.json +11 -11
- package/skills/jevitate-demo/SKILL.md +4 -1
- package/skills/jevitate-run-journey/SKILL.md +4 -0
- package/skills/jevitate-test-campaign/SKILL.md +13 -0
- package/skills/jevitate-verify-fix/SKILL.md +5 -0
package/dist/catalog-bundle.d.ts
CHANGED
|
@@ -18,23 +18,30 @@ import { CatalogInputError, type Catalog } from "./catalog.js";
|
|
|
18
18
|
* - checks: one per check item that ran, keyed (target.id, commit, runId); a baseline only from a
|
|
19
19
|
* clean run whose `journeyHash` is the exported Journey's approved hash.
|
|
20
20
|
* - findings: whatever the d464c source hands in (`CatalogBundleFinding`), guarded; none by default.
|
|
21
|
-
* - demos and media:
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* - demos and media (#471): one `demos[]` entry per exported, APPROVED Journey that has an
|
|
22
|
+
* approved-demo record (approved-demo.ts: written by `demo approve` only on a `synthetic: true`
|
|
23
|
+
* environment, from a jz-mask-v1 render), whose `renderedFrom` is the Journey's current approved
|
|
24
|
+
* hash. Its `privacy` is the record's attestation. Media is only what the record proved masked
|
|
25
|
+
* and the API re-verified (present, same sha256, the right file type): `media/<id>/step-NN.png`,
|
|
26
|
+
* `media/<id>/demo.webm` and `demo.vtt` (subtitles only with the video), each listed in `files[]`.
|
|
27
|
+
* Over a §10 limit (2 MiB / 50 MiB / 256 KiB per file, 512 MiB and 4,000 files per bundle), a
|
|
28
|
+
* file is left out with a warning, never sent. A demo whose caption breaks §7's text rules is
|
|
29
|
+
* left out whole (its subtitles carry the same text). A draft demo has no record: never exported.
|
|
24
30
|
* - `generatedAt` is left out on purpose: the same catalog gives the same bytes, so the upload's
|
|
25
31
|
* `Idempotency-Key` (sha256 of bundle.json) dedupes a re-publish.
|
|
26
32
|
*
|
|
27
33
|
* Failure policy: personal data or a credential typed in clear anywhere, or a job the bundle cannot
|
|
28
34
|
* carry (a structural reference problem, text that is not one line), REFUSES the export
|
|
29
|
-
* (`CatalogBundleInputError`, exit 64) — nothing is guessed or silently rewritten.
|
|
30
|
-
*
|
|
35
|
+
* (`CatalogBundleInputError`, exit 64) — nothing is guessed or silently rewritten. So does a
|
|
36
|
+
* PROMOTED Journey the bundle cannot carry (#478: never a silent omission). A draft Journey, or a
|
|
37
|
+
* check the bundle cannot carry, is LEFT OUT with a warning (nothing else references it in a way the
|
|
31
38
|
* reader would refuse). A persona key the bundle cannot carry drops that key and the persona's
|
|
32
39
|
* approval (its hash could not recompute) with a warning.
|
|
33
40
|
*/
|
|
34
41
|
export declare const BUNDLE_KIND: "journeeze.catalog-bundle";
|
|
35
42
|
export declare const BUNDLE_VERSION: 1;
|
|
36
|
-
/** The schema minor this producer is built against (
|
|
37
|
-
export declare const BUNDLE_MINOR:
|
|
43
|
+
/** The schema minor this producer is built against (minor 1, journeeze-saas #173: optional `finding.locator` and `check.appVersion`). */
|
|
44
|
+
export declare const BUNDLE_MINOR: 1;
|
|
38
45
|
export interface BundleApproval {
|
|
39
46
|
readonly contentHash: string;
|
|
40
47
|
readonly at: string;
|
|
@@ -90,6 +97,8 @@ export interface BundleCheck {
|
|
|
90
97
|
readonly kind?: BundleCheckTargetKind;
|
|
91
98
|
};
|
|
92
99
|
readonly commit: string;
|
|
100
|
+
/** #479 (spec minor 1 §2): the app's release/version label the check ran against; `commit` stays the build identity. */
|
|
101
|
+
readonly appVersion?: string;
|
|
93
102
|
readonly at: string;
|
|
94
103
|
readonly runId?: string;
|
|
95
104
|
readonly journey?: string;
|
|
@@ -101,6 +110,18 @@ export interface BundleCheck {
|
|
|
101
110
|
readonly durationMs?: number;
|
|
102
111
|
readonly baseline?: BundleBaseline;
|
|
103
112
|
}
|
|
113
|
+
export interface BundleLocator {
|
|
114
|
+
/** The element in words, e.g. `the "Save" button`. One printable line, 1-200, no URL or personal data. */
|
|
115
|
+
readonly element: string;
|
|
116
|
+
/** The attribute the fix adds (the convention's first). */
|
|
117
|
+
readonly attribute: string;
|
|
118
|
+
/** The suggested test id. */
|
|
119
|
+
readonly testId: string;
|
|
120
|
+
/** The fix, e.g. `add data-testid="save-contact" to the "Save" button on /contacts/new`. One line, 1-300. */
|
|
121
|
+
readonly fix: string;
|
|
122
|
+
/** The steps (across Journeys) that use the element. */
|
|
123
|
+
readonly steps: number;
|
|
124
|
+
}
|
|
104
125
|
/**
|
|
105
126
|
* d464c HOOK: one machine finding as the contract's `findings[]` carries it (§4.4) — UX claims with
|
|
106
127
|
* the derived fingerprint, defects and hangs with jevitate's. 0.10 exports no media, so no
|
|
@@ -127,11 +148,47 @@ export interface CatalogBundleFinding {
|
|
|
127
148
|
readonly observation: string;
|
|
128
149
|
readonly userImpact?: string;
|
|
129
150
|
readonly recommendation?: string;
|
|
151
|
+
/** Minor 1 (§1): the brittle-element fix; only on kind `ux` + claim `other` + producerClaim `locator-brittle`, required there. */
|
|
152
|
+
readonly locator?: BundleLocator;
|
|
130
153
|
readonly citation?: {
|
|
131
154
|
readonly source: string;
|
|
132
155
|
readonly ref: string;
|
|
133
156
|
};
|
|
134
157
|
}
|
|
158
|
+
/** #471: one guide step of a demo (contract §4.2). */
|
|
159
|
+
export interface BundleDemoStep {
|
|
160
|
+
readonly number: number;
|
|
161
|
+
readonly caption: string;
|
|
162
|
+
readonly expected?: string;
|
|
163
|
+
/** `media/<journey>/step-NN.png`. */
|
|
164
|
+
readonly screenshot?: string;
|
|
165
|
+
}
|
|
166
|
+
/** #471: the jz-mask-v1 + synthetic-data attestation every demo carries (contract §7). */
|
|
167
|
+
export interface BundlePrivacy {
|
|
168
|
+
readonly mask: "jz-mask-v1";
|
|
169
|
+
readonly data: "synthetic";
|
|
170
|
+
readonly method: "dom-before-capture" | "pixels-after-capture";
|
|
171
|
+
readonly regions: number;
|
|
172
|
+
}
|
|
173
|
+
/** #471: one approved demo's media manifest (contract §4.2). */
|
|
174
|
+
export interface BundleDemo {
|
|
175
|
+
readonly journey: string;
|
|
176
|
+
readonly renderedFrom: string;
|
|
177
|
+
readonly status: "approved";
|
|
178
|
+
readonly title?: string;
|
|
179
|
+
readonly steps: readonly BundleDemoStep[];
|
|
180
|
+
readonly video?: string;
|
|
181
|
+
readonly subtitles?: string;
|
|
182
|
+
readonly privacy: BundlePrivacy;
|
|
183
|
+
}
|
|
184
|
+
export type BundleFileType = "image/png" | "image/webp" | "video/webm" | "text/vtt";
|
|
185
|
+
/** #471: one media file of the bundle (contract §2): listed ⇔ present ⇔ referenced. */
|
|
186
|
+
export interface BundleFile {
|
|
187
|
+
readonly path: string;
|
|
188
|
+
readonly sha256: string;
|
|
189
|
+
readonly bytes: number;
|
|
190
|
+
readonly type: BundleFileType;
|
|
191
|
+
}
|
|
135
192
|
export interface CatalogBundleV1 {
|
|
136
193
|
readonly kind: typeof BUNDLE_KIND;
|
|
137
194
|
readonly version: typeof BUNDLE_VERSION;
|
|
@@ -149,8 +206,40 @@ export interface CatalogBundleV1 {
|
|
|
149
206
|
readonly jobs: readonly BundleJob[];
|
|
150
207
|
readonly journeys: readonly BundleJourneyEntry[];
|
|
151
208
|
};
|
|
209
|
+
/** #471: present (with `files`) only when at least one approved demo is exported. */
|
|
210
|
+
readonly demos?: readonly BundleDemo[];
|
|
152
211
|
readonly checks: readonly BundleCheck[];
|
|
153
212
|
readonly findings: readonly CatalogBundleFinding[];
|
|
213
|
+
readonly files?: readonly BundleFile[];
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* #471: an approved demo as the API hands it in: the record's manifest, and the media files the API
|
|
217
|
+
* re-verified on disk (present, sha256 and size as recorded, the right file type), by record name.
|
|
218
|
+
*/
|
|
219
|
+
export interface CatalogBundleDemoInput {
|
|
220
|
+
readonly journey: string;
|
|
221
|
+
readonly renderedFrom: string;
|
|
222
|
+
readonly title?: string;
|
|
223
|
+
readonly steps: ReadonlyArray<{
|
|
224
|
+
readonly number: number;
|
|
225
|
+
readonly caption: string;
|
|
226
|
+
readonly expected?: string;
|
|
227
|
+
readonly screenshot?: string;
|
|
228
|
+
}>;
|
|
229
|
+
readonly video?: string;
|
|
230
|
+
readonly subtitles?: string;
|
|
231
|
+
readonly privacy: BundlePrivacy;
|
|
232
|
+
/** Verified media by record file name (`step-01.png`, `demo.webm`, `demo.vtt`): `source` is the file on disk. */
|
|
233
|
+
readonly media: Readonly<Record<string, {
|
|
234
|
+
readonly source: string;
|
|
235
|
+
readonly sha256: string;
|
|
236
|
+
readonly bytes: number;
|
|
237
|
+
}>>;
|
|
238
|
+
}
|
|
239
|
+
/** #471: a media file to place in the bundle: `source` on disk → `path` under the bundle root. */
|
|
240
|
+
export interface BundleMediaCopy {
|
|
241
|
+
readonly path: string;
|
|
242
|
+
readonly source: string;
|
|
154
243
|
}
|
|
155
244
|
/** One `jevitate check` record (`check.json`'s `data`, or the bare result) and where it came from. */
|
|
156
245
|
export interface CheckRecordInput {
|
|
@@ -170,9 +259,13 @@ export interface CatalogBundleInput {
|
|
|
170
259
|
readonly checks: readonly CheckRecordInput[];
|
|
171
260
|
/** d464c: the machine findings to export (default none). */
|
|
172
261
|
readonly findings?: readonly CatalogBundleFinding[];
|
|
262
|
+
/** #471: approved demos with their verified media (default none). */
|
|
263
|
+
readonly demos?: readonly CatalogBundleDemoInput[];
|
|
173
264
|
}
|
|
174
265
|
export interface CatalogBundleBuild {
|
|
175
266
|
readonly bundle: CatalogBundleV1;
|
|
267
|
+
/** #471: the media files to place beside bundle.json (exactly `bundle.files`, with their sources). */
|
|
268
|
+
readonly media: readonly BundleMediaCopy[];
|
|
176
269
|
/** Items left out or degraded, each with why. */
|
|
177
270
|
readonly warnings: readonly string[];
|
|
178
271
|
}
|
|
@@ -182,10 +275,21 @@ export declare class CatalogBundleInputError extends CatalogInputError {
|
|
|
182
275
|
readonly code: string;
|
|
183
276
|
constructor(issues: readonly string[]);
|
|
184
277
|
}
|
|
278
|
+
/** #471: contract §10's media limits. */
|
|
279
|
+
export declare const MEDIA_LIMITS: {
|
|
280
|
+
readonly screenshotBytes: number;
|
|
281
|
+
readonly videoBytes: number;
|
|
282
|
+
readonly subtitlesBytes: number;
|
|
283
|
+
readonly totalBytes: number;
|
|
284
|
+
readonly files: 4000;
|
|
285
|
+
readonly demoSteps: 200;
|
|
286
|
+
};
|
|
185
287
|
/** Looks like personal data (an email, or 9+ digits) — the contract's `noPersonalData`. */
|
|
186
288
|
export declare function looksPersonal(s: string): boolean;
|
|
187
289
|
/** The contract's `routeTemplate`: a path template — no query string, fragment, host or record id. */
|
|
188
290
|
export declare function isRouteTemplate(route: string): boolean;
|
|
291
|
+
/** Why a subtitles file's text breaks §7 (a cue line that is not machine text, a DRAFT note), or null. */
|
|
292
|
+
export declare function subtitlesTextProblem(vtt: string): string | null;
|
|
189
293
|
/** Why a finding handed in by the d464c source breaks the contract's privacy rules (§7), or []. */
|
|
190
294
|
export declare function findingIssues(f: CatalogBundleFinding): string[];
|
|
191
295
|
/** Builds the bundle (pure). Throws `CatalogBundleInputError` when the catalog cannot be exported. */
|
package/dist/catalog-bundle.js
CHANGED
|
@@ -20,24 +20,31 @@ import { describeRefIssue, jobRefIssues, journeyRefIssues, RESERVED_ANCHORS } fr
|
|
|
20
20
|
* - checks: one per check item that ran, keyed (target.id, commit, runId); a baseline only from a
|
|
21
21
|
* clean run whose `journeyHash` is the exported Journey's approved hash.
|
|
22
22
|
* - findings: whatever the d464c source hands in (`CatalogBundleFinding`), guarded; none by default.
|
|
23
|
-
* - demos and media:
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* - demos and media (#471): one `demos[]` entry per exported, APPROVED Journey that has an
|
|
24
|
+
* approved-demo record (approved-demo.ts: written by `demo approve` only on a `synthetic: true`
|
|
25
|
+
* environment, from a jz-mask-v1 render), whose `renderedFrom` is the Journey's current approved
|
|
26
|
+
* hash. Its `privacy` is the record's attestation. Media is only what the record proved masked
|
|
27
|
+
* and the API re-verified (present, same sha256, the right file type): `media/<id>/step-NN.png`,
|
|
28
|
+
* `media/<id>/demo.webm` and `demo.vtt` (subtitles only with the video), each listed in `files[]`.
|
|
29
|
+
* Over a §10 limit (2 MiB / 50 MiB / 256 KiB per file, 512 MiB and 4,000 files per bundle), a
|
|
30
|
+
* file is left out with a warning, never sent. A demo whose caption breaks §7's text rules is
|
|
31
|
+
* left out whole (its subtitles carry the same text). A draft demo has no record: never exported.
|
|
26
32
|
* - `generatedAt` is left out on purpose: the same catalog gives the same bytes, so the upload's
|
|
27
33
|
* `Idempotency-Key` (sha256 of bundle.json) dedupes a re-publish.
|
|
28
34
|
*
|
|
29
35
|
* Failure policy: personal data or a credential typed in clear anywhere, or a job the bundle cannot
|
|
30
36
|
* carry (a structural reference problem, text that is not one line), REFUSES the export
|
|
31
|
-
* (`CatalogBundleInputError`, exit 64) — nothing is guessed or silently rewritten.
|
|
32
|
-
*
|
|
37
|
+
* (`CatalogBundleInputError`, exit 64) — nothing is guessed or silently rewritten. So does a
|
|
38
|
+
* PROMOTED Journey the bundle cannot carry (#478: never a silent omission). A draft Journey, or a
|
|
39
|
+
* check the bundle cannot carry, is LEFT OUT with a warning (nothing else references it in a way the
|
|
33
40
|
* reader would refuse). A persona key the bundle cannot carry drops that key and the persona's
|
|
34
41
|
* approval (its hash could not recompute) with a warning.
|
|
35
42
|
*/
|
|
36
43
|
// ── The bundle (catalog-bundle.v1.json), as jevitate produces it ─────────────────────────────
|
|
37
44
|
export const BUNDLE_KIND = "journeeze.catalog-bundle";
|
|
38
45
|
export const BUNDLE_VERSION = 1;
|
|
39
|
-
/** The schema minor this producer is built against (
|
|
40
|
-
export const BUNDLE_MINOR =
|
|
46
|
+
/** The schema minor this producer is built against (minor 1, journeeze-saas #173: optional `finding.locator` and `check.appVersion`). */
|
|
47
|
+
export const BUNDLE_MINOR = 1;
|
|
41
48
|
export const CHECK_TARGET_KINDS = ["journey", "goal", "mission", "invariant", "verify_fix", "regression"];
|
|
42
49
|
/** The catalog cannot be exported as it is (personal data, a credential typed in clear, a job the bundle cannot carry) — exit 64. */
|
|
43
50
|
export class CatalogBundleInputError extends CatalogInputError {
|
|
@@ -56,6 +63,7 @@ const COMMIT_RE = /^[0-9a-f]{7,40}$/;
|
|
|
56
63
|
const SEMVER_RE = /^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]{1,40})?$/;
|
|
57
64
|
const DATE_TIME_RE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?(Z|[+-][0-9]{2}:[0-9]{2})$/;
|
|
58
65
|
const RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
66
|
+
const APP_VERSION_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/;
|
|
59
67
|
const FAILURE_KIND_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
60
68
|
const OTHER_FIELD_KEY_RE = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
|
|
61
69
|
const ONE_LINE_RE = /^(?=.*\S)[^\u0000-\u001f\u007f]+$/u;
|
|
@@ -69,13 +77,25 @@ const RECORD_ID_RES = [
|
|
|
69
77
|
const ROUTE_TEMPLATE_RE = /^(\/(\{[A-Za-z][A-Za-z0-9_]{0,63}\}|[A-Za-z0-9._~-]+))*\/?$/u;
|
|
70
78
|
const TFLOW_ID_RE = /^[a-z0-9._:-]{1,96}$/u;
|
|
71
79
|
const FINGERPRINT_RE = /^[0-9a-f]{16}$/u;
|
|
80
|
+
const LOCATOR_ATTRIBUTE_RE = /^[a-z][a-z0-9-]{0,39}$/u;
|
|
81
|
+
const LOCATOR_TEST_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,99}$/u;
|
|
82
|
+
const LOCATOR_MAX_STEPS = 10_000;
|
|
72
83
|
const APPROVAL_CHANNELS = new Set(["tty", "mcp", "ci", "non-interactive", "pr-review", "other"]);
|
|
73
84
|
const RESERVED = new Set(RESERVED_ANCHORS);
|
|
74
85
|
const SESSION_KEYS = new Set(["storageState", "login"]);
|
|
75
86
|
const PERSONA_NAMED_KEYS = new Set(["id", "name", "description", "role", "approval", "storageState", "login"]);
|
|
76
87
|
const MAX_MS = 86_400_000;
|
|
77
88
|
const MAX_STEP = 200;
|
|
78
|
-
const LIMITS = { personas: 200, jobs: 500, journeys: 1000, checks: 5000, findings: 5000 };
|
|
89
|
+
const LIMITS = { personas: 200, jobs: 500, journeys: 1000, checks: 5000, findings: 5000, demos: 1000 };
|
|
90
|
+
/** #471: contract §10's media limits. */
|
|
91
|
+
export const MEDIA_LIMITS = {
|
|
92
|
+
screenshotBytes: 2 * 1024 * 1024,
|
|
93
|
+
videoBytes: 50 * 1024 * 1024,
|
|
94
|
+
subtitlesBytes: 256 * 1024,
|
|
95
|
+
totalBytes: 512 * 1024 * 1024,
|
|
96
|
+
files: 4000,
|
|
97
|
+
demoSteps: 200,
|
|
98
|
+
};
|
|
79
99
|
/** Looks like personal data (an email, or 9+ digits) — the contract's `noPersonalData`. */
|
|
80
100
|
export function looksPersonal(s) {
|
|
81
101
|
return PERSONAL_DATA_RES.some((re) => re.test(s));
|
|
@@ -329,8 +349,9 @@ function bundleJourneys(catalog, refuse, warn) {
|
|
|
329
349
|
refuse.push(`journey ${cj.id}: ${c.path.slice(1)} types personal data in clear (an email or 9+ digits) — use a parameter or a synthetic value`);
|
|
330
350
|
}
|
|
331
351
|
const linked = journeyLink(catalog, cj);
|
|
352
|
+
// #478: a promoted Journey the bundle cannot carry refuses the export — never a silent omission.
|
|
332
353
|
if ("omit" in linked) {
|
|
333
|
-
|
|
354
|
+
refuse.push(`journey ${cj.id}: ${linked.omit} — fix it (the bundle cannot carry it), or demote it`);
|
|
334
355
|
continue;
|
|
335
356
|
}
|
|
336
357
|
const file = asJson(cj.journey);
|
|
@@ -446,6 +467,16 @@ function bundleChecks(input, journeys, warn) {
|
|
|
446
467
|
warn.push(`${source}: the check names no commit (run it with --target-build <commit sha>) — its items are left out`);
|
|
447
468
|
continue;
|
|
448
469
|
}
|
|
470
|
+
// #479 (spec minor 1 §2): the app's release label travels beside the commit; an invalid one is
|
|
471
|
+
// left out with a warning (the check itself still exports).
|
|
472
|
+
const rawAppVersion = typeof data.appVersion === "string" ? data.appVersion : undefined;
|
|
473
|
+
let appVersion;
|
|
474
|
+
if (rawAppVersion !== undefined) {
|
|
475
|
+
if (APP_VERSION_RE.test(rawAppVersion))
|
|
476
|
+
appVersion = rawAppVersion;
|
|
477
|
+
else
|
|
478
|
+
warn.push(`${source}: the check's appVersion is not a release label (1-64 of [A-Za-z0-9._+-], starting alphanumeric) — left out`);
|
|
479
|
+
}
|
|
449
480
|
const at = typeof data.startedAt === "string" ? data.startedAt : "";
|
|
450
481
|
if (!DATE_TIME_RE.test(at)) {
|
|
451
482
|
warn.push(`${source}: the check's start time is not an ISO date-time — its items are left out`);
|
|
@@ -493,6 +524,7 @@ function bundleChecks(input, journeys, warn) {
|
|
|
493
524
|
const check = {
|
|
494
525
|
target: { id, ...(kind === undefined ? {} : { kind }) },
|
|
495
526
|
commit,
|
|
527
|
+
...(appVersion === undefined ? {} : { appVersion }),
|
|
496
528
|
at,
|
|
497
529
|
runId,
|
|
498
530
|
...(journeyId === undefined ? {} : { journey: journeyId }),
|
|
@@ -509,6 +541,147 @@ function bundleChecks(input, journeys, warn) {
|
|
|
509
541
|
}
|
|
510
542
|
return [...out.values()].sort((a, b) => a.at.localeCompare(b.at) || a.target.id.localeCompare(b.target.id) || (a.runId ?? "").localeCompare(b.runId ?? ""));
|
|
511
543
|
}
|
|
544
|
+
// ── Demos and media (#471) ─────────────────────────────────────────────────────────────────
|
|
545
|
+
/** Why a subtitles file's text breaks §7 (a cue line that is not machine text, a DRAFT note), or null. */
|
|
546
|
+
export function subtitlesTextProblem(vtt) {
|
|
547
|
+
const lines = vtt.split(/\r?\n/u);
|
|
548
|
+
if (lines[0]?.trim() !== "WEBVTT")
|
|
549
|
+
return "it does not start with WEBVTT";
|
|
550
|
+
for (const [i, raw] of lines.entries()) {
|
|
551
|
+
const line = raw.trim();
|
|
552
|
+
if (i === 0 || line === "" || /^step-[0-9]{1,3}$/u.test(line) || /^[0-9:.]+ --> [0-9:.]+$/u.test(line))
|
|
553
|
+
continue;
|
|
554
|
+
if (/^NOTE\b/u.test(line))
|
|
555
|
+
return "it carries a NOTE block";
|
|
556
|
+
const p = machineTextProblem(line, 600);
|
|
557
|
+
if (p !== null)
|
|
558
|
+
return `a cue ${p}`;
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
function bundleDemos(input, journeys, warn) {
|
|
563
|
+
const demos = [];
|
|
564
|
+
const files = [];
|
|
565
|
+
const media = [];
|
|
566
|
+
let total = 0;
|
|
567
|
+
const byId = new Map(journeys.map((j) => [j.cj.id, j]));
|
|
568
|
+
const sorted = [...(input.demos ?? [])].sort((a, b) => (a.journey < b.journey ? -1 : a.journey > b.journey ? 1 : 0));
|
|
569
|
+
for (const d of sorted) {
|
|
570
|
+
const where = `demo ${d.journey}`;
|
|
571
|
+
const ex = byId.get(d.journey);
|
|
572
|
+
if (ex === undefined) {
|
|
573
|
+
warn.push(`${where}: its Journey is not in the bundle (not promoted, or left out) — the demo is left out`);
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
const approved = ex.cj.approval !== undefined && ex.cj.approval.contentHash === ex.cj.contentHash;
|
|
577
|
+
if (!approved) {
|
|
578
|
+
warn.push(`${where}: its Journey changed since it was approved — the demo is left out (approve the Journey and its demo again)`);
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (d.renderedFrom !== ex.cj.contentHash) {
|
|
582
|
+
warn.push(`${where}: rendered from another version of its Journey (${d.renderedFrom.slice(0, 12)}, approved ${ex.cj.contentHash.slice(0, 12)}) — the demo is left out (run \`jevitate demo approve\` again)`);
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if (d.steps.length === 0 || d.steps.length > MEDIA_LIMITS.demoSteps) {
|
|
586
|
+
warn.push(`${where}: ${d.steps.length} steps (a demo has 1-${MEDIA_LIMITS.demoSteps}) — the demo is left out`);
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
const captionIssues = d.steps.flatMap((s) => {
|
|
590
|
+
const p = machineTextProblem(s.caption, 200);
|
|
591
|
+
return p === null ? [] : [`step ${s.number} caption ${p}`];
|
|
592
|
+
});
|
|
593
|
+
if (captionIssues.length > 0) {
|
|
594
|
+
warn.push(`${where}: ${captionIssues.join("; ")} — the demo is left out (captions are producer text: one line, no personal data, URL or record id)`);
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
let title;
|
|
598
|
+
if (d.title !== undefined) {
|
|
599
|
+
const p = machineTextProblem(d.title, 200);
|
|
600
|
+
if (p !== null)
|
|
601
|
+
warn.push(`${where}: title ${p} — sent without a title`);
|
|
602
|
+
else if (/^DRAFT/u.test(d.title))
|
|
603
|
+
warn.push(`${where}: title starts with DRAFT — sent without a title`);
|
|
604
|
+
else
|
|
605
|
+
title = d.title;
|
|
606
|
+
}
|
|
607
|
+
// Media, each within its per-file and the bundle's limits; a left-out file is never referenced.
|
|
608
|
+
const dir = `media/${d.journey}`;
|
|
609
|
+
const take = (name, type, max, what) => {
|
|
610
|
+
if (name === undefined)
|
|
611
|
+
return undefined;
|
|
612
|
+
const f = d.media[name];
|
|
613
|
+
if (f === undefined)
|
|
614
|
+
return undefined; // the API already warned why
|
|
615
|
+
if (f.bytes > max) {
|
|
616
|
+
warn.push(`${where}: ${what} ${name} is ${f.bytes} bytes, over the contract's ${max} — left out`);
|
|
617
|
+
return undefined;
|
|
618
|
+
}
|
|
619
|
+
if (files.length + 1 > MEDIA_LIMITS.files || total + f.bytes > MEDIA_LIMITS.totalBytes) {
|
|
620
|
+
warn.push(`${where}: ${what} ${name} would take the bundle over ${MEDIA_LIMITS.files} files or ${MEDIA_LIMITS.totalBytes} bytes of media — left out`);
|
|
621
|
+
return undefined;
|
|
622
|
+
}
|
|
623
|
+
const path = `${dir}/${name}`;
|
|
624
|
+
files.push({ path, sha256: f.sha256, bytes: f.bytes, type });
|
|
625
|
+
media.push({ path, source: f.source });
|
|
626
|
+
total += f.bytes;
|
|
627
|
+
return path;
|
|
628
|
+
};
|
|
629
|
+
const steps = d.steps.map((s) => {
|
|
630
|
+
let expected;
|
|
631
|
+
if (s.expected !== undefined) {
|
|
632
|
+
const p = machineTextProblem(s.expected, 500);
|
|
633
|
+
if (p !== null)
|
|
634
|
+
warn.push(`${where}: step ${s.number} expected result ${p} — sent without it`);
|
|
635
|
+
else
|
|
636
|
+
expected = s.expected;
|
|
637
|
+
}
|
|
638
|
+
const shot = s.screenshot !== undefined && /^step-[0-9]{2,3}\.png$/u.test(s.screenshot) ? take(s.screenshot, "image/png", MEDIA_LIMITS.screenshotBytes, "screenshot") : undefined;
|
|
639
|
+
return { number: s.number, caption: s.caption, ...(expected === undefined ? {} : { expected }), ...(shot === undefined ? {} : { screenshot: shot }) };
|
|
640
|
+
});
|
|
641
|
+
// Subtitles only with their video: reserve both or neither.
|
|
642
|
+
let video;
|
|
643
|
+
let subtitles;
|
|
644
|
+
const v = d.video === undefined ? undefined : d.media[d.video];
|
|
645
|
+
const t = d.subtitles === undefined ? undefined : d.media[d.subtitles];
|
|
646
|
+
if (v !== undefined) {
|
|
647
|
+
if (d.subtitles !== undefined && t === undefined)
|
|
648
|
+
warn.push(`${where}: its subtitles could not be verified — the video is left out with them`);
|
|
649
|
+
else if (t !== undefined && t.bytes > MEDIA_LIMITS.subtitlesBytes)
|
|
650
|
+
warn.push(`${where}: subtitles ${d.subtitles} are ${t.bytes} bytes, over the contract's ${MEDIA_LIMITS.subtitlesBytes} — the video is left out with them`);
|
|
651
|
+
else {
|
|
652
|
+
video = take(d.video, "video/webm", MEDIA_LIMITS.videoBytes, "video");
|
|
653
|
+
if (video !== undefined && t !== undefined) {
|
|
654
|
+
subtitles = take(d.subtitles, "text/vtt", MEDIA_LIMITS.subtitlesBytes, "subtitles");
|
|
655
|
+
if (subtitles === undefined) {
|
|
656
|
+
// Never a video without its captions file: un-take the video.
|
|
657
|
+
const at = files.findIndex((f) => f.path === video);
|
|
658
|
+
total -= files[at].bytes;
|
|
659
|
+
files.splice(at, 1);
|
|
660
|
+
media.splice(media.findIndex((m) => m.path === video), 1);
|
|
661
|
+
video = undefined;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
demos.push({
|
|
667
|
+
journey: d.journey,
|
|
668
|
+
renderedFrom: d.renderedFrom,
|
|
669
|
+
status: "approved",
|
|
670
|
+
...(title === undefined ? {} : { title }),
|
|
671
|
+
steps,
|
|
672
|
+
...(video === undefined ? {} : { video }),
|
|
673
|
+
...(subtitles === undefined ? {} : { subtitles }),
|
|
674
|
+
privacy: { mask: d.privacy.mask, data: d.privacy.data, method: d.privacy.method, regions: d.privacy.regions },
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
if (demos.length > LIMITS.demos) {
|
|
678
|
+
warn.push(`${demos.length} demos: only the first ${LIMITS.demos} are exported`);
|
|
679
|
+
const kept = new Set(demos.slice(0, LIMITS.demos).map((d) => `media/${d.journey}/`));
|
|
680
|
+
const keep = (p) => [...kept].some((k) => p.startsWith(k));
|
|
681
|
+
return { demos: demos.slice(0, LIMITS.demos), files: files.filter((f) => keep(f.path)), media: media.filter((m) => keep(m.path)) };
|
|
682
|
+
}
|
|
683
|
+
return { demos, files, media };
|
|
684
|
+
}
|
|
512
685
|
// ── Findings (the d464c hook's guard) ────────────────────────────────────────────────────────
|
|
513
686
|
/** Why a finding handed in by the d464c source breaks the contract's privacy rules (§7), or []. */
|
|
514
687
|
export function findingIssues(f) {
|
|
@@ -531,6 +704,28 @@ export function findingIssues(f) {
|
|
|
531
704
|
text("userImpact", f.userImpact, 600);
|
|
532
705
|
text("recommendation", f.recommendation, 600);
|
|
533
706
|
(f.controls ?? []).forEach((c, i) => text(`controls[${i}]`, c, 120));
|
|
707
|
+
// Minor 1 §1: `locator` belongs only to a locator claim, and a locator claim must carry it.
|
|
708
|
+
const locatorClaim = f.kind === "ux" && f.claim === "other" && f.producerClaim === "locator-brittle";
|
|
709
|
+
if (locatorClaim && f.locator === undefined)
|
|
710
|
+
out.push(`${where}: a locator finding needs its locator`);
|
|
711
|
+
if (f.locator !== undefined) {
|
|
712
|
+
if (!locatorClaim)
|
|
713
|
+
out.push(`${where}: locator is only for kind ux + claim other + producerClaim locator-brittle`);
|
|
714
|
+
const loc = f.locator;
|
|
715
|
+
const locText = (k, max) => {
|
|
716
|
+
const p = machineTextProblem(loc[k], max);
|
|
717
|
+
if (p !== null)
|
|
718
|
+
out.push(`${where}: locator.${k} ${p}`);
|
|
719
|
+
};
|
|
720
|
+
locText("element", 200);
|
|
721
|
+
locText("fix", 300);
|
|
722
|
+
if (!LOCATOR_ATTRIBUTE_RE.test(loc.attribute))
|
|
723
|
+
out.push(`${where}: locator.attribute is not a test-id attribute name`);
|
|
724
|
+
if (!LOCATOR_TEST_ID_RE.test(loc.testId))
|
|
725
|
+
out.push(`${where}: locator.testId is not a test id`);
|
|
726
|
+
if (!Number.isInteger(loc.steps) || loc.steps < 1 || loc.steps > LOCATOR_MAX_STEPS)
|
|
727
|
+
out.push(`${where}: locator.steps is not an integer 1-${LOCATOR_MAX_STEPS}`);
|
|
728
|
+
}
|
|
534
729
|
return out;
|
|
535
730
|
}
|
|
536
731
|
// ── The bundle ───────────────────────────────────────────────────────────────────────────────
|
|
@@ -561,7 +756,9 @@ export function buildCatalogBundle(input) {
|
|
|
561
756
|
checks = checks.slice(-LIMITS.checks);
|
|
562
757
|
}
|
|
563
758
|
const commit = input.producer.commit !== undefined && COMMIT_RE.test(input.producer.commit) ? input.producer.commit : undefined;
|
|
759
|
+
const shown = bundleDemos(input, journeys, warn);
|
|
564
760
|
return {
|
|
761
|
+
media: shown.media,
|
|
565
762
|
bundle: {
|
|
566
763
|
kind: BUNDLE_KIND,
|
|
567
764
|
version: BUNDLE_VERSION,
|
|
@@ -569,8 +766,10 @@ export function buildCatalogBundle(input) {
|
|
|
569
766
|
producer: { tool: "jevitate", version: input.producer.version, ...(commit === undefined ? {} : { commit }) },
|
|
570
767
|
product: { name: input.productName },
|
|
571
768
|
catalog: { personas, jobs, journeys: journeys.map((j) => j.entry) },
|
|
769
|
+
...(shown.demos.length === 0 ? {} : { demos: shown.demos }),
|
|
572
770
|
checks,
|
|
573
771
|
findings: [...findings],
|
|
772
|
+
...(shown.demos.length === 0 ? {} : { files: shown.files }),
|
|
574
773
|
},
|
|
575
774
|
warnings: warn,
|
|
576
775
|
};
|