@momentiq/dark-factory-cli 2.4.0 → 2.6.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.
Files changed (37) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +83 -39
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/verify.d.ts +6 -0
  5. package/dist/commands/verify.d.ts.map +1 -0
  6. package/dist/commands/verify.js +223 -0
  7. package/dist/commands/verify.js.map +1 -0
  8. package/dist/evidence/route-runner.d.ts +1 -0
  9. package/dist/evidence/route-runner.d.ts.map +1 -1
  10. package/dist/evidence/route-runner.js +23 -2
  11. package/dist/evidence/route-runner.js.map +1 -1
  12. package/dist/handoff/handoff-verb.d.ts +6 -0
  13. package/dist/handoff/handoff-verb.d.ts.map +1 -1
  14. package/dist/handoff/handoff-verb.js +58 -0
  15. package/dist/handoff/handoff-verb.js.map +1 -1
  16. package/dist/mcp/tools/handoff.d.ts.map +1 -1
  17. package/dist/mcp/tools/handoff.js +7 -0
  18. package/dist/mcp/tools/handoff.js.map +1 -1
  19. package/dist/policy/gate.d.ts.map +1 -1
  20. package/dist/policy/gate.js +30 -18
  21. package/dist/policy/gate.js.map +1 -1
  22. package/dist/runner.d.ts.map +1 -1
  23. package/dist/runner.js +11 -6
  24. package/dist/runner.js.map +1 -1
  25. package/dist/skills/install.d.ts.map +1 -1
  26. package/dist/skills/install.js +1 -0
  27. package/dist/skills/install.js.map +1 -1
  28. package/package.json +2 -2
  29. package/skills/chief-engineer-blitz/SKILL.md.tmpl +7 -6
  30. package/skills/chief-engineer-blitz/skill.json +1 -1
  31. package/skills/verify/SKILL.md.tmpl +100 -0
  32. package/skills/verify/producer/README.md +73 -0
  33. package/skills/verify/producer/coverage.ts +121 -0
  34. package/skills/verify/producer/playwright-route.sh +161 -0
  35. package/skills/verify/producer/playwright.ui-route.config.ts +111 -0
  36. package/skills/verify/producer/ui-route.producer.spec.ts +281 -0
  37. package/skills/verify/skill.json +22 -0
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Reusable **playwright (UI) verification-route producer** (momentiq-ai/
3
+ * dark-factory#193).
4
+ *
5
+ * Generalized from the dark-factory-dashboard dogfood (Cycle 21 EC2/EC8, ADR
6
+ * 2026-06). This is the `playwright` route's producer: for every UI surface
7
+ * armed by the change under review it captures the required floor of the SOTA
8
+ * UI-grounding triad:
9
+ *
10
+ * (1) an **ARIA snapshot** (Playwright `ariaSnapshot()` — the accessibility
11
+ * tree, a structural, tamper-resistant assertion that the surface renders
12
+ * its expected landmark/heading/role structure) — ALWAYS produced, and
13
+ * (2) an **after screenshot** at the SHA under review — ALWAYS produced; plus
14
+ * an optional **before screenshot** baseline (completing the before/after
15
+ * pair) ONLY when `DF_UI_ROUTE_BEFORE_BASE_URL` is set. A missing baseline
16
+ * never fails the route.
17
+ *
18
+ * The **optional independent VLM check** (the triad's third leg) is DEFERRED in
19
+ * v1: the ARIA + after capture is the required floor; the VLM leg is additive
20
+ * corroboration gated behind a provider/cost decision. The hook is
21
+ * `runOptionalVlmCheck()` below — a no-op unless `DF_UI_ROUTE_VLM=1` AND a
22
+ * provider is wired, and it can only ADD a finding, never relax the floor.
23
+ *
24
+ * ## Fail-closed coverage
25
+ *
26
+ * A `web/**`/`*.tsx` change ARMS the route, but the producer maps each *changed*
27
+ * UI path to a capture surface; a changed product-UI path with NO mapped surface
28
+ * BLOCKS the route (the `ui-route-coverage` test below). This is the difference
29
+ * between an evidence gate and a false-positive generator — a `*.tsx` change
30
+ * cannot be satisfied by rendering only an unaffected surface. The mapping logic
31
+ * lives in the unit-tested `coverage.ts`.
32
+ *
33
+ * ## Auth (consumer-supplied storage-state — NOT vendor-coupled)
34
+ *
35
+ * Public surfaces need no auth. For PROTECTED surfaces, supply an authenticated
36
+ * Playwright `storageState` JSON and point `DF_UI_ROUTE_STORAGE_STATE` at it
37
+ * (see `playwright.ui-route.config.ts`). How you mint that session (Clerk, Auth0,
38
+ * a bot login, a long-lived test cookie) is YOUR repo's concern — the producer
39
+ * only consumes the storage-state. v1 ships a single public surface so it works
40
+ * zero-auth out of the box; add protected surfaces + storage-state as needed.
41
+ *
42
+ * ## Evidence shape
43
+ *
44
+ * Artifacts land under `DF_UI_ROUTE_OUT` (default
45
+ * `agent-reviews/quality-gates/ui/<sha>/`), one subdir per surface:
46
+ * <surface-slug>/aria.snapshot.txt — the ARIA tree (floor)
47
+ * <surface-slug>/after.png — screenshot at the gated SHA (floor)
48
+ * <surface-slug>/before.png — baseline (only when BEFORE_BASE_URL set)
49
+ * <surface-slug>/meta.json — capture provenance
50
+ *
51
+ * The spec FAILS (→ route blocks) if a routed UI surface does not render its
52
+ * required structure — the point: an over-claimed "the UI works" cannot pass the
53
+ * gate without a real, passing capture bound to the SHA.
54
+ *
55
+ * ── CONSUMER REFERENCE FILE — copy into your repo and OWN it. The one block you
56
+ * MUST edit is `SURFACES[]` below (your routes + their required headings +
57
+ * the source globs each one covers). `df skills install verify` does NOT
58
+ * overwrite this file; it is yours to maintain.
59
+ */
60
+ import { test, expect, type Page } from "@playwright/test";
61
+ import { mkdirSync, writeFileSync } from "node:fs";
62
+ import { join } from "node:path";
63
+ import { partitionChangedPaths, type UiSurface } from "./coverage";
64
+
65
+ const SHA = process.env.DF_UI_ROUTE_SHA ?? "unknown";
66
+ const OUT_DIR =
67
+ process.env.DF_UI_ROUTE_OUT ?? `agent-reviews/quality-gates/ui/${SHA}`;
68
+ // Optional baseline origin for the "before" screenshot (e.g. the deployed
69
+ // tip-of-main app). When unset, only the "after" capture is taken — the ARIA
70
+ // snapshot + after screenshot are the required floor regardless.
71
+ const BEFORE_BASE_URL = process.env.DF_UI_ROUTE_BEFORE_BASE_URL?.replace(
72
+ /\/$/,
73
+ "",
74
+ );
75
+
76
+ /**
77
+ * ▼▼▼ EDIT ME ▼▼▼ — the UI surfaces this producer asserts.
78
+ *
79
+ * Each entry pairs a route `path` with a stable, STRUCTURAL expectation
80
+ * (`requiredHeading` — an accessible-name regex for a landmark heading) so the
81
+ * capture also VERIFIES the surface (not just photographs it), AND a `covers`
82
+ * glob list declaring which changed source paths it is the evidence for (the
83
+ * fail-closed coverage map). Prefer role-based, copy-stable assertions.
84
+ *
85
+ * v1 ships ONE public surface (the home page) so the route works zero-auth out
86
+ * of the box. Replace it with your app's surfaces; add protected routes (they
87
+ * render under the storage-state session from the config) as your auth harness
88
+ * hardens.
89
+ */
90
+ const SURFACES: readonly UiSurface[] = [
91
+ {
92
+ path: "/",
93
+ slug: "home",
94
+ // EDIT: a heading name (role=heading, accessible name) YOUR home page must
95
+ // expose. Keep it a REAL assertion — a vacuous `/.+/` ("some heading
96
+ // exists") neither verifies the surface nor reliably renders.
97
+ requiredHeading: /Welcome/i,
98
+ // EDIT: the source globs THIS surface (the home page) ACTUALLY renders.
99
+ // Keep them SCOPED. A broad glob like `web/app/**` or `web/**/*.tsx` would
100
+ // claim the home surface is the evidence for your WHOLE app, so a change to
101
+ // any OTHER page (e.g. web/app/settings/page.tsx) would be wrongly "covered"
102
+ // by `/` and the route would PASS without ever rendering it — defeating the
103
+ // fail-closed coverage below. Add one SURFACES[] entry per real surface; let
104
+ // anything you have not scoped fall into `uncovered` and fail closed.
105
+ covers: ["web/app/page.tsx"],
106
+ },
107
+ ];
108
+
109
+ /**
110
+ * Changed UI paths that are NOT a renderable surface and so do not require a
111
+ * capture — the route's own producer harness/coverage lib, test scaffolding,
112
+ * type decls, styles, config. Excluding these keeps the fail-closed check
113
+ * honest: it fires for genuinely-uncovered *product* UI, not for the harness
114
+ * that implements the route. Anything NOT excluded and NOT covered by a surface
115
+ * blocks (fail closed) until a surface is added for it.
116
+ */
117
+ const NON_SURFACE_GLOBS: readonly string[] = [
118
+ "web/tests/**",
119
+ "web/playwright.ui-route.config.ts",
120
+ "web/playwright.config.ts",
121
+ "web/**/coverage.ts",
122
+ "web/**/ui-route.producer.spec.ts",
123
+ "web/**/*.test.ts",
124
+ "web/**/*.test.tsx",
125
+ "web/**/*.spec.ts",
126
+ "web/**/*.d.ts",
127
+ "web/**/*.css",
128
+ "web/**/*.json",
129
+ "web/**/*.md",
130
+ ];
131
+
132
+ const CHANGED_UI_PATHS = (process.env.DF_UI_ROUTE_CHANGED_PATHS ?? "")
133
+ .split(/\r?\n/)
134
+ .map((p) => p.trim())
135
+ .filter(Boolean);
136
+
137
+ const {
138
+ armed: ARMED_SURFACES,
139
+ uncovered: UNCOVERED_PATHS,
140
+ smokeAllSurfaces: SMOKE_ALL_SURFACES,
141
+ } = partitionChangedPaths(CHANGED_UI_PATHS, SURFACES, NON_SURFACE_GLOBS);
142
+
143
+ function routeOutDir(slug: string): string {
144
+ return join(OUT_DIR, slug);
145
+ }
146
+
147
+ /**
148
+ * Optional independent VLM check (DEFERRED in v1).
149
+ *
150
+ * Additive-only: it may push a soft finding but can NEVER turn a failed floor
151
+ * into a pass, nor is it required for the route to pass. Wired off by default;
152
+ * enable with DF_UI_ROUTE_VLM=1 once you wire a provider + cost ceiling.
153
+ * Intentionally surfaces its intent loudly rather than faking a check (which
154
+ * would be the exact reward-hack this feature prevents).
155
+ */
156
+ async function runOptionalVlmCheck(
157
+ _afterScreenshotPath: string,
158
+ _surface: UiSurface,
159
+ ): Promise<void> {
160
+ if (process.env.DF_UI_ROUTE_VLM !== "1") return;
161
+ test.info().annotations.push({
162
+ type: "vlm-check",
163
+ description:
164
+ "DF_UI_ROUTE_VLM=1 set but no VLM provider wired in this producer " +
165
+ "(v1 deferred). The ARIA + after floor is the gate; the VLM leg is " +
166
+ "additive corroboration only. No check performed.",
167
+ });
168
+ }
169
+
170
+ async function captureBefore(
171
+ page: Page,
172
+ surface: UiSurface,
173
+ dir: string,
174
+ ): Promise<void> {
175
+ if (!BEFORE_BASE_URL) return;
176
+ const beforeUrl = `${BEFORE_BASE_URL}${surface.path}`;
177
+ try {
178
+ await page.goto(beforeUrl, { waitUntil: "networkidle", timeout: 30_000 });
179
+ await page.screenshot({ path: join(dir, "before.png"), fullPage: true });
180
+ } catch (err) {
181
+ // The baseline is best-effort; a missing/unreachable baseline must not fail
182
+ // the route (the ARIA + after capture is the floor). Record why.
183
+ test.info().annotations.push({
184
+ type: "before-capture-skipped",
185
+ description: `baseline ${beforeUrl} unreachable: ${
186
+ err instanceof Error ? err.message : String(err)
187
+ }`,
188
+ });
189
+ }
190
+ }
191
+
192
+ // FAIL-CLOSED coverage gate: if the change touched a UI path that maps to NO
193
+ // capture surface, the route MUST block — it cannot be satisfied by rendering
194
+ // only an unaffected surface. This is the difference between an evidence gate
195
+ // and a false-positive generator. The fix is to ADD a surface (path +
196
+ // requiredHeading + covers glob) for the changed area, not to widen
197
+ // NON_SURFACE_GLOBS to hide it.
198
+ test("ui-route-coverage — every changed UI path maps to a capture surface", async () => {
199
+ if (UNCOVERED_PATHS.length > 0) {
200
+ throw new Error(
201
+ "Evidence-Gated Validation playwright route: the following changed UI " +
202
+ "path(s) have NO mapped capture surface, so this route cannot produce " +
203
+ "real evidence for them and FAILS CLOSED:\n" +
204
+ UNCOVERED_PATHS.map((p) => ` - ${p}`).join("\n") +
205
+ "\n\nAdd a surface for the affected area to SURFACES[] in " +
206
+ "ui-route.producer.spec.ts. Do NOT widen NON_SURFACE_GLOBS to suppress " +
207
+ "a real UI change — that re-opens the false-positive hole this gate closes.",
208
+ );
209
+ }
210
+ expect(UNCOVERED_PATHS).toEqual([]);
211
+ // Harness-only change → smoke-capture ALL surfaces (the route still produces
212
+ // real evidence; it is never passable with zero captures). Surfaced so the
213
+ // evidence log shows why every surface was captured.
214
+ if (SMOKE_ALL_SURFACES) {
215
+ test.info().annotations.push({
216
+ type: "ui-route-smoke",
217
+ description:
218
+ "Harness/non-UI-only change: capturing ALL surfaces as a regression " +
219
+ "smoke (no product-UI surface was directly armed). The route still " +
220
+ "produces ARIA + after evidence — it cannot pass with zero captures.",
221
+ });
222
+ }
223
+ });
224
+
225
+ for (const surface of ARMED_SURFACES) {
226
+ test(`UI route evidence — ${surface.slug} (${surface.path})`, async ({
227
+ page,
228
+ }) => {
229
+ const dir = routeOutDir(surface.slug);
230
+ mkdirSync(dir, { recursive: true });
231
+
232
+ // --- (optional) BEFORE: baseline screenshot from the configured origin ---
233
+ await captureBefore(page, surface, dir);
234
+
235
+ // --- AFTER: the SHA under review (the locally-served / configured app) ---
236
+ await page.goto(surface.path, { waitUntil: "networkidle", timeout: 30_000 });
237
+
238
+ // Floor assertion — the surface renders its required structure. A REAL gate:
239
+ // an over-claimed render that doesn't expose the heading FAILS here, blocking
240
+ // the route. (Role-based → robust to copy churn.)
241
+ await expect(
242
+ page.getByRole("heading", { name: surface.requiredHeading }),
243
+ ).toBeVisible({ timeout: 15_000 });
244
+
245
+ // Floor evidence #1 — ARIA snapshot (the accessibility tree). Structural,
246
+ // diff-able, hard to stage. Assert it is non-trivial, then persist it.
247
+ const ariaSnapshot = await page.locator("body").ariaSnapshot();
248
+ expect(ariaSnapshot.trim().length).toBeGreaterThan(0);
249
+ writeFileSync(join(dir, "aria.snapshot.txt"), `${ariaSnapshot}\n`, "utf8");
250
+
251
+ // Floor evidence #2 — the "after" screenshot bound to the gated SHA.
252
+ const afterPath = join(dir, "after.png");
253
+ await page.screenshot({ path: afterPath, fullPage: true });
254
+
255
+ // Per-surface metadata (the human-reviewable provenance of this capture).
256
+ writeFileSync(
257
+ join(dir, "meta.json"),
258
+ `${JSON.stringify(
259
+ {
260
+ surface: surface.slug,
261
+ path: surface.path,
262
+ sha: SHA,
263
+ capturedAt: new Date().toISOString(),
264
+ baseURL: test.info().project.use.baseURL ?? null,
265
+ beforeBaseURL: BEFORE_BASE_URL ?? null,
266
+ ariaSnapshotBytes: Buffer.byteLength(ariaSnapshot, "utf8"),
267
+ evidenceKind: "playwright",
268
+ floor: ["aria-snapshot", "after-screenshot"],
269
+ beforeScreenshot: BEFORE_BASE_URL ? "before.png" : null,
270
+ vlm: "deferred-v1",
271
+ },
272
+ null,
273
+ 2,
274
+ )}\n`,
275
+ "utf8",
276
+ );
277
+
278
+ // --- optional, additive-only VLM corroboration (deferred in v1) ---
279
+ await runOptionalVlmCheck(afterPath, surface);
280
+ });
281
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "../skill-schema.json",
3
+ "name": "verify",
4
+ "version": "1.0.0",
5
+ "summary": "Produce evidence-gated validation-route evidence for a change before pushing — drive `df verify` over the armed verification routes (terraform/helm/docker/targeted-test/UI), writing per-SHA, diffHash-bound QualityGateEvidence the local pre-push gate asserts.",
6
+ "originatingRepo": "momentiq-ai/dark-factory-platform",
7
+ "files": [
8
+ { "template": "SKILL.md.tmpl", "target": "SKILL.md" }
9
+ ],
10
+ "variables": {
11
+ "REPO_NAME": {
12
+ "description": "Display name of the consumer repo (e.g. 'Dark Factory Platform').",
13
+ "source": "darkfactory.yaml: repo.displayName",
14
+ "default": "this repo"
15
+ },
16
+ "ARTIFACT_DIR": {
17
+ "description": "Display path to the per-SHA quality-gate artifact dir (where `<sha>.json` evidence lands), used in the skill's example commands. The real value is resolved at runtime from .agent-review/config.json:git.artifactDir under the git common dir; this is the default for the docs.",
18
+ "source": "fixed default (the canonical .agent-review/config.json: git.artifactDir resolution)",
19
+ "default": ".git/agent-reviews"
20
+ }
21
+ }
22
+ }