@ontrails/source 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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,53 @@
1
+ # @ontrails/source
2
+
3
+ ## 1.0.0-beta.41
4
+
5
+ ## 1.0.0-beta.40
6
+
7
+ ### Minor Changes
8
+
9
+ - [`35cbe28`](https://github.com/outfitter-dev/trails/commit/35cbe289db46539b3689dbf6cf8ab0e5d9a1b09c): Found `@ontrails/source` as the shared source-code AST kernel for parsing,
10
+ walking, locations, edits, literals, and generic Trails syntax recognition.
11
+ Warden, Regrade, Wayfinder, and the Trails operator now import those shared
12
+ mechanics from `@ontrails/source`; the legacy Warden AST route is removed by the
13
+ stacked hard cutover.
14
+
15
+ ### Patch Changes
16
+
17
+ - [`3531b58`](https://github.com/outfitter-dev/trails/commit/3531b58ba5320753d6d2594257ef71bc950d28a1): Add the advisory captured-kernel Warden rule for ownership review when a public
18
+ subpath re-exports package internals and multiple production workspaces consume
19
+ that subpath, including import-then-export barrels that preserve the internal
20
+ binding through a local alias or default export.
21
+
22
+ Expose typed import-kind inspection from `@ontrails/source` so project rules
23
+ can keep erased type bindings separate from runtime exports.
24
+
25
+ - [`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.
26
+ - [`35e5fed`](https://github.com/outfitter-dev/trails/commit/35e5fedd228e498783f479f0dd502e2f3ec772b8): Fold the Wayfinder graph-read catalog into `@ontrails/topography`. Wayfind
27
+ remains the product, trail-id, CLI, and MCP brand, but there is no longer an
28
+ `@ontrails/wayfinder` package to install or import. Programmatic consumers
29
+ should move imports such as `wayfinderTopo`, `wayfindOverviewTrail`,
30
+ `loadWayfinderArtifacts`, and the Wayfinder filter/provenance types to
31
+ `@ontrails/topography`.
32
+
33
+ Expose that package move as a governed Regrade transition so exact
34
+ `@ontrails/wayfinder` imports can move safely while product vocabulary and near
35
+ routes remain unchanged for review. Regrade routes package manifests through
36
+ structured review instead of rewriting dependency keys as plain text.
37
+
38
+ The Trails operator now reads all `wayfind.*` query trails and artifact helpers
39
+ from `@ontrails/topography` while preserving the existing CLI/MCP schemas,
40
+ route IDs, output shapes, and internal trail visibility.
41
+
42
+ - [`3a65ae3`](https://github.com/outfitter-dev/trails/commit/3a65ae363e05b7589f4a9876da4346886353b48c): Rename the durable graph substrate package from `@ontrails/topographer` to
43
+ `@ontrails/topography` after folding Wayfind graph queries into that owner.
44
+
45
+ Update imports to `@ontrails/topography` or
46
+ `@ontrails/topography/backend-support`. The pre-1.0 cutover does not ship a
47
+ compatibility package. TopoGraph, lock, topo-store, semantic diff, and Wayfind
48
+ APIs keep their existing contracts, and the `trails wayfind` CLI and MCP names
49
+ remain unchanged.
50
+
51
+ The governed package-route transition moves legacy `@ontrails/wayfinder`
52
+ imports directly to `@ontrails/topography`; it does not emit the retired
53
+ intermediate `@ontrails/topographer` route.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # @ontrails/source
2
+
3
+ Shared source-code machinery for Trails packages and repo tooling.
4
+
5
+ `source` means source code: TypeScript and JavaScript text parsed into an OXC AST. It does not mean activation source, signal source, data source, event source, or execution source.
6
+
7
+ ## What It Owns
8
+
9
+ `@ontrails/source` owns reusable source-code mechanics:
10
+
11
+ - AST node guards and accessors for the OXC node shapes Trails tooling uses.
12
+ - `parse` and `parseWithDiagnostics` wrappers over `oxc-parser`.
13
+ - `walk`, parent-aware walking, and scope-aware walking over `oxc-walker`.
14
+ - Source locations, source edits, literal extraction, and generic Trails syntax recognition.
15
+ - Generic trail/entity discovery helpers such as `findTrailDefinitions`, `findImplementationBodies`, `findEntityDefinitions`, and `isImplementationCall`.
16
+
17
+ The package root is the public API. Import from `@ontrails/source`; there are no supported `/ast`, `/trails`, or `/utils` subpaths.
18
+
19
+ ## Package Admission Test
20
+
21
+ `@ontrails/source` exists because the same source-code contract is reused by independent toolchain owners:
22
+
23
+ - Warden uses it to implement source-static governance without owning the parser facade.
24
+ - Regrade uses it for safe downstream source rewrites.
25
+ - The Trails operator uses it to assemble live source-file outlines.
26
+ - The `trails` operator uses it for draft promotion and version-lifecycle support.
27
+
28
+ The package is admitted only for reusable source machinery with at least two independent toolchain owners and a genuinely shared contract. It must not absorb product verdicts, release plans, query semantics, rendering, Warden rule policy, Regrade engines, Topography artifact assembly, or Wayfinder answer composition.
29
+
30
+ This boundary follows the package-worthiness rule in [ADR-0051: Package Ownership Follows Natural Altitude](../../docs/adr/0051-package-ownership-follows-natural-altitude.md): move code when the natural owner is above one consumer, not when a file merely feels crowded.
31
+
32
+ ## Examples
33
+
34
+ Parse source and inspect trail declarations:
35
+
36
+ ```ts
37
+ import { findTrailDefinitions, parse } from '@ontrails/source';
38
+
39
+ const ast = parse(
40
+ 'example.ts',
41
+ "import { trail } from '@ontrails/core';\nexport const show = trail('user.show', {});\n"
42
+ );
43
+
44
+ const trailIds = ast ? findTrailDefinitions(ast).map((trail) => trail.id) : [];
45
+ ```
46
+
47
+ Walk source with parent context:
48
+
49
+ ```ts
50
+ import { parse, walkWithParents } from '@ontrails/source';
51
+
52
+ const ast = parse('example.ts', 'const value = trail("demo.show", {});\n');
53
+ const callParents: string[] = [];
54
+
55
+ if (ast) {
56
+ walkWithParents(ast, (node, context) => {
57
+ if (node.type === 'CallExpression') {
58
+ callParents.push(`${context.parent?.type ?? 'root'}:${String(context.key)}`);
59
+ }
60
+ });
61
+ }
62
+ ```
63
+
64
+ Apply source edits:
65
+
66
+ ```ts
67
+ import { applySourceEdits, createSourceEdit } from '@ontrails/source';
68
+
69
+ const updated = applySourceEdits('const name = "old";\n', [
70
+ createSourceEdit(14, 17, 'new'),
71
+ ]);
72
+ ```
73
+
74
+ ## Non-Goals
75
+
76
+ - Warden rule policy and rule-specific facts remain in `@ontrails/warden`.
77
+ - Regrade migration planning and execution remain in `@ontrails/regrade`.
78
+ - Topography graph artifacts and outline assembly remain outside this package.
79
+ - Wayfind query trails and answer rendering remain in `@ontrails/topography`.
80
+ - Runtime Trails contracts remain in `@ontrails/core`.
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@ontrails/source",
3
+ "version": "1.0.0-beta.41",
4
+ "description": "Shared source-code AST parsing, walking, location, edit, literal, and Trails syntax helpers.",
5
+ "files": [
6
+ "src/**/*.ts",
7
+ "!src/**/__tests__/**",
8
+ "!src/**/*.test.ts",
9
+ "!src/**/*.test-d.ts",
10
+ "README.md",
11
+ "CHANGELOG.md"
12
+ ],
13
+ "type": "module",
14
+ "exports": {
15
+ ".": "./src/index.ts",
16
+ "./package.json": "./package.json"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc -b",
20
+ "test": "bun test",
21
+ "typecheck": "tsc --noEmit",
22
+ "lint": "oxlint ./src",
23
+ "clean": "rm -rf dist *.tsbuildinfo"
24
+ },
25
+ "dependencies": {
26
+ "oxc-parser": "^0.121.0",
27
+ "oxc-walker": "^1.0.0"
28
+ }
29
+ }
package/src/edits.ts ADDED
@@ -0,0 +1,57 @@
1
+ /** Shared source-edit helpers. */
2
+
3
+ import type { SourceEdit } from './nodes.js';
4
+
5
+ export const createSourceEdit = (
6
+ start: number,
7
+ end: number,
8
+ replacement: string
9
+ ): SourceEdit => ({ end, replacement, start });
10
+
11
+ export const validateSourceEdits = (
12
+ edits: readonly SourceEdit[],
13
+ sourceLength?: number
14
+ ): readonly SourceEdit[] => {
15
+ const ordered = [...edits].toSorted(
16
+ (left, right) => left.start - right.start
17
+ );
18
+ for (let i = 0; i < ordered.length; i += 1) {
19
+ const edit = ordered[i];
20
+ if (!edit) {
21
+ continue;
22
+ }
23
+ if (
24
+ !Number.isSafeInteger(edit.start) ||
25
+ !Number.isSafeInteger(edit.end) ||
26
+ edit.start < 0 ||
27
+ edit.end < edit.start ||
28
+ (sourceLength !== undefined && edit.end > sourceLength)
29
+ ) {
30
+ throw new Error(`Invalid source edit range ${edit.start}-${edit.end}.`);
31
+ }
32
+
33
+ const previous = ordered[i - 1];
34
+ if (previous && edit.start < previous.end) {
35
+ throw new Error(
36
+ `Overlapping source edits ${previous.start}-${previous.end} and ${edit.start}-${edit.end}.`
37
+ );
38
+ }
39
+ }
40
+
41
+ return ordered;
42
+ };
43
+
44
+ export const applySourceEdits = (
45
+ sourceCode: string,
46
+ edits: readonly SourceEdit[]
47
+ ): string => {
48
+ validateSourceEdits(edits, sourceCode.length);
49
+
50
+ return [...edits]
51
+ .toSorted((left, right) => right.start - left.start)
52
+ .reduce(
53
+ (output, edit) =>
54
+ output.slice(0, edit.start) + edit.replacement + output.slice(edit.end),
55
+ sourceCode
56
+ );
57
+ };
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ export * from './nodes.js';
2
+ export * from './parse.js';
3
+ export * from './walk.js';
4
+ export * from './scopes.js';
5
+ export * from './locations.js';
6
+ export * from './edits.js';
7
+ export * from './literals.js';
8
+ export type {
9
+ EntityDefinition,
10
+ FindEntityDefinitionsOptions,
11
+ FrameworkNamespaceContext,
12
+ TrailDefinition,
13
+ } from './trails.js';
14
+ export {
15
+ buildFrameworkNamespaceContext,
16
+ extractEntityDefinition,
17
+ extractTrailDefinition,
18
+ findEntityDefinitions,
19
+ findImplementationBodies,
20
+ findTrailDefinitions,
21
+ getImportSourceValue,
22
+ isImplementationCall,
23
+ isFrameworkNamespaceSource,
24
+ } from './trails.js';
@@ -0,0 +1,226 @@
1
+ /** Shared literal, string, and static property-key helpers. */
2
+
3
+ import { isAstNode } from './nodes.js';
4
+ import type { AstNode, StringLiteralNode } from './nodes.js';
5
+ import { walk } from './walk.js';
6
+
7
+ export const identifierName = (node: AstNode | undefined): string | null => {
8
+ if (node?.type !== 'Identifier') {
9
+ return null;
10
+ }
11
+ return (node as unknown as { name?: string }).name ?? null;
12
+ };
13
+
14
+ /** Check if a node is a string literal. */
15
+ export const isStringLiteral = (
16
+ node: AstNode | undefined
17
+ ): node is StringLiteralNode => {
18
+ if (!node) {
19
+ return false;
20
+ }
21
+ if (node.type === 'StringLiteral') {
22
+ return true;
23
+ }
24
+ if (node.type === 'Literal') {
25
+ return typeof (node as unknown as { value?: unknown }).value === 'string';
26
+ }
27
+ return false;
28
+ };
29
+
30
+ /** Extract the string value from a string literal node. */
31
+ export const getStringValue = (node: AstNode): string | null => {
32
+ const val = (node as unknown as { value?: unknown }).value;
33
+ return typeof val === 'string' ? val : null;
34
+ };
35
+
36
+ /**
37
+ * Best-effort resolution of `const NAME = 'value'` declarations via regex.
38
+ *
39
+ * Returns the string value if a simple `const <name> = '...'` or `"..."` is
40
+ * found in the source. Returns null for anything more complex. Shared between
41
+ * warden rules that need to resolve identifier references to signal / trail
42
+ * IDs at lint time.
43
+ */
44
+ export const deriveConstString = (
45
+ name: string,
46
+ sourceCode: string
47
+ ): string | null => {
48
+ const pattern = new RegExp(
49
+ `const\\s+${name}\\s*=\\s*(?:'([^']*)'|"([^"]*)")`
50
+ );
51
+ const match = pattern.exec(sourceCode);
52
+ if (!match) {
53
+ return null;
54
+ }
55
+ return match[1] ?? match[2] ?? null;
56
+ };
57
+
58
+ /** Extract a string literal value, or null when the node is not a string. */
59
+ export const extractStringLiteral = (
60
+ node: AstNode | undefined
61
+ ): string | null =>
62
+ node && isStringLiteral(node) ? getStringValue(node) : null;
63
+
64
+ /**
65
+ * Extract the cooked value from a `TemplateLiteral` with no interpolations
66
+ * (e.g. `` `entity.fallback` ``). Template literals with `${...}` expressions
67
+ * cannot be resolved at lint time and return null.
68
+ *
69
+ * Shared helper used by rules that accept both string literals and simple
70
+ * backtick-literal IDs (e.g. `valid-describe-refs`).
71
+ */
72
+ const getSingleQuasi = (node: AstNode): AstNode | null => {
73
+ const expressions =
74
+ (node['expressions'] as readonly AstNode[] | undefined) ?? [];
75
+ if (expressions.length > 0) {
76
+ return null;
77
+ }
78
+ const quasis = (node['quasis'] as readonly AstNode[] | undefined) ?? [];
79
+ return quasis.length === 1 ? (quasis[0] ?? null) : null;
80
+ };
81
+
82
+ export const extractPlainTemplateLiteral = (
83
+ node: AstNode | undefined
84
+ ): string | null => {
85
+ if (!node || node.type !== 'TemplateLiteral') {
86
+ return null;
87
+ }
88
+ const quasi = getSingleQuasi(node);
89
+ if (!quasi) {
90
+ return null;
91
+ }
92
+ const cooked = (quasi as unknown as { value?: { cooked?: unknown } }).value
93
+ ?.cooked;
94
+ return typeof cooked === 'string' ? cooked : null;
95
+ };
96
+
97
+ /**
98
+ * Extract a string value from either a string literal or a plain template
99
+ * literal (no `${...}` expressions). Returns null for anything else.
100
+ */
101
+ export const extractStringOrTemplateLiteral = (
102
+ node: AstNode | undefined
103
+ ): string | null =>
104
+ extractStringLiteral(node) ?? extractPlainTemplateLiteral(node);
105
+
106
+ export interface StringLiteralMatch {
107
+ readonly end: number;
108
+ readonly node: AstNode;
109
+ readonly start: number;
110
+ readonly value: string;
111
+ }
112
+
113
+ export const findStringLiterals = (
114
+ ast: AstNode,
115
+ predicate?: (value: string, node: AstNode) => boolean
116
+ ): StringLiteralMatch[] => {
117
+ const matches: StringLiteralMatch[] = [];
118
+
119
+ walk(ast, (node) => {
120
+ if (!isStringLiteral(node)) {
121
+ return;
122
+ }
123
+
124
+ const value = getStringValue(node);
125
+ if (value === null) {
126
+ return;
127
+ }
128
+
129
+ if (predicate && !predicate(value, node)) {
130
+ return;
131
+ }
132
+
133
+ matches.push({
134
+ end: node.end,
135
+ node,
136
+ start: node.start,
137
+ value,
138
+ });
139
+ });
140
+
141
+ return matches;
142
+ };
143
+
144
+ /** Extract the first string argument from a CallExpression. */
145
+ export const extractFirstStringArg = (node: AstNode): string | null => {
146
+ if (node.type !== 'CallExpression') {
147
+ return null;
148
+ }
149
+
150
+ const args = node['arguments'] as readonly AstNode[] | undefined;
151
+ const [firstArg] = args ?? [];
152
+ return extractStringLiteral(firstArg);
153
+ };
154
+
155
+ export const extractBindingName = (
156
+ node: AstNode | undefined
157
+ ): string | null => {
158
+ if (!node) {
159
+ return null;
160
+ }
161
+ if (node.type === 'Identifier') {
162
+ return identifierName(node);
163
+ }
164
+ if (node.type === 'AssignmentPattern') {
165
+ return identifierName((node as unknown as { left?: AstNode }).left);
166
+ }
167
+ return null;
168
+ };
169
+
170
+ export const staticPropertyKeyName = (key: AstNode): string | null => {
171
+ if (key.type === 'Identifier') {
172
+ return (key as unknown as { name?: string }).name ?? null;
173
+ }
174
+ return isStringLiteral(key) ? getStringValue(key) : null;
175
+ };
176
+
177
+ export const propertyKeyName = (prop: AstNode): string | null => {
178
+ if (prop.type !== 'Property') {
179
+ return null;
180
+ }
181
+ const { computed } = prop as unknown as { computed?: boolean };
182
+ if (computed) {
183
+ return null;
184
+ }
185
+ const key = prop.key as AstNode | undefined;
186
+ return key ? staticPropertyKeyName(key) : null;
187
+ };
188
+
189
+ /** Find a Property node by key name inside an ObjectExpression config. */
190
+ export const findConfigProperty = (
191
+ config: AstNode,
192
+ propertyName: string
193
+ ): AstNode | null => {
194
+ if (config.type !== 'ObjectExpression') {
195
+ return null;
196
+ }
197
+ const properties = config['properties'] as readonly AstNode[] | undefined;
198
+ if (!properties) {
199
+ return null;
200
+ }
201
+ for (const prop of properties) {
202
+ if (propertyKeyName(prop) === propertyName) {
203
+ return prop;
204
+ }
205
+ }
206
+ return null;
207
+ };
208
+
209
+ /**
210
+ * Read a property key or member access identifier.
211
+ *
212
+ * Returns the identifier name for `Identifier` keys, or the underlying
213
+ * string literal value for computed access via `['name']` / `"name"`.
214
+ */
215
+ export const getPropertyName = (node: unknown): string | null => {
216
+ if (typeof node !== 'object' || node === null) {
217
+ return null;
218
+ }
219
+
220
+ const { name } = node as { readonly name?: unknown };
221
+ if (typeof name === 'string') {
222
+ return name;
223
+ }
224
+
225
+ return isAstNode(node) ? extractStringLiteral(node) : null;
226
+ };
@@ -0,0 +1,35 @@
1
+ /** Shared source location helpers. */
2
+
3
+ import type { SourceLocation } from './nodes.js';
4
+
5
+ /** Find the byte offset's line number (1-based) in source code. */
6
+ export const offsetToLine = (sourceCode: string, offset: number): number => {
7
+ let line = 1;
8
+ for (let i = 0; i < offset && i < sourceCode.length; i += 1) {
9
+ if (sourceCode[i] === '\n') {
10
+ line += 1;
11
+ }
12
+ }
13
+ return line;
14
+ };
15
+
16
+ /** Find the byte offset's line and column (1-based) in source code. */
17
+ export const offsetToLineColumn = (
18
+ sourceCode: string,
19
+ offset: number
20
+ ): SourceLocation => {
21
+ let line = 1;
22
+ let column = 1;
23
+ const limit = Math.min(Math.max(offset, 0), sourceCode.length);
24
+
25
+ for (let i = 0; i < limit; i += 1) {
26
+ if (sourceCode[i] === '\n') {
27
+ line += 1;
28
+ column = 1;
29
+ } else {
30
+ column += 1;
31
+ }
32
+ }
33
+
34
+ return { column, line };
35
+ };