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

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.
@@ -43,6 +43,7 @@ export type RegistryResult =
43
43
  readonly status: 'published';
44
44
  readonly version: string;
45
45
  readonly versionPublished: boolean | undefined;
46
+ readonly versionProof?: RegistryVersionProof | undefined;
46
47
  readonly workspaceVersion: string;
47
48
  }
48
49
  | {
@@ -82,18 +83,20 @@ export interface PackageRegistryFacts {
82
83
  readonly error?: string | undefined;
83
84
  }
84
85
 
86
+ /** Consumer-facing evidence for an exact package version. */
87
+ export type RegistryVersionProof =
88
+ | { readonly kind: 'consumer-pack'; readonly published: true }
89
+ | { readonly kind: 'exact-metadata'; readonly published: true }
90
+ | { readonly kind: 'unavailable'; readonly published: false };
91
+
85
92
  /**
86
93
  * Classify a package's registry state from two orthogonal facts — whether the
87
94
  * target version is published, and where the dist-tag points relative to it —
88
95
  * plus reachability. Members are mutually exclusive by construction.
89
96
  *
90
- * `versionPublished` is read strictly only in the behind-tag case: a behind tag
91
- * becomes a `needs-tag-repair` only when the target is known published (`true`);
92
- * `undefined` or `false` route to `needs-publish`, preserving the conservative
93
- * release-policy behavior. When the tag already points at the target, an
94
- * unprobed (`undefined`) state counts as published and yields `complete`, which
95
- * matches the policy `versionPublished ?? true` default. `undefined` means the
96
- * exact-version probe was not run, including policy inputs and compatibility
97
+ * `complete` requires affirmative consumer proof. A matching dist-tag cannot
98
+ * substitute for exact-version metadata or an equivalent package fetch.
99
+ * `undefined` means the consumer probe was not run, including compatibility
97
100
  * callers that supply an injected registry view without a version probe.
98
101
  */
99
102
  export const classifyPackageRegistryState = (
@@ -120,9 +123,9 @@ export const classifyPackageRegistryState = (
120
123
  return { currentTagVersion: expectedTagVersion, kind: 'tag-points-ahead' };
121
124
  }
122
125
  if (tagAtTarget) {
123
- return versionPublished === false
124
- ? { kind: 'needs-publish' }
125
- : { kind: 'complete' };
126
+ return versionPublished === true
127
+ ? { kind: 'complete' }
128
+ : { kind: 'needs-publish' };
126
129
  }
127
130
  if (versionPublished === true) {
128
131
  return { currentTagVersion: expectedTagVersion, kind: 'needs-tag-repair' };
@@ -131,13 +134,16 @@ export const classifyPackageRegistryState = (
131
134
  };
132
135
 
133
136
  /** Map a registry probe result into the classifier's fact shape. */
134
- const factsFromResult = (result: RegistryResult): PackageRegistryFacts => {
137
+ export const factsFromRegistryResult = (
138
+ result: RegistryResult
139
+ ): PackageRegistryFacts => {
135
140
  if (result.status === 'published') {
136
141
  return {
137
142
  expectedTagVersion: result.expectedTagVersion,
138
143
  status: 'published',
139
144
  targetVersion: result.workspaceVersion,
140
- versionPublished: result.versionPublished,
145
+ versionPublished:
146
+ result.versionProof?.published ?? result.versionPublished,
141
147
  };
142
148
  }
143
149
  if (result.status === 'inaccessible') {
@@ -165,7 +171,8 @@ Options:
165
171
  --tag <tag> Expected npm dist-tag. Defaults to .changeset/pre.json
166
172
  tag while in prerelease mode, otherwise "latest".
167
173
  --require-published Fail when any workspace package is missing from npm.
168
- Use after publication to verify every package exists.
174
+ Use after publication to require exact metadata or
175
+ equivalent consumer package-fetch proof.
169
176
  -h, --help Show this help and exit.
170
177
 
171
178
  Exit codes: 0 success, 1 registry posture failure, 2 arg-parse error.`;
@@ -459,12 +466,23 @@ export type RegistryVersionView = (
459
466
  version: string
460
467
  ) => Promise<boolean | undefined>;
461
468
 
469
+ /** Probe with evidence that distinguishes metadata from package fetch proof. */
470
+ export type RegistryVersionProofView = (
471
+ name: string,
472
+ version: string
473
+ ) => Promise<RegistryVersionProof>;
474
+
475
+ export type RegistryVersionProbeView = (
476
+ name: string,
477
+ version: string
478
+ ) => Promise<boolean | RegistryVersionProof | undefined>;
479
+
462
480
  const UNKNOWN_REGISTRY_VERSION_STATE: { readonly published?: boolean } = {};
463
481
  const unknownRegistryVersionView: RegistryVersionView = async () =>
464
482
  UNKNOWN_REGISTRY_VERSION_STATE.published;
465
483
 
466
- export const createNpmRegistryVersionView =
467
- (runNpm: NpmCommandRunner = runNpmCommand): RegistryVersionView =>
484
+ export const createNpmRegistryVersionProofView =
485
+ (runNpm: NpmCommandRunner = runNpmCommand): RegistryVersionProofView =>
468
486
  async (name, version) => {
469
487
  const { exitCode, stderr, stdout } = await runNpm([
470
488
  'view',
@@ -474,12 +492,14 @@ export const createNpmRegistryVersionView =
474
492
  ]);
475
493
 
476
494
  if (exitCode === 0) {
477
- return JSON.parse(stdout.trim()) === version;
495
+ return JSON.parse(stdout.trim()) === version
496
+ ? { kind: 'exact-metadata', published: true }
497
+ : { kind: 'unavailable', published: false };
478
498
  }
479
- if (isNpmExactVersionMissingOutput(stdout, stderr)) {
480
- return false;
481
- }
482
- if (!isNpmNotFoundOutput(stdout, stderr)) {
499
+ if (
500
+ !isNpmExactVersionMissingOutput(stdout, stderr) &&
501
+ !isNpmNotFoundOutput(stdout, stderr)
502
+ ) {
483
503
  throw new Error(
484
504
  stderr.trim() || `npm view failed for ${name}@${version}`
485
505
  );
@@ -496,21 +516,35 @@ export const createNpmRegistryVersionView =
496
516
  packResult.stdout,
497
517
  name,
498
518
  version
499
- );
519
+ )
520
+ ? { kind: 'consumer-pack', published: true }
521
+ : { kind: 'unavailable', published: false };
500
522
  }
501
523
  if (isNpmExactVersionMissingOutput(packResult.stdout, packResult.stderr)) {
502
- return false;
524
+ return { kind: 'unavailable', published: false };
503
525
  }
504
526
  if (isNpmNotFoundOutput(packResult.stdout, packResult.stderr)) {
505
- return false;
527
+ return { kind: 'unavailable', published: false };
506
528
  }
507
529
  throw new Error(
508
530
  packResult.stderr.trim() || `npm pack failed for ${name}@${version}`
509
531
  );
510
532
  };
511
533
 
534
+ export const createNpmRegistryVersionView = (
535
+ runNpm: NpmCommandRunner = runNpmCommand
536
+ ): RegistryVersionView => {
537
+ const proofView = createNpmRegistryVersionProofView(runNpm);
538
+ return async (name, version) => {
539
+ const proof = await proofView(name, version);
540
+ return proof.published;
541
+ };
542
+ };
543
+
512
544
  export const npmRegistryVersionView: RegistryVersionView =
513
545
  createNpmRegistryVersionView();
546
+ export const npmRegistryVersionProofView: RegistryVersionProofView =
547
+ createNpmRegistryVersionProofView();
514
548
 
515
549
  /** Run async tasks with a bounded number in flight, preserving input order. */
516
550
  const mapBounded = async <T, R>(
@@ -536,7 +570,7 @@ const mapBounded = async <T, R>(
536
570
  const checkWorkspaceRegistryPosture = async (
537
571
  workspace: RegistryWorkspace,
538
572
  view: RegistryView,
539
- versionView: RegistryVersionView,
573
+ versionView: RegistryVersionProbeView,
540
574
  expectedTag: string
541
575
  ): Promise<RegistryResult> => {
542
576
  try {
@@ -549,13 +583,19 @@ const checkWorkspaceRegistryPosture = async (
549
583
  };
550
584
  }
551
585
  const distTags = registry['dist-tags'] ?? {};
586
+ const versionProbe = await versionView(workspace.name, workspace.version);
587
+ const versionProof =
588
+ typeof versionProbe === 'object' ? versionProbe : undefined;
589
+ const versionPublished =
590
+ typeof versionProbe === 'object' ? versionProbe.published : versionProbe;
552
591
  return {
553
592
  distTags,
554
593
  expectedTagVersion: distTags[expectedTag],
555
594
  name: workspace.name,
556
595
  status: 'published',
557
596
  version: registry.version ?? '(unknown)',
558
- versionPublished: await versionView(workspace.name, workspace.version),
597
+ ...(versionProof === undefined ? {} : { versionProof }),
598
+ versionPublished,
559
599
  workspaceVersion: workspace.version,
560
600
  };
561
601
  } catch (error) {
@@ -569,14 +609,14 @@ const checkWorkspaceRegistryPosture = async (
569
609
  };
570
610
 
571
611
  type CheckRegistryPostureArgs =
572
- | readonly [versionView: RegistryVersionView, expectedTag: string]
612
+ | readonly [versionView: RegistryVersionProbeView, expectedTag: string]
573
613
  | readonly [expectedTag: string];
574
614
 
575
615
  const normalizeCheckRegistryPostureArgs = (
576
616
  args: CheckRegistryPostureArgs
577
617
  ): {
578
618
  readonly expectedTag: string;
579
- readonly versionView: RegistryVersionView;
619
+ readonly versionView: RegistryVersionProbeView;
580
620
  } => {
581
621
  if (args.length === 1) {
582
622
  return { expectedTag: args[0], versionView: unknownRegistryVersionView };
@@ -592,7 +632,7 @@ export function checkRegistryPosture(
592
632
  export function checkRegistryPosture(
593
633
  workspaces: readonly RegistryWorkspace[],
594
634
  view: RegistryView,
595
- versionView: RegistryVersionView,
635
+ versionView: RegistryVersionProbeView,
596
636
  expectedTag: string
597
637
  ): Promise<RegistryResult[]>;
598
638
  export async function checkRegistryPosture(
@@ -624,6 +664,19 @@ const normalizeRegistryCheckPhase = (
624
664
  return phaseOrRequirePublished ? 'published' : 'ready';
625
665
  };
626
666
 
667
+ const targetVersionFailure = (result: RegistryResult): string => {
668
+ if (result.status !== 'published') {
669
+ return 'is not published';
670
+ }
671
+ if (result.versionProof?.kind === 'unavailable') {
672
+ return 'lacks exact-version metadata and consumer pack proof';
673
+ }
674
+ if (result.versionPublished === undefined) {
675
+ return 'publish state was not probed';
676
+ }
677
+ return 'is not published';
678
+ };
679
+
627
680
  export const registryPostureErrors = (
628
681
  results: readonly RegistryResult[],
629
682
  expectedTag: string,
@@ -632,7 +685,7 @@ export const registryPostureErrors = (
632
685
  const phase = normalizeRegistryCheckPhase(phaseOrRequirePublished);
633
686
  const errors: string[] = [];
634
687
  for (const result of results) {
635
- const state = classifyPackageRegistryState(factsFromResult(result));
688
+ const state = classifyPackageRegistryState(factsFromRegistryResult(result));
636
689
  if (state.kind === 'registry-inaccessible') {
637
690
  errors.push(`${result.name}: registry probe failed: ${state.error}`);
638
691
  continue;
@@ -649,12 +702,8 @@ export const registryPostureErrors = (
649
702
  if (state.kind === 'first-time-package') {
650
703
  errors.push(`${result.name}: package is missing from the registry`);
651
704
  } else if (state.kind === 'needs-publish') {
652
- const targetState =
653
- result.status === 'published' && result.versionPublished === undefined
654
- ? 'publish state was not probed'
655
- : 'is not published';
656
705
  errors.push(
657
- `${result.name}: target version ${result.workspaceVersion} ${targetState}`
706
+ `${result.name}: target version ${result.workspaceVersion} ${targetVersionFailure(result)}`
658
707
  );
659
708
  } else if (state.kind === 'needs-tag-repair') {
660
709
  errors.push(
@@ -673,8 +722,18 @@ export const formatDistTagSummary = (
673
722
  );
674
723
 
675
724
  const formatTargetVersionStatus = (
725
+ proof: RegistryVersionProof | undefined,
676
726
  versionPublished: boolean | undefined
677
727
  ): string => {
728
+ if (proof?.kind === 'exact-metadata') {
729
+ return 'exact-version metadata available';
730
+ }
731
+ if (proof?.kind === 'consumer-pack') {
732
+ return 'exact-version metadata unavailable, consumer pack available';
733
+ }
734
+ if (proof?.kind === 'unavailable') {
735
+ return 'exact-version metadata and consumer pack unavailable';
736
+ }
678
737
  if (versionPublished === true) {
679
738
  return 'target version published';
680
739
  }
@@ -691,7 +750,10 @@ const printResults = (
691
750
  console.log(`Registry preflight for dist-tag "${expectedTag}"`);
692
751
  for (const result of results) {
693
752
  if (result.status === 'published') {
694
- const targetStatus = formatTargetVersionStatus(result.versionPublished);
753
+ const targetStatus = formatTargetVersionStatus(
754
+ result.versionProof,
755
+ result.versionPublished
756
+ );
695
757
  console.log(
696
758
  `✓ ${result.name}@${result.workspaceVersion}: package exists, ${targetStatus} (registry version ${result.version}, expected ${expectedTag}=${result.expectedTagVersion ?? 'missing'}, tags ${formatDistTagSummary(result.distTags)})`
697
759
  );
@@ -709,11 +771,14 @@ const normalizeRegistryPreflightViews = (
709
771
  view: RegistryView | undefined,
710
772
  versionView: RegistryVersionView | undefined
711
773
  ): {
712
- readonly versionView: RegistryVersionView;
774
+ readonly versionView: RegistryVersionProbeView;
713
775
  readonly view: RegistryView;
714
776
  } => {
715
777
  if (view === undefined) {
716
- return { versionView: npmRegistryVersionView, view: npmRegistryView };
778
+ return {
779
+ versionView: npmRegistryVersionProofView,
780
+ view: npmRegistryView,
781
+ };
717
782
  }
718
783
  return { versionView: versionView ?? unknownRegistryVersionView, view };
719
784
  };
@@ -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
+ };
@@ -999,13 +999,13 @@ const registryPackagesFromResults = (
999
999
  const readRegistryPackages = async (
1000
1000
  distTag: string
1001
1001
  ): Promise<readonly ReleasePolicyRegistryPackage[]> => {
1002
- const { checkRegistryPosture, npmRegistryView, npmRegistryVersionView } =
1002
+ const { checkRegistryPosture, npmRegistryView, npmRegistryVersionProofView } =
1003
1003
  await import('./native-bun-registry.js');
1004
1004
  const workspaces = await discoverRegistryWorkspaces(repoRoot);
1005
1005
  const results = await checkRegistryPosture(
1006
1006
  workspaces,
1007
1007
  npmRegistryView,
1008
- npmRegistryVersionView,
1008
+ npmRegistryVersionProofView,
1009
1009
  distTag
1010
1010
  );
1011
1011
  return registryPackagesFromResults(results);
@@ -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,