@adhdev/daemon-core 0.9.82-rc.486 → 0.9.82-rc.487
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/git/git-status.d.ts +23 -0
- package/dist/index.js +98 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +98 -29
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-refine-gates.d.ts +29 -0
- package/package.json +3 -3
- package/src/commands/router-refine.ts +27 -2
- package/src/git/git-status.ts +84 -29
- package/src/mesh/mesh-refine-gates.ts +113 -7
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* keep working. `CommandRouterResult` is imported type-only from router.ts
|
|
12
12
|
* (erased at compile time — no runtime import cycle).
|
|
13
13
|
*/
|
|
14
|
+
import type { ChangedPackageClassification } from '../git/git-status.js';
|
|
14
15
|
import type { WorktreeBootstrapState } from '../mesh/worktree-bootstrap-config.js';
|
|
15
16
|
import type { CommandRouterResult } from '../commands/router.js';
|
|
16
17
|
export type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
@@ -53,6 +54,19 @@ type MeshRefineValidationSummary = {
|
|
|
53
54
|
};
|
|
54
55
|
/** M2-2: deprecation notices from the refine config (e.g. bootstrapCommands). */
|
|
55
56
|
deprecationWarnings?: string[];
|
|
57
|
+
/**
|
|
58
|
+
* Coarse daemon-vs-web change-impact used to scope the validation command set.
|
|
59
|
+
* When `isDaemonAffecting === false`, daemon-scoped commands are recorded in
|
|
60
|
+
* `commandsRun` with `skipped: true, skipReason: 'unaffected_daemon_scope'`
|
|
61
|
+
* rather than executed; web + typecheck commands always run. Absent when no
|
|
62
|
+
* change-impact was threaded in (legacy: full command set runs).
|
|
63
|
+
*/
|
|
64
|
+
changeImpact?: {
|
|
65
|
+
isDaemonAffecting: boolean;
|
|
66
|
+
affectedPackages: string[];
|
|
67
|
+
/** displayCommands skipped because the daemon scope is unaffected. */
|
|
68
|
+
skippedDaemonCommands?: string[];
|
|
69
|
+
};
|
|
56
70
|
};
|
|
57
71
|
type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
|
|
58
72
|
type MeshRefinePatchEquivalenceSummary = {
|
|
@@ -277,6 +291,13 @@ export interface RefineContext {
|
|
|
277
291
|
baseBranch: string;
|
|
278
292
|
baseHead: string;
|
|
279
293
|
branchHead: string;
|
|
294
|
+
/**
|
|
295
|
+
* Coarse daemon-vs-web change-impact for baseHead..branchHead, resolved in the
|
|
296
|
+
* resolve_refs stage and threaded into the validation gate to scope its command
|
|
297
|
+
* set. `undefined` means "could not classify" → the gate fails open and runs ALL
|
|
298
|
+
* commands (never skip on uncertainty).
|
|
299
|
+
*/
|
|
300
|
+
changeImpact?: ChangedPackageClassification;
|
|
280
301
|
validationSummary: Awaited<ReturnType<typeof runMeshRefineValidationGate>>;
|
|
281
302
|
patchEquivalence: Awaited<ReturnType<typeof runMeshRefinePatchEquivalenceGate>>;
|
|
282
303
|
submoduleReachability: Awaited<ReturnType<typeof runMeshRefineSubmoduleReachabilityGate>>;
|
|
@@ -424,5 +445,13 @@ export declare function runMeshRefineValidationGate(mesh: any, workspace: string
|
|
|
424
445
|
persistedBootstrapState?: WorktreeBootstrapState | null;
|
|
425
446
|
/** M2-2: called after an inherit-mode bootstrap run so the caller can persist the new state. */
|
|
426
447
|
onBootstrapStateChange?: (state: WorktreeBootstrapState) => void;
|
|
448
|
+
/**
|
|
449
|
+
* Coarse daemon-vs-web change-impact for the branch (resolve_refs computes it
|
|
450
|
+
* over baseHead..branchHead). When provided and `isDaemonAffecting === false`,
|
|
451
|
+
* daemon-scoped validation commands are skipped (web + typecheck still run).
|
|
452
|
+
* When omitted or `isDaemonAffecting === true`, the full command set runs —
|
|
453
|
+
* fail-open to full validation on any uncertainty.
|
|
454
|
+
*/
|
|
455
|
+
changeImpact?: ChangedPackageClassification;
|
|
427
456
|
}): Promise<MeshRefineValidationSummary>;
|
|
428
457
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.487",
|
|
4
4
|
"description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"author": "vilmire",
|
|
48
48
|
"license": "AGPL-3.0-or-later",
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
51
|
-
"@adhdev/session-host-core": "0.9.82-rc.
|
|
50
|
+
"@adhdev/mesh-shared": "0.9.82-rc.487",
|
|
51
|
+
"@adhdev/session-host-core": "0.9.82-rc.487",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -17,6 +17,8 @@ import { handleMeshForwardEvent, queuePendingMeshCoordinatorEvent } from '../mes
|
|
|
17
17
|
import { analyzeMeshRefineNodeChangeArea, orderMeshRefineBatchNodes } from '../mesh/mesh-refine-batch.js';
|
|
18
18
|
import type { WorktreeBootstrapState } from '../mesh/worktree-bootstrap-config.js';
|
|
19
19
|
import { DEFAULT_MESH_POLICY } from '../repo-mesh-types.js';
|
|
20
|
+
import { classifyChangedPackages } from '../git/git-status.js';
|
|
21
|
+
import type { ChangedPackageClassification } from '../git/git-status.js';
|
|
20
22
|
import { readStringValue } from '../mesh/mesh-node-identity.js';
|
|
21
23
|
import {
|
|
22
24
|
alignRefinerySubmodulesAfterMerge,
|
|
@@ -312,7 +314,24 @@ export async function refineResolveRefsStage(self: DaemonCommandRouter,
|
|
|
312
314
|
const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
313
315
|
const baseHead = baseHeadRaw;
|
|
314
316
|
const branchHead = branchHeadStdout.trim();
|
|
315
|
-
|
|
317
|
+
|
|
318
|
+
// Coarse daemon-vs-web change-impact for baseHead..branchHead, computed
|
|
319
|
+
// against the worktree so the same policy (.adhdev/change-impact.*) as the
|
|
320
|
+
// stale-build detector applies. Threaded onto ctx so the validation gate
|
|
321
|
+
// can scope its command set: a web-only branch skips daemon-scoped commands.
|
|
322
|
+
// FAIL-OPEN: any classification error leaves changeImpact undefined → the
|
|
323
|
+
// gate runs the full command set (never skip on uncertainty).
|
|
324
|
+
let changeImpact: ChangedPackageClassification | undefined;
|
|
325
|
+
try {
|
|
326
|
+
changeImpact = await classifyChangedPackages(node.workspace, baseHead, branchHead);
|
|
327
|
+
} catch {
|
|
328
|
+
changeImpact = undefined;
|
|
329
|
+
}
|
|
330
|
+
recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, {
|
|
331
|
+
branch, baseBranch, baseHead, branchHead,
|
|
332
|
+
...(changeImpact ? { changeImpact } : {}),
|
|
333
|
+
...(fetchWarning ? { fetchWarning } : {}),
|
|
334
|
+
});
|
|
316
335
|
|
|
317
336
|
return {
|
|
318
337
|
kind: 'continue',
|
|
@@ -330,6 +349,7 @@ export async function refineResolveRefsStage(self: DaemonCommandRouter,
|
|
|
330
349
|
baseBranch,
|
|
331
350
|
baseHead,
|
|
332
351
|
branchHead,
|
|
352
|
+
changeImpact,
|
|
333
353
|
validationSummary: undefined as any,
|
|
334
354
|
patchEquivalence: undefined as any,
|
|
335
355
|
submoduleReachability: undefined as any,
|
|
@@ -346,6 +366,9 @@ export async function refineValidationStage(self: DaemonCommandRouter, ctx: Refi
|
|
|
346
366
|
const { mesh, node, branch, baseBranch, refineStages } = ctx;
|
|
347
367
|
const validationStarted = Date.now();
|
|
348
368
|
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace, {
|
|
369
|
+
// (a) Scope the validation command set by coarse change-impact (resolved
|
|
370
|
+
// in resolve_refs). Undefined → gate runs the full command set (fail-open).
|
|
371
|
+
changeImpact: ctx.changeImpact,
|
|
349
372
|
// M2-2: consume the node's persisted bootstrap state; persist re-runs.
|
|
350
373
|
persistedBootstrapState: (node as any).worktreeBootstrap as WorktreeBootstrapState | undefined,
|
|
351
374
|
onBootstrapStateChange: (state) => {
|
|
@@ -369,7 +392,9 @@ export async function refineValidationStage(self: DaemonCommandRouter, ctx: Refi
|
|
|
369
392
|
: undefined;
|
|
370
393
|
const buildValidationFailedError = (): string => {
|
|
371
394
|
const base = validationSummary.failureCode === 'missing_dependencies'
|
|
372
|
-
? 'Refinery validation dependencies are missing; merge/refine was not attempted.
|
|
395
|
+
? 'Refinery validation dependencies are missing for a change-affected package; merge/refine was not attempted. '
|
|
396
|
+
+ 'To make this self-service, either (1) configure .adhdev/worktree_bootstrap.json (or validation.bootstrapCommands in .adhdev/refine.json) so Refinery installs deps before validation, '
|
|
397
|
+
+ 'or (2) converge the branch via the documented manual fast-forward-only bypass (rebase onto the fetched base, verify strict ancestry, then push ff-only) instead of the refine gate.'
|
|
373
398
|
: validationSummary.failureCode === 'dependency_bootstrap_failed'
|
|
374
399
|
? 'Refinery dependency/bootstrap command failed; merge/refine was not attempted.'
|
|
375
400
|
: validationSummary.failureCode === 'spawn_resolution_failed'
|
package/src/git/git-status.ts
CHANGED
|
@@ -438,6 +438,54 @@ function isNonRuntimeRootFile(file: string, policy: ResolvedChangeImpactPolicy):
|
|
|
438
438
|
return false;
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
+
/** Coarse change-impact verdict produced from a changed-file list. */
|
|
442
|
+
export interface ChangedPackageClassification {
|
|
443
|
+
isDaemonAffecting: boolean;
|
|
444
|
+
affectedPackages: string[];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Pure bucketer: classify an already-collected changed-file list into the coarse
|
|
449
|
+
* daemon-vs-web verdict, per the resolved policy. Shared by classifyDaemonBuildChange
|
|
450
|
+
* (buildCommit..HEAD) and classifyChangedPackages (arbitrary ref range) so the
|
|
451
|
+
* daemon/web boundary logic lives in exactly one place. An empty list stays
|
|
452
|
+
* conservative (daemon-affecting) so an actionable warning is never suppressed.
|
|
453
|
+
*/
|
|
454
|
+
function classifyChangedFileList(
|
|
455
|
+
files: string[],
|
|
456
|
+
policy: ResolvedChangeImpactPolicy,
|
|
457
|
+
): ChangedPackageClassification {
|
|
458
|
+
if (files.length === 0) {
|
|
459
|
+
// No file diff (e.g. only merge metadata) — nothing actionable, but stay
|
|
460
|
+
// conservative and treat as daemon-affecting so we don't suppress a real warning.
|
|
461
|
+
return { isDaemonAffecting: true, affectedPackages: [] };
|
|
462
|
+
}
|
|
463
|
+
const pkgs = new Set<string>();
|
|
464
|
+
// A non-package path that is NOT a recognized benign root file (marker/doc).
|
|
465
|
+
// Only these force daemon-affecting; benign markers/docs are ignored so a
|
|
466
|
+
// gitlink-moving root commit over a marker-only oss commit no longer over-warns.
|
|
467
|
+
let sawRuntimeAmbiguousNonPackage = false;
|
|
468
|
+
for (const file of files) {
|
|
469
|
+
const match = file.match(/(?:^|\/)packages\/([^/]+)\//);
|
|
470
|
+
if (!match) {
|
|
471
|
+
if (!isNonRuntimeRootFile(file, policy)) sawRuntimeAmbiguousNonPackage = true;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
pkgs.add(match[1]);
|
|
475
|
+
}
|
|
476
|
+
const affectedPackages = [...pkgs].sort();
|
|
477
|
+
// Daemon-affecting if: any runtime-ambiguous non-package file changed, any
|
|
478
|
+
// unknown package changed, or any explicit daemon-runtime package changed.
|
|
479
|
+
// The daemon is unaffected only when every changed file is either a known
|
|
480
|
+
// web-only package or a recognized benign root file (and at least one such
|
|
481
|
+
// file changed) — i.e. nothing runtime-ambiguous remains. Unlisted/new packages
|
|
482
|
+
// therefore stay daemon-affecting (fail-safe default preserved).
|
|
483
|
+
const allBenign =
|
|
484
|
+
!sawRuntimeAmbiguousNonPackage &&
|
|
485
|
+
affectedPackages.every((p) => policy.webOnlyPackages.has(p) && !policy.daemonRuntimePackages.has(p));
|
|
486
|
+
return { isDaemonAffecting: !allBenign, affectedPackages };
|
|
487
|
+
}
|
|
488
|
+
|
|
441
489
|
/**
|
|
442
490
|
* Determine whether the changes between buildCommit..HEAD touch any daemon-runtime
|
|
443
491
|
* package, per the resolved policy. Returns isDaemonAffecting:true conservatively
|
|
@@ -449,47 +497,54 @@ async function classifyDaemonBuildChange(
|
|
|
449
497
|
buildCommit: string,
|
|
450
498
|
options: GitStatusOptions,
|
|
451
499
|
policy: ResolvedChangeImpactPolicy,
|
|
452
|
-
): Promise<
|
|
500
|
+
): Promise<ChangedPackageClassification> {
|
|
453
501
|
try {
|
|
454
502
|
const diff = await runGit(repoPath, ['diff', '--name-only', `${buildCommit}..HEAD`], options);
|
|
455
503
|
const files = diff.stdout
|
|
456
504
|
.split('\n')
|
|
457
505
|
.map((line) => line.trim())
|
|
458
506
|
.filter(Boolean);
|
|
459
|
-
|
|
460
|
-
// No file diff (e.g. only merge metadata) — nothing actionable, but stay
|
|
461
|
-
// conservative and treat as daemon-affecting so we don't suppress a real warning.
|
|
462
|
-
return { isDaemonAffecting: true, affectedPackages: [] };
|
|
463
|
-
}
|
|
464
|
-
const pkgs = new Set<string>();
|
|
465
|
-
// A non-package path that is NOT a recognized benign root file (marker/doc).
|
|
466
|
-
// Only these force daemon-affecting; benign markers/docs are ignored so a
|
|
467
|
-
// gitlink-moving root commit over a marker-only oss commit no longer over-warns.
|
|
468
|
-
let sawRuntimeAmbiguousNonPackage = false;
|
|
469
|
-
for (const file of files) {
|
|
470
|
-
const match = file.match(/(?:^|\/)packages\/([^/]+)\//);
|
|
471
|
-
if (!match) {
|
|
472
|
-
if (!isNonRuntimeRootFile(file, policy)) sawRuntimeAmbiguousNonPackage = true;
|
|
473
|
-
continue;
|
|
474
|
-
}
|
|
475
|
-
pkgs.add(match[1]);
|
|
476
|
-
}
|
|
477
|
-
const affectedPackages = [...pkgs].sort();
|
|
478
|
-
// Daemon-affecting if: any runtime-ambiguous non-package file changed, any
|
|
479
|
-
// unknown package changed, or any explicit daemon-runtime package changed.
|
|
480
|
-
// The daemon is unaffected only when every changed file is either a known
|
|
481
|
-
// web-only package or a recognized benign root file (and at least one such
|
|
482
|
-
// file changed) — i.e. nothing runtime-ambiguous remains.
|
|
483
|
-
const allBenign =
|
|
484
|
-
!sawRuntimeAmbiguousNonPackage &&
|
|
485
|
-
affectedPackages.every((p) => policy.webOnlyPackages.has(p) && !policy.daemonRuntimePackages.has(p));
|
|
486
|
-
return { isDaemonAffecting: !allBenign, affectedPackages };
|
|
507
|
+
return classifyChangedFileList(files, policy);
|
|
487
508
|
} catch {
|
|
488
509
|
// diff probe failed → can't prove web-only; stay conservative.
|
|
489
510
|
return { isDaemonAffecting: true, affectedPackages: [] };
|
|
490
511
|
}
|
|
491
512
|
}
|
|
492
513
|
|
|
514
|
+
/**
|
|
515
|
+
* Ref-parameterized change-impact classification for a repo/worktree, reusing the
|
|
516
|
+
* exact daemon-vs-web bucketing that the stale-build detector uses — but over an
|
|
517
|
+
* arbitrary `fromRef..toRef` range (e.g. a refine base head → branch head) instead
|
|
518
|
+
* of the live daemon's build commit → HEAD, and WITHOUT any daemonBuildInfo caching.
|
|
519
|
+
*
|
|
520
|
+
* Policy is resolved the same way as getGitRepoStatus: an explicit
|
|
521
|
+
* `options.changeImpactConfig` wins; otherwise the repo's `.adhdev/change-impact.*`
|
|
522
|
+
* is auto-loaded; otherwise the built-in ADHDev default policy applies. The
|
|
523
|
+
* classification uses `git diff --name-only fromRef..toRef`.
|
|
524
|
+
*
|
|
525
|
+
* FAIL-OPEN on error: if the diff can't be collected (bad ref, not a repo), the
|
|
526
|
+
* caller should treat "no verdict" as "run everything" — so we throw rather than
|
|
527
|
+
* returning a misleading benign verdict. Callers wrap this in try/catch and leave
|
|
528
|
+
* changeImpact undefined on failure. Unclassified/new packages still default to
|
|
529
|
+
* isDaemonAffecting:true (never silently skipped).
|
|
530
|
+
*/
|
|
531
|
+
export async function classifyChangedPackages(
|
|
532
|
+
repoPath: string,
|
|
533
|
+
fromRef: string,
|
|
534
|
+
toRef: string,
|
|
535
|
+
options: GitStatusOptions = {},
|
|
536
|
+
): Promise<ChangedPackageClassification> {
|
|
537
|
+
const repo = await resolveGitRepository(repoPath, options);
|
|
538
|
+
const { config } = resolveChangeImpactConfigForRepo(repo.repoRoot, options);
|
|
539
|
+
const policy = resolveChangeImpactPolicy(config);
|
|
540
|
+
const diff = await runGit(repoPath, ['diff', '--name-only', `${fromRef}..${toRef}`], options);
|
|
541
|
+
const files = diff.stdout
|
|
542
|
+
.split('\n')
|
|
543
|
+
.map((line) => line.trim())
|
|
544
|
+
.filter(Boolean);
|
|
545
|
+
return classifyChangedFileList(files, policy);
|
|
546
|
+
}
|
|
547
|
+
|
|
493
548
|
/**
|
|
494
549
|
* Resolve the Change Impact config to apply for this status read. Priority:
|
|
495
550
|
* - options.changeImpactConfig === null → force built-in default policy (no load).
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { getGitRepoStatus } from '../git/git-status.js';
|
|
16
|
+
import type { ChangedPackageClassification } from '../git/git-status.js';
|
|
16
17
|
import * as yaml from 'js-yaml';
|
|
17
18
|
import { loadMeshRefineConfig, resolveMeshRefineValidationPlan } from '../mesh/refine-config.js';
|
|
18
19
|
import type { MeshRefineValidationCommandPlan } from '../mesh/refine-config.js';
|
|
@@ -74,6 +75,19 @@ type MeshRefineValidationSummary = {
|
|
|
74
75
|
};
|
|
75
76
|
/** M2-2: deprecation notices from the refine config (e.g. bootstrapCommands). */
|
|
76
77
|
deprecationWarnings?: string[];
|
|
78
|
+
/**
|
|
79
|
+
* Coarse daemon-vs-web change-impact used to scope the validation command set.
|
|
80
|
+
* When `isDaemonAffecting === false`, daemon-scoped commands are recorded in
|
|
81
|
+
* `commandsRun` with `skipped: true, skipReason: 'unaffected_daemon_scope'`
|
|
82
|
+
* rather than executed; web + typecheck commands always run. Absent when no
|
|
83
|
+
* change-impact was threaded in (legacy: full command set runs).
|
|
84
|
+
*/
|
|
85
|
+
changeImpact?: {
|
|
86
|
+
isDaemonAffecting: boolean;
|
|
87
|
+
affectedPackages: string[];
|
|
88
|
+
/** displayCommands skipped because the daemon scope is unaffected. */
|
|
89
|
+
skippedDaemonCommands?: string[];
|
|
90
|
+
};
|
|
77
91
|
};
|
|
78
92
|
|
|
79
93
|
type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
|
|
@@ -343,6 +357,13 @@ export interface RefineContext {
|
|
|
343
357
|
baseBranch: string;
|
|
344
358
|
baseHead: string;
|
|
345
359
|
branchHead: string;
|
|
360
|
+
/**
|
|
361
|
+
* Coarse daemon-vs-web change-impact for baseHead..branchHead, resolved in the
|
|
362
|
+
* resolve_refs stage and threaded into the validation gate to scope its command
|
|
363
|
+
* set. `undefined` means "could not classify" → the gate fails open and runs ALL
|
|
364
|
+
* commands (never skip on uncertainty).
|
|
365
|
+
*/
|
|
366
|
+
changeImpact?: ChangedPackageClassification;
|
|
346
367
|
validationSummary: Awaited<ReturnType<typeof runMeshRefineValidationGate>>;
|
|
347
368
|
patchEquivalence: Awaited<ReturnType<typeof runMeshRefinePatchEquivalenceGate>>;
|
|
348
369
|
submoduleReachability: Awaited<ReturnType<typeof runMeshRefineSubmoduleReachabilityGate>>;
|
|
@@ -1477,6 +1498,14 @@ export async function runMeshRefineValidationGate(
|
|
|
1477
1498
|
persistedBootstrapState?: WorktreeBootstrapState | null;
|
|
1478
1499
|
/** M2-2: called after an inherit-mode bootstrap run so the caller can persist the new state. */
|
|
1479
1500
|
onBootstrapStateChange?: (state: WorktreeBootstrapState) => void;
|
|
1501
|
+
/**
|
|
1502
|
+
* Coarse daemon-vs-web change-impact for the branch (resolve_refs computes it
|
|
1503
|
+
* over baseHead..branchHead). When provided and `isDaemonAffecting === false`,
|
|
1504
|
+
* daemon-scoped validation commands are skipped (web + typecheck still run).
|
|
1505
|
+
* When omitted or `isDaemonAffecting === true`, the full command set runs —
|
|
1506
|
+
* fail-open to full validation on any uncertainty.
|
|
1507
|
+
*/
|
|
1508
|
+
changeImpact?: ChangedPackageClassification;
|
|
1480
1509
|
},
|
|
1481
1510
|
): Promise<MeshRefineValidationSummary> {
|
|
1482
1511
|
const { execFile } = await import('node:child_process');
|
|
@@ -1574,6 +1603,63 @@ export async function runMeshRefineValidationGate(
|
|
|
1574
1603
|
return ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb', 'bun.lock']
|
|
1575
1604
|
.some(lock => fs.existsSync(pathJoin(cwd, lock)));
|
|
1576
1605
|
};
|
|
1606
|
+
// A validation command needs installed node_modules to run. Only these can hit
|
|
1607
|
+
// the missing-deps hard-block; non-package-manager commands (e.g. a plain
|
|
1608
|
+
// `node scripts/check-vendor-drift.mjs`) need no deps and must never be aborted
|
|
1609
|
+
// by a preceding command's missing-deps.
|
|
1610
|
+
const needsNodeModules = (candidate: MeshRefineValidationCommand, cwd: string): boolean =>
|
|
1611
|
+
isPackageManagerValidation(candidate) && dependenciesLikelyMissing(cwd);
|
|
1612
|
+
|
|
1613
|
+
// (a) Coarse change-impact scoping. When the branch is web-only
|
|
1614
|
+
// (changeImpact.isDaemonAffecting === false), daemon-scoped validation commands
|
|
1615
|
+
// are pointless — and often un-runnable in a web-only worktree that never
|
|
1616
|
+
// bootstrapped daemon deps. Identify daemon-scoped commands ONLY by the coarse
|
|
1617
|
+
// daemon-vs-web bucket: a command whose script/args reference a daemon package
|
|
1618
|
+
// (daemon-core / daemon-cloud) or the vendor-drift check. web-side commands
|
|
1619
|
+
// (test:web-core / test:web-cloud) and `typecheck` ALWAYS run — the daemon/web
|
|
1620
|
+
// boundary is the human-curated safe line; we deliberately do NOT do fine
|
|
1621
|
+
// per-package skipping (web-cloud consumes web-core, so it must still run).
|
|
1622
|
+
const isDaemonScopedCommand = (candidate: MeshRefineValidationCommand): boolean => {
|
|
1623
|
+
const haystack = [candidate.command, ...(candidate.args || []), candidate.displayCommand || '']
|
|
1624
|
+
.join(' ')
|
|
1625
|
+
.toLowerCase();
|
|
1626
|
+
// Never treat a typecheck or an explicit web-side command as daemon-scoped.
|
|
1627
|
+
if (candidate.category === 'typecheck') return false;
|
|
1628
|
+
if (/\btypecheck\b/.test(haystack)) return false;
|
|
1629
|
+
if (/\bweb-core\b|\bweb-cloud\b|\bweb-standalone\b|\btest:web\b/.test(haystack)) return false;
|
|
1630
|
+
// Daemon-scoped signals: a daemon package name, a daemon test script, or the
|
|
1631
|
+
// vendor-drift check (which validates the daemon vendor bundle).
|
|
1632
|
+
return /\bdaemon-core\b|\bdaemon-cloud\b|\btest:daemon\b|check-vendor-drift/.test(haystack);
|
|
1633
|
+
};
|
|
1634
|
+
|
|
1635
|
+
const scopeUnaffectedDaemon = opts?.changeImpact?.isDaemonAffecting === false;
|
|
1636
|
+
const skippedDaemonCommands: string[] = [];
|
|
1637
|
+
const commandsToRun: MeshRefineValidationCommand[] = [];
|
|
1638
|
+
for (const candidate of selection.commands) {
|
|
1639
|
+
if (scopeUnaffectedDaemon && isDaemonScopedCommand(candidate)) {
|
|
1640
|
+
skippedDaemonCommands.push(candidate.displayCommand);
|
|
1641
|
+
// Record the skip so it's visible in the summary, never silently dropped.
|
|
1642
|
+
summary.commandsRun.push({
|
|
1643
|
+
command: candidate.command,
|
|
1644
|
+
args: candidate.args,
|
|
1645
|
+
displayCommand: candidate.displayCommand,
|
|
1646
|
+
category: candidate.category,
|
|
1647
|
+
source: candidate.source,
|
|
1648
|
+
passed: true,
|
|
1649
|
+
skipped: true,
|
|
1650
|
+
skipReason: 'unaffected_daemon_scope',
|
|
1651
|
+
});
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
commandsToRun.push(candidate);
|
|
1655
|
+
}
|
|
1656
|
+
if (opts?.changeImpact) {
|
|
1657
|
+
summary.changeImpact = {
|
|
1658
|
+
isDaemonAffecting: opts.changeImpact.isDaemonAffecting,
|
|
1659
|
+
affectedPackages: opts.changeImpact.affectedPackages,
|
|
1660
|
+
...(skippedDaemonCommands.length ? { skippedDaemonCommands } : {}),
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1577
1663
|
|
|
1578
1664
|
if (runLegacyBootstrapCommands) {
|
|
1579
1665
|
summary.bootstrap = { stage: 'legacy' };
|
|
@@ -1619,23 +1705,31 @@ export async function runMeshRefineValidationGate(
|
|
|
1619
1705
|
}
|
|
1620
1706
|
}
|
|
1621
1707
|
|
|
1622
|
-
|
|
1708
|
+
// (b) Track a genuine missing-deps block for an AFFECTED command. Instead of
|
|
1709
|
+
// aborting the whole gate at the first missing-deps hit (which also killed
|
|
1710
|
+
// trailing no-dep commands like check-vendor-drift.mjs), we mark the blocked
|
|
1711
|
+
// command and CONTINUE evaluating the rest: commands whose deps are present, or
|
|
1712
|
+
// which need no deps at all, still run. missing_dependencies only becomes the
|
|
1713
|
+
// gate failure if at least one command that truly needed deps could not run.
|
|
1714
|
+
let missingDepsBlocked = false;
|
|
1715
|
+
for (const candidate of commandsToRun) {
|
|
1623
1716
|
const startedAt = Date.now();
|
|
1624
1717
|
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
1625
1718
|
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
1626
1719
|
const bootstrapProvidedDependencies = summary.bootstrap?.stage === 'cached' || summary.bootstrap?.stage === 'ran' || summary.bootstrap?.stage === 'legacy';
|
|
1627
|
-
if (!bootstrapProvidedDependencies &&
|
|
1720
|
+
if (!bootstrapProvidedDependencies && needsNodeModules(candidate, cwd)) {
|
|
1721
|
+
// This command genuinely needs node_modules that are absent. Mark it
|
|
1722
|
+
// blocked, but do NOT abort — a following no-dep command (or one in a
|
|
1723
|
+
// different cwd that DOES have deps) must still get its chance to run.
|
|
1628
1724
|
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, {
|
|
1629
|
-
stderr: 'Dependencies appear to be missing: package.json and a lockfile are present, but node_modules is absent. Configure validation.bootstrapCommands in repo mesh/refine config if Refinery should install/bootstrap before validation.',
|
|
1725
|
+
stderr: 'Dependencies appear to be missing: package.json and a lockfile are present, but node_modules is absent. Configure validation.bootstrapCommands (or .adhdev/worktree_bootstrap.json) in repo mesh/refine config if Refinery should install/bootstrap before validation.',
|
|
1630
1726
|
}, false, {
|
|
1631
1727
|
exitCode: null,
|
|
1632
1728
|
skipped: true,
|
|
1633
1729
|
failureKind: 'missing_dependencies',
|
|
1634
1730
|
}));
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
summary.failureCode = 'missing_dependencies';
|
|
1638
|
-
return summary;
|
|
1731
|
+
missingDepsBlocked = true;
|
|
1732
|
+
continue;
|
|
1639
1733
|
}
|
|
1640
1734
|
// See the bootstrap loop above: resolve the win32 .cmd shim to an
|
|
1641
1735
|
// absolute path before handing it to the spawn boundary.
|
|
@@ -1681,6 +1775,18 @@ export async function runMeshRefineValidationGate(
|
|
|
1681
1775
|
}
|
|
1682
1776
|
}
|
|
1683
1777
|
|
|
1778
|
+
// (b) A command that genuinely needed deps could not run. Surface it as the
|
|
1779
|
+
// gate failure now (after letting no-dep / deps-present commands run), so the
|
|
1780
|
+
// caller can classify it blocked_review and emit a self-service hint. Every
|
|
1781
|
+
// daemon-scoped command in a web-only branch was already filtered above, so a
|
|
1782
|
+
// missing-deps block here is a real affected-command block.
|
|
1783
|
+
if (missingDepsBlocked) {
|
|
1784
|
+
summary.status = 'failed';
|
|
1785
|
+
summary.failureKind = 'missing_dependencies';
|
|
1786
|
+
summary.failureCode = 'missing_dependencies';
|
|
1787
|
+
return summary;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1684
1790
|
summary.status = 'passed';
|
|
1685
1791
|
return summary;
|
|
1686
1792
|
}
|