@astrale-os/cli 1.0.0-beta.17 → 1.0.0-beta.18

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.
@@ -8242,14 +8242,14 @@ function isPidAlive(pid) {
8242
8242
  function sleep(milliseconds) {
8243
8243
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
8244
8244
  }
8245
- // node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.10/node_modules/@astrale-os/kernel-dsl/dist/value/limits.js
8245
+ // node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.12/node_modules/@astrale-os/kernel-dsl/dist/value/limits.js
8246
8246
  var defaultLimits = Object.freeze({
8247
8247
  maxBytes: 16 * 1024 * 1024,
8248
8248
  maxDepth: 64,
8249
8249
  maxMembers: 1e5,
8250
8250
  maxStringBytes: 1024 * 1024
8251
8251
  });
8252
- // node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.10/node_modules/@astrale-os/kernel-dsl/dist/value/canonical.js
8252
+ // node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.12/node_modules/@astrale-os/kernel-dsl/dist/value/canonical.js
8253
8253
  var encoder2 = new TextEncoder;
8254
8254
  // node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
8255
8255
  var CharacterCodes;
@@ -8429,7 +8429,7 @@ var ParseErrorCode;
8429
8429
  ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
8430
8430
  })(ParseErrorCode || (ParseErrorCode = {}));
8431
8431
 
8432
- // node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.10/node_modules/@astrale-os/kernel-dsl/dist/value/decode.js
8432
+ // node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.12/node_modules/@astrale-os/kernel-dsl/dist/value/decode.js
8433
8433
  var decoder2 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
8434
8434
  // node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
8435
8435
  var exports_external = {};
@@ -23659,7 +23659,7 @@ var log = {
23659
23659
  dim: (msg) => console.log(source_default.dim(msg))
23660
23660
  };
23661
23661
  var IS_CI = !!(process.env.CI || process.env.CONTINUOUS_INTEGRATION || process.env.NO_SPINNER);
23662
- // node_modules/.pnpm/@astrale-os+kernel-core@0.9.0-beta.13/node_modules/@astrale-os/kernel-core/dist/dns-label.js
23662
+ // node_modules/.pnpm/@astrale-os+kernel-core@0.9.0-beta.15/node_modules/@astrale-os/kernel-core/dist/dns-label.js
23663
23663
  var DNS_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
