Skip to main content
xspec gives you two ways to create a living connection between your implementation code and your specification: markers, which record a references edge in the dependency graph, and the text() function, which retrieves a node’s requirement text as a string at runtime and records an embeds edge. Both are valid TypeScript — no custom runtime or loader is required — and both are the primary mechanism by which xspec builds its queryable graph of coverage and impact.

Markers

A marker is a bare node expression used as an expression statement — that is, appearing on its own line in statement position, not assigned, not passed anywhere:
At runtime, this is a harmless property read on a plain object — it has no side effects and requires no xspec tooling to execute. The dependency graph edge is recorded at build time by xspec build, not at runtime. In the graph, the marker records a references edge from the code location of the enclosing function to the node AUTH.auth.login.valid. This edge participates in coverage analysis (which requirements are referenced by your code) and impact analysis (which code is affected when a requirement changes).
Placing a marker on the root node (e.g. AUTH alone as a statement) records a graph edge, but the root node never participates in coverage paths. Marker to root is valid but has no coverage effect.

The text() function

When you need the actual requirement text as a string at runtime — to display it, include it in a prompt, or embed it in output — use the text() function imported from the same spec module:
text(node) returns the node’s full subtree text as a string. It also records an embeds edge from the calling code location to the node, indicating that this code site directly embeds the requirement content. text() is the only way to obtain requirement text at runtime. You cannot read text off a node directly, and the string-path form that is available in MDX is not valid in TypeScript:
Always pass a fully-qualified node expression as the argument.

Rules for using spec bindings

xspec enforces a strict set of rules about how node bindings and the text binding may be used in TypeScript. These rules exist so that the static analyzer can reliably track all edges and so that rename and move refactors work correctly.

Permitted uses

  • A node expression as a marker — alone as an expression statement.
  • A node expression as the sole argument to text() — where text is the binding imported from the same module.
  • Dot access and string-literal bracket access to traverse the node tree.

Invalid uses (build errors)

Any of the above patterns will be reported as a build error by xspec build. They are also structurally ambiguous for graph analysis, so xspec cannot infer the intended edges from them.

Shadowing

If a local declaration shadows an imported spec binding, xspec treats the identifier as an ordinary local variable and does not record any graph edge:

Type-only references

Using a node in a type position — for example typeof AUTH.auth.login — is unrestricted. Type references record nothing in the graph and are not rewritten by rename or move operations.

Module branding and aliasing text

Each spec module’s node types are branded to that module. You cannot pass a node from one module to another module’s text function — this is both a TypeScript type error and a runtime throw:
When your file imports multiple spec modules, alias each module’s text export to avoid ambiguity and to make the pairing explicit, as shown above.

Code locations and edge attribution

Every edge recorded in the graph is attributed to a code location — either a whole file or a named unit within a file. A file-level location looks like:
A named-unit location appends a # and the unit’s qualified name:

What counts as a named unit

The following constructs create named units:
  • Function and class declarations
  • Class members with identifier names: methods, getters, setters, and function-valued properties
  • const, let, or var bindings whose initializer is a function or class expression
  • Namespacesnamespace A.B yields the unit name A.B
  • Default exports — an anonymous default export uses the unit name default
A marker or text() call is attributed to the innermost enclosing named unit. If no named unit encloses the reference, it is attributed to the file as a whole.

Disambiguation

When the same unit name appears more than once in a file (overloaded functions, repeated class names in different scopes), xspec disambiguates in document order:
This means renaming a unit in your source will update the attributed location across the graph — another reason to keep spec references inside properly named functions and classes rather than at the top level of a file.