@ontrails/trails 1.0.0-beta.42 → 1.0.0-beta.43

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.
@@ -8,6 +8,8 @@ import { defaultReleaseConfig, releaseConfigSchema } from './config.js';
8
8
  import type { ReleaseConfigInput, ReleaseFactType } from './config.js';
9
9
  import { findPublicTrailContractChangeFacts } from './contract-facts.js';
10
10
  import type { ContractReleaseFact } from './contract-facts.js';
11
+ import { findPackageRouteReleaseFacts } from './package-route-facts.js';
12
+ import type { PackageRouteReleaseFact } from './package-route-facts.js';
11
13
 
12
14
  interface PackageJson {
13
15
  readonly name?: string;
@@ -23,6 +25,8 @@ export interface WorkspaceInfo {
23
25
 
24
26
  export interface ReleaseCheckInput {
25
27
  readonly baseRef?: string;
28
+ readonly baseWorkspaceError?: string;
29
+ readonly baseWorkspaces?: readonly WorkspaceInfo[];
26
30
  readonly changedFiles: readonly string[];
27
31
  readonly contractFacts?: readonly ContractReleaseFact[];
28
32
  readonly noReleaseOverride?: boolean;
@@ -42,6 +46,7 @@ export interface ReleaseCheckResult {
42
46
  readonly errors: readonly string[];
43
47
  readonly matchedRuleIds: readonly string[];
44
48
  readonly noReleaseOverride: boolean;
49
+ readonly packageRouteFacts: readonly PackageRouteReleaseFact[];
45
50
  readonly passed: boolean;
46
51
  /** Compatibility alias for the existing GitHub label and package script. */
47
52
  readonly releaseNone: boolean;
@@ -97,11 +102,21 @@ const VERSION_RELEASE_WORKSPACE_FILES = new Set([
97
102
  'package.json',
98
103
  ]);
99
104
  const normalizePath = (path: string): string =>
100
- path.replaceAll('\\', '/').replace(/^\.\//u, '');
105
+ path.replaceAll('\\', '/').replace(/^\.\//u, '').replace(/\/+$/u, '');
106
+
107
+ const normalizeWorkspacePattern = (pattern: string): string =>
108
+ normalizePath(pattern) || '.';
101
109
 
102
110
  const hasWorkspaceGlobSyntax = (pattern: string): boolean =>
103
111
  WORKSPACE_GLOB_SYNTAX_PATTERN.test(pattern);
104
112
 
113
+ const isSupportedWorkspacePattern = (pattern: string): boolean =>
114
+ !hasWorkspaceGlobSyntax(pattern) ||
115
+ (pattern.endsWith('/*') && !hasWorkspaceGlobSyntax(pattern.slice(0, -2)));
116
+
117
+ const unsupportedWorkspacePatternError = (pattern: string): string =>
118
+ `Unsupported workspace pattern '${pattern}'. The release check supports exact workspace paths and one-level '/*' globs.`;
119
+
105
120
  const readJson = async <T>(path: string): Promise<T> =>
106
121
  (await Bun.file(path).json()) as T;
107
122
 
@@ -112,6 +127,10 @@ const discoverWorkspaceDirs = async (
112
127
  const dirs: string[] = [];
113
128
 
114
129
  for (const pattern of patterns) {
130
+ if (!isSupportedWorkspacePattern(pattern)) {
131
+ throw new Error(unsupportedWorkspacePatternError(pattern));
132
+ }
133
+
115
134
  if (pattern.endsWith('/*')) {
116
135
  const parent = join(repoRoot, pattern.slice(0, -2));
117
136
  let names: string[] = [];
@@ -145,12 +164,6 @@ const discoverWorkspaceDirs = async (
145
164
  continue;
146
165
  }
147
166
 
148
- if (hasWorkspaceGlobSyntax(pattern)) {
149
- throw new Error(
150
- `Unsupported workspace pattern '${pattern}'. The release check supports exact workspace paths and one-level '/*' globs.`
151
- );
152
- }
153
-
154
167
  throw new Error(
155
168
  `Workspace pattern '${pattern}' did not resolve to a package.json`
156
169
  );
@@ -190,6 +203,126 @@ export const discoverWorkspaces = async (
190
203
  );
191
204
  };
192
205
 
206
+ interface BaseWorkspaceDiscovery {
207
+ readonly error?: string;
208
+ readonly workspaces: readonly WorkspaceInfo[];
209
+ }
210
+
211
+ const baseWorkspaceReadError = (baseRef: string): string =>
212
+ `Release check could not read the base workspace inventory from '${baseRef}'. Fetch or provide a valid --base-ref before checking package routes.`;
213
+
214
+ const discoverBaseWorkspaces = (
215
+ repoRoot: string,
216
+ baseRef: string | undefined
217
+ ): BaseWorkspaceDiscovery => {
218
+ if (baseRef === undefined) {
219
+ return { workspaces: [] };
220
+ }
221
+
222
+ const root = Bun.spawnSync({
223
+ cmd: ['git', 'show', `${baseRef}:package.json`],
224
+ cwd: repoRoot,
225
+ stderr: 'pipe',
226
+ stdout: 'pipe',
227
+ });
228
+ if (root.exitCode !== 0) {
229
+ return { error: baseWorkspaceReadError(baseRef), workspaces: [] };
230
+ }
231
+ const workspacePatterns = (
232
+ JSON.parse(root.stdout.toString()) as PackageJson
233
+ ).workspaces?.map(normalizeWorkspacePattern);
234
+ if (!workspacePatterns || workspacePatterns.length === 0) {
235
+ return { workspaces: [] };
236
+ }
237
+ const unsupportedPattern = workspacePatterns.find(
238
+ (pattern) => !isSupportedWorkspacePattern(pattern)
239
+ );
240
+ if (unsupportedPattern) {
241
+ return {
242
+ error: unsupportedWorkspacePatternError(unsupportedPattern),
243
+ workspaces: [],
244
+ };
245
+ }
246
+
247
+ const listed = Bun.spawnSync({
248
+ cmd: ['git', 'ls-tree', '-r', '--name-only', baseRef],
249
+ cwd: repoRoot,
250
+ stderr: 'pipe',
251
+ stdout: 'pipe',
252
+ });
253
+ if (listed.exitCode !== 0) {
254
+ return { error: baseWorkspaceReadError(baseRef), workspaces: [] };
255
+ }
256
+
257
+ const workspaces: WorkspaceInfo[] = [];
258
+ for (const relativePath of listed.stdout.toString().split(/\r?\n/u)) {
259
+ if (
260
+ !relativePath.endsWith('/package.json') &&
261
+ relativePath !== 'package.json'
262
+ ) {
263
+ continue;
264
+ }
265
+ const workspacePath =
266
+ relativePath === 'package.json'
267
+ ? '.'
268
+ : normalizePath(relativePath.replace(/\/package\.json$/u, ''));
269
+ const isWorkspace = workspacePatterns.some(
270
+ (pattern) =>
271
+ pattern === workspacePath ||
272
+ (pattern.endsWith('/*') &&
273
+ workspacePath.startsWith(pattern.slice(0, -1)) &&
274
+ !workspacePath.slice(pattern.length - 1).includes('/'))
275
+ );
276
+ if (!isWorkspace) {
277
+ continue;
278
+ }
279
+ const shown = Bun.spawnSync({
280
+ cmd: ['git', 'show', `${baseRef}:${relativePath}`],
281
+ cwd: repoRoot,
282
+ stderr: 'pipe',
283
+ stdout: 'pipe',
284
+ });
285
+ if (shown.exitCode !== 0) {
286
+ return { error: baseWorkspaceReadError(baseRef), workspaces: [] };
287
+ }
288
+ const pkg = JSON.parse(shown.stdout.toString()) as PackageJson;
289
+ if (!pkg.name) {
290
+ continue;
291
+ }
292
+ workspaces.push({
293
+ isPrivate: pkg.private === true,
294
+ name: pkg.name,
295
+ relativePath: workspacePath,
296
+ });
297
+ }
298
+ return {
299
+ workspaces: workspaces.toSorted((left, right) =>
300
+ left.name.localeCompare(right.name)
301
+ ),
302
+ };
303
+ };
304
+
305
+ const resolveBaseWorkspaceInput = (
306
+ input: ReleaseCheckInput
307
+ ): BaseWorkspaceDiscovery => {
308
+ if (input.baseWorkspaces !== undefined) {
309
+ return {
310
+ ...(input.baseWorkspaceError === undefined
311
+ ? {}
312
+ : { error: input.baseWorkspaceError }),
313
+ workspaces: input.baseWorkspaces,
314
+ };
315
+ }
316
+
317
+ const discovered = discoverBaseWorkspaces(input.repoRoot, input.baseRef);
318
+ const error = input.baseWorkspaceError ?? discovered.error;
319
+
320
+ return {
321
+ ...(error === undefined ? {} : { error }),
322
+ workspaces: discovered.workspaces,
323
+ };
324
+ };
325
+
193
326
  const isPublishableOnTrailsWorkspace = (workspace: WorkspaceInfo): boolean =>
194
327
  !workspace.isPrivate && workspace.name.startsWith('@ontrails/');
195
328
 
@@ -303,6 +436,84 @@ const parseChangesetPackages = (content: string): readonly string[] => {
303
436
  });
304
437
  };
305
438
 
439
+ const readBaseChangeset = (
440
+ repoRoot: string,
441
+ baseRef: string | undefined,
442
+ path: string
443
+ ): string | null => {
444
+ if (baseRef === undefined) {
445
+ return null;
446
+ }
447
+
448
+ const result = Bun.spawnSync({
449
+ cmd: ['git', 'show', `${baseRef}:${path}`],
450
+ cwd: repoRoot,
451
+ stderr: 'pipe',
452
+ stdout: 'pipe',
453
+ });
454
+
455
+ return result.exitCode === 0 ? result.stdout.toString() : null;
456
+ };
457
+
458
+ const removeChangesetPackageRows = (
459
+ content: string,
460
+ packages: ReadonlySet<string>
461
+ ): string => {
462
+ let insideFrontmatter = false;
463
+
464
+ return content
465
+ .split('\n')
466
+ .filter((line, index) => {
467
+ const normalizedLine = line.replace(/\r$/u, '');
468
+
469
+ if (normalizedLine === '---') {
470
+ insideFrontmatter = index === 0;
471
+ return true;
472
+ }
473
+
474
+ if (!insideFrontmatter) {
475
+ return true;
476
+ }
477
+
478
+ const packageName = normalizedLine.match(CHANGESET_PACKAGE_PATTERN)?.[1];
479
+ return packageName === undefined || !packages.has(packageName);
480
+ })
481
+ .join('\n');
482
+ };
483
+
484
+ const findRetiredPackageChangesetCleanups = (
485
+ changedChangesetPaths: readonly string[],
486
+ input: ReleaseCheckInput
487
+ ): readonly string[] => {
488
+ const workspaceNames = new Set(
489
+ input.workspaces.map((workspace) => workspace.name)
490
+ );
491
+
492
+ return changedChangesetPaths.filter((path) => {
493
+ const absolutePath = join(input.repoRoot, path);
494
+ if (!existsSync(absolutePath)) {
495
+ return false;
496
+ }
497
+
498
+ const baseContent = readBaseChangeset(input.repoRoot, input.baseRef, path);
499
+ if (baseContent === null) {
500
+ return false;
501
+ }
502
+
503
+ const retiredPackages = new Set(
504
+ parseChangesetPackages(baseContent).filter(
505
+ (packageName) => !workspaceNames.has(packageName)
506
+ )
507
+ );
508
+
509
+ return (
510
+ retiredPackages.size > 0 &&
511
+ removeChangesetPackageRows(baseContent, retiredPackages) ===
512
+ readFileSync(absolutePath, 'utf8')
513
+ );
514
+ });
515
+ };
516
+
306
517
  const findChangedChangesetPaths = (
307
518
  changedFiles: readonly string[]
308
519
  ): readonly string[] =>
@@ -380,9 +591,45 @@ const findMatchedRuleIds = (input: ReleaseCheckInput): readonly string[] => {
380
591
  .toSorted();
381
592
  };
382
593
 
594
+ const findPackageRouteRuleErrors = ({
595
+ coveredPackages,
596
+ packageRoute,
597
+ requiresPackageRoute,
598
+ }: {
599
+ readonly coveredPackages: readonly string[];
600
+ readonly packageRoute: ReturnType<typeof findPackageRouteReleaseFacts>;
601
+ readonly requiresPackageRoute: boolean;
602
+ }): readonly string[] => {
603
+ if (!requiresPackageRoute) {
604
+ return [];
605
+ }
606
+
607
+ const errors = packageRoute.diagnostics.map(
608
+ (diagnostic) => diagnostic.message
609
+ );
610
+
611
+ const uncoveredIntents = packageRoute.intents.filter(
612
+ (intent) =>
613
+ !intent.eligiblePackages.some((packageName) =>
614
+ coveredPackages.includes(packageName)
615
+ )
616
+ );
617
+
618
+ if (uncoveredIntents.length > 0) {
619
+ errors.push(
620
+ `Public package route changesets must cover a surviving owner: ${uncoveredIntents
621
+ .map((intent) => intent.sourcePackage)
622
+ .join(', ')}`
623
+ );
624
+ }
625
+
626
+ return errors;
627
+ };
628
+
383
629
  export const checkReleaseRules = (
384
630
  input: ReleaseCheckInput
385
631
  ): ReleaseCheckResult => {
632
+ const baseWorkspaceInput = resolveBaseWorkspaceInput(input);
386
633
  const noReleaseOverride =
387
634
  input.noReleaseOverride === true || input.releaseNone === true;
388
635
  const affectedPackages = findAffectedPackages(
@@ -394,11 +641,18 @@ export const checkReleaseRules = (
394
641
  input.changedFiles,
395
642
  input.repoRoot
396
643
  );
644
+ const retiredPackageChangesetCleanups = new Set(
645
+ findRetiredPackageChangesetCleanups(changedChangesets, input)
646
+ );
397
647
  const changesets = findChangedChangesets(changedChangesets, input.repoRoot);
398
648
  const coveredPackages = [
399
649
  ...new Set(changesets.flatMap((changeset) => changeset.packages)),
400
650
  ].toSorted();
401
651
  const contractFacts = findGateContractFacts(input);
652
+ const packageRoute = findPackageRouteReleaseFacts({
653
+ baseWorkspaces: baseWorkspaceInput.workspaces,
654
+ workspaces: input.workspaces,
655
+ });
402
656
  const versionRelease = isVersionReleaseChangeSet(
403
657
  input.changedFiles,
404
658
  input.workspaces,
@@ -411,10 +665,15 @@ export const checkReleaseRules = (
411
665
  (fact) => !isContractFactCovered(fact, coveredPackages)
412
666
  );
413
667
  const hasReleaseFacts =
414
- affectedPackages.length > 0 || contractFacts.length > 0 || versionRelease;
668
+ affectedPackages.length > 0 ||
669
+ contractFacts.length > 0 ||
670
+ packageRoute.facts.length > 0 ||
671
+ versionRelease;
415
672
  const activePackageChangesetsWithoutReleaseFacts =
416
673
  activeChangedChangesets.length > 0 && !hasReleaseFacts
417
- ? activeChangedChangesets
674
+ ? activeChangedChangesets.filter(
675
+ (path) => !retiredPackageChangesetCleanups.has(path)
676
+ )
418
677
  : [];
419
678
  const matchedRuleIds = findMatchedRuleIds(input);
420
679
  const requiresPackageIntent = ruleMatchesFactType(input, 'package-content');
@@ -422,8 +681,16 @@ export const checkReleaseRules = (
422
681
  input,
423
682
  'public-trail-contract'
424
683
  );
684
+ const requiresPackageRoute = ruleMatchesFactType(
685
+ input,
686
+ 'public-package-route'
687
+ );
425
688
  const errors: string[] = [];
426
689
 
690
+ if (baseWorkspaceInput.error) {
691
+ errors.push(baseWorkspaceInput.error);
692
+ }
693
+
427
694
  if (noReleaseOverride && changedChangesets.length > 0) {
428
695
  errors.push(
429
696
  '`release:none` conflicts with changed changeset files. Remove the label or the changeset.'
@@ -447,6 +714,14 @@ export const checkReleaseRules = (
447
714
  );
448
715
  }
449
716
 
717
+ errors.push(
718
+ ...findPackageRouteRuleErrors({
719
+ coveredPackages,
720
+ packageRoute,
721
+ requiresPackageRoute,
722
+ })
723
+ );
724
+
450
725
  if (
451
726
  !noReleaseOverride &&
452
727
  !versionRelease &&
@@ -469,6 +744,7 @@ export const checkReleaseRules = (
469
744
  errors,
470
745
  matchedRuleIds,
471
746
  noReleaseOverride,
747
+ packageRouteFacts: packageRoute.facts,
472
748
  passed: errors.length === 0,
473
749
  releaseNone: noReleaseOverride,
474
750
  uncoveredContractFacts,
@@ -516,6 +792,17 @@ const readLocalChangedFiles = (
516
792
  return parseChangedFilesOutput(result.stdout.toString());
517
793
  };
518
794
 
795
+ const hasGitRef = (repoRoot: string, ref: string): boolean => {
796
+ const result = Bun.spawnSync({
797
+ cmd: ['git', 'rev-parse', '--verify', '--quiet', `${ref}^{commit}`],
798
+ cwd: repoRoot,
799
+ stderr: 'pipe',
800
+ stdout: 'pipe',
801
+ });
802
+
803
+ return result.exitCode === 0;
804
+ };
805
+
519
806
  const isRecord = (value: unknown): value is Record<string, unknown> =>
520
807
  typeof value === 'object' && value !== null;
521
808
 
@@ -769,9 +1056,16 @@ export const runReleaseCheck = async (
769
1056
  options: RunReleaseCheckOptions
770
1057
  ): Promise<ReleaseCheckReport> => {
771
1058
  const workspaces = await discoverWorkspaces(options.repoRoot);
1059
+ const changedFilesBaseError =
1060
+ options.changedFilesPath !== undefined && options.baseRef === undefined
1061
+ ? 'Release check requires --base-ref when --changed-files is used.'
1062
+ : undefined;
772
1063
  const baseRef =
773
1064
  options.baseRef ??
774
- (options.changedFilesPath === undefined ? 'origin/main' : undefined);
1065
+ (options.changedFilesPath === undefined &&
1066
+ (workspaces.length > 0 || hasGitRef(options.repoRoot, 'origin/main'))
1067
+ ? 'origin/main'
1068
+ : undefined);
775
1069
  let changedFiles: readonly string[];
776
1070
 
777
1071
  if (options.changedFilesPath !== undefined) {
@@ -792,8 +1086,16 @@ export const runReleaseCheck = async (
792
1086
  env: options.env,
793
1087
  repoRoot: options.repoRoot,
794
1088
  });
1089
+ const baseWorkspaceDiscovery = discoverBaseWorkspaces(
1090
+ options.repoRoot,
1091
+ baseRef
1092
+ );
1093
+ const baseWorkspaceError =
1094
+ changedFilesBaseError ?? baseWorkspaceDiscovery.error;
795
1095
  const result = checkReleaseRules({
796
1096
  ...(baseRef === undefined ? {} : { baseRef }),
1097
+ ...(baseWorkspaceError === undefined ? {} : { baseWorkspaceError }),
1098
+ baseWorkspaces: baseWorkspaceDiscovery.workspaces,
797
1099
  changedFiles,
798
1100
  ...(loadedConfig.config === undefined
799
1101
  ? {}
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
 
3
3
  export const releaseFactTypeValues = [
4
4
  'package-content',
5
+ 'public-package-route',
5
6
  'public-trail-contract',
6
7
  ] as const;
7
8
 
@@ -31,6 +32,15 @@ export type ReleaseRule = z.output<typeof releaseRuleSchema>;
31
32
  export type ReleaseRuleInput = z.input<typeof releaseRuleSchema>;
32
33
 
33
34
  export const defaultReleaseRules = [
35
+ {
36
+ description:
37
+ 'Public package removals require an exact governed Regrade route or classified multi-owner fold plus branch-local release intent.',
38
+ enabled: true,
39
+ facts: ['public-package-route'],
40
+ id: 'public-package-route-requires-regrade',
41
+ intent: ['changeset'],
42
+ severity: 'error',
43
+ },
34
44
  {
35
45
  description:
36
46
  'Publishable package content changes require positive release intent.',
@@ -31,6 +31,11 @@ export {
31
31
  type ContractReleaseFactInput,
32
32
  type ContractSourceSnapshot,
33
33
  } from './contract-facts.js';
34
+ export {
35
+ findPackageRouteReleaseFacts,
36
+ type PackageRouteReleaseDiagnostic,
37
+ type PackageRouteReleaseFact,
38
+ } from './package-route-facts.js';
34
39
  export {
35
40
  defaultReleaseConfig,
36
41
  defaultReleaseRules,
@@ -0,0 +1,146 @@
1
+ import { listGovernedVocabularyTransitions } from '@ontrails/warden';
2
+
3
+ import type { WorkspaceInfo } from './check.js';
4
+
5
+ export interface PackageRouteReleaseFact {
6
+ readonly kind: 'classified' | 'single';
7
+ readonly sourcePackage: string;
8
+ readonly targetPackage?: string;
9
+ readonly transitionId: string;
10
+ }
11
+
12
+ export interface PackageRouteReleaseDiagnostic {
13
+ readonly code:
14
+ | 'missing-governed-package-route'
15
+ | 'missing-governed-package-route-target';
16
+ readonly message: string;
17
+ readonly sourcePackage: string;
18
+ }
19
+
20
+ export interface PackageRouteReleaseIntent {
21
+ readonly eligiblePackages: readonly string[];
22
+ readonly sourcePackage: string;
23
+ }
24
+
25
+ const isPublishableTrailsPackage = (workspace: WorkspaceInfo): boolean =>
26
+ !workspace.isPrivate && workspace.name.startsWith('@ontrails/');
27
+
28
+ const findPackageRouteTransition = (
29
+ transitions: ReturnType<typeof listGovernedVocabularyTransitions>,
30
+ sourcePackage: string
31
+ ) => {
32
+ const direct = transitions.find(
33
+ (transition) => transition.from === sourcePackage
34
+ );
35
+
36
+ if (direct) {
37
+ return { target: direct.target, transition: direct };
38
+ }
39
+
40
+ for (const transition of transitions) {
41
+ const moduleSpecifierRoute = transition.stringLiteralRenames.find(
42
+ (route) =>
43
+ route.from === sourcePackage &&
44
+ route.moduleSpecifier?.targetPackage !== undefined
45
+ );
46
+
47
+ if (moduleSpecifierRoute?.moduleSpecifier?.targetPackage) {
48
+ return {
49
+ target: {
50
+ kind: 'single' as const,
51
+ to: moduleSpecifierRoute.moduleSpecifier.targetPackage,
52
+ },
53
+ transition,
54
+ };
55
+ }
56
+ }
57
+
58
+ return null;
59
+ };
60
+
61
+ export const findPackageRouteReleaseFacts = ({
62
+ baseWorkspaces,
63
+ workspaces,
64
+ }: {
65
+ readonly baseWorkspaces: readonly WorkspaceInfo[];
66
+ readonly workspaces: readonly WorkspaceInfo[];
67
+ }): {
68
+ readonly diagnostics: readonly PackageRouteReleaseDiagnostic[];
69
+ readonly facts: readonly PackageRouteReleaseFact[];
70
+ readonly intents: readonly PackageRouteReleaseIntent[];
71
+ } => {
72
+ const currentPackages = new Set(
73
+ workspaces
74
+ .filter(isPublishableTrailsPackage)
75
+ .map((workspace) => workspace.name)
76
+ );
77
+ const removedPackages = baseWorkspaces
78
+ .filter(isPublishableTrailsPackage)
79
+ .map((workspace) => workspace.name)
80
+ .filter((name) => !currentPackages.has(name))
81
+ .toSorted();
82
+ const transitions = listGovernedVocabularyTransitions();
83
+ const diagnostics: PackageRouteReleaseDiagnostic[] = [];
84
+ const facts: PackageRouteReleaseFact[] = [];
85
+ const intents: PackageRouteReleaseIntent[] = [];
86
+
87
+ for (const sourcePackage of removedPackages) {
88
+ const resolvedRoute = findPackageRouteTransition(
89
+ transitions,
90
+ sourcePackage
91
+ );
92
+
93
+ if (!resolvedRoute) {
94
+ diagnostics.push({
95
+ code: 'missing-governed-package-route',
96
+ message: `Public package removal '${sourcePackage}' requires an exact governed Regrade route. Add the route to the governed transition registry, then run trails regrade plan/check.`,
97
+ sourcePackage,
98
+ });
99
+ continue;
100
+ }
101
+
102
+ const { target, transition } = resolvedRoute;
103
+
104
+ if (target.kind === 'classified') {
105
+ const classifiedTarget = target;
106
+ const eligiblePackages = [...currentPackages]
107
+ .filter((packageName) =>
108
+ classifiedTarget.options.some(
109
+ (option) =>
110
+ option.to === packageName ||
111
+ option.to.startsWith(`${packageName}/`)
112
+ )
113
+ )
114
+ .toSorted();
115
+ facts.push({
116
+ kind: 'classified',
117
+ sourcePackage,
118
+ transitionId: transition.id,
119
+ });
120
+ intents.push({ eligiblePackages, sourcePackage });
121
+ continue;
122
+ }
123
+
124
+ if (!currentPackages.has(target.to)) {
125
+ diagnostics.push({
126
+ code: 'missing-governed-package-route-target',
127
+ message: `Governed Regrade route '${transition.id}' maps '${sourcePackage}' to '${target.to}', but that publishable target package is absent. Add the target package before applying the route, or use a classified transition with an explicit non-migratable reason for a multi-owner fold.`,
128
+ sourcePackage,
129
+ });
130
+ continue;
131
+ }
132
+
133
+ facts.push({
134
+ kind: 'single',
135
+ sourcePackage,
136
+ targetPackage: target.to,
137
+ transitionId: transition.id,
138
+ });
139
+ intents.push({
140
+ eligiblePackages: [target.to],
141
+ sourcePackage,
142
+ });
143
+ }
144
+
145
+ return { diagnostics, facts, intents };
146
+ };
@@ -12,7 +12,7 @@ import { homedir } from 'node:os';
12
12
  import { dirname, join } from 'node:path';
13
13
 
14
14
  import {
15
- projectPublicSurfaceError,
15
+ renderPublicSurfaceError,
16
16
  Result,
17
17
  ValidationError,
18
18
  } from '@ontrails/core';
@@ -138,9 +138,9 @@ export const runCompletionsInstall = async (
138
138
 
139
139
  const handleCliError = (error: unknown): void => {
140
140
  const err = error instanceof Error ? error : new Error(String(error));
141
- const projection = projectPublicSurfaceError('cli', err);
142
- process.stderr.write(`Error: ${projection.message}\n`);
143
- process.exit(projection.code);
141
+ const rendering = renderPublicSurfaceError('cli', err);
142
+ process.stderr.write(`Error: ${rendering.message}\n`);
143
+ process.exit(rendering.code);
144
144
  };
145
145
 
146
146
  const findCompletionsCommand = (program: Command): Command | undefined =>
package/src/run-trace.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * intrinsic tracing pipeline in `@ontrails/core` records every trail-,
6
6
  * span-, signal-, and activation-level event during the invocation. After
7
7
  * the trail completes (success or failure), the records are rendered as a
8
- * tree to stderr via `renderTraceTree` from `@ontrails/observe`. Under
8
+ * tree to stderr via `renderTraceTree` from `@ontrails/observability`. Under
9
9
  * `--json`, the structured `TraceRecord[]` is also emitted on stdout as
10
10
  * the `tracing` field of a Result envelope.
11
11
  *
@@ -25,8 +25,8 @@
25
25
 
26
26
  import { getTraceSink, registerTraceSink } from '@ontrails/core';
27
27
  import type { TraceRecord, TraceSink } from '@ontrails/core';
28
- import { createMemorySink, renderTraceTree } from '@ontrails/observe';
29
- import type { MemoryTraceSink } from '@ontrails/observe';
28
+ import { createMemorySink, renderTraceTree } from '@ontrails/observability';
29
+ import type { MemoryTraceSink } from '@ontrails/observability';
30
30
 
31
31
  // ---------------------------------------------------------------------------
32
32
  // Argv detection
@@ -166,7 +166,7 @@ const errorFromUnknown = (
166
166
  * On success, the inner value is taken straight from the trail's
167
167
  * `Result.ok(...)` payload. On failure the envelope captures the error's
168
168
  * `name` and `message` -- both safe to serialize and consistent with how
169
- * other Trails surfaces project errors.
169
+ * other Trails surfaces render errors.
170
170
  */
171
171
  export const buildTraceJsonEnvelope = (
172
172
  result: ResultLike,
@@ -18,7 +18,7 @@ import {
18
18
  applyTraceCleanup,
19
19
  countTraceRecords,
20
20
  previewTraceCleanup,
21
- } from '@ontrails/tracing';
21
+ } from '@ontrails/observability/dev';
22
22
 
23
23
  import {
24
24
  removeFileIfPresent,