@ontrails/trails 1.0.0-beta.39 → 1.0.0-beta.41

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +1 -1
  3. package/package.json +17 -17
  4. package/src/app.ts +4 -4
  5. package/src/cli.ts +1 -1
  6. package/src/completions.ts +1 -1
  7. package/src/mcp-app.ts +2 -2
  8. package/src/regrade/history.ts +148 -38
  9. package/src/regrade/plan-artifact.ts +31 -11
  10. package/src/release/native-bun-registry.ts +12 -1
  11. package/src/release/policy.ts +4 -1
  12. package/src/retired-topo-command.ts +1 -1
  13. package/src/run-watch.ts +1 -1
  14. package/src/run-wayfind-outline.ts +6 -2
  15. package/src/trails/adapter-check.ts +8 -8
  16. package/src/trails/add-surface.ts +2 -2
  17. package/src/trails/add-trail.ts +3 -3
  18. package/src/trails/add-verify.ts +2 -2
  19. package/src/trails/compile.ts +9 -9
  20. package/src/trails/completions-complete.ts +10 -10
  21. package/src/trails/completions.ts +2 -2
  22. package/src/trails/create-adapter.ts +185 -424
  23. package/src/trails/create-scaffold.ts +12 -12
  24. package/src/trails/create-versions.ts +8 -8
  25. package/src/trails/create.ts +35 -35
  26. package/src/trails/deprecate.ts +2 -2
  27. package/src/trails/dev-clean.ts +11 -11
  28. package/src/trails/dev-reset.ts +11 -11
  29. package/src/trails/dev-stats.ts +8 -8
  30. package/src/trails/dev-support.ts +1 -1
  31. package/src/trails/doctor.ts +4 -4
  32. package/src/trails/draft-promote.ts +3 -3
  33. package/src/trails/guide.ts +9 -9
  34. package/src/trails/load-app.ts +3 -3
  35. package/src/trails/regrade.ts +56 -27
  36. package/src/trails/release-check.ts +8 -8
  37. package/src/trails/release-smoke.ts +2 -2
  38. package/src/trails/revise.ts +2 -2
  39. package/src/trails/run-example.ts +9 -9
  40. package/src/trails/run-examples.ts +9 -9
  41. package/src/trails/run.ts +26 -26
  42. package/src/trails/survey.ts +45 -45
  43. package/src/trails/topo-activation.ts +2 -2
  44. package/src/trails/topo-history.ts +8 -8
  45. package/src/trails/topo-output-schemas.ts +5 -5
  46. package/src/trails/topo-pin.ts +6 -6
  47. package/src/trails/topo-read-support.ts +4 -3
  48. package/src/trails/topo-reports.ts +16 -16
  49. package/src/trails/topo-store-support.ts +18 -9
  50. package/src/trails/topo-support.ts +2 -2
  51. package/src/trails/topo-unpin.ts +12 -12
  52. package/src/trails/topo.ts +4 -4
  53. package/src/trails/validate.ts +2 -2
  54. package/src/trails/version-lifecycle-support.ts +17 -16
  55. package/src/trails/warden-guide.ts +8 -8
  56. package/src/trails/warden.ts +21 -21
  57. package/src/trails/wayfind-outline.ts +876 -0
  58. package/src/trails/wayfind.ts +74 -74
package/CHANGELOG.md CHANGED
@@ -1,5 +1,93 @@
1
1
  # trails
2
2
 