23664
23664
  function isDnsLabel(value) {
23665
23665
  return DNS_LABEL_RE.test(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/cli",
3
- "version": "1.0.0-beta.17",
3
+ "version": "1.0.0-beta.18",
4
4
  "description": "Astrale CLI — connect to existing Astrale kernels",
5
5
  "keywords": [
6
6
  "astrale",
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "devDependencies": {
64
64
  "@astrale-os/ox": ">=0.1.3 <1.0.0",
65
- "@astrale-os/sdk": "0.5.0-beta.38",
65
+ "@astrale-os/sdk": "0.5.0-beta.49",
66
66
  "@astrale-os/shell": "0.4.2-beta.3",
67
67
  "@astrale/commitlint-config": "npm:@jsr/astrale__commitlint-config@~2.0.1",
68
68
  "@commitlint/cli": "21.2.2",
@@ -38,9 +38,7 @@ describe('direct domain install operation identity', () => {
38
38
  recovery = input.recovery
39
39
  requests.push(
40
40
  await input.fn({
41
- session: {
42
- call: async (call: { input: unknown }) => call.input,
43
- },
41
+ session: { schema: { install: async (input: unknown) => input } },
44
42
  } as never),
45
43
  )
46
44
  },
@@ -51,7 +49,7 @@ describe('direct domain install operation identity', () => {
51
49
  expect(requests).toEqual([
52
50
  {
53
51
  operation: GENERATED,
54
- domains: [{ source: { kind: 'remote', url: 'https://crm.test' } }],
52
+ domains: [{ publication: { url: 'https://crm.test' } }],
55
53
  },
56
54
  ])
57
55
  expect(recovery).toEqual({
@@ -78,9 +76,7 @@ describe('direct domain install operation identity', () => {
78
76
  runKernelCommand: async (input) => {
79
77
  requests.push(
80
78
  await input.fn({
81
- session: {
82
- call: async (call: { input: unknown }) => call.input,
83
- },
79
+ session: { schema: { install: async (input: unknown) => input } },
84
80
  } as never),
85
81
  )
86
82
  },
@@ -92,7 +88,7 @@ describe('direct domain install operation identity', () => {
92
88
  expect(requests).toEqual([
93
89
  {
94
90
  operation: RETRY,
95
- domains: [{ source: { kind: 'remote', url: 'https://crm.test' } }],
91
+ domains: [{ publication: { url: 'https://crm.test' } }],
96
92
  },
97
93
  ])
98
94
  })
@@ -1,21 +1,128 @@
1
+ import type { InstallResult } from '@astrale-os/sdk/client/schema'
2
+
3
+ import { defineSchema, schema } from '@astrale-os/sdk/schema'
1
4
  import { describe, expect, test } from 'bun:test'
2
5
 
3
- import { directInstallCallInput } from '../domain/install'
6
+ import { directInstallCallInput, directInstallPresentation } from '../domain/install'
7
+
8
+ const currentRevision = schema.revision(defineSchema('tasks.astrale.ai', {}))
4
9
 
5
10
  describe('directInstallCallInput', () => {
6
11
  test('sends the current remote install syscall, not a legacy url list', () => {
7
12
  const operation = '4a4c9a18-50f6-4d84-a7b7-2d83e3e45dc8'
8
- expect(directInstallCallInput('https://tasks.example.test', operation, 'secret')).toEqual({
9
- operation,
13
+ const input = directInstallCallInput('https://tasks.example.test', operation, 'secret')
14
+
15
+ expect(String(input.operation)).toBe(operation)
16
+ expect(input.domains).toEqual([
17
+ {
18
+ publication: {
19
+ url: 'https://tasks.example.test',
20
+ token: 'secret',
21
+ },
22
+ },
23
+ ])
24
+ })
25
+
26
+ test('presents a committed install from its receipt', () => {
27
+ const operation = directInstallCallInput(
28
+ 'https://tasks.example.test',
29
+ '4a4c9a18-50f6-4d84-a7b7-2d83e3e45dc8',
30
+ ).operation
31
+ const result = {
32
+ changed: true,
33
+ receipt: {
34
+ operation,
35
+ transitions: [
36
+ {
37
+ intent: {
38
+ transition: 'transition-1',
39
+ operation,
40
+ origin: 'tasks.astrale.ai',
41
+ previous: null,
42
+ generation: {
43
+ origin: 'tasks.astrale.ai',
44
+ revision: currentRevision,
45
+ generation: 'sha256:generation',
46
+ },
47
+ },
48
+ phase: 'cutover',
49
+ state: 'committed',
50
+ },
51
+ ],
52
+ },
53
+ } satisfies InstallResult
54
+
55
+ expect(directInstallPresentation(result, 'ignored')).toEqual({
56
+ operation: '4a4c9a18-50f6-4d84-a7b7-2d83e3e45dc8',
57
+ origin: 'tasks.astrale.ai',
58
+ revision: currentRevision,
59
+ status: 'installed',
60
+ })
61
+ })
62
+
63
+ test('presents an idempotent install from the current Domain observation', () => {
64
+ const operation = '4a4c9a18-50f6-4d84-a7b7-2d83e3e45dc8'
65
+ const result = {
66
+ changed: false,
10
67
  domains: [
11
68
  {
12
- source: {
13
- kind: 'remote',
14
- url: 'https://tasks.example.test',
15
- token: 'secret',
16
- },
69
+ origin: 'tasks.astrale.ai',
70
+ revision: currentRevision,
71
+ generation: 'sha256:generation',
72
+ publication: null,
73
+ readiness: 'sha256:readiness',
74
+ capabilities: { requested: {}, materialized: {} },
75
+ bindings: { callables: [], views: [] },
17
76
  },
18
77
  ],
78
+ } satisfies InstallResult
79
+
80
+ expect(directInstallPresentation(result, operation)).toEqual({
81
+ operation,
82
+ origin: 'tasks.astrale.ai',
83
+ revision: currentRevision,
84
+ status: 'already current',
19
85
  })
20
86
  })
87
+
88
+ test('rejects a changed result without a committed transition', () => {
89
+ const result = {
90
+ changed: true,
91
+ receipt: { operation: '4a4c9a18-50f6-4d84-a7b7-2d83e3e45dc8', transitions: [] },
92
+ } as unknown as InstallResult
93
+
94
+ expect(() => directInstallPresentation(result, 'ignored')).toThrow(
95
+ 'Kernel install returned no committed Domain transition.',
96
+ )
97
+ })
98
+
99
+ test('rejects a committed transition without an installed generation', () => {
100
+ const operation = directInstallCallInput(
101
+ 'https://tasks.example.test',
102
+ '4a4c9a18-50f6-4d84-a7b7-2d83e3e45dc8',
103
+ ).operation
104
+ const result = {
105
+ changed: true,
106
+ receipt: {
107
+ operation,
108
+ transitions: [
109
+ {
110
+ intent: {
111
+ transition: 'transition-1',
112
+ operation,
113
+ origin: 'tasks.astrale.ai',
114
+ previous: null,
115
+ generation: null,
116
+ },
117
+ phase: 'cutover',
118
+ state: 'committed',
119
+ },
120
+ ],
121
+ },
122
+ } satisfies InstallResult
123
+
124
+ expect(() => directInstallPresentation(result, 'ignored')).toThrow(
125
+ 'Kernel install returned a committed transition without a Domain generation.',
126
+ )
127
+ })
21
128
  })
@@ -1,6 +1,44 @@
1
+ import type { DomainBundle, DomainInfo } from '@astrale-os/sdk/client/schema'
2
+
3
+ import { bundle, defineSchema, func, method, nodeClass, schema } from '@astrale-os/sdk/schema'
1
4
  import { describe, expect, test } from 'bun:test'
2
5
 
3
- import { parseIntrospectTarget } from '../introspect'
6
+ import { introspectCommand, parseIntrospectTarget } from '../introspect'
7
+
8
+ const source = defineSchema('host.astrale.ai', {
9
+ classes: {
10
+ Manager: nodeClass({
11
+ methods: {
12
+ createInstance: method({
13
+ auth: 'authenticated',
14
+ input: { type: 'object', properties: {}, additionalProperties: false },
15
+ output: { type: 'object', additionalProperties: true },
16
+ static: true,
17
+ }),
18
+ },
19
+ }),
20
+ },
21
+ functions: {
22
+ journal: func({
23
+ auth: 'authenticated',
24
+ input: { type: 'object', properties: {}, additionalProperties: false },
25
+ output: { type: 'object', additionalProperties: true },
26
+ }),
27
+ },
28
+ })
29
+
30
+ const info = {
31
+ origin: source.origin,
32
+ revision: schema.revision(source),
33
+ generation: 'sha256:generation',
34
+ publication: null,
35
+ readiness: 'sha256:readiness',
36
+ capabilities: { requested: {}, materialized: {} },
37
+ bindings: { callables: [], views: [] },
38
+ } satisfies DomainInfo
39
+
40
+ const installed = bundle.create(source)
41
+ const bundled = { domain: info, bundle: installed } satisfies DomainBundle
4
42
 
5
43
  describe('parseIntrospectTarget', () => {
6
44
  test('accepts a bare origin', () => {
@@ -18,4 +56,45 @@ describe('parseIntrospectTarget', () => {
18
56
  test('rejects an @id', () => {
19
57
  expect(() => parseIntrospectTarget('@abc')).toThrow('not an @id')
20
58
  })
59
+
60
+ test('routes bare, bundle, Method, and Function commands through the current Schema API', async () => {
61
+ const calls: string[] = []
62
+ const schema = {
63
+ inspect: async (origin: string) => {
64
+ calls.push(`inspect:${origin}`)
65
+ return info
66
+ },
67
+ bundle: async (origin: string) => {
68
+ calls.push(`bundle:${origin}`)
69
+ return bundled
70
+ },
71
+ }
72
+ const results: unknown[] = []
73
+ const runKernelCommand = async (input: {
74
+ fn: (context: unknown) => Promise<unknown>
75
+ }): Promise<void> => {
76
+ results.push(await input.fn({ session: { schema } }))
77
+ }
78
+ const dependencies = { runKernelCommand: runKernelCommand as never }
79
+
80
+ await introspectCommand('host.astrale.ai', { json: true }, dependencies)
81
+ await introspectCommand('host.astrale.ai', { bundle: true, json: true }, dependencies)
82
+ await introspectCommand(
83
+ '/:host.astrale.ai:class.Manager:createInstance',
84
+ { json: true },
85
+ dependencies,
86
+ )
87
+ await introspectCommand('/:host.astrale.ai:function.journal', { json: true }, dependencies)
88
+
89
+ expect(results[0]).toBe(info)
90
+ expect(results[1]).toBe(bundled)
91
+ expect(results[2]).toMatchObject({ class: 'Manager', method: 'createInstance' })
92
+ expect(results[3]).toMatchObject({ function: 'journal' })
93
+ expect(calls).toEqual([
94
+ 'inspect:host.astrale.ai',
95
+ 'bundle:host.astrale.ai',
96
+ 'bundle:host.astrale.ai',
97
+ 'bundle:host.astrale.ai',
98
+ ])
99
+ })
21
100
  })
@@ -1,11 +1,11 @@
1
- import { Path } from '@astrale-os/sdk/graph/path'
2
- import { K } from '@astrale-os/sdk/schema'
1
+ import type { InstallRequest, InstallResult, OperationId } from '@astrale-os/sdk/client/schema'
2
+
3
3
  import chalk from 'chalk'
4
4
 
5
5
  import type { KernelCommandOpts } from '../../connection'
6
6
  import type { CommandDefinition } from '../../program/index'
7
7
 
8
- import { createPathCall, runKernelCommand, withAdminClientSession } from '../../connection'
8
+ import { runKernelCommand, withAdminClientSession } from '../../connection'
9
9
  import { formatKernelError } from '../../connection/errors'
10
10
  import { AstraleError } from '../../errors'
11
11
  import {
@@ -23,35 +23,61 @@ import { confirmWithInput, promptText, selectFrom } from '../../lib/prompt'
23
23
  import { isHttpUrl } from '../../lib/validation'
24
24
 
25
25
  /** Public Kernel install syscall input for one remote URL. */
26
- export function directInstallCallInput(url: string, operation: string, token?: string) {
26
+ export function directInstallCallInput(
27
+ url: string,
28
+ operation: string,
29
+ token?: string,
30
+ ): InstallRequest {
31
+ const domain = Object.freeze({
32
+ publication: Object.freeze({
33
+ url,
34
+ ...(token === undefined ? {} : { token }),
35
+ }),
36
+ })
27
37
  return Object.freeze({
28
- operation,
29
- domains: [
30
- Object.freeze({
31
- source: Object.freeze({
32
- kind: 'remote' as const,
33
- url,
34
- ...(token === undefined ? {} : { token }),
35
- }),
36
- }),
37
- ],
38
+ operation: acceptOperationId(operation),
39
+ domains: Object.freeze([domain] as const),
38
40
  })
39
41
  }
40
42
 
41
- type DirectInstallResult = {
43
+ export interface DirectInstallPresentation {
42
44
  readonly operation: string
43
- readonly transitions: readonly {
44
- readonly intent: {
45
- readonly origin: string
46
- readonly target?: { readonly schemaRevision?: string } | null
45
+ readonly origin: string
46
+ readonly revision: string
47
+ readonly status: 'installed' | 'already current'
48
+ }
49
+
50
+ /** Stable CLI presentation derived from the canonical binary install result. */
51
+ export function directInstallPresentation(
52
+ result: InstallResult,
53
+ requestedOperation: string,
54
+ ): DirectInstallPresentation {
55
+ if (result.changed) {
56
+ const installed = result.receipt.transitions[0]?.intent
57
+ if (!installed) throw new Error('Kernel install returned no committed Domain transition.')
58
+ if (!installed.generation) {
59
+ throw new Error('Kernel install returned a committed transition without a Domain generation.')
47
60
  }
48
- }[]
61
+ return Object.freeze({
62
+ operation: result.receipt.operation,
63
+ origin: installed.origin,
64
+ revision: installed.generation.revision,
65
+ status: 'installed' as const,
66
+ })
67
+ }
68
+ const installed = result.domains[0]
69
+ return Object.freeze({
70
+ operation: requestedOperation,
71
+ origin: installed.origin,
72
+ revision: installed.revision,
73
+ status: 'already current' as const,
74
+ })
49
75
  }
50
76
 
51
77
  const OPERATION_ID_PATTERN =
52
78
  /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u
53
79
 
54
- function acceptOperationId(input: unknown): string {
80
+ function acceptOperationId(input: unknown): OperationId {
55
81
  if (typeof input !== 'string' || !OPERATION_ID_PATTERN.test(input)) {
56
82
  throw new AstraleError(
57
83
  'INVALID_FLAG',
@@ -59,10 +85,10 @@ function acceptOperationId(input: unknown): string {
59
85
  'Omit --operation for a fresh install; use it only with the exact UUID printed for recovery.',
60
86
  )
61
87
  }
62
- return input
88
+ return input as OperationId
63
89
  }
64
90
 
65
- function createOperationId(): string {
91
+ function createOperationId(): OperationId {
66
92
  return acceptOperationId(globalThis.crypto.randomUUID())
67
93
  }
68
94
 
@@ -407,27 +433,20 @@ export async function installDirect(
407
433
  const url = target as string
408
434
  const retry = directInstallRetry(url, operation, opts)
409
435
 
410
- await direct.runKernelCommand<DirectInstallResult>({
436
+ await direct.runKernelCommand<InstallResult>({
411
437
  opts,
412
438
  label: `Installing domain from ${url} (operation ${operation})`,
413
439
  recovery: { operation, retry },
414
440
  fn: async ({ session }) =>
415
- (await session.call(
416
- createPathCall(
417
- Path.project(K.functions.install.ref).raw,
418
- directInstallCallInput(url, operation, opts.token),
419
- ),
420
- )) as DirectInstallResult,
441
+ session.schema.install(directInstallCallInput(url, operation, opts.token)),
421
442
  format: (result, fmtOpts, isRaw) => {
422
443
  if (isRaw) {
423
444
  output(result, fmtOpts)
424
445
  return
425
446
  }
426
- const installed = result.transitions[0]?.intent
427
- if (!installed) throw new Error('Kernel install returned no committed Domain transition.')
428
- const revision = installed.target?.schemaRevision ?? result.operation
429
- log.success(`Domain installed: ${installed.origin}@${revision}`)
430
- log.dim(` operation: ${result.operation}`)
447
+ const installed = directInstallPresentation(result, operation)
448
+ log.success(`Domain ${installed.status}: ${installed.origin}@${installed.revision}`)
449
+ log.dim(` operation: ${installed.operation}`)
431
450
  // Belt-and-braces: the kernel-confirmed origin is authoritative. If it
432
451
  // aliases the host and the pre-install gate never consented to THAT
433
452
  // origin (lying or unavailable Publication), say so loudly after the fact.
@@ -1,3 +1,5 @@
1
+ import type { DomainBundle, DomainInfo, SchemaApi } from '@astrale-os/sdk/client/schema'
2
+
1
3
  import { Path } from '@astrale-os/sdk/graph/path'
2
4
 
3
5
  import type { KernelCommandOpts } from '../connection'
@@ -11,7 +13,18 @@ import { describeCallableFromBundle, missingCallableDescription } from './call-d
11
13
 
12
14
  type IntrospectOpts = KernelCommandOpts & { bundle?: boolean }
13
15
 
14
- export async function introspectCommand(target: string, opts: IntrospectOpts): Promise<void> {
16
+ interface IntrospectDependencies {
17
+ readonly runKernelCommand: typeof runKernelCommand
18
+ }
19
+
20
+ const defaultIntrospectDependencies = Object.freeze({ runKernelCommand })
21
+
22
+ export async function introspectCommand(
23
+ target: string,
24
+ opts: IntrospectOpts,
25
+ dependencies: Partial<IntrospectDependencies> = {},
26
+ ): Promise<void> {
27
+ const introspect = { ...defaultIntrospectDependencies, ...dependencies }
15
28
  let origin: string
16
29
  let path: Path
17
30
  try {
@@ -21,30 +34,14 @@ export async function introspectCommand(target: string, opts: IntrospectOpts): P
21
34
  }
22
35
  const wantsCallable = isCallablePath(path)
23
36
 
24
- await runKernelCommand({
37
+ await introspect.runKernelCommand({
25
38
  opts,
26
39
  label: `Introspect ${origin}`,
27
40
  fn: async ({ session }) => {
28
41
  const includeBundle = wantsCallable || opts.bundle === true
29
- const result = await session.schema.introspect({
30
- from: { kind: 'installation', origin },
31
- select: {
32
- state: true,
33
- target: true,
34
- source: true,
35
- readiness: true,
36
- capabilities: true,
37
- ...(includeBundle ? { bundle: true as const } : {}),
38
- },
39
- })
40
- if (result === null) {
41
- throw new AstraleError(
42
- 'DOMAIN_NOT_INSTALLED',
43
- `Domain ${origin} is not installed on this Kernel.`,
44
- )
45
- }
42
+ const result = await readInstalledDomain(session.schema, origin, includeBundle)
46
43
  if (wantsCallable) {
47
- const described = describeCallableFromBundle(path, result.bundle)
44
+ const described = describeCallableFromBundle(path, (result as DomainBundle).bundle)
48
45
  if (described === undefined) throw missingCallableDescription(path.raw)
49
46
  return described
50
47
  }
@@ -54,6 +51,29 @@ export async function introspectCommand(target: string, opts: IntrospectOpts): P
54
51
  })
55
52
  }
56
53
 
54
+ export function readInstalledDomain(
55
+ schema: Pick<SchemaApi, 'inspect' | 'bundle'>,
56
+ origin: string,
57
+ includeBundle: true,
58
+ ): Promise<DomainBundle>
59
+ export function readInstalledDomain(
60
+ schema: Pick<SchemaApi, 'inspect' | 'bundle'>,
61
+ origin: string,
62
+ includeBundle: false,
63
+ ): Promise<DomainInfo>
64
+ export function readInstalledDomain(
65
+ schema: Pick<SchemaApi, 'inspect' | 'bundle'>,
66
+ origin: string,
67
+ includeBundle: boolean,
68
+ ): Promise<DomainInfo | DomainBundle>
69
+ export function readInstalledDomain(
70
+ schema: Pick<SchemaApi, 'inspect' | 'bundle'>,
71
+ origin: string,
72
+ includeBundle: boolean,
73
+ ): Promise<DomainInfo | DomainBundle> {
74
+ return includeBundle ? schema.bundle(origin) : schema.inspect(origin)
75
+ }
76
+
57
77
  export function parseIntrospectTarget(target: string): { origin: string; path: Path } {
58
78
  if (target.startsWith('@')) {
59
79
  throw new AstraleError(
@@ -21,7 +21,7 @@
21
21
  "typecheck": "tsgo --noEmit"
22
22
  },
23
23
  "dependencies": {
24
- "@astrale-os/sdk": "0.5.0-beta.38",
24
+ "@astrale-os/sdk": "0.5.0-beta.49",
25
25
  "@astrale-os/shell": "0.4.2-beta.3",
26
26
  "@dagrejs/dagre": "^3.0.0",
27
27
  "@radix-ui/react-collapsible": "^1.1.0",