|
32 | 32 | * their sources ("watch hints"). That derivation is honest but heuristic: |
33 | 33 | * |
34 | 34 | * - a MATCHED check is one whose own source names a directory/file that |
35 | | - * covers the input path — high-signal, paste it into the dispatch prompt; |
| 35 | + * covers the input path — high-signal, paste it into the dispatch prompt. |
| 36 | + * It is printed as the RUNNABLE invocation (`pnpm --filter <pkg> run |
| 37 | + * check:x` for a package-scoped gate, `pnpm check:x` for a root-scoped |
| 38 | + * one), not as the bare script name: the bare name sends a dev to the root |
| 39 | + * `package.json`, where a package-scoped gate is absent and therefore reads |
| 40 | + * as nonexistent (#7440); |
36 | 41 | * - a check with NO discoverable path hints is listed once in the |
37 | 42 | * "repo-wide / undetermined" bucket. It is NOT known to be irrelevant — |
38 | 43 | * many gates read the whole tree (check:nul-bytes) or a convention rather |
@@ -99,6 +104,22 @@ export function extractWatchHints(scriptSource) { |
99 | 104 | return [...hints]; |
100 | 105 | } |
101 | 106 |
|
| 107 | +/** |
| 108 | + * Render an invocation a dev can paste and run, from the same parse the |
| 109 | + * workflow line produced. The script NAME alone is not runnable for a |
| 110 | + * package-scoped check: `check:doc-formula-expressions` lives in |
| 111 | + * `@objectstack/lint`, not in the root `package.json`, so a dev who searched |
| 112 | + * the obvious place found nothing and concluded the gate did not exist — twice, |
| 113 | + * in independent sessions, within one hour (#7440, PR #7416 / #7417). The |
| 114 | + * `--filter` package is the one piece of provenance this tool parsed and then |
| 115 | + * dropped, and it is the piece needed to run the thing. |
| 116 | + */ |
| 117 | +export function runnableInvocation({ check, filter, direct }) { |
| 118 | + if (direct) return `node ${check}`; // already a script path, never a pnpm script |
| 119 | + if (filter) return `pnpm --filter ${filter} run ${check}`; |
| 120 | + return `pnpm ${check}`; |
| 121 | +} |
| 122 | + |
102 | 123 | /** |
103 | 124 | * Does a watch hint cover an input path? Prefix either way, with globs |
104 | 125 | * collapsed. A hint that collapses to a bare top-level directory name |
@@ -175,7 +196,7 @@ function derive(paths) { |
175 | 196 | console.log('Local gates for this card (paste into the dispatch prompt):'); |
176 | 197 | for (const [check, { entry, hits }] of [...matched].sort()) { |
177 | 198 | const via = hits.map((h) => `${h.path} ⇢ '${h.hint}'`).join('; '); |
178 | | - console.log(` - ${check} [${[...entry.workflows].join(', ')}] matched via ${via}`); |
| 199 | + console.log(` - ${runnableInvocation(entry)} [${[...entry.workflows].join(', ')}] matched via ${via}`); |
179 | 200 | } |
180 | 201 | } else { |
181 | 202 | console.log('No check family names the given paths in its own source.'); |
@@ -213,6 +234,14 @@ function selfTest() { |
213 | 234 | t('extracts direct node scripts/check-*.mjs', invs.some((i) => i.check === 'scripts/check-nul-bytes.mjs' && i.direct)); |
214 | 235 | t('ignores non-check runs', !invs.some((i) => String(i.check).includes('build'))); |
215 | 236 |
|
| 237 | + // #7440: the printed line must be runnable as-is. The three shapes come from |
| 238 | + // the same three fixtures above, so the sample workflow and the print site |
| 239 | + // cannot drift apart. |
| 240 | + const inv = (name) => invs.find((i) => i.check === name); |
| 241 | + t('prints a package-scoped check as its full --filter invocation', runnableInvocation(inv('check:authorable-surface')) === 'pnpm --filter @objectstack/spec run check:authorable-surface'); |
| 242 | + t('prints a root-scoped check unchanged', runnableInvocation(inv('check:engine-double-contract')) === 'pnpm check:engine-double-contract'); |
| 243 | + t('prints a direct script as a node invocation', runnableInvocation(inv('scripts/check-nul-bytes.mjs')) === 'node scripts/check-nul-bytes.mjs'); |
| 244 | + |
216 | 245 | const scripts = { 'check:foo': 'node scripts/check-foo.mjs --self-test && node scripts/check-foo.mjs' }; |
217 | 246 | t('resolves script file from package.json', resolveCheckToFiles('check:foo', scripts).join() === 'scripts/check-foo.mjs'); |
218 | 247 | t('unknown check resolves to nothing', resolveCheckToFiles('check:bar', scripts).length === 0); |
|
0 commit comments