@astrale-os/sdk 0.4.12 → 0.4.13

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 (158) hide show
  1. package/dist/cli/run.d.ts +8 -2
  2. package/dist/cli/run.d.ts.map +1 -1
  3. package/dist/cli/run.js +51 -1
  4. package/dist/cli/run.js.map +1 -1
  5. package/dist/define/remote-function.d.ts +3 -1
  6. package/dist/define/remote-function.d.ts.map +1 -1
  7. package/dist/define/remote-function.js.map +1 -1
  8. package/dist/dispatch/dispatcher.d.ts +10 -20
  9. package/dist/dispatch/dispatcher.d.ts.map +1 -1
  10. package/dist/dispatch/dispatcher.js +55 -54
  11. package/dist/dispatch/dispatcher.js.map +1 -1
  12. package/dist/dispatch/execute.d.ts +2 -1
  13. package/dist/dispatch/execute.d.ts.map +1 -1
  14. package/dist/dispatch/execute.js +1 -0
  15. package/dist/dispatch/execute.js.map +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/linter/analyze.d.ts +6 -0
  19. package/dist/linter/analyze.d.ts.map +1 -0
  20. package/dist/linter/analyze.js +12 -0
  21. package/dist/linter/analyze.js.map +1 -0
  22. package/dist/linter/diagnostic.d.ts +32 -0
  23. package/dist/linter/diagnostic.d.ts.map +1 -0
  24. package/dist/linter/diagnostic.js +35 -0
  25. package/dist/linter/diagnostic.js.map +1 -0
  26. package/dist/linter/index.d.ts +10 -0
  27. package/dist/linter/index.d.ts.map +1 -0
  28. package/dist/linter/index.js +6 -0
  29. package/dist/linter/index.js.map +1 -0
  30. package/dist/linter/lint.d.ts +8 -0
  31. package/dist/linter/lint.d.ts.map +1 -0
  32. package/dist/linter/lint.js +26 -0
  33. package/dist/linter/lint.js.map +1 -0
  34. package/dist/linter/oxlint/config.d.ts +14 -0
  35. package/dist/linter/oxlint/config.d.ts.map +1 -0
  36. package/dist/linter/oxlint/config.js +18 -0
  37. package/dist/linter/oxlint/config.js.map +1 -0
  38. package/dist/linter/oxlint/plugin.d.ts +13 -0
  39. package/dist/linter/oxlint/plugin.d.ts.map +1 -0
  40. package/dist/linter/oxlint/plugin.js +14 -0
  41. package/dist/linter/oxlint/plugin.js.map +1 -0
  42. package/dist/linter/oxlint/run.d.ts +7 -0
  43. package/dist/linter/oxlint/run.d.ts.map +1 -0
  44. package/dist/linter/oxlint/run.js +122 -0
  45. package/dist/linter/oxlint/run.js.map +1 -0
  46. package/dist/linter/preflight.d.ts +2 -0
  47. package/dist/linter/preflight.d.ts.map +1 -0
  48. package/dist/linter/preflight.js +162 -0
  49. package/dist/linter/preflight.js.map +1 -0
  50. package/dist/linter/project.d.ts +12 -0
  51. package/dist/linter/project.d.ts.map +1 -0
  52. package/dist/linter/project.js +88 -0
  53. package/dist/linter/project.js.map +1 -0
  54. package/dist/linter/report.d.ts +4 -0
  55. package/dist/linter/report.d.ts.map +1 -0
  56. package/dist/linter/report.js +22 -0
  57. package/dist/linter/report.js.map +1 -0
  58. package/dist/linter/rules/catalog.d.ts +56 -0
  59. package/dist/linter/rules/catalog.d.ts.map +1 -0
  60. package/dist/linter/rules/catalog.js +53 -0
  61. package/dist/linter/rules/catalog.js.map +1 -0
  62. package/dist/linter/rules/core-has-no-async.d.ts +3 -0
  63. package/dist/linter/rules/core-has-no-async.d.ts.map +1 -0
  64. package/dist/linter/rules/core-has-no-async.js +43 -0
  65. package/dist/linter/rules/core-has-no-async.js.map +1 -0
  66. package/dist/linter/rules/core-is-pure.d.ts +3 -0
  67. package/dist/linter/rules/core-is-pure.d.ts.map +1 -0
  68. package/dist/linter/rules/core-is-pure.js +74 -0
  69. package/dist/linter/rules/core-is-pure.js.map +1 -0
  70. package/dist/linter/rules/no-nested-steps.d.ts +3 -0
  71. package/dist/linter/rules/no-nested-steps.d.ts.map +1 -0
  72. package/dist/linter/rules/no-nested-steps.js +32 -0
  73. package/dist/linter/rules/no-nested-steps.js.map +1 -0
  74. package/dist/linter/rules/no-read-inside-mutate.d.ts +3 -0
  75. package/dist/linter/rules/no-read-inside-mutate.d.ts.map +1 -0
  76. package/dist/linter/rules/no-read-inside-mutate.js +64 -0
  77. package/dist/linter/rules/no-read-inside-mutate.js.map +1 -0
  78. package/dist/linter/rules/one-remote-definition-per-file.d.ts +3 -0
  79. package/dist/linter/rules/one-remote-definition-per-file.d.ts.map +1 -0
  80. package/dist/linter/rules/one-remote-definition-per-file.js +23 -0
  81. package/dist/linter/rules/one-remote-definition-per-file.js.map +1 -0
  82. package/dist/linter/rules/oxlint-ast.d.ts +30 -0
  83. package/dist/linter/rules/oxlint-ast.d.ts.map +1 -0
  84. package/dist/linter/rules/oxlint-ast.js +104 -0
  85. package/dist/linter/rules/oxlint-ast.js.map +1 -0
  86. package/dist/linter/rules/oxlint-rule.d.ts +45 -0
  87. package/dist/linter/rules/oxlint-rule.d.ts.map +1 -0
  88. package/dist/linter/rules/oxlint-rule.js +2 -0
  89. package/dist/linter/rules/oxlint-rule.js.map +1 -0
  90. package/dist/linter/rules/project-rule.d.ts +10 -0
  91. package/dist/linter/rules/project-rule.d.ts.map +1 -0
  92. package/dist/linter/rules/project-rule.js +24 -0
  93. package/dist/linter/rules/project-rule.js.map +1 -0
  94. package/dist/linter/rules/thin-schema-composition-root.d.ts +3 -0
  95. package/dist/linter/rules/thin-schema-composition-root.d.ts.map +1 -0
  96. package/dist/linter/rules/thin-schema-composition-root.js +23 -0
  97. package/dist/linter/rules/thin-schema-composition-root.js.map +1 -0
  98. package/dist/linter/source.d.ts +24 -0
  99. package/dist/linter/source.d.ts.map +1 -0
  100. package/dist/linter/source.js +279 -0
  101. package/dist/linter/source.js.map +1 -0
  102. package/dist/linter/suppression.d.ts +3 -0
  103. package/dist/linter/suppression.d.ts.map +1 -0
  104. package/dist/linter/suppression.js +11 -0
  105. package/dist/linter/suppression.js.map +1 -0
  106. package/dist/method/context.d.ts +3 -1
  107. package/dist/method/context.d.ts.map +1 -1
  108. package/dist/server/auxiliary-routes.d.ts.map +1 -1
  109. package/dist/server/auxiliary-routes.js +4 -4
  110. package/dist/server/auxiliary-routes.js.map +1 -1
  111. package/dist/server/create.js +1 -1
  112. package/dist/server/create.js.map +1 -1
  113. package/dist/server/start.d.ts.map +1 -1
  114. package/dist/server/start.js.map +1 -1
  115. package/package.json +27 -7
  116. package/src/cli/run.ts +53 -2
  117. package/src/define/remote-function.ts +3 -1
  118. package/src/dispatch/dispatcher.ts +62 -69
  119. package/src/dispatch/execute.ts +3 -1
  120. package/src/index.ts +2 -2
  121. package/src/linter/analyze.ts +17 -0
  122. package/src/linter/diagnostic.ts +68 -0
  123. package/src/linter/docs/DIAGNOSTICS.md +47 -0
  124. package/src/linter/docs/JUDGMENT.md +81 -0
  125. package/src/linter/docs/PRINCIPLES.md +97 -0
  126. package/src/linter/docs/README.md +38 -0
  127. package/src/linter/docs/RULE-AUTHORING.md +144 -0
  128. package/src/linter/docs/RULES.md +153 -0
  129. package/src/linter/index.ts +14 -0
  130. package/src/linter/lint.ts +32 -0
  131. package/src/linter/oxlint/astrale-ox.d.ts +5 -0
  132. package/src/linter/oxlint/config.ts +19 -0
  133. package/src/linter/oxlint/plugin.js +16 -0
  134. package/src/linter/oxlint/run.ts +162 -0
  135. package/src/linter/preflight.ts +181 -0
  136. package/src/linter/project.ts +108 -0
  137. package/src/linter/report.ts +25 -0
  138. package/src/linter/rules/catalog.ts +67 -0
  139. package/src/linter/rules/core-has-no-async.js +43 -0
  140. package/src/linter/rules/core-is-pure.ts +103 -0
  141. package/src/linter/rules/no-nested-steps.js +37 -0
  142. package/src/linter/rules/no-read-inside-mutate.js +72 -0
  143. package/src/linter/rules/one-remote-definition-per-file.ts +29 -0
  144. package/src/linter/rules/oxlint-ast.js +110 -0
  145. package/src/linter/rules/oxlint-rule.ts +35 -0
  146. package/src/linter/rules/project-rule.ts +37 -0
  147. package/src/linter/rules/thin-schema-composition-root.ts +26 -0
  148. package/src/linter/source.ts +323 -0
  149. package/src/linter/suppression.ts +11 -0
  150. package/src/method/context.ts +3 -1
  151. package/src/server/auxiliary-routes.ts +9 -4
  152. package/src/server/create.ts +1 -1
  153. package/src/server/start.ts +2 -1
  154. package/dist/defer/index.d.ts +0 -24
  155. package/dist/defer/index.d.ts.map +0 -1
  156. package/dist/defer/index.js +0 -42
  157. package/dist/defer/index.js.map +0 -1
  158. package/src/defer/index.ts +0 -64
