create-cmp-cli 0.23.0 → 0.25.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/bin/create-cmp.mjs +33 -3
- package/package.json +10 -4
- package/packages/harness/package.json +18 -8
- package/packages/harness/src/approve.mjs +46 -18
- package/packages/harness/src/console/console-data.mjs +117 -0
- package/packages/harness/src/console/console-evidence.mjs +122 -0
- package/packages/harness/src/console/console-overview.mjs +642 -0
- package/packages/harness/src/console/console-shell.mjs +1139 -0
- package/packages/harness/src/console/console-tabs.mjs +2828 -0
- package/packages/harness/src/console/contrast.mjs +74 -0
- package/packages/harness/src/console/preview-service.mjs +1384 -0
- package/packages/harness/src/framework-check.mjs +747 -0
- package/packages/harness/src/lib/a11y.mjs +1 -1
- package/packages/harness/src/lib/affected-tests.mjs +99 -44
- package/packages/harness/src/lib/agent-hold.mjs +234 -0
- package/packages/harness/src/lib/approvals.mjs +177 -350
- package/packages/harness/src/lib/audit-cadence.mjs +1 -1
- package/packages/harness/src/lib/comments.mjs +18 -4
- package/packages/harness/src/lib/determinism.mjs +275 -14
- package/packages/harness/src/lib/evidence-badge.mjs +38 -6
- package/packages/harness/src/lib/evidence-ladder.mjs +202 -0
- package/packages/harness/src/lib/evidence-level.mjs +144 -59
- package/packages/harness/src/lib/feature-brief.mjs +53 -16
- package/packages/harness/src/lib/flight-recorder.mjs +72 -8
- package/packages/harness/src/lib/framework-check.mjs +778 -0
- package/packages/harness/src/lib/harness-lock.mjs +87 -19
- package/packages/harness/src/lib/harness-manifest.mjs +208 -0
- package/packages/harness/src/lib/harness-region.mjs +106 -2
- package/packages/harness/src/lib/harness-source.mjs +142 -0
- package/packages/harness/src/lib/inputs-hash.mjs +180 -22
- package/packages/harness/src/lib/lane-markers.mjs +78 -0
- package/packages/harness/src/lib/lane-narrator.mjs +4 -2
- package/packages/harness/src/lib/lane-runner.mjs +72 -6
- package/packages/harness/src/lib/plan.mjs +44 -11
- package/packages/harness/src/lib/plant-calibration.mjs +135 -0
- package/packages/harness/src/lib/profile-loader.mjs +323 -0
- package/packages/harness/src/lib/profiles/cmp/affected.mjs +70 -0
- package/packages/harness/src/lib/profiles/cmp/android-checks.mjs +56 -0
- package/packages/harness/src/lib/profiles/cmp/artifacts.mjs +381 -0
- package/packages/harness/src/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
- package/packages/harness/src/lib/profiles/cmp/console-copy.mjs +29 -0
- package/packages/harness/src/lib/profiles/cmp/declarations.mjs +181 -0
- package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
- package/packages/harness/src/lib/profiles/cmp/index.mjs +47 -0
- package/packages/harness/src/lib/profiles/cmp/ladder.mjs +67 -0
- package/packages/harness/src/lib/profiles/cmp/maestro.mjs +113 -0
- package/packages/harness/src/lib/profiles/cmp/plants.mjs +95 -0
- package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/reachability.mjs +1 -1
- package/packages/harness/src/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +230 -28
- package/packages/harness/src/lib/receipt-validate.mjs +84 -16
- package/packages/harness/src/lib/spec-coverage.mjs +179 -92
- package/packages/harness/src/lib/spec-model.mjs +262 -0
- package/packages/harness/src/lib/step-cache.mjs +11 -2
- package/packages/harness/src/lib/step-outcomes.mjs +84 -151
- package/packages/harness/src/lib/walk.mjs +1 -1
- package/packages/harness/src/plan.mjs +30 -1
- package/packages/harness/src/preview-gallery.mjs +1 -1
- package/packages/harness/src/receipt-check.mjs +95 -26
- package/packages/harness/src/record-audit.mjs +1 -1
- package/packages/harness/src/scaffold-feature.mjs +10 -2
- package/packages/harness/src/verify.mjs +222 -142
- package/packages/harness/src/watch.mjs +146 -33
- package/packages/receipts/package.json +1 -1
- package/packages/receipts/src/index.mjs +2 -2
- package/packages/receipts/src/inputs-hash.mjs +180 -22
- package/packages/receipts/src/receipt-validate.mjs +84 -16
- package/src/commands/attach.mjs +128 -4
- package/src/commands/harden.mjs +7 -1
- package/src/commands/upgrade.mjs +7 -0
- package/src/lib/minimal.mjs +23 -1
- package/src/scaffold.mjs +8 -0
- package/template/.claude/settings.json +2 -2
- package/template/.claude/skills/add-feature/SKILL.md +7 -3
- package/template/.claude/skills/add-repository/SKILL.md +2 -2
- package/template/.claude/skills/add-screen/SKILL.md +8 -4
- package/template/.githooks/pre-push +9 -0
- package/template/CLAUDE.md +55 -186
- package/template/docs/TESTING.md +1 -1
- package/template/gitignore +4 -0
- package/template/qa/approve.mjs +46 -18
- package/template/qa/comments.json +1 -1
- package/template/qa/evidence/schema.json +258 -34
- package/template/qa/framework-check.mjs +747 -0
- package/template/qa/harness-manifest.json +10 -0
- package/template/qa/lib/a11y.mjs +1 -1
- package/template/qa/lib/affected-tests.mjs +99 -44
- package/template/qa/lib/agent-hold.mjs +234 -0
- package/template/qa/lib/approvals.mjs +177 -350
- package/template/qa/lib/audit-cadence.mjs +1 -1
- package/template/qa/lib/comments.mjs +18 -4
- package/template/qa/lib/determinism.mjs +275 -14
- package/template/qa/lib/evidence-badge.mjs +38 -6
- package/template/qa/lib/evidence-ladder.mjs +202 -0
- package/template/qa/lib/evidence-level.mjs +144 -59
- package/template/qa/lib/feature-brief.mjs +53 -16
- package/template/qa/lib/flight-recorder.mjs +72 -8
- package/template/qa/lib/framework-check.mjs +778 -0
- package/template/qa/lib/harness-lock.mjs +87 -19
- package/template/qa/lib/harness-manifest.mjs +208 -0
- package/template/qa/lib/harness-region.mjs +106 -2
- package/template/qa/lib/harness-source.mjs +142 -0
- package/template/qa/lib/inputs-hash.mjs +180 -22
- package/template/qa/lib/lane-markers.mjs +78 -0
- package/template/qa/lib/lane-narrator.mjs +4 -2
- package/template/qa/lib/lane-runner.mjs +72 -6
- package/template/qa/lib/plan.mjs +44 -11
- package/template/qa/lib/plant-calibration.mjs +135 -0
- package/template/qa/lib/profile-loader.mjs +323 -0
- package/template/qa/lib/profiles/cmp/affected.mjs +70 -0
- package/template/qa/lib/profiles/cmp/android-checks.mjs +56 -0
- package/template/qa/lib/profiles/cmp/artifacts.mjs +381 -0
- package/template/qa/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
- package/template/qa/lib/profiles/cmp/console-copy.mjs +29 -0
- package/template/qa/lib/profiles/cmp/declarations.mjs +181 -0
- package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
- package/template/qa/lib/profiles/cmp/index.mjs +47 -0
- package/template/qa/lib/profiles/cmp/ladder.mjs +67 -0
- package/template/qa/lib/profiles/cmp/maestro.mjs +113 -0
- package/template/qa/lib/profiles/cmp/plants.mjs +95 -0
- package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/reachability.mjs +1 -1
- package/template/qa/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +230 -28
- package/template/qa/lib/receipt-validate.mjs +84 -16
- package/template/qa/lib/spec-coverage.mjs +179 -92
- package/template/qa/lib/spec-model.mjs +262 -0
- package/template/qa/lib/step-cache.mjs +11 -2
- package/template/qa/lib/step-outcomes.mjs +84 -151
- package/template/qa/lib/walk.mjs +1 -1
- package/template/qa/plan.mjs +30 -1
- package/template/qa/preview-gallery.mjs +1 -1
- package/template/qa/receipt-check.mjs +95 -26
- package/template/qa/record-audit.mjs +1 -1
- package/template/qa/scaffold-feature.mjs +10 -2
- package/template/qa/verified-surface.json +17 -0
- package/template/qa/verify.mjs +222 -142
- package/template/qa/watch.mjs +146 -33
- /package/packages/harness/src/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
- /package/packages/harness/src/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
- /package/packages/harness/src/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
- /package/packages/harness/src/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
- /package/packages/harness/src/lib/{tree.mjs → profiles/cmp/tree.mjs} +0 -0
- /package/template/qa/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
- /package/template/qa/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
- /package/template/qa/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
- /package/template/qa/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
- /package/template/qa/lib/{tree.mjs → profiles/cmp/tree.mjs} +0 -0
package/template/qa/watch.mjs
CHANGED
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
// COORDINATION (non-negotiable): two concurrent Gradle invocations against one
|
|
27
27
|
// project corrupt each other's output (KSP cache collisions, half-written
|
|
28
28
|
// classes dirs — a real 20+ bogus-failure cascade). The lane and the preview
|
|
29
|
-
// daemon already coordinate via marker files
|
|
30
|
-
//
|
|
29
|
+
// daemon already coordinate via marker files:
|
|
30
|
+
// qa/.lane-in-progress — stamped by verify.mjs for a run's duration (core
|
|
31
|
+
// state; qa/lib/lane-markers.mjs)
|
|
31
32
|
// .cmp-render-in-progress — stamped by the preview daemon while its Gradle
|
|
32
33
|
// build is in flight
|
|
33
34
|
// Watch mode participates as a third citizen: it never launches a run while a
|
|
@@ -51,17 +52,20 @@ import fs from "node:fs";
|
|
|
51
52
|
import path from "node:path";
|
|
52
53
|
import { fileURLToPath } from "node:url";
|
|
53
54
|
|
|
55
|
+
import { LANE_MARKER_REL, LANE_MARKER_STALE_MS, RENDER_MARKER_FRESH_MS, laneMarkerPath, renderMarkerPath } from "./lib/lane-markers.mjs";
|
|
56
|
+
import { resolveSpecModel } from "./lib/spec-model.mjs";
|
|
57
|
+
|
|
54
58
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
55
59
|
|
|
56
60
|
// ── CLI ─────────────────────────────────────────────────────────────────────
|
|
57
61
|
|
|
58
62
|
export const USAGE = `node qa/watch.mjs [--once] [--json] [--help]
|
|
59
63
|
|
|
60
|
-
Resident watch mode — the inner verification loop. Watches
|
|
61
|
-
specs/, and qa/ and runs \`node qa/verify.mjs --fast\` on every save (debounced;
|
|
64
|
+
Resident watch mode — the inner verification loop. Watches the project's
|
|
65
|
+
source roots, specs/, and qa/ and runs \`node qa/verify.mjs --fast\` on every save (debounced;
|
|
62
66
|
a save storm triggers ONE run, changes during a run coalesce into one
|
|
63
67
|
follow-up). It defers while a verify lane or a preview-daemon render holds the
|
|
64
|
-
project (
|
|
68
|
+
project (qa/.lane-in-progress and the eyes' own render marker), so two build
|
|
65
69
|
invocations never collide.
|
|
66
70
|
|
|
67
71
|
THIS IS NOT A GATE. It runs the fast tier only: every receipt records
|
|
@@ -93,23 +97,106 @@ export function parseWatchArgs(rawArgs) {
|
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
// ── The watch set ───────────────────────────────────────────────────────────
|
|
96
|
-
//
|
|
97
|
-
// a golden-tree or
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
100
|
+
// The profile's source roots (the app), its specs directory (the contract),
|
|
101
|
+
// and qa/ (the harness itself — a golden-tree or flow edit should re-verify
|
|
102
|
+
// too). NOT watched: the build output directory the PROFILE declares, at any
|
|
103
|
+
// depth (see shouldIgnorePath); qa/evidence/ (verify.mjs writes latest.json
|
|
104
|
+
// there on every run — watching your own output is an infinite loop); and
|
|
105
|
+
// dotfiles (VCS internals, editor droppings, and the in-progress markers
|
|
106
|
+
// themselves).
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The trees this loop watches: the profile's own source roots and specs
|
|
110
|
+
* directory, plus qa/ (the harness judging itself — always core).
|
|
111
|
+
*
|
|
112
|
+
* Stage 0 PR 6b: this was `["composeApp/src", "specs", "qa"]`, so the inner
|
|
113
|
+
* loop in a repo whose code lives anywhere else watched two directories out of
|
|
114
|
+
* three and never fired on a source edit — a watcher that looks idle and is.
|
|
115
|
+
* A project with no usable manifest gets the core roots alone and the startup
|
|
116
|
+
* banner says so, rather than guessing a source layout.
|
|
117
|
+
* @param {string} root
|
|
118
|
+
* @returns {{roots: string[], degraded: string|null}}
|
|
119
|
+
*/
|
|
120
|
+
export function watchRoots(root) {
|
|
121
|
+
const model = resolveSpecModel(root);
|
|
122
|
+
if (!model.ok) {
|
|
123
|
+
return { roots: ["specs", "qa"], degraded: model.reason };
|
|
124
|
+
}
|
|
125
|
+
return { roots: [...new Set([...model.model.sourceRoots, model.model.specsDir, "qa"])], degraded: null };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Build output: the profile's word, not one stack's ───────────────────────
|
|
129
|
+
// The ignore set's whole job is to keep the loop from watching its own output.
|
|
130
|
+
// It used to find that output by the single literal directory name `build`,
|
|
131
|
+
// which is Gradle's — Rust writes `target/`, .NET `bin/` and `obj/`, Python
|
|
132
|
+
// `__pycache__`, Xcode `DerivedData`. Both halves were wrong, and both
|
|
133
|
+
// silently, with a watcher that looked like it was working:
|
|
134
|
+
//
|
|
135
|
+
// on those stacks the lane's own writes woke the watcher, which ran the lane
|
|
136
|
+
// again — a feedback loop, in the one process whose entire purpose is to fire
|
|
137
|
+
// on a human's edit
|
|
138
|
+
// and on EVERY stack a real source directory named `build/` (a build script
|
|
139
|
+
// package, a `build/` module, Python's `app/build/`) was never watched at
|
|
140
|
+
// all — the "watcher that looks idle and is" that Stage 0 PR 6b already
|
|
141
|
+
// fixed once for source ROOTS and left in place here
|
|
142
|
+
//
|
|
143
|
+
// The profile declares `layout.buildDir` (qa/lib/spec-model.mjs), so the name
|
|
144
|
+
// is available and was simply not asked for.
|
|
145
|
+
//
|
|
146
|
+
// THE DECLARED DIRECTORY'S NAME MATCHES AT ANY DEPTH, not just the one declared
|
|
147
|
+
// path. A profile declares its app module's output, but a multi-module tree has
|
|
148
|
+
// one such directory PER MODULE (Gradle writes one into every module; the
|
|
149
|
+
// harness's own qa/ subtrees get one too — see test/watch-loop.test.mjs's
|
|
150
|
+
// `qa/somewhere/build/out.json`). Ignoring only the declared path would re-open
|
|
151
|
+
// the feedback loop one directory over. Only that name matches, so a stack that
|
|
152
|
+
// calls its output `target` starts watching `build/` as the ordinary source it
|
|
153
|
+
// is there.
|
|
154
|
+
//
|
|
155
|
+
// That rule is deliberately BROADER than the declaration, and the price is
|
|
156
|
+
// named here rather than discovered: a profile declaring a nested `buildDir`
|
|
157
|
+
// gets its LEAF name ignored everywhere (`build/outputs` ignores every
|
|
158
|
+
// `outputs/` in the tree, not just that one). The startup banner prints the
|
|
159
|
+
// glob it settled on for exactly this reason — an ignore set a reader can see
|
|
160
|
+
// is one they can correct in `layout.buildDir`.
|
|
101
161
|
|
|
102
|
-
|
|
162
|
+
/**
|
|
163
|
+
* The historical name, kept ONLY for a project whose profile could not be
|
|
164
|
+
* resolved or which declares no `layout.buildDir`. It is a FALLBACK and not a
|
|
165
|
+
* default in the approving sense — the startup banner names it, exactly as
|
|
166
|
+
* spec-model.mjs's `DEFAULT_GRAMMAR.isDefault` makes the borrowed citation
|
|
167
|
+
* grammar say so out loud. The two failures are not symmetric: watching your
|
|
168
|
+
* own output loops forever, while missing a source directory named `build/`
|
|
169
|
+
* costs one manual run. When we know nothing, we take the cheaper mistake.
|
|
170
|
+
*/
|
|
171
|
+
export const FALLBACK_BUILD_OUTPUT_NAME = "build";
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The directory NAMES this project's build output wears, from the profile's
|
|
175
|
+
* declaration. `composeApp/build` → ["build"]; `target` → ["target"].
|
|
176
|
+
* @param {{buildDir?: string|null}|null} [model] a SpecModel, or null when none resolved
|
|
177
|
+
* @returns {string[]}
|
|
178
|
+
*/
|
|
179
|
+
export function buildOutputNames(model) {
|
|
180
|
+
const declared = typeof model?.buildDir === "string" ? model.buildDir : "";
|
|
181
|
+
const name = declared.replace(/\\/g, "/").split("/").filter(Boolean).pop();
|
|
182
|
+
return name ? [name] : [FALLBACK_BUILD_OUTPUT_NAME];
|
|
183
|
+
}
|
|
103
184
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Ignore predicate over a ROOT-relative path (forward slashes or backslashes).
|
|
187
|
+
* @param {string} rel
|
|
188
|
+
* @param {{buildDir?: string|null}|null} [model] the project's SpecModel — supplies the
|
|
189
|
+
* build output name. Omitted/null falls back to FALLBACK_BUILD_OUTPUT_NAME.
|
|
190
|
+
*/
|
|
191
|
+
export function shouldIgnorePath(rel, model = null) {
|
|
106
192
|
const norm = String(rel).replace(/\\/g, "/");
|
|
107
193
|
if (!norm) return true;
|
|
108
194
|
const parts = norm.split("/");
|
|
109
|
-
// Any dotted segment: .git, .gradle, .DS_Store, .
|
|
195
|
+
// Any dotted segment: .git, .gradle, .DS_Store, .lane-in-progress, …
|
|
110
196
|
if (parts.some((s) => s.startsWith("."))) return true;
|
|
111
|
-
//
|
|
112
|
-
|
|
197
|
+
// This profile's build output, at any depth (see the note above).
|
|
198
|
+
const buildNames = buildOutputNames(model);
|
|
199
|
+
if (parts.some((s) => buildNames.includes(s))) return true;
|
|
113
200
|
// The lane's own output — the one path that would make watch feed itself.
|
|
114
201
|
if (norm === "qa/evidence" || norm.startsWith("qa/evidence/")) return true;
|
|
115
202
|
return false;
|
|
@@ -133,10 +220,11 @@ export const POLL_MS = 2000; // marker-wait poll AND the no-recursive-watch fall
|
|
|
133
220
|
// around its Gradle builds. Freshness is mtime-bounded so a crashed stamper
|
|
134
221
|
// never wedges us.
|
|
135
222
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
223
|
+
// Paths and bounds are qa/lib/lane-markers.mjs's — the lane marker is core state
|
|
224
|
+
// under qa/, the render marker is the profile's (under its buildDir, or none).
|
|
225
|
+
// Re-exported because markerDecision's bounds are part of this module's
|
|
226
|
+
// contract (qa/watch.mjs --json consumers and the engine suite read them).
|
|
227
|
+
export { LANE_MARKER_STALE_MS, RENDER_MARKER_FRESH_MS };
|
|
140
228
|
|
|
141
229
|
/**
|
|
142
230
|
* The launch decision, pure: given the two markers' mtimes (null = absent) and
|
|
@@ -145,10 +233,10 @@ export const RENDER_MARKER_FRESH_MS = 5 * 60 * 1000; // verify.mjs's bound for t
|
|
|
145
233
|
*/
|
|
146
234
|
export function markerDecision({ laneMtimeMs = null, renderMtimeMs = null, nowMs = Date.now() } = {}) {
|
|
147
235
|
if (laneMtimeMs != null && nowMs - laneMtimeMs < LANE_MARKER_STALE_MS) {
|
|
148
|
-
return { launch: false, reason:
|
|
236
|
+
return { launch: false, reason: `a verify lane is in progress (${LANE_MARKER_REL} is fresh) — deferring; changes coalesce into one run when it finishes` };
|
|
149
237
|
}
|
|
150
238
|
if (renderMtimeMs != null && nowMs - renderMtimeMs < RENDER_MARKER_FRESH_MS) {
|
|
151
|
-
return { launch: false, reason: "the preview daemon has a
|
|
239
|
+
return { launch: false, reason: "the preview daemon has a build in flight (.cmp-render-in-progress is fresh) — deferring; changes coalesce into one run when it finishes" };
|
|
152
240
|
}
|
|
153
241
|
return { launch: true };
|
|
154
242
|
}
|
|
@@ -304,8 +392,8 @@ function markerMtime(absPath) {
|
|
|
304
392
|
|
|
305
393
|
function launchDecisionNow() {
|
|
306
394
|
return markerDecision({
|
|
307
|
-
laneMtimeMs: markerMtime(
|
|
308
|
-
renderMtimeMs: markerMtime(
|
|
395
|
+
laneMtimeMs: markerMtime(laneMarkerPath(ROOT)),
|
|
396
|
+
renderMtimeMs: markerMtime(renderMarkerPath(ROOT)),
|
|
309
397
|
});
|
|
310
398
|
}
|
|
311
399
|
|
|
@@ -383,7 +471,7 @@ function main() {
|
|
|
383
471
|
currentChild = null;
|
|
384
472
|
// A signal-killed child never ran verify's `finally` — clean the lane
|
|
385
473
|
// marker it stamped so nothing defers on a ghost for 30 minutes.
|
|
386
|
-
if (signal) clearMarkerIfOwnedBy(
|
|
474
|
+
if (signal) clearMarkerIfOwnedBy(laneMarkerPath(ROOT), child.pid);
|
|
387
475
|
const durationMs = Date.now() - started;
|
|
388
476
|
const receipt = parseReceipt(stdout);
|
|
389
477
|
const rawTail = `${stdout}\n${stderr}`.split("\n").filter(Boolean);
|
|
@@ -453,7 +541,18 @@ function main() {
|
|
|
453
541
|
|
|
454
542
|
const watchers = [];
|
|
455
543
|
const pollTimers = [];
|
|
456
|
-
const
|
|
544
|
+
const { roots: declaredRoots, degraded: rootsDegraded } = watchRoots(ROOT);
|
|
545
|
+
const watchedRoots = declaredRoots.filter((rel) => fs.existsSync(path.join(ROOT, rel)));
|
|
546
|
+
// The ignore set is this profile's too: WHICH directory holds build output is
|
|
547
|
+
// a stack fact (`layout.buildDir`), and a watcher that guesses it either
|
|
548
|
+
// feeds itself or skips real source. resolveSpecModel shares the module cache
|
|
549
|
+
// with watchRoots's own call, so this is a map lookup, not a second load.
|
|
550
|
+
const specModel = (() => {
|
|
551
|
+
const r = resolveSpecModel(ROOT);
|
|
552
|
+
return r.ok ? r.model : null;
|
|
553
|
+
})();
|
|
554
|
+
const ignoredBuildNames = buildOutputNames(specModel);
|
|
555
|
+
const ignorePath = (rel) => shouldIgnorePath(rel, specModel);
|
|
457
556
|
|
|
458
557
|
// Poll fallback for platforms without recursive fs.watch: a full mtime scan
|
|
459
558
|
// per tick, diffed against the previous one so changed paths still get
|
|
@@ -470,7 +569,7 @@ function main() {
|
|
|
470
569
|
}
|
|
471
570
|
for (const e of entries) {
|
|
472
571
|
const rel = `${dirRel}/${e.name}`;
|
|
473
|
-
if (
|
|
572
|
+
if (ignorePath(rel)) continue;
|
|
474
573
|
const abs = path.join(dirAbs, e.name);
|
|
475
574
|
if (e.isDirectory()) walk(abs, rel);
|
|
476
575
|
else {
|
|
@@ -511,7 +610,7 @@ function main() {
|
|
|
511
610
|
try {
|
|
512
611
|
const w = fs.watch(path.join(ROOT, rootRel), { recursive: true }, (_event, filename) => {
|
|
513
612
|
const rel = filename ? `${rootRel}/${String(filename).replace(/\\/g, "/")}` : rootRel;
|
|
514
|
-
if (
|
|
613
|
+
if (ignorePath(rel)) return;
|
|
515
614
|
loop.change(rel);
|
|
516
615
|
});
|
|
517
616
|
w.on("error", () => {
|
|
@@ -531,9 +630,23 @@ function main() {
|
|
|
531
630
|
}
|
|
532
631
|
|
|
533
632
|
// Startup: what is watched, what is respected, what this is NOT.
|
|
633
|
+
// The ignore globs are PRINTED, never assumed: they name the directory this
|
|
634
|
+
// profile declared, so a reader can see at a glance that the loop is ignoring
|
|
635
|
+
// this stack's output and not some other stack's word for it.
|
|
636
|
+
const ignoreGlobs = [...ignoredBuildNames.map((n) => `**/${n}/**`), "qa/evidence/**", "dotfiles"];
|
|
534
637
|
say("qa/watch.mjs — resident inner loop: runs `node qa/verify.mjs --fast` on save");
|
|
535
|
-
say(`watching: ${watchedRoots.join(", ")} (ignoring
|
|
536
|
-
|
|
638
|
+
say(`watching: ${watchedRoots.join(", ")} (ignoring ${ignoreGlobs.join(", ")})`);
|
|
639
|
+
// Never watch a guessed layout silently: if the manifest could not be read,
|
|
640
|
+
// the source roots are unknown and this loop is watching less than it looks.
|
|
641
|
+
if (rootsDegraded) say(`NOTE: watching the core roots only — ${rootsDegraded}`);
|
|
642
|
+
// And never ignore a guessed layout silently either. A profile that declares
|
|
643
|
+
// no layout.buildDir leaves this loop on FALLBACK_BUILD_OUTPUT_NAME — one
|
|
644
|
+
// stack's convention, applied to a stack that never claimed it — so it says
|
|
645
|
+
// so, the way a borrowed citation grammar does (spec-model.mjs).
|
|
646
|
+
else if (specModel && typeof specModel.buildDir !== "string") {
|
|
647
|
+
say(`NOTE: this profile declares no layout.buildDir — ignoring **/${FALLBACK_BUILD_OUTPUT_NAME}/** as a fallback, which may be this project's real source`);
|
|
648
|
+
}
|
|
649
|
+
say(`coordination: defers while ${LANE_MARKER_REL} or the eyes' render marker is fresh — never two builds against this project`);
|
|
537
650
|
say(`debounce: ${DEBOUNCE_MS}ms — a save storm triggers one run; changes during a run coalesce into one follow-up`);
|
|
538
651
|
say(FOOTER);
|
|
539
652
|
say("waiting for changes… (Ctrl-C to stop · --once for a single pass · --json for line-per-run output)");
|
|
@@ -541,9 +654,9 @@ function main() {
|
|
|
541
654
|
event: "start",
|
|
542
655
|
pid: process.pid,
|
|
543
656
|
watching: watchedRoots,
|
|
544
|
-
ignoring:
|
|
657
|
+
ignoring: ignoreGlobs,
|
|
545
658
|
debounceMs: DEBOUNCE_MS,
|
|
546
|
-
coordinates: [LANE_MARKER_REL.
|
|
659
|
+
coordinates: [LANE_MARKER_REL, renderMarkerPath(ROOT) ? path.relative(ROOT, renderMarkerPath(ROOT)).split(path.sep).join("/") : null].filter(Boolean),
|
|
547
660
|
runs: "node qa/verify.mjs --fast",
|
|
548
661
|
note: FOOTER,
|
|
549
662
|
});
|
|
@@ -598,7 +711,7 @@ function installSignalHandlers(stopWork, getChild, emit, say) {
|
|
|
598
711
|
signalTree("SIGKILL");
|
|
599
712
|
await Promise.race([exited, sleep(1000)]);
|
|
600
713
|
}
|
|
601
|
-
clearMarkerIfOwnedBy(
|
|
714
|
+
clearMarkerIfOwnedBy(laneMarkerPath(ROOT), child.pid);
|
|
602
715
|
}
|
|
603
716
|
say(`watch mode stopped (${sig}) — no receipt was made valid by watching; the done-gate is still one full \`node qa/verify.mjs\` run`);
|
|
604
717
|
emit({ event: "shutdown", reason: sig });
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|