Skip to content

Latest commit

 

History

History
616 lines (539 loc) · 34.3 KB

grammar.md

File metadata and controls

616 lines (539 loc) · 34.3 KB

Octoscript Grammar v0.2

This document specifies the portable source subset for Octoscript producers, formatters, editors, and LLMs. It is intentionally narrower than the vendored Makepad parser: compatibility syntax outside this document is not a stable Octoscript language promise. octoscript check and octoscript_core::check_syntax enforce this profile and lower canonical statement boundaries before reporting VM parser compatibility.

The parser accepts a few legacy separator and declaration forms for Makepad compatibility. Generated workflow source must use the canonical forms below. Use octoscript check before executing generated code.

Version 0.2 adds the canonical try ... catch ... expression. Every v0.1 program remains valid v0.2 source; the new form does not enable an error value, ambient effect, or new host API.

Lexical Rules

identifier       = identifier-start, { identifier-continue } ;
identifier-start = "A"..."Z" | "a"..."z" | "_" ;
identifier-continue = identifier-start | "0"..."9" ;
digit            = "0"..."9" ;
hex-digit        = digit | "a"..."f" | "A"..."F" ;

integer          = digit, { digit } ;
number           = integer, [ ".", integer ], [ exponent ] ;
exponent         = ( "e" | "E" ), [ "+" | "-" ], integer ;
string           = '"', { string-character | escape }, '"' ;
escape           = "\\", ( '"' | "\\" | "n" | "r" | "t" | unicode-escape ) ;
unicode-escape   = "u", hex-digit, hex-digit, hex-digit, hex-digit
                 | "u", "{", hex-digit, { hex-digit }, "}" ;

line-comment     = "//", { any-character-except-newline } ;
block-comment    = "/*", { any-character }, "*/" ;

Identifiers are case-sensitive. Unicode escapes use either exactly four hexadecimal digits or one through six digits between braces. if, elif, else, try, for, in, loop, while, fn, let, return, break, continue, use, true, false, and nil are canonical keywords. var, match, ok, do, and, or, is, mut, me, and scope are reserved compatibility or contextual words and are rejected in canonical source. catch is a contextual separator after a try branch and remains an ordinary identifier elsewhere. A Unicode escape must encode a valid Unicode scalar value: surrogate code points and values above U+10FFFF are rejected. Strings use double quotes. A decimal point in a number must be immediately followed by a digit; write 5 .field or (5).field for intentional numeric field access. Block-comment terminators follow the inherited streaming tokenizer: **/ does not form an overlapping terminator, so use a single * immediately followed by / when closing a block comment.

Program and Statements

program            = { statement, statement-end } ;
statement-end      = newline | ";" ;

statement          = import
                   | declaration
                   | function-declaration
                   | return-statement
                   | break-statement
                   | continue-statement
                   | expression ;

import             = "use", module-path ;
module-path        = "mod", ".", identifier, { ".", identifier } ;
declaration        = "let", identifier, [ "=", expression ] ;
function-declaration = "fn", identifier, parameter-list, block ;
parameter-list     = "(", [ identifier, { ",", identifier } ], ")" ;
return-statement   = "return", [ expression ] ;
break-statement    = "break" ;
continue-statement = "continue" ;
block              = "{", { statement, statement-end }, "}" ;

Use a newline after every top-level and block statement. Semicolons are accepted when emitting a compact one-line program; commas are reserved for argument, array, record-member, and parameter separation. let bindings may be reassigned with an assignment operator such as =, +=, or -=. Multiline records may include leading, separating, and closing newlines, but not a trailing comma. Before canonical preflight or execution reaches the inherited VM, Octoscript lowers each validated newline statement boundary to an explicit VM semicolon. This preserves portable newline semantics even though the inherited streaming tokenizer otherwise treats newlines as whitespace.

Expressions

expression         = control-expression | assignment ;
control-expression = conditional-expression
                   | try-expression
                   | loop-expression ;