3
+ ## 1.0.0-beta.41
4
+
5
+ ### Patch Changes
6
+
7
+ - [`19406e0`](https://github.com/outfitter-dev/trails/commit/19406e06f825c5ba4460b153eaf2ade721a749d7): Classify unauthenticated scoped-package dist-tag fallbacks as first publications after npm has already reported the package missing, and preserve registry probe details in release-policy blockers.
8
+
9
+ ## 1.0.0-beta.40
10
+
11
+ ### Minor Changes
12
+
13
+ - [`5adb995`](https://github.com/outfitter-dev/trails/commit/5adb99551c2dda6190d46cce7f60bb08d63c99aa): Complete the v1 hard cutover from the authored `blaze` field to
14
+ `implementation` across trail contracts, surface projections, tests, examples,
15
+ and public source-analysis helpers. Existing applications must rename authored
16
+ trail behavior fields and direct trail-object access before upgrading.
17
+ - [`6712075`](https://github.com/outfitter-dev/trails/commit/67120754df3f614c7f4dd98be1fa0ba9d69b7765): Complete the v1 hard cutover from the `contour` domain-object declaration
18
+ vocabulary to `entity` across contracts, topo facts, store helpers, Warden,
19
+ Wayfinder, operator surfaces, examples, and generated locks. Existing
20
+ applications must rename contour APIs, run `trails dev reset --yes` to discard
21
+ pre-cutover local Topographer snapshots, and then recompile committed
22
+ `trails.lock` artifacts before upgrading. Those derived snapshots are
23
+ intentionally not read through a compatibility layer.
24
+ The entity-shaped wire contract advances `TopoGraph` and split lock manifests
25
+ from schema version 3 to 4; old split artifacts fail with regeneration guidance,
26
+ while the canonical root `trails.lock` remains schema version 5.
27
+ Wayfinder reports those stale rows as topo-store drift while keeping current
28
+ committed lock facts available for inspection.
29
+
30
+ ### Patch Changes
31
+
32
+ - [`4def007`](https://github.com/outfitter-dev/trails/commit/4def007a53c42881fba1d958a67f0c05f417e811): Move adapter source export scanning into adapter-kit and have `create.adapter`
33
+ consume the shared helper.
34
+ - [`78575d5`](https://github.com/outfitter-dev/trails/commit/78575d5193242053b4dc1f4fa6150b94bacaff44): Discover owner-package subpath adapter subjects in shared adapter checks and
35
+ enable `trails create adapter --placement subpath` to generate immediately
36
+ checkable owner subpaths.
37
+ - [`35cbe28`](https://github.com/outfitter-dev/trails/commit/35cbe289db46539b3689dbf6cf8ab0e5d9a1b09c): Found `@ontrails/source` as the shared source-code AST kernel for parsing,
38
+ walking, locations, edits, literals, and generic Trails syntax recognition.
39
+ Warden, Regrade, Wayfinder, and the Trails operator now import those shared
40
+ mechanics from `@ontrails/source`; the legacy Warden AST route is removed by the
41
+ stacked hard cutover.
42
+ - [`10f2492`](https://github.com/outfitter-dev/trails/commit/10f24928d3bc9d995abf7aa261ecf515c295855d): Own the `wayfind.outline` implementation in the Trails operator app while preserving the existing `trails wayfind file <file> --outline` CLI and MCP composition behavior, and document `@ontrails/source` as the operator's live-source analysis kernel.
43
+ - [`35e5fed`](https://github.com/outfitter-dev/trails/commit/35e5fedd228e498783f479f0dd502e2f3ec772b8): Fold the Wayfinder graph-read catalog into `@ontrails/topography`. Wayfind
44
+ remains the product, trail-id, CLI, and MCP brand, but there is no longer an
45
+ `@ontrails/wayfinder` package to install or import. Programmatic consumers
46
+ should move imports such as `wayfinderTopo`, `wayfindOverviewTrail`,
47
+ `loadWayfinderArtifacts`, and the Wayfinder filter/provenance types to
48
+ `@ontrails/topography`.
49
+
50
+ Expose that package move as a governed Regrade transition so exact
51
+ `@ontrails/wayfinder` imports can move safely while product vocabulary and near
52
+ routes remain unchanged for review. Regrade routes package manifests through
53
+ structured review instead of rewriting dependency keys as plain text.
54
+
55
+ The Trails operator now reads all `wayfind.*` query trails and artifact helpers
56
+ from `@ontrails/topography` while preserving the existing CLI/MCP schemas,
57
+ route IDs, output shapes, and internal trail visibility.
58
+
59
+ - [`3a65ae3`](https://github.com/outfitter-dev/trails/commit/3a65ae363e05b7589f4a9876da4346886353b48c): Rename the durable graph substrate package from `@ontrails/topographer` to
60
+ `@ontrails/topography` after folding Wayfind graph queries into that owner.
61
+
62
+ Update imports to `@ontrails/topography` or
63
+ `@ontrails/topography/backend-support`. The pre-1.0 cutover does not ship a
64
+ compatibility package. TopoGraph, lock, topo-store, semantic diff, and Wayfind
65
+ APIs keep their existing contracts, and the `trails wayfind` CLI and MCP names
66
+ remain unchanged.
67
+
68
+ The governed package-route transition moves legacy `@ontrails/wayfinder`
69
+ imports directly to `@ontrails/topography`; it does not emit the retired
70
+ intermediate `@ontrails/topographer` route.
71
+
72
+ - [`e5edace`](https://github.com/outfitter-dev/trails/commit/e5edace60281c76157512acc6e1446c8415633ba): Make Regrade history hashes stable across report serialization and preserve
73
+ pre-apply occurrence evidence alongside truthful completion counts and a
74
+ freshly scanned post-apply completion report for replay detection. Existing
75
+ plans and history stamped with the earlier hash serializer remain valid.
76
+ - [`4030698`](https://github.com/outfitter-dev/trails/commit/40306984467625844564f0f84156530d7118a79c): Keep structured input on nested child commands from being reinterpreted as a
77
+ bare child-name positional fallback, while preserving schema-authored
78
+ `inputJson` flags as ordinary trail input, including through the public Trails
79
+ CLI. Optional numeric flags now consume negative values with Commander's own
80
+ parsing semantics, and variadic flags consume every following value, before
81
+ nested command routing is resolved.
82
+ - [`8a1ac00`](https://github.com/outfitter-dev/trails/commit/8a1ac00b5d789be41ca6e464358c96b01e442bf4): Govern the exact `@ontrails/warden/ast` to `@ontrails/source` package route
83
+ transition for Regrade string-literal and module-specifier rewrites exposed
84
+ through the Trails CLI and MCP tools. Safe rewrites now require the owning
85
+ manifest to already declare the target package; otherwise Regrade preserves the
86
+ occurrence with dependency repair guidance. Invalid manifests remain unchanged
87
+ and produce structured repair guidance that names the owning manifest. Explicit
88
+ preserve rules remain no-ops before dependency validation, and dotted or
89
+ subpath-like near routes remain deferred instead of becoming invented imports.
90
+
3
91
  ## 1.0.0-beta.39
4
92
 
5
93
  ### Minor Changes
package/README.md CHANGED
@@ -15,7 +15,7 @@ Common workflows:
15
15
  - `trails topo` inspects topo state and manages pins/history.
16
16
  - `trails compile` writes root `trails.lock`.
17
17
  - `trails validate` checks root `trails.lock` for drift.
18
- - `trails wayfind`, `trails wayfind --trails --intent read`, `trails wayfind <id> --contract`, `trails wayfind <id> --deps`, `trails wayfind <id> --impact`, `trails wayfind pattern "wayfind.*"`, `trails wayfind query "release drift"`, `trails wayfind file <file> --outline`, and `trails wayfind diff ...` read graph artifacts and source outlines through Wayfinder for local navigation.
18
+ - `trails wayfind`, `trails wayfind --trails --intent read`, `trails wayfind <id> --contract`, `trails wayfind <id> --deps`, `trails wayfind <id> --impact`, `trails wayfind pattern "wayfind.*"`, `trails wayfind query "release drift"`, and `trails wayfind diff ...` read graph artifacts through Wayfinder. `trails wayfind file <file> --outline` is the operator-owned live-source exception, assembled with `@ontrails/source` and enriched with saved graph context when available.
19
19
  - `trails schema <command...>` shows accepted CLI routes, aliases, flags, and schemas for an operator command or command namespace.
20
20
  - `trails warden` runs Trails governance checks for contract and architecture drift. Use `--scope-exclude <glob>` or project `warden.scope.exclude` config when local notes, scratch space, or generated state should not be governed by Warden.
21
21
  - `trails regrade plan <from> <to> --root-dir <path> --json` writes an active Regrade plan, `trails regrade plan --expand` stages wide-net review candidates in that plan, `trails regrade preview` reruns it without writing, and `trails regrade apply` consumes the plan and writes history. Use `plan --expand --dry-run` to inspect staged candidates without writing the active plan, and add `--dry-run` to `apply` to prove the apply path without mutating source. Use `trails regrade plans` and `trails regrade check` when a workspace has active plans that need inspection before apply. MCP exposes the same lifecycle as `trails_plan_regrade`, `trails_list_regrades`, `trails_check_regrade`, `trails_preview_regrade`, and `trails_apply_regrade`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/trails",
3
- "version": "1.0.0-beta.39",
3
+ "version": "1.0.0-beta.41",
4
4
  "bin": {
5
5
  "trails": "./bin/trails.ts"
6
6
  },
@@ -27,26 +27,26 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@clack/prompts": "^1.1.0",
30
- "@ontrails/adapter-kit": "^1.0.0-beta.39",
31
- "@ontrails/cli": "^1.0.0-beta.39",
32
- "@ontrails/commander": "^1.0.0-beta.39",
33
- "@ontrails/config": "^1.0.0-beta.39",
34
- "@ontrails/core": "^1.0.0-beta.39",
35
- "@ontrails/http": "^1.0.0-beta.39",
36
- "@ontrails/mcp": "^1.0.0-beta.39",
37
- "@ontrails/observe": "^1.0.0-beta.39",
38
- "@ontrails/permits": "^1.0.0-beta.39",
39
- "@ontrails/regrade": "^1.0.0-beta.39",
40
- "@ontrails/topographer": "^1.0.0-beta.39",
41
- "@ontrails/tracing": "^1.0.0-beta.39",
42
- "@ontrails/warden": "^1.0.0-beta.39",
43
- "@ontrails/wayfinder": "^1.0.0-beta.39",
30
+ "@ontrails/adapter-kit": "^1.0.0-beta.41",
31
+ "@ontrails/cli": "^1.0.0-beta.41",
32
+ "@ontrails/commander": "^1.0.0-beta.41",
33
+ "@ontrails/config": "^1.0.0-beta.41",
34
+ "@ontrails/core": "^1.0.0-beta.41",
35
+ "@ontrails/http": "^1.0.0-beta.41",
36
+ "@ontrails/mcp": "^1.0.0-beta.41",
37
+ "@ontrails/observe": "^1.0.0-beta.41",
38
+ "@ontrails/permits": "^1.0.0-beta.41",
39
+ "@ontrails/regrade": "^1.0.0-beta.41",
40
+ "@ontrails/source": "^1.0.0-beta.41",
41
+ "@ontrails/topography": "^1.0.0-beta.41",
42
+ "@ontrails/tracing": "^1.0.0-beta.41",
43
+ "@ontrails/warden": "^1.0.0-beta.41",
44
44
  "commander": "^14.0.3",
45
45
  "typescript": "^5.9.3",
46
46
  "zod": "^4.3.5"
47
47
  },
48
48
  "devDependencies": {
49
- "@ontrails/cloudflare": "^1.0.0-beta.39",
50
- "@ontrails/testing": "^1.0.0-beta.39"
49
+ "@ontrails/cloudflare": "^1.0.0-beta.41",
50
+ "@ontrails/testing": "^1.0.0-beta.41"
51
51
  }
52
52
  }
package/src/app.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { surfaceOverlay, topo } from '@ontrails/core';
2
2
  import {
3
3
  wayfindAdaptersTrail,
4
- wayfindContoursTrail,
4
+ wayfindEntitiesTrail,
5
5
  wayfindContractTrail,
6
6
  wayfindDescribeTrail,
7
7
  wayfindDiffTrail,
@@ -12,14 +12,13 @@ import {
12
12
  wayfindImpactTrail,
13
13
  wayfindNearbyTrail,
14
14
  wayfindOverviewTrail,
15
- wayfindOutlineTrail,
16
15
  wayfindResourcesTrail,
17
16
  wayfindSearchTrail,
18
17
  wayfindSignalsTrail,
19
18
  wayfindSurfacesTrail,
20
19
  wayfindTrailsTrail,
21
20
  wayfindVersionsTrail,
22
- } from '@ontrails/wayfinder';
21
+ } from '@ontrails/topography';
23
22
 
24
23
  import * as addSurface from './trails/add-surface.js';
25
24
  import * as addTrail from './trails/add-trail.js';
@@ -55,6 +54,7 @@ import * as validate from './trails/validate.js';
55
54
  import * as warden from './trails/warden.js';
56
55
  import * as wardenGuide from './trails/warden-guide.js';
57
56
  import * as wayfind from './trails/wayfind.js';
57
+ import { wayfindOutlineTrail } from './trails/wayfind-outline.js';
58
58
 
59
59
  export const operatorApp = topo(
60
60
  'trails',
@@ -107,9 +107,9 @@ const cliWayfinderCommandTrails = {
107
107
 
108
108
  const cliWayfinderSupportTrails = {
109
109
  wayfindAdaptersTrail,
110
- wayfindContoursTrail,
111
110
  wayfindContractTrail,
112
111
  wayfindDescribeTrail,
112
+ wayfindEntitiesTrail,
113
113
  wayfindErrorsTrail,
114
114
  wayfindExamplesTrail,
115
115
  wayfindImpactTrail,
package/src/cli.ts CHANGED
@@ -19,7 +19,7 @@ import type {
19
19
  import { createProgram } from '@ontrails/commander';
20
20
  import type { CreateProgramOptions } from '@ontrails/commander';
21
21
  import { resolvePermitFromBearerToken } from '@ontrails/permits';
22
- import { deriveTopoGraph } from '@ontrails/topographer';
22
+ import { deriveTopoGraph } from '@ontrails/topography';
23
23
 
24
24
  import { app, trailsCliIncludedTrails, trailsOverlays } from './app.js';
25
25
  import { resolveInputWithClack } from './clack.js';
@@ -21,7 +21,7 @@ import {
21
21
  Result,
22
22
  ValidationError,
23
23
  } from '@ontrails/core';
24
- import { buildWorkspaceTrailIndex } from '@ontrails/topographer';
24
+ import { buildWorkspaceTrailIndex } from '@ontrails/topography';
25
25
 
26
26
  import { tryLoadFreshAppLease } from './trails/load-app.js';
27
27
 
package/src/mcp-app.ts CHANGED
@@ -10,14 +10,14 @@ import {
10
10
  wayfindImpactTrail,
11
11
  wayfindNearbyTrail,
12
12
  wayfindOverviewTrail,
13
- wayfindOutlineTrail,
14
13
  wayfindResourcesTrail,
15
14
  wayfindSearchTrail,
16
15
  wayfindSurfacesTrail,
17
16
  wayfindTrailsTrail,
18
- } from '@ontrails/wayfinder';
17
+ } from '@ontrails/topography';
19
18
 
20
19
  import { operatorApp } from './app.js';
20
+ import { wayfindOutlineTrail } from './trails/wayfind-outline.js';
21
21
 
22
22
  const operatorTrails = Object.fromEntries(
23
23
  operatorApp.list().map((trailItem) => [trailItem.id, trailItem])
@@ -19,7 +19,10 @@ import {
19
19
  regradePlanContentHash,
20
20
  regradePlanDirectory,
21
21
  regradePlanSlugForBody,
22
+ legacyRegradeSourceHash,
22
23
  regradeSourceHash,
24
+ regradeSourceHashMatches,
25
+ regradeSourceHashes,
23
26
  rootRelativePath,
24
27
  } from './plan-artifact.js';
25
28
  import type { RegradePlanArtifact, RegradePlanBody } from './plan-artifact.js';
@@ -30,8 +33,23 @@ import type { RegradePlanArtifact, RegradePlanBody } from './plan-artifact.js';
30
33
  */
31
34
  export const REGRADE_HISTORY_SCHEMA_VERSION = 2;
32
35
 
36
+ const rawCompletionReportHash = Symbol('rawCompletionReportHash');
37
+ const rawReportHash = Symbol('rawReportHash');
38
+ const rawCompletionReport = Symbol('rawCompletionReport');
39
+ const rawReport = Symbol('rawReport');
40
+
33
41
  const regradeHistoryRunSchema = z
34
42
  .object({
43
+ completionReport: regradeReportOutput
44
+ .optional()
45
+ .describe(
46
+ 'Post-apply source evidence used to recognize a completed-state replay'
47
+ ),
48
+ completionReportHash: z
49
+ .string()
50
+ .regex(/^[0-9a-f]{64}$/)
51
+ .optional()
52
+ .describe('Canonical hash of the post-apply completion report'),
35
53
  lockHashAtRun: z
36
54
  .string()
37
55
  .min(1)
@@ -58,6 +76,12 @@ const regradeHistoryArtifactSchema = z
58
76
  .strict();
59
77
 
60
78
  interface RegradeHistoryRun {
79
+ readonly [rawCompletionReportHash]?: string;
80
+ readonly [rawCompletionReport]?: RegradeReport;
81
+ readonly [rawReportHash]?: string;
82
+ readonly [rawReport]?: RegradeReport;
83
+ readonly completionReport: RegradeReport;
84
+ readonly completionReportHash: string;
61
85
  readonly lockHashAtRun: string;
62
86
  readonly plan: RegradePlanArtifact;
63
87
  readonly planContentHash: string;
@@ -115,7 +139,46 @@ export const readRegradeHistoryArtifact = (
115
139
  })
116
140
  );
117
141
  }
118
- return Result.ok(parsed.data as unknown as RegradeHistoryArtifact);
142
+ const rawRuns = (
143
+ parsedJson as {
144
+ readonly runs: readonly {
145
+ readonly completionReport?: RegradeReport;
146
+ readonly report: RegradeReport;
147
+ }[];
148
+ }
149
+ ).runs;
150
+ return Result.ok({
151
+ ...parsed.data,
152
+ runs: parsed.data.runs.map((run, index) => {
153
+ // Early schema-v2 histories recorded only the post-apply report. Treat
154
+ // that report and its lock hash as completion evidence when reading
155
+ // those artifacts.
156
+ const completionReport = run.completionReport ?? run.report;
157
+ const normalized = {
158
+ ...run,
159
+ completionReport,
160
+ completionReportHash: run.completionReportHash ?? run.lockHashAtRun,
161
+ } as RegradeHistoryRun;
162
+ const rawRun = rawRuns[index];
163
+ if (rawRun !== undefined) {
164
+ Object.defineProperties(normalized, {
165
+ [rawCompletionReport]: {
166
+ value: rawRun.completionReport ?? rawRun.report,
167
+ },
168
+ [rawCompletionReportHash]: {
169
+ value: legacyRegradeSourceHash(
170
+ rawRun.completionReport ?? rawRun.report
171
+ ),
172
+ },
173
+ [rawReportHash]: {
174
+ value: legacyRegradeSourceHash(rawRun.report),
175
+ },
176
+ [rawReport]: { value: rawRun.report },
177
+ });
178
+ }
179
+ return normalized;
180
+ }),
181
+ } as RegradeHistoryArtifact);
119
182
  };
120
183
 
121
184
  /**
@@ -132,14 +195,66 @@ export const mintTransitionId = (
132
195
  .digest('hex')
133
196
  .slice(0, 12);
134
197
 
198
+ /**
199
+ * Verify every recorded run at its own stamped lock: recompute the plan
200
+ * content hash and lock hash from the recorded plan and report, then compare
201
+ * with the stamped values.
202
+ */
203
+ export const verifyRegradeHistoryRuns = (
204
+ artifact: RegradeHistoryArtifact
205
+ ): TrailsResult<{ readonly runs: number }, ValidationError> => {
206
+ for (const [index, run] of artifact.runs.entries()) {
207
+ if (regradePlanContentHash(run.plan.plan) !== run.planContentHash) {
208
+ return Result.err(
209
+ new ValidationError('Regrade history run stamp mismatch.', {
210
+ context: {
211
+ field: 'planContentHash',
212
+ path: artifact.path,
213
+ run: index,
214
+ },
215
+ })
216
+ );
217
+ }
218
+ if (
219
+ !regradeSourceHashMatches(run.lockHashAtRun, run.report) &&
220
+ run[rawReportHash] !== run.lockHashAtRun
221
+ ) {
222
+ return Result.err(
223
+ new ValidationError('Regrade history run stamp mismatch.', {
224
+ context: { field: 'lockHashAtRun', path: artifact.path, run: index },
225
+ })
226
+ );
227
+ }
228
+ if (
229
+ !regradeSourceHashMatches(
230
+ run.completionReportHash,
231
+ run.completionReport
232
+ ) &&
233
+ run[rawCompletionReportHash] !== run.completionReportHash
234
+ ) {
235
+ return Result.err(
236
+ new ValidationError('Regrade history run stamp mismatch.', {
237
+ context: {
238
+ field: 'completionReportHash',
239
+ path: artifact.path,
240
+ run: index,
241
+ },
242
+ })
243
+ );
244
+ }
245
+ }
246
+ return Result.ok({ runs: artifact.runs.length });
247
+ };
248
+
135
249
  /**
136
250
  * Append one applied run to the transition's consolidated history file. A
137
- * run whose plan content hash and lock hash both equal the last recorded
138
- * run's stamps is a replay: nothing is written and `status: 'replay'` is
139
- * surfaced instead of a duplicate record.
251
+ * run whose plan content hash and source evidence equal either the last run's
252
+ * pre-apply report or completed state is a replay: nothing is written and
253
+ * `status: 'replay'` is surfaced instead of a duplicate record.
140
254
  */
141
255
  export const appendRegradeHistoryRun = (params: {
142
256
  readonly artifact: RegradePlanArtifact;
257
+ readonly completedReport?: RegradeReport;
143
258
  readonly report: RegradeReport;
144
259
  readonly rootDir: string;
145
260
  }): TrailsResult<RegradeHistorySummary, Error> => {
@@ -150,7 +265,11 @@ export const appendRegradeHistoryRun = (params: {
150
265
  const relativePath = rootRelativePath(params.rootDir, absolutePath);
151
266
  const planContentHash = regradePlanContentHash(params.artifact.plan);
152
267
  const lockHashAtRun = regradeSourceHash(params.report);
268
+ const currentSourceHashes = regradeSourceHashes(params.report);
269
+ const completionReport = params.completedReport ?? params.report;
153
270
  const entry: RegradeHistoryRun = {
271
+ completionReport,
272
+ completionReportHash: regradeSourceHash(completionReport),
154
273
  lockHashAtRun,
155
274
  plan: params.artifact,
156
275
  planContentHash,
@@ -164,6 +283,10 @@ export const appendRegradeHistoryRun = (params: {
164
283
  return existing;
165
284
  }
166
285
  prior = existing.value;
286
+ const verified = verifyRegradeHistoryRuns(prior);
287
+ if (verified.isErr()) {
288
+ return verified;
289
+ }
167
290
  if (
168
291
  params.artifact.transitionId !== undefined &&
169
292
  params.artifact.transitionId !== prior.id
@@ -208,7 +331,8 @@ export const appendRegradeHistoryRun = (params: {
208
331
  if (
209
332
  lastRun !== undefined &&
210
333
  lastRun.planContentHash === planContentHash &&
211
- lastRun.lockHashAtRun === lockHashAtRun
334
+ (currentSourceHashes.includes(lastRun.lockHashAtRun) ||
335
+ currentSourceHashes.includes(lastRun.completionReportHash))
212
336
  ) {
213
337
  return Result.ok({
214
338
  path: relativePath,
@@ -233,7 +357,21 @@ export const appendRegradeHistoryRun = (params: {
233
357
  runs: prior === undefined ? [entry] : [...prior.runs, entry],
234
358
  schemaVersion: REGRADE_HISTORY_SCHEMA_VERSION,
235
359
  };
236
- const parsed = regradeHistoryArtifactSchema.safeParse(artifact);
360
+ const writableArtifact = {
361
+ id: artifact.id,
362
+ kind: artifact.kind,
363
+ path: artifact.path,
364
+ runs: artifact.runs.map((run) => ({
365
+ completionReport: run[rawCompletionReport] ?? run.completionReport,
366
+ completionReportHash: run.completionReportHash,
367
+ lockHashAtRun: run.lockHashAtRun,
368
+ plan: run.plan,
369
+ planContentHash: run.planContentHash,
370
+ report: run[rawReport] ?? run.report,
371
+ })),
372
+ schemaVersion: artifact.schemaVersion,
373
+ };
374
+ const parsed = regradeHistoryArtifactSchema.safeParse(writableArtifact);
237
375
  if (!parsed.success) {
238
376
  return Result.err(
239
377
  new ValidationError('Invalid Regrade history artifact.', {
@@ -243,7 +381,10 @@ export const appendRegradeHistoryRun = (params: {
243
381
  }
244
382
  try {
245
383
  mkdirSync(dirname(absolutePath), { recursive: true });
246
- writeFileSync(absolutePath, `${JSON.stringify(parsed.data, null, 2)}\n`);
384
+ writeFileSync(
385
+ absolutePath,
386
+ `${JSON.stringify(writableArtifact, null, 2)}\n`
387
+ );
247
388
  } catch (error) {
248
389
  return Result.err(
249
390
  new InternalError('Failed to write Regrade history entry.', {
@@ -259,37 +400,6 @@ export const appendRegradeHistoryRun = (params: {
259
400
  });
260
401
  };
261
402
 
262
- /**
263
- * Verify every recorded run at its own stamped lock: recompute the plan
264
- * content hash and lock hash from the recorded plan and report, then compare
265
- * with the stamped values.
266
- */
267
- export const verifyRegradeHistoryRuns = (
268
- artifact: RegradeHistoryArtifact
269
- ): TrailsResult<{ readonly runs: number }, ValidationError> => {
270
- for (const [index, run] of artifact.runs.entries()) {
271
- if (regradePlanContentHash(run.plan.plan) !== run.planContentHash) {
272
- return Result.err(
273
- new ValidationError('Regrade history run stamp mismatch.', {
274
- context: {
275
- field: 'planContentHash',
276
- path: artifact.path,
277
- run: index,
278
- },
279
- })
280
- );
281
- }
282
- if (regradeSourceHash(run.report) !== run.lockHashAtRun) {
283
- return Result.err(
284
- new ValidationError('Regrade history run stamp mismatch.', {
285
- context: { field: 'lockHashAtRun', path: artifact.path, run: index },
286
- })
287
- );
288
- }
289
- }
290
- return Result.ok({ runs: artifact.runs.length });
291
- };
292
-
293
403
  const hasPathSeparator = (value: string): boolean =>
294
404
  value.includes('/') || value.includes('\\');
295
405
 
@@ -223,17 +223,6 @@ const sourceHashEntryFacts = (
223
223
  ...(reviewDetails === undefined ? {} : { reviewDetails }),
224
224
  }));
225
225
 
226
- export const regradeSourceHash = (report: RegradeReport): string =>
227
- createHash('sha256')
228
- .update(
229
- JSON.stringify({
230
- entries: sourceHashEntryFacts(report.entries),
231
- ledger: report.run?.ledger,
232
- selectedClassIds: report.selectedClassIds,
233
- })
234
- )
235
- .digest('hex');
236
-
237
226
  const canonicalizeJsonValue = (value: unknown): unknown => {
238
227
  if (Array.isArray(value)) {
239
228
  return value.map((entry) => canonicalizeJsonValue(entry));
@@ -256,6 +245,37 @@ const canonicalizeJsonValue = (value: unknown): unknown => {
256
245
  export const canonicalJsonStringify = (value: unknown): string =>
257
246
  JSON.stringify(canonicalizeJsonValue(value));
258
247
 
248
+ const regradeSourceHashFacts = (report: RegradeReport): unknown => ({
249
+ entries: sourceHashEntryFacts(report.entries),
250
+ ledger: report.run?.ledger,
251
+ selectedClassIds: report.selectedClassIds,
252
+ });
253
+
254
+ const hashSerializedSourceFacts = (serialized: string): string =>
255
+ createHash('sha256').update(serialized).digest('hex');
256
+
257
+ export const regradeSourceHash = (report: RegradeReport): string =>
258
+ hashSerializedSourceFacts(
259
+ canonicalJsonStringify(regradeSourceHashFacts(report))
260
+ );
261
+
262
+ export const legacyRegradeSourceHash = (report: RegradeReport): string =>
263
+ hashSerializedSourceFacts(JSON.stringify(regradeSourceHashFacts(report)));
264
+
265
+ /** Match source evidence written before canonical JSON hashing. */
266
+ export const regradeSourceHashMatches = (
267
+ stampedHash: string,
268
+ report: RegradeReport
269
+ ): boolean =>
270
+ stampedHash === regradeSourceHash(report) ||
271
+ stampedHash === legacyRegradeSourceHash(report);
272
+
273
+ export const regradeSourceHashes = (
274
+ report: RegradeReport
275
+ ): readonly string[] => [
276
+ ...new Set([regradeSourceHash(report), legacyRegradeSourceHash(report)]),
277
+ ];
278
+
259
279
  /**
260
280
  * Canonical content hash of a resolved Regrade plan body — the authored
261
281
  * migration intent. Stable across key insertion order; changes on any edit to
@@ -339,6 +339,11 @@ const isNpmNotFoundOutput = (stdout: string, stderr: string): boolean => {
339
339
  return combined.includes('E404') || combined.includes('404 Not Found');
340
340
  };
341
341
 
342
+ const isNpmUnauthorizedOutput = (stdout: string, stderr: string): boolean => {
343
+ const combined = `${stdout}\n${stderr}`;
344
+ return combined.includes('E401') || combined.includes('401 Unauthorized');
345
+ };
346
+
342
347
  const isNpmExactVersionMissingOutput = (
343
348
  stdout: string,
344
349
  stderr: string
@@ -403,7 +408,13 @@ const npmDistTagRegistryView = async (
403
408
  ): Promise<NpmView | null> => {
404
409
  const { exitCode, stderr, stdout } = await runNpm(['dist-tag', 'ls', name]);
405
410
  if (exitCode !== 0) {
406
- if (isNpmNotFoundOutput(stdout, stderr)) {
411
+ // npm returns E401 for the dist-tag endpoint of an unpublished scoped
412
+ // package, even though the preceding package view returned E404. At this
413
+ // fallback boundary both responses mean the package does not exist yet.
414
+ if (
415
+ isNpmNotFoundOutput(stdout, stderr) ||
416
+ isNpmUnauthorizedOutput(stdout, stderr)
417
+ ) {
407
418
  return null;
408
419
  }
409
420
  throw new Error(stderr.trim() || `npm dist-tag ls failed for ${name}`);
@@ -59,6 +59,7 @@ export interface ReleasePolicySourcePullRequest {
59
59
  }
60
60
 
61
61
  export interface ReleasePolicyRegistryPackage {
62
+ readonly error?: string | undefined;
62
63
  readonly expectedTagVersion?: string | undefined;
63
64
  readonly name: string;
64
65
  readonly status: 'inaccessible' | 'missing' | 'published';
@@ -462,6 +463,7 @@ const readLabelFamily = <T extends string>(
462
463
  const factsFromPolicyPackage = (
463
464
  entry: ReleasePolicyRegistryPackage
464
465
  ): PackageRegistryFacts => ({
466
+ error: entry.error,
465
467
  expectedTagVersion: entry.expectedTagVersion,
466
468
  status: entry.status,
467
469
  targetVersion: entry.version,
@@ -485,7 +487,7 @@ const registryBlockers = (
485
487
  packages.flatMap((entry) => {
486
488
  const state = classifyPackageRegistryState(factsFromPolicyPackage(entry));
487
489
  if (state.kind === 'registry-inaccessible') {
488
- return [`${entry.name}: registry state is inaccessible`];
490
+ return [`${entry.name}: registry state is inaccessible: ${state.error}`];
489
491
  }
490
492
  if (state.kind === 'tag-points-ahead') {
491
493
  return [
@@ -986,6 +988,7 @@ const registryPackagesFromResults = (
986
988
  };
987
989
  }
988
990
  return {
991
+ ...(result.status === 'inaccessible' ? { error: result.error } : {}),
989
992
  name: result.name,
990
993
  status: result.status,
991
994
  version: result.workspaceVersion,
@@ -30,7 +30,7 @@ export const getRetiredTopoCommandDiagnostic = (
30
30
 
31
31
  return {
32
32
  attempted,
33
- message: `"${attempted}" was retired. Use "${replacement}" instead.\nTopographer artifact commands now live at the top level: "trails compile", "trails validate", and "trails diff". "trails topo" is for topo-store history, pin, and unpin.`,
33
+ message: `"${attempted}" was retired. Use "${replacement}" instead.\nTopography artifact commands now live at the top level: "trails compile", "trails validate", and "trails diff". "trails topo" is for topo-store history, pin, and unpin.`,
34
34
  replacement,
35
35
  };
36
36
  };
package/src/run-watch.ts CHANGED
@@ -27,7 +27,7 @@ import { watch as nodeWatch } from 'node:fs';
27
27
  import type { FSWatcher } from 'node:fs';
28
28
  import { dirname, extname } from 'node:path';
29
29
 
30
- import type { TopoGraphEntry } from '@ontrails/topographer';
30
+ import type { TopoGraphEntry } from '@ontrails/topography';
31
31
 
32
32
  // ---------------------------------------------------------------------------
33
33
  // Constants
@@ -1,7 +1,11 @@
1
1
  import { deriveOutputMode, output } from '@ontrails/cli';
2
2
  import type { ActionResultContext } from '@ontrails/cli';
3
- import { outlineOutputSchema } from '@ontrails/wayfinder';
4
- import type { OutlineFeature, OutlineOutput } from '@ontrails/wayfinder';
3
+
4
+ import { outlineOutputSchema } from './trails/wayfind-outline.js';
5
+ import type {
6
+ OutlineFeature,
7
+ OutlineOutput,
8
+ } from './trails/wayfind-outline.js';
5
9
 
6
10
  const includesFeature = (
7
11
  outline: OutlineOutput,