@@ -0,0 +1,14 @@
1
+ export { ruleCatalog, ruleId, ruleInfo } from './rules/catalog.js'
2
+ export type { ImplementedRuleSlug, RuleInfo, RuleOwner } from './rules/catalog.js'
3
+ export type {
4
+ DiagnosticLocation,
5
+ DiagnosticSeverity,
6
+ LintDiagnostic,
7
+ LintResult,
8
+ } from './diagnostic.js'
9
+ export { LinterToolError } from './diagnostic.js'
10
+ export { lintDomain } from './lint.js'
11
+ export type { LintDomainOptions } from './lint.js'
12
+ export { domainOxlintConfig } from './oxlint/config.js'
13
+ export { formatLintResult } from './report.js'
14
+ export type { LintReportFormat } from './report.js'
@@ -0,0 +1,32 @@
1
+ import { resolve } from 'node:path'
2
+ import { performance } from 'node:perf_hooks'
3
+
4
+ import { analyzeProject } from './analyze.js'
5
+ import { dedupeDiagnostics, type LintResult } from './diagnostic.js'
6
+ import { runOxlint } from './oxlint/run.js'
7
+ import { assertSingleTypeUniverse } from './preflight.js'
8
+ import { discoverProject } from './project.js'
9
+
10
+ export type LintDomainOptions = {
11
+ root?: string
12
+ fix?: boolean
13
+ preflight?: boolean
14
+ }
15
+
16
+ export async function lintDomain(options: LintDomainOptions = {}): Promise<LintResult> {
17
+ const started = performance.now()
18
+ const root = resolve(options.root ?? process.cwd())
19
+ if (options.preflight !== false) await assertSingleTypeUniverse(root)
20
+ const [project, oxlint] = await Promise.all([
21
+ discoverProject(root),
22
+ runOxlint(root, options.fix ?? false),
23
+ ])
24
+ const diagnostics = dedupeDiagnostics([...oxlint.diagnostics, ...analyzeProject(project)])
25
+ return {
26
+ root: project.root,
27
+ diagnostics,
28
+ files: Math.max(project.files.length, oxlint.files),
29
+ durationMs: Math.max(0, Math.round(performance.now() - started)),
30
+ exitCode: diagnostics.some((diagnostic) => diagnostic.severity === 'error') ? 1 : 0,
31
+ }
32
+ }
@@ -0,0 +1,5 @@
1
+ declare module '@astrale-os/ox/lint' {
2
+ // oxlint-disable-next-line consistent-type-imports -- ambient modules cannot import this type
3
+ const config: import('oxlint').OxlintConfig
4
+ export default config
5
+ }
@@ -0,0 +1,19 @@
1
+ import base from '@astrale-os/ox/lint'
2
+ import { defineConfig } from 'oxlint'
3
+
4
+ export const domainOxlintConfig = defineConfig({
5
+ extends: [base],
6
+ jsPlugins: [
7
+ {
8
+ name: 'astrale',
9
+ specifier: '@astrale-os/sdk/linter/oxlint-plugin',
10
+ },
11
+ ],
12
+ rules: {
13
+ 'astrale/core-has-no-async': 'error',
14
+ 'astrale/no-nested-steps': 'error',
15
+ 'astrale/no-read-inside-mutate': 'error',
16
+ },
17
+ })
18
+
19
+ export default domainOxlintConfig
@@ -0,0 +1,16 @@
1
+ // @ts-check
2
+
3
+ import { coreHasNoAsync } from '../rules/core-has-no-async.js'
4
+ import { noNestedSteps } from '../rules/no-nested-steps.js'
5
+ import { noReadInsideMutate } from '../rules/no-read-inside-mutate.js'
6
+
7
+ const astraleOxlintPlugin = {
8
+ meta: { name: 'astrale', version: '0.1.0' },
9
+ rules: {
10
+ 'core-has-no-async': coreHasNoAsync,
11
+ 'no-nested-steps': noNestedSteps,
12
+ 'no-read-inside-mutate': noReadInsideMutate,
13
+ },
14
+ }
15
+
16
+ export default astraleOxlintPlugin
@@ -0,0 +1,162 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { existsSync } from 'node:fs'
3
+ import { createRequire } from 'node:module'
4
+ import { dirname, join, relative, resolve, sep } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ import { LinterToolError, type DiagnosticSeverity, type LintDiagnostic } from '../diagnostic.js'
8
+ import { ruleInfo } from '../rules/catalog.js'
9
+
10
+ type OxlintOutput = {
11
+ diagnostics?: OxlintDiagnostic[]
12
+ number_of_files?: number
13
+ }
14
+
15
+ type OxlintDiagnostic = {
16
+ message?: string
17
+ code?: string
18
+ severity?: string
19
+ url?: string
20
+ help?: string
21
+ filename?: string
22
+ labels?: Array<{
23
+ span?: { offset?: number; length?: number; line?: number; column?: number }
24
+ }>
25
+ }
26
+
27
+ export type OxlintResult = {
28
+ diagnostics: LintDiagnostic[]
29
+ files: number
30
+ }
31
+
32
+ export async function runOxlint(root: string, fix: boolean): Promise<OxlintResult> {
33
+ const packageJson = join(root, 'package.json')
34
+ const projectRequire = createRequire(packageJson)
35
+ const sdkRequire = createRequire(import.meta.url)
36
+ const oxlintPackage = resolvePackage('oxlint/package.json', projectRequire, sdkRequire)
37
+ const bin = join(dirname(oxlintPackage), 'bin', 'oxlint')
38
+ const config = projectConfig(root) ?? internalConfigPath()
39
+ const args = [bin, '.', '--format=json', '--no-error-on-unmatched-pattern', '--config', config]
40
+ if (fix) args.push('--fix')
41
+
42
+ const child = spawn(process.execPath, args, {
43
+ cwd: root,
44
+ env: process.env,
45
+ stdio: ['ignore', 'pipe', 'pipe'],
46
+ })
47
+ let stdout = ''
48
+ let stderr = ''
49
+ child.stdout.setEncoding('utf8')
50
+ child.stderr.setEncoding('utf8')
51
+ child.stdout.on('data', (chunk: string) => (stdout += chunk))
52
+ child.stderr.on('data', (chunk: string) => (stderr += chunk))
53
+ const status = await new Promise<number | null>((resolveStatus, reject) => {
54
+ child.once('error', reject)
55
+ child.once('close', resolveStatus)
56
+ }).catch((error: unknown) => {
57
+ throw new LinterToolError(`Could not start Oxlint at ${bin}.`, { cause: error })
58
+ })
59
+
60
+ let output: OxlintOutput
61
+ try {
62
+ output = JSON.parse(stdout) as OxlintOutput
63
+ } catch (error) {
64
+ const detail =
65
+ stderr.trim() || stdout.trim() || `Oxlint exited with status ${status ?? 'unknown'}`
66
+ throw new LinterToolError(`Oxlint did not produce a diagnostic report.\n${detail}`, {
67
+ cause: error,
68
+ })
69
+ }
70
+ if (status !== 0 && status !== 1) {
71
+ throw new LinterToolError(
72
+ `Oxlint failed with status ${status ?? 'unknown'}.${stderr.trim() ? `\n${stderr.trim()}` : ''}`,
73
+ )
74
+ }
75
+
76
+ return {
77
+ diagnostics: (output.diagnostics ?? []).map((diagnostic) =>
78
+ normalizeDiagnostic(root, diagnostic),
79
+ ),
80
+ files: output.number_of_files ?? 0,
81
+ }
82
+ }
83
+
84
+ function resolvePackage(
85
+ id: string,
86
+ projectRequire: NodeJS.Require,
87
+ sdkRequire: NodeJS.Require,
88
+ ): string {
89
+ try {
90
+ return projectRequire.resolve(id)
91
+ } catch (projectError) {
92
+ try {
93
+ return sdkRequire.resolve(id)
94
+ } catch {
95
+ throw new LinterToolError(
96
+ 'Oxlint is not installed. Add the scaffolded Oxlint dev dependency and run your package manager install.',
97
+ { cause: projectError },
98
+ )
99
+ }
100
+ }
101
+ }
102
+
103
+ function projectConfig(root: string): string | undefined {
104
+ for (const name of [
105
+ 'oxlint.config.ts',
106
+ 'oxlint.config.js',
107
+ 'oxlint.config.mjs',
108
+ 'oxlint.config.cjs',
109
+ '.oxlintrc.json',
110
+ '.oxlintrc.jsonc',
111
+ ]) {
112
+ const path = join(root, name)
113
+ if (existsSync(path)) return path
114
+ }
115
+ return undefined
116
+ }
117
+
118
+ function internalConfigPath(): string {
119
+ const current = fileURLToPath(import.meta.url)
120
+ const extension = current.endsWith('.ts') ? '.ts' : '.js'
121
+ return join(dirname(current), `config${extension}`)
122
+ }
123
+
124
+ function normalizeDiagnostic(root: string, diagnostic: OxlintDiagnostic): LintDiagnostic {
125
+ const id = normalizeCode(diagnostic.code ?? 'oxlint/unknown')
126
+ const info = ruleInfo(id)
127
+ const span = diagnostic.labels?.[0]?.span
128
+ return {
129
+ id,
130
+ severity: normalizeSeverity(diagnostic.severity),
131
+ message: diagnostic.message ?? info?.message ?? 'Oxlint reported a violation.',
132
+ ...(diagnostic.help || info?.help ? { help: diagnostic.help ?? info?.help } : {}),
133
+ ...(diagnostic.url || info?.url ? { url: diagnostic.url ?? info?.url } : {}),
134
+ owner: info?.owner ?? 'generic',
135
+ ...(diagnostic.filename
136
+ ? {
137
+ location: {
138
+ path: relativePath(root, diagnostic.filename),
139
+ line: span?.line ?? 1,
140
+ column: span?.column ?? 1,
141
+ ...(span?.offset !== undefined ? { offset: span.offset } : {}),
142
+ ...(span?.length !== undefined ? { length: span.length } : {}),
143
+ },
144
+ }
145
+ : {}),
146
+ }
147
+ }
148
+
149
+ function normalizeCode(code: string): string {
150
+ const match = /^([^()]+)\(([^()]+)\)$/.exec(code)
151
+ return match ? `${match[1]}/${match[2]}` : code
152
+ }
153
+
154
+ function normalizeSeverity(severity: string | undefined): DiagnosticSeverity {
155
+ return severity === 'error' ? 'error' : 'warning'
156
+ }
157
+
158
+ function relativePath(root: string, filename: string): string {
159
+ const absolute = resolve(root, filename)
160
+ const path = relative(root, absolute)
161
+ return sep === '/' ? path : path.split(sep).join('/')
162
+ }
@@ -0,0 +1,181 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { readFile, realpath } from 'node:fs/promises'
3
+ import { createRequire } from 'node:module'
4
+ import { dirname, join, relative, resolve } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ import { LinterToolError } from './diagnostic.js'
8
+
9
+ const brandedPackages = [
10
+ '@astrale-os/kernel-client',
11
+ '@astrale-os/kernel-core',
12
+ '@astrale-os/kernel-dsl',
13
+ ] as const
14
+
15
+ const conventionalMembers = ['client', 'frontend', 'schema', 'worker'] as const
16
+
17
+ type Resolution = {
18
+ consumer: string
19
+ path: string
20
+ }
21
+
22
+ type ConsumerPackageJson = {
23
+ name?: string
24
+ dependencies?: Record<string, string>
25
+ optionalDependencies?: Record<string, string>
26
+ workspaces?: string[] | { packages?: string[] }
27
+ }
28
+
29
+ export async function assertSingleTypeUniverse(root: string): Promise<void> {
30
+ const manifests = await collectConsumerManifests(root)
31
+ const consumers = [
32
+ ...manifests.map((path) => ({ label: relative(root, dirname(path)) || '.', path })),
33
+ { label: '@astrale-os/sdk/linter', path: fileURLPackageJson() },
34
+ ]
35
+
36
+ for (const packageName of brandedPackages) {
37
+ const resolutions: Resolution[] = []
38
+ for (const consumer of consumers) {
39
+ const resolved = await resolvePackageRoot(packageName, consumer.path)
40
+ if (resolved) resolutions.push({ consumer: consumer.label, path: resolved })
41
+ }
42
+ const paths = new Set(resolutions.map((resolution) => resolution.path))
43
+ if (paths.size <= 1) continue
44
+ const detail = formatResolutions(resolutions)
45
+ throw new LinterToolError(
46
+ `Multiple physical installations of ${packageName} would create incompatible Astrale types.\n` +
47
+ `${detail}\nReinstall the workspace so every member resolves one compatible package instance.`,
48
+ )
49
+ }
50
+ }
51
+
52
+ function formatResolutions(resolutions: readonly Resolution[]): string {
53
+ const byPath = new Map<string, string[]>()
54
+ for (const resolution of resolutions) {
55
+ const consumers = byPath.get(resolution.path) ?? []
56
+ consumers.push(resolution.consumer)
57
+ byPath.set(resolution.path, consumers)
58
+ }
59
+ return [...byPath]
60
+ .sort(([a], [b]) => a.localeCompare(b))
61
+ .map(([path, consumers]) => {
62
+ const sorted = [...new Set(consumers)].sort()
63
+ const shown = sorted.slice(0, 4).join(', ')
64
+ const omitted = sorted.length > 4 ? ` (+${sorted.length - 4} more)` : ''
65
+ return ` ${path}\n from: ${shown}${omitted}`
66
+ })
67
+ .join('\n')
68
+ }
69
+
70
+ async function collectConsumerManifests(root: string): Promise<string[]> {
71
+ const rootManifest = join(root, 'package.json')
72
+ if (!existsSync(rootManifest)) return []
73
+
74
+ const manifests = new Set([rootManifest])
75
+ const packageJson = await readPackageJson(rootManifest)
76
+ const workspaces = Array.isArray(packageJson?.workspaces)
77
+ ? packageJson.workspaces
78
+ : packageJson?.workspaces?.packages
79
+
80
+ for (const workspace of workspaces ?? []) {
81
+ if (typeof workspace !== 'string' || hasGlob(workspace)) continue
82
+ addManifestWithinRoot(manifests, root, workspace)
83
+ }
84
+
85
+ for (const member of conventionalMembers) addManifestWithinRoot(manifests, root, member)
86
+ await addDomainContractDependencyManifests(manifests)
87
+ return [...manifests].sort()
88
+ }
89
+
90
+ async function readPackageJson(manifest: string): Promise<ConsumerPackageJson | undefined> {
91
+ try {
92
+ return JSON.parse(await readFile(manifest, 'utf8'))
93
+ } catch {
94
+ return undefined
95
+ }
96
+ }
97
+
98
+ async function addDomainContractDependencyManifests(manifests: Set<string>): Promise<void> {
99
+ const pending = [...manifests]
100
+ for (let index = 0; index < pending.length; index++) {
101
+ const consumer = pending[index]!
102
+ const packageJson = await readPackageJson(consumer)
103
+ const dependencies = {
104
+ ...packageJson?.optionalDependencies,
105
+ ...packageJson?.dependencies,
106
+ }
107
+ for (const packageName of Object.keys(dependencies).sort()) {
108
+ if (!isDomainContractPackage(packageName)) continue
109
+ const manifest = await resolveDependencyManifest(packageName, consumer)
110
+ if (!manifest || manifests.has(manifest)) continue
111
+ manifests.add(manifest)
112
+ pending.push(manifest)
113
+ }
114
+ }
115
+ }
116
+
117
+ async function resolveDependencyManifest(
118
+ packageName: string,
119
+ consumerManifest: string,
120
+ ): Promise<string | undefined> {
121
+ const require = createRequire(consumerManifest)
122
+ try {
123
+ return await realpath(require.resolve(`${packageName}/package.json`))
124
+ } catch {
125
+ try {
126
+ let directory = dirname(await realpath(require.resolve(packageName)))
127
+ for (;;) {
128
+ const manifest = join(directory, 'package.json')
129
+ if (existsSync(manifest)) {
130
+ const packageJson = await readPackageJson(manifest)
131
+ if (packageJson?.name === packageName) return await realpath(manifest)
132
+ }
133
+ const parent = dirname(directory)
134
+ if (parent === directory) return undefined
135
+ directory = parent
136
+ }
137
+ } catch {
138
+ return undefined
139
+ }
140
+ }
141
+ }
142
+
143
+ function addManifestWithinRoot(manifests: Set<string>, root: string, member: string): void {
144
+ const manifest = resolve(root, member, 'package.json')
145
+ const parent = relative(root, manifest)
146
+ if (parent.startsWith('..') || parent === '') return
147
+ if (existsSync(manifest)) manifests.add(manifest)
148
+ }
149
+
150
+ function hasGlob(path: string): boolean {
151
+ return /[*?[\]{}!]/u.test(path)
152
+ }
153
+
154
+ function isDomainContractPackage(packageName: string): boolean {
155
+ return packageName.startsWith('@astrale-domains/')
156
+ }
157
+
158
+ async function resolvePackageRoot(
159
+ packageName: (typeof brandedPackages)[number],
160
+ manifest: string,
161
+ ): Promise<string | undefined> {
162
+ if (!existsSync(manifest)) return undefined
163
+ try {
164
+ const resolved = createRequire(manifest).resolve(`${packageName}/package.json`)
165
+ return await realpath(dirname(resolved))
166
+ } catch {
167
+ return undefined
168
+ }
169
+ }
170
+
171
+ function fileURLPackageJson(): string {
172
+ let directory = dirname(fileURLToPath(import.meta.url))
173
+ for (;;) {
174
+ const candidate = join(directory, 'package.json')
175
+ if (existsSync(candidate)) return candidate
176
+ const parent = dirname(directory)
177
+ if (parent === directory) break
178
+ directory = parent
179
+ }
180
+ return join(directory, 'package.json')
181
+ }
@@ -0,0 +1,108 @@
1
+ import { readdir, readFile, realpath } from 'node:fs/promises'
2
+ import { dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path'
3
+
4
+ import { scanSource, type SourceFile } from './source.js'
5
+
6
+ const ignoredDirectories = new Set([
7
+ '.astrale',
8
+ '.dist',
9
+ '.domain-studio',
10
+ '.git',
11
+ '.output',
12
+ '.turbo',
13
+ '.wrangler',
14
+ 'coverage',
15
+ 'dist',
16
+ 'dist-client',
17
+ 'node_modules',
18
+ ])
19
+
20
+ const sourceExtensions = new Set(['.cjs', '.cts', '.js', '.jsx', '.mjs', '.mts', '.ts', '.tsx'])
21
+
22
+ export type DomainProject = {
23
+ root: string
24
+ files: readonly SourceFile[]
25
+ filesByPath: ReadonlyMap<string, SourceFile>
26
+ }
27
+
28
+ export async function discoverProject(root: string): Promise<DomainProject> {
29
+ const projectRoot = await realpath(resolve(root))
30
+ const paths: string[] = []
31
+ await collectSourcePaths(projectRoot, paths)
32
+ paths.sort()
33
+
34
+ const files = await Promise.all(
35
+ paths.map(async (path) => {
36
+ const text = await readFile(path, 'utf8')
37
+ return scanSource(path, normalizePath(relative(projectRoot, path)), text)
38
+ }),
39
+ )
40
+ return {
41
+ root: projectRoot,
42
+ files,
43
+ filesByPath: new Map(files.map((file) => [file.path, file])),
44
+ }
45
+ }
46
+
47
+ async function collectSourcePaths(directory: string, result: string[]): Promise<void> {
48
+ const entries = await readdir(directory, { withFileTypes: true })
49
+ await Promise.all(
50
+ entries.map(async (entry) => {
51
+ if (entry.isSymbolicLink()) return
52
+ const path = join(directory, entry.name)
53
+ if (entry.isDirectory()) {
54
+ if (!ignoredDirectories.has(entry.name)) await collectSourcePaths(path, result)
55
+ return
56
+ }
57
+ if (!entry.isFile() || !sourceExtensions.has(extname(entry.name))) return
58
+ if (entry.name.endsWith('.d.ts') || entry.name.includes('.gen.')) return
59
+ result.push(path)
60
+ }),
61
+ )
62
+ }
63
+
64
+ export function normalizePath(path: string): string {
65
+ return sep === '/' ? path : path.split(sep).join('/')
66
+ }
67
+
68
+ export function isProductionSource(relativePath: string): boolean {
69
+ const normalized = normalizePath(relativePath)
70
+ return (
71
+ !normalized.split('/').includes('__tests__') &&
72
+ !/\.(?:test|spec|bench|perf)\.[cm]?[jt]sx?$/.test(normalized) &&
73
+ !normalized.includes('.gen.')
74
+ )
75
+ }
76
+
77
+ export function isProductionCore(relativePath: string): boolean {
78
+ const normalized = normalizePath(relativePath)
79
+ return (normalized === 'core' || normalized.startsWith('core/')) && isProductionSource(normalized)
80
+ }
81
+
82
+ export function resolveProjectImport(
83
+ project: DomainProject,
84
+ importer: SourceFile,
85
+ specifier: string,
86
+ ): SourceFile | undefined {
87
+ if (!specifier.startsWith('.')) return undefined
88
+ const unresolved = resolve(dirname(importer.path), specifier)
89
+ for (const candidate of importCandidates(unresolved)) {
90
+ const file = project.filesByPath.get(candidate)
91
+ if (file) return file
92
+ }
93
+ return undefined
94
+ }
95
+
96
+ function importCandidates(path: string): string[] {
97
+ const extension = extname(path)
98
+ const candidates = [path]
99
+ if (extension === '.js' || extension === '.jsx' || extension === '.mjs' || extension === '.cjs') {
100
+ const stem = path.slice(0, -extension.length)
101
+ candidates.push(`${stem}.ts`, `${stem}.tsx`, `${stem}.mts`, `${stem}.cts`)
102
+ }
103
+ if (!extension) {
104
+ candidates.push(`${path}.ts`, `${path}.tsx`, `${path}.mts`, `${path}.cts`)
105
+ candidates.push(join(path, 'index.ts'), join(path, 'index.tsx'))
106
+ }
107
+ return candidates.map((candidate) => (isAbsolute(candidate) ? candidate : resolve(candidate)))
108
+ }
@@ -0,0 +1,25 @@
1
+ import type { LintResult } from './diagnostic.js'
2
+
3
+ export type LintReportFormat = 'json' | 'stylish'
4
+
5
+ export function formatLintResult(result: LintResult, format: LintReportFormat = 'stylish'): string {
6
+ if (format === 'json') return `${JSON.stringify(result, null, 2)}\n`
7
+ const lines: string[] = []
8
+ for (const diagnostic of result.diagnostics) {
9
+ const location = diagnostic.location
10
+ ? `${diagnostic.location.path}:${diagnostic.location.line}:${diagnostic.location.column}`
11
+ : result.root
12
+ lines.push(`${location} ${diagnostic.severity} ${diagnostic.id} ${diagnostic.message}`)
13
+ if (diagnostic.help) lines.push(` ${diagnostic.help}`)
14
+ }
15
+
16
+ const errors = result.diagnostics.filter((diagnostic) => diagnostic.severity === 'error').length
17
+ const warnings = result.diagnostics.length - errors
18
+ if (lines.length > 0) lines.push('')
19
+ lines.push(
20
+ errors === 0
21
+ ? `\u2713 ${result.files} files linted in ${result.durationMs}ms${warnings ? ` (${warnings} warnings)` : ''}`
22
+ : `\u2717 ${errors} ${errors === 1 ? 'error' : 'errors'}, ${warnings} ${warnings === 1 ? 'warning' : 'warnings'} in ${result.durationMs}ms`,
23
+ )
24
+ return `${lines.join('\n')}\n`
25
+ }
@@ -0,0 +1,67 @@
1
+ export type RuleOwner = 'analyzer' | 'oxlint' | 'sdk'
2
+
3
+ export type RuleInfo = {
4
+ slug: string
5
+ owner: RuleOwner
6
+ message: string
7
+ help: string
8
+ url: string
9
+ }
10
+
11
+ const ruleDocsUrl = (slug: string) => `https://docs.astrale.ai/linter/rules/${slug}`
12
+
13
+ export const ruleCatalog = {
14
+ 'thin-schema-composition-root': {
15
+ slug: 'thin-schema-composition-root',
16
+ owner: 'analyzer',
17
+ message: 'The schema composition root defines a schema member inline.',
18
+ help: 'Move this member definition to its bounded-context module and import it here.',
19
+ url: ruleDocsUrl('thin-schema-composition-root'),
20
+ },
21
+ 'one-remote-definition-per-file': {
22
+ slug: 'one-remote-definition-per-file',
23
+ owner: 'analyzer',
24
+ message: 'This file defines more than one Astrale remote callable.',
25
+ help: 'Keep one remoteMethod or defineRemoteFunction definition per implementation file.',
26
+ url: ruleDocsUrl('one-remote-definition-per-file'),
27
+ },
28
+ 'core-is-pure': {
29
+ slug: 'core-is-pure',
30
+ owner: 'analyzer',
31
+ message: 'Pure core code depends on an effectful or runtime-only module.',
32
+ help: 'Move I/O orchestration to runtime or integrations and pass pure data into core.',
33
+ url: ruleDocsUrl('core-is-pure'),
34
+ },
35
+ 'core-has-no-async': {
36
+ slug: 'core-has-no-async',
37
+ owner: 'oxlint',
38
+ message: 'Production core code contains asynchronous syntax.',
39
+ help: 'Keep core deterministic and synchronous; perform asynchronous work in runtime or integrations.',
40
+ url: ruleDocsUrl('core-has-no-async'),
41
+ },
42
+ 'no-nested-steps': {
43
+ slug: 'no-nested-steps',
44
+ owner: 'oxlint',
45
+ message: 'A durable step directly contains another durable step.',
46
+ help: 'Give each stage one step boundary and call helper-owned steps outside another step callback.',
47
+ url: ruleDocsUrl('no-nested-steps'),
48
+ },
49
+ 'no-read-inside-mutate': {
50
+ slug: 'no-read-inside-mutate',
51
+ owner: 'oxlint',
52
+ message: 'A graph mutation builder performs a read or is asynchronous.',
53
+ help: 'Read before mutate, then keep the mutation callback synchronous and limited to patch construction.',
54
+ url: ruleDocsUrl('no-read-inside-mutate'),
55
+ },
56
+ } as const satisfies Record<string, RuleInfo>
57
+
58
+ export type ImplementedRuleSlug = keyof typeof ruleCatalog
59
+
60
+ export function ruleId(slug: ImplementedRuleSlug): `astrale/${ImplementedRuleSlug}` {
61
+ return `astrale/${slug}`
62
+ }
63
+
64
+ export function ruleInfo(id: string): RuleInfo | undefined {
65
+ const slug = id.startsWith('astrale/') ? id.slice('astrale/'.length) : id
66
+ return ruleCatalog[slug as ImplementedRuleSlug]
67
+ }
@@ -0,0 +1,43 @@
1
+ // @ts-check
2
+
3
+ import { isProductionCoreFile, nearestFunction, reportUnlessSuppressed } from './oxlint-ast.js'
4
+
5
+ const id = 'astrale/core-has-no-async'
6
+
7
+ /** @type {import('./oxlint-rule.js').OxlintRule} */
8
+ export const coreHasNoAsync = {
9
+ meta: {
10
+ type: 'problem',
11
+ docs: {
12
+ description: 'Production core code contains asynchronous syntax.',
13
+ url: 'https://docs.astrale.ai/linter/rules/core-has-no-async',
14
+ },
15
+ messages: {
16
+ asyncFunction: 'Production core functions must be synchronous.',
17
+ topLevelAwait: 'Production core modules must not use top-level await.',
18
+ },
19
+ schema: [],
20
+ },
21
+ create(context) {
22
+ if (!isProductionCoreFile(context)) return {}
23
+ /** @param {import('./oxlint-rule.js').AstNode} node */
24
+ const reportFunction = (node) => {
25
+ if (node.async) reportUnlessSuppressed(context, node, id, 'asyncFunction')
26
+ }
27
+ return {
28
+ FunctionDeclaration: reportFunction,
29
+ FunctionExpression: reportFunction,
30
+ ArrowFunctionExpression: reportFunction,
31
+ AwaitExpression(node) {
32
+ if (!nearestFunction(node.parent)) {
33
+ reportUnlessSuppressed(context, node, id, 'topLevelAwait')
34
+ }
35
+ },
36
+ ForOfStatement(node) {
37
+ if (node.await && !nearestFunction(node.parent)) {
38
+ reportUnlessSuppressed(context, node, id, 'topLevelAwait')
39
+ }
40
+ },
41
+ }
42
+ },
43
+ }