conditional-expression = "if", assignment, expression-or-block,
                         { "elif", assignment, expression-or-block },
                         [ "else", expression-or-block ] ;
try-expression     = "try", try-branch, "catch", try-branch ;
try-branch         = expression | value-block ;
value-block        = "{", { statement, statement-end },
                     expression, statement-end, "}" ;
expression-or-block = block | expression ;

loop-expression    = "for", for-bindings, "in", assignment, block
                   | "loop", block
                   | "while", assignment, block ;
for-bindings       = identifier, [ ",", identifier, [ ",", identifier ] ] ;

assignment         = logical-or, [ assignment-operator, assignment ] ;
logical-or         = logical-and, { "||", logical-and } ;
logical-and        = equality, { "&&", equality } ;
equality           = comparison, { ( "==" | "!=" ), comparison } ;
comparison         = additive, { ( "<" | "<=" | ">" | ">=" ), additive } ;
additive           = multiplicative, { ( "+" | "-" ), multiplicative } ;
multiplicative     = unary, { ( "*" | "/" | "%" ), unary } ;
unary              = [ "!" | "-" | "+" ], postfix ;
postfix            = primary,
                     { call | field-access | index-access | ".await()" } ;
call               = "(", [ expression, { ",", expression } ], ")" ;
field-access       = ".", identifier ;
index-access       = "[", expression, "]" ;

primary            = literal
                   | identifier
                   | array
                   | record
                   | "(", expression, ")"
                   | lambda ;
literal            = number | string | "true" | "false" | "nil" ;
array              = "[", [ expression, { ",", expression } ], "]" ;
record             = "{", { newline },
                     [ record-member, { record-separator, record-member }, { newline } ],
                     "}" ;
record-member      = identifier, ":", expression ;
record-separator   = ",", { newline } | newline, { newline } ;
lambda             = "||", lambda-body
                   | "|", identifier, { ",", identifier }, "|", lambda-body ;
lambda-body        = block | expression ;

assignment-operator = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ;

The { immediately after try or catch always starts a value block. To use a record literal as the whole branch, parenthesize it, for example try ({value: 1}) catch ({value: 0}). A value block's final statement must be a value-producing expression. for, loop, and while do not produce a value; place an explicit nil or another result expression after them.

An if/elif/else block produces its final expression value, including when that expression ends in a newline or semicolon. An empty branch or a branch ending in a declaration or loop produces nil. Function blocks still use explicit return. Statement separators do not discard conditional results.

! always produces a Boolean by negating truth conversion. Comparisons bind more tightly than equality, && more tightly than ||; logical operators short-circuit their entire right-hand expression. ~ is an inherited logging operator, not a canonical unary operator, and the standalone runtime denies its output even through its trusted compatibility entry point.

Array indexing accepts only finite, non-negative integral numbers. A negative, fractional, string, Boolean, or nil index raises a catchable error before any read or write. Numeric string conversion is independent of inline versus heap storage: valid numeric text converts to a number; invalid text produces NaN. NaN is unequal to itself and cannot cross the JSON boundary.

Canonical numeric literals must be finite. Integer literals and VM JSON input and output are restricted to -9007199254740991 through 9007199254740991. Larger integer identifiers must be strings. An unsafe literal is a syntax error; unsafe JSON conversion is a catchable error. Arithmetic uses binary64 and may produce values outside that range, but integer results outside it cannot be serialized as JSON. Fractional numbers retain binary64 rounding.

Structural equality visits each distinct container pair once, including cycles and shared subgraphs. Comparison consumes VM instruction fuel and checks the hard deadline, with an independent ceiling of 65,536 work units per comparison. Exhaustion terminates evaluation and cannot be caught. An uncaught ordinary script error also terminates evaluation before any later instruction or host effect; try/catch is the explicit recovery mechanism.

The grammar makes the portable operator precedence explicit rather than making every inherited VM operator part of the language contract. Use parentheses when a generated expression mixes control expressions and operators, including when a control expression or lambda supplies an if/elif/while condition or for iterable. For example, write if (|| ready) { ... } rather than an unparenthesized lambda condition. A lambda cannot be an unparenthesized if/elif/else branch; place it in a block, for example if ready {\n |value| value\n}. A tool promise is explicitly awaited with tool.start(...).await(); await is not a standalone keyword or scheduler.

Recoverable Errors

try protected catch fallback evaluates to the protected branch's value when that branch succeeds. An ordinary script or native-binding error unwinds Octoscript function calls to the nearest active try, discards the error, and evaluates the fallback branch. A fallback error can be caught only by an enclosing try or otherwise reaches the host. Generated source should normally use blocks for both branches when either branch has more than one expression. Each branch block must end with a value-producing expression so the enclosing try always has a value; write nil explicitly when no other value is needed. Parenthesize a protected expression that is itself an identifier named catch to disambiguate it from the separator.

The language deliberately exposes no error object, message, stack, or pattern binding to the fallback. Assertions, type and lookup errors, denied or failed tool calls, and a failed deferred tool after await() are catchable. A hard string-allocation, heap-allocation, operand-stack, call-frame, instruction-limit, hard evaluation deadline, or internal VM bail is not catchable.

Recovery is control flow, not a transaction. It does not roll back a Rust adapter effect, refund a capability call, widen a lease, erase an audit event, or bypass a workflow input/output contract. A tool invoked by the fallback is a separate call that must pass the same host policy. Hosts must not treat a caught failure as proof that an external effect did not happen.

Compatibility Boundary

octoscript check rejects Makepad-only compatibility forms even when the vendored VM parser would accept them. This includes var, match, the legacy catch-less try protected fallback [ok success] form, standalone ok, typed or destructuring declarations, numeric suffixes, single-quoted strings, range and other noncanonical operators, and record members separated only by spaces. The checker also rejects trailing commas where this grammar does not admit them. This keeps LLM output deterministic: valid source has one documented producer grammar rather than an inherited parser superset.

For a generated indexed loop, use the bounded core helper array.range(start, end) rather than an inherited range operator. It builds a fresh half-open array without host access or authority.

The VM remains the execution engine, but Runtime::eval and CapabilityRuntime::eval now enforce this profile and perform canonical statement-boundary lowering before evaluation. The explicit check_vm_compatibility and check_vm_compatibility_named APIs instead let a trusted migration or UI host inspect raw inherited Makepad syntax with source, VM-token, and delimiter-nesting bounds but without canonical lowering or evaluation. Runtime::eval_vm_compatibility uses that same preflight before it evaluates. These compatibility APIs must not receive LLM-generated or otherwise untrusted source, and they do not resolve imports, install modules, or grant authority. They also reject Makepad @(index) host-value tokens because standalone Octoscript has no host value table; reviewed capability adapters are the Rust integration boundary. A canonical profile rejection returns before the inherited tokenizer or parser sees the source. The development CLI performs canonical preflight automatically for eval and run.

Before either canonical or compatibility evaluation, standalone Runtime masks inherited Makepad UI, debug, shader, pod, GC, math, regex, HTML, and direct-output entry points and clears inherited primitive type methods. In particular, generated source cannot reach the vendored std.log, std.print, std.println, std.regex, String.parse_html(), raw array.push(), or record-prototype APIs. The only direct primitive boundary restored by Octoscript is bounded value.to_json(), document.parse_json(), and string.to_bytes(); local array mutation uses array.push(value, item). mod.std.assert, normal language operations, the frozen Octoscript-owned mod.std.math scalar helpers, and the bounded mod.std.json data, mod.std.text, mod.std.array, and mod.std.object helpers remain available, along with reviewed host-installed mod.tool or direct capability modules. A trusted host that needs the broader Makepad surface must embed the raw Makepad VM itself; the compatibility APIs do not restore those bindings.

The tracked makepad_ui_counter.octoscript fixture passes the bounded compatibility preflight to catch parser drift, but it remains outside this grammar and cannot run through octoscript-cli. It requires a Makepad UI host that supplies widget modules and ui; see Makepad UI compatibility.

Generated UI source

The two boundaries above leave a gap. This grammar rejects UI constructs, and the compatibility APIs that accept them must not receive generated source — so a host wanting LLM-authored UI has no supported path and must embed the raw Makepad VM, which restores the very bindings this profile masks.

UI Profile Level 0 is that third path: a sibling canonical profile that admits UI constructs while admitting nothing which can reach a capability. Sources are queries the runtime resolves before realization and injects as data.

It is implemented — octoscript_core::ui_l0::check_ui_l0 accepts or rejects, and realize produces a renderer-neutral node tree. Note what that entry point does not do: L0 has no expression form, so there is nothing to evaluate and realization never enters the VM. Authority confinement is therefore structural in the strongest sense available — not a sandbox blocking calls, and not an evaluator holding an empty module table, but no execution machinery in the path at all.

Canonical Workflow Source

use mod.tool
use mod.std.assert

let values = [20, 22]
let request = {left: values[0], right: values[1]}
let raw = tool.call_json("math.add", request)
let result = raw.parse_json()

fn valid_total(value) {
    return value == 42
}

assert(valid_total(result.total))

use only names a module. It never grants authority. Tool names, JSON contracts, capabilities, and effectful execution remain host policy; the grammar checker deliberately does not resolve them.

Syntax Preflight

Use the CLI before evaluating a generated file:

cargo run -p octoscript-cli -- check workflow.octoscript

The command prints one JSON object containing valid, a bounded diagnostics list with one-based line and column fields, and diagnostics_truncated. It validates the canonical grammar first and then checks that accepted source is compatible with the vendored VM. It never creates a capability runtime, loads a module, invokes a tool, or executes bytecode. By default, preflight accepts at most 256 KiB of source, 32,768 lexical tokens, and 128 nesting levels. Canonical validation applies the nesting limit to grammar recursion; compatibility validation applies it to structural delimiters before the vendored parser runs. Canonical source uses that same bounded VM preflight after grammar admission, so it does not bypass the VM tokenizer limits.

Rust hosts can call octoscript_core::check_syntax or octoscript_core::check_syntax_named. These functions apply the normal source-size syntax-token, and nesting limits but not instruction or deadline execution limits because they do not execute source. Embedded hosts can lower any of those bounds with ExecutionLimits.

Editor and generator tooling that needs an outline can call octoscript_core::top_level_declarations or octoscript_core::top_level_declarations_named. They apply the same bounded canonical and VM-compatibility checks, then return only top-level fn and let declarations with UTF-8 byte spans for the declaration and identifier. Invalid source returns an empty outline; call check_syntax for diagnostics. The API never evaluates source, resolves imports, or creates a capability host.

The development CLI exposes the same operation as octoscript outline <file>. It prints JSON with valid, bounded diagnostics, and a declarations array. A declaration has kind (function or let), name, and declaration and selection UTF-8 byte spans. Invalid source prints its diagnostics with an empty declaration list and exits nonzero, just as octoscript check does.

Formatting

Format canonical source through the same profile and VM-compatibility checks:

cargo run -p octoscript-cli -- format workflow.octoscript

The formatter writes to standard output and never modifies the input file. It preserves comments and the spelling of identifiers, numbers, and string literals, while normalizing horizontal whitespace, existing-line indentation, line endings, and trailing whitespace. It is idempotent. It rejects invalid source and Makepad-only compatibility syntax instead of applying recovery or rewriting it into a different language contract.

Canonical source accepts LF and CRLF line endings. A bare carriage return is rejected because the vendored VM does not treat it as a statement separator; formatted output uses LF.

Use octoscript format --check workflow.octoscript to exit nonzero when formatting would change the source. Rust hosts can call octoscript_core::format_source or octoscript_core::format_source_named; both use the supplied ExecutionLimits source, syntax-token, and nesting bounds, cap output at four times the source budget, and never evaluate code.

Editor protocol

octoscript-lsp exposes the same effect-free validation and formatting operations over stdio LSP. It uses UTF-16 positions, requests full-document sync, and supports textDocument/didOpen, textDocument/didChange, textDocument/didClose, textDocument/formatting, textDocument/documentSymbol, textDocument/definition, textDocument/references, textDocument/hover, and textDocument/documentHighlight, textDocument/completion, and textDocument/signatureHelp. When the client supports versioned documentChanges, it also advertises textDocument/prepareRename and textDocument/rename. Document symbols list only top-level fn and let declarations after canonical syntax succeeds. The other semantic requests use a grammar-aware same-document lexical index for the final binding introduced by use, named functions, let, function and lambda parameters, and for bindings already introduced in a visible runtime scope. Lexical hover reports only that binding kind, and highlights use the neutral text kind rather than claiming read/write analysis. The index is bounded to 4,096 retained definition/reference occurrences. Retained definition and hover results remain sound after truncation, but exhaustive reference, highlight, and rename requests fail when that bound is exceeded.

For a direct expression call name(...), signature help can show the parameter names of an exact visible named fn declaration from the same document. It requires a retained expression site and untruncated lexical symbols, scans at most 64 KiB and 64 parameters from the completed declaration header, and presents no partial signature. Aliases, members, computed callees, shadowed or forward bindings, incomplete headers, and source outside the safe diagnostic prefix have no named-function result. This is lexical source metadata only: it does not infer a result type, inspect a runtime value, or grant authority.

Completion sites are identifiers parsed in expression position. Declarations, import paths, record keys, and names after . are excluded. The cursor may be inside the identifier or exactly at its end. The result contains the complete retained set of lexical bindings visible at the token start, deduplicated by the innermost binding and sorted by name; it is deliberately not filtered by the current token spelling. Each item carries a textEdit that replaces the whole identifier. The site list has its own 4,096-entry cap, and either a truncated symbol index or site list sets isIncomplete. A retained site can be served after site-list truncation, but symbol-index truncation returns no candidates because an omitted inner definition could shadow a retained outer binding. In invalid source, a site is eligible only when it ends at or before the first syntax diagnostic.

Separately, the server recognizes an exact visible direct initializer of the form let binding = { ... } plus exact let alias = binding, let alias = binding.child, or let alias = binding.child.grandchild source edges. At a direct binding.field member site, or a direct binding.child.grandchild.field site where both child values are whole literals, including a lexical alias chain of at most 16 hops with at most two direct alias child selections in total, whether in one edge or spread across the chain, it offers the retained field names, hovers a known field, and defines it at the literal key. Alias targets resolve at their source position, preserving lexical shadowing. This is bounded source metadata, not general type inference: it does not follow parenthesized or computed aliases, parenthesized or computed child values, alias or member paths beyond that two-level budget, assignments, control flow, function returns, imports, or runtime values. Duplicate fields at any retained literal level discard that level's nested shape. It retains at most 1,024 root shapes, 4,096 aggregate retained literal fields, and 1,024 alias edges. A truncated shape report marks its completion incomplete and never exposes a partial field list for a binding. A truncated alias report returns no static field items, marks completion incomplete, and disables static field hover and definition. The LSP stops using a shape after an earlier direct write or a potentially mutating member, index, call, or escape path through the root or any retained root, child, or grandchild alias that resolves to it. Static field hover and definition also fail closed when the lexical index is truncated, because an omitted earlier reference could be a mutation.

Separately, the core imported_module_call_hint_report review API can preserve an imported mod.* path through an exact local let alias = binding chain of at most 16 hops for an exact alias.method(...) call. It examines every reference in that import-alias group and accepts only another exact group alias or a direct member call; writes, member aliases, indexing, calls that pass the value onward, and incomplete alias metadata suppress the hint. This whole-group rule also rejects a call captured in a function when a later source statement could rewrite the receiver before invocation. This is a bounded pre-approval presentation aid, not module resolution or authorization. The LSP catalog behavior below can use the same stable exact root-alias form for advisory module metadata, but remains source-only and keeps fixed mod.tool suggestions limited to a direct visible import binding.

The server separately recognizes an exact visible use mod.tool binding. At a direct tool. member site it offers the fixed call, call_json, start, and start_json methods. An editor integration may also provide a bounded advisory tool-catalog projection at initializationOptions.octoscript.toolCatalog or a later settings.octoscript.toolCatalog configuration update. The LSP retains only bounded name, format, and description metadata and uses it only for the first literal argument of direct visible tool.call/tool.start (matching text) or tool.call_json/tool.start_json (matching json). It never reads a host runtime, URI, file, adapter, or environment to obtain that projection, and a suggestion does not make a capability installed, approved, or callable. An omitted key retains prior metadata, JSON null explicitly clears it, and malformed, duplicate, or over-limit input is discarded as a whole rather than presented partially.

An exact visible direct use mod.std.math binding has a separate fixed core projection. At a direct math. member site it offers only the documented scalar functions and pi/e constants, with plain-text member hover and function signature help. Its descriptors are compiled into the LSP, not read from an advisory module catalog; the server does not resolve a module, follow a local alias, or derive a host capability. A shadowed binding, chained receiver, unknown member, or invalid source prefix has no fixed-math result, and advisory metadata cannot add members to this core module.

An exact visible direct use mod.std.json binding has a separate fixed core projection for only json.parse(document) and json.stringify(value), with plain-text member hover and function signature help. Its descriptors are compiled into the LSP rather than read from a catalog, and describe the same bounded JSON data boundary as the runtime without resolving a host module or granting a capability. A shadowed binding, chained receiver, unknown member, or invalid source prefix has no fixed-JSON result, and advisory metadata cannot add members to this core module.

An exact visible direct use mod.std.array binding has a separate fixed core projection for only array.len(value), array.has_index(value, index), array.get(value, index, fallback), array.contains(value, item), array.index_of(value, item), array.slice(value, start, end), array.range(start, end), array.concat(left, right), array.compact(value), array.unique(value), array.reverse(value), array.flatten(value), and array.push(value, item), with plain-text member hover and function signature help. Its descriptors are compiled into the LSP rather than read from a catalog, and describe the runtime's bounded local array operations without resolving a host module or granting a capability. A shadowed binding, chained receiver, unknown member, or invalid source prefix has no fixed-array result, and advisory metadata cannot add members to this core module.

An exact visible direct use mod.std.object binding has a separate fixed core projection for only object.len(value), object.has(value, key), object.get(value, key, fallback), object.pick(value, keys), object.omit(value, keys), object.from_entries(entries), object.with(value, key, item), object.keys(value), object.entries(value), object.values(value), and object.merge(left, right), with plain-text member hover and function signature help. Its descriptors are compiled into the LSP rather than read from a catalog, and describe bounded own-field record shaping without resolving a host module or granting a capability. A shadowed binding, chained receiver, unknown member, or invalid source prefix has no fixed-object result, and advisory metadata cannot add members to this core module.

An exact visible direct use mod.std.text binding has a separate fixed core projection for the documented text-shaping functions, including text.slice(value, start, end), text.index_of(value, needle), text.last_index_of(value, needle), text.split(value, delimiter), and text.join(values, separator), with plain-text member hover and function signature help. Its descriptors are compiled into the LSP rather than read from a catalog, and describe bounded local string operations without resolving a host module or granting a capability. A shadowed binding, chained receiver, unknown member, or invalid source prefix has no fixed-text result, and advisory metadata cannot add members to this core module.

An exact visible direct use mod.std.assert binding receives fixed plain-text hover and assert(condition) signature help. A direct std.assert(...) call after use mod.std receives the same fixed signature. These assertions are compiled-in source metadata, not a host lookup or capability grant; aliases, shadowed bindings, and non-core imports receive no fixed assertion result.

The same compiled-in projection completes std at a statement-position use mod. path and array/assert/json/math/object/text below use mod.std.. The frozen mod.std namespace has no catalog descendants: advisory metadata cannot add log, inspect, or children below mod.std.assert, mod.std.json, mod.std.math, mod.std.object, mod.std.text, or mod.std.array. If an advisory catalog is unavailable, the fixed std root candidate remains visible but is incomplete because non-core mod.* siblings could be absent; the closed mod.std list remains complete.

For an exact visible imported capability-module leaf, the optional advisory moduleCatalog may additionally declare callMode, callShape: "single_json", and compact inputFields and outputFields. Each field has a canonical identifier, one fixed JSON type, a required bit, and optional plain-text description. An object input or output field may carry one direct child fields list; children cannot. The LSP shows both bounded field lists in leaf hover and signature help, and can complete an undeclared root key or one direct object-child key and hover an exact known key in the first direct literal-record argument from inputFields. For an exact source binding through a direct visible import or qualifying exact root alias, such as let result = receiver.method(input) on a synchronous leaf, or the exact let result = receiver.method(input).await() form on a deferred leaf, it also completes and hovers root result.field names from outputFields and one retained object-child path such as result.summary.total. It can follow exact local let alias = result chains of at most 16 hops and serves the same paths at alias.summary.total. The whole result-alias group, including aliases declared after the member site, must remain stable; truncated alias metadata makes output completion empty and incomplete. That bounded recognizer rejects parenthesized or computed aliases, deeper alias chains, mutations or escapes, parenthesized or computed initializers, extra arguments, other postfix chains, input paths below that direct child level, result paths below that child level, shadowed imports, and source beyond the safe diagnostic prefix. It does not evaluate JSON Schema, read a runtime, validate a contract, or grant a capability. Record fields without the exact one-JSON-value call shape, and any malformed or over-limit field projection, fail closed with the rest of the advisory module metadata.

For host-managed dataflow authoring, an editor may separately provide the bounded initializationOptions.octoscript.workflowDataCatalog projection. The LSP uses it only for direct unshadowed workflow.input.* and workflow.outputs.<stepId>.* completion and field hover. It is static advisory metadata, not a JSON Schema loader or a runtime snapshot: it cannot establish that an output is completed, validate a value, approve a plan, issue a lease, or make an adapter callable. A visible local or imported workflow binding shadows it, absent metadata creates no namespace, and malformed input fails closed. The optional workflowDataStepContext admits only an exact prefix of the catalog's projected outputs and its next projected step, then filters output completion and hover to that prefix. A host using octoscript-workflow can generate that pair from an exact suspended continuation or validated checkpoint, but the LSP remains unable to inspect or authorize runtime state. A host can replace the complete pair through workspace/didChangeConfiguration; partial or malformed relevant updates fail closed instead of retaining stale fields. See Editor workflow-data projection. Sending both workflow keys as JSON null explicitly clears a prior projection after terminal state.

Rename does not edit the final segment of a use path. For another indexed binding it accepts exactly one non-reserved canonical identifier, rewrites the definition and resolved references in memory, validates the resulting canonical and VM-compatible source, remaps every symbol span, and requires the entire new lexical report to match. A capture or shadowing change therefore fails closed. The returned edit targets only the client-supplied URI and carries the exact open-document version. This preserves the indexed lexical model only; unindexed forward references, modules, fields, types, and name-coupled runtime behavior remain outside the claim.

The lexical service does not load or resolve imported modules, infer forward references or general types, follow arbitrary aliases or mutations, treat record keys or general member fields as variables, enumerate builtins, discover a host tool catalog, or grant tool authority. The bounded direct literal-record and direct-alias metadata above is advisory and remains separate from those lexical bindings. The optional initialization-time or configuration-refresh catalog projection is advisory client metadata, not a catalog lookup. Tool and module keys refresh independently; neither affects the atomic workflow-data pair. The server does not open the URI supplied by the client or run source. A malformed settings value or non-object settings.octoscript clears all advisory catalogs. All diagnostics, edits, symbols, definitions, references, hovers, highlights, completions, and rename validation derive from client-provided document text and optional advisory metadata.