Skip to content
Merged
36 changes: 36 additions & 0 deletions bench/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def _at(base: int, scale: float) -> int:
Bench('hover', lambda root, scale: corpus.write_hover(root, family_count=_at(400, scale))),
Bench('effective-types', lambda root, scale: corpus.write_hover(root, family_count=_at(300, scale))),
Bench('inlays', lambda root, scale: corpus.write_hover(root, family_count=_at(400, scale))),
Bench('source-structure', lambda root, scale: corpus.write_hover(root, family_count=_at(400, scale))),
Bench('service-session', lambda root, scale: corpus.write_hover(root, family_count=_at(400, scale))),
Bench('service-source-first', lambda root, scale: corpus.write_hover(root, family_count=_at(400, scale))),
)
Expand Down Expand Up @@ -161,6 +162,7 @@ def run_one(bench: str, config: str, entry: Path, repeat: int) -> Measurement:
'hover',
'effective-types',
'inlays',
'source-structure',
'service-session',
'service-source-first',
}:
Expand Down Expand Up @@ -206,6 +208,7 @@ def _measure_view(bench: str, entry: Path, repeat: int) -> Measurement:
'hover': _measure_hover,
'effective-types': _measure_effective_types,
'inlays': _measure_inlays,
'source-structure': _measure_source_structure,
'service-session': _measure_service_session,
'service-source-first': lambda entry, repeat: _measure_service_session(entry, repeat, source_first=True),
}.get(bench)
Expand Down Expand Up @@ -369,6 +372,38 @@ def hints() -> object:
return measure('inlays', 'unwrap', hints, repeat=repeat)


def _measure_source_structure(entry: Path, repeat: int) -> Measurement:
"""Folding and selection requests served from the workspace's source cache."""
from fastraml.gctuning import tuned_gc # noqa: PLC0415 - feature workload only
from fastraml.service import queries # noqa: PLC0415 - feature workload only
from fastraml.service.workspace import Workspace # noqa: PLC0415 - feature workload only
from fastraml.uris import path_to_file_uri # noqa: PLC0415 - feature workload only

text = entry.read_text(encoding='utf-8')
uri, folder = path_to_file_uri(entry), path_to_file_uri(entry.parent)
lines = text.splitlines()
probes = 12
step = max(1, len(lines) // probes)
positions = [
(number, len(raw) - len(raw.lstrip()) + 1)
for number, raw in enumerate(lines[2:], start=3)
if ':' in raw and not raw.lstrip().startswith('#')
][::step][:probes]

def structure() -> object:
workspace = Workspace([folder])
workspace.open(uri, text, 1)
folded = [queries.folding_ranges_of(workspace.source(uri)) for _ in range(3)]
selected = [queries.selection_ranges_of(workspace.source(uri), line, column) for line, column in positions]
if len(positions) < probes or not folded[0] or any(not spans for spans in selected):
message = 'source-structure workload no longer reaches folding and selection'
raise RuntimeError(message)
return workspace.source(uri), folded, selected

with tuned_gc():
return measure('source-structure', 'unwrap', structure, repeat=repeat)


def _measure_service_session(entry: Path, repeat: int, *, source_first: bool = False) -> Measurement:
from bench.service_session import exercise, prepare # noqa: PLC0415 - feature workload only
from fastraml.gctuning import tuned_gc # noqa: PLC0415 - feature workload only
Expand Down Expand Up @@ -547,6 +582,7 @@ def compare(results: Sequence[Measurement], tolerance: float) -> int:
'hover': 'unwrap',
'effective-types': 'unwrap',
'inlays': 'unwrap',
'source-structure': 'unwrap',
'service-session': 'unwrap',
'service-source-first': 'unwrap',
}
Expand Down
40 changes: 35 additions & 5 deletions bench/corpus.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,19 @@ def _derived(name: str, _ordinal: int, previous: str) -> str:
)


def _derived_schema(name: str, _ordinal: int, previous: str) -> str:
"""`_derived` under the deprecated `schema:` spelling (docs/18 § 6)."""
return (
f' {name}:\n'
f' schema: {previous}\n'
f' properties:\n'
f' label?: string\n'
f' weight:\n'
f' type: number\n'
f' minimum: 0\n'
)


def _array(name: str, _ordinal: int, previous: str) -> str:
return f' {name}: {previous}[]\n'

Expand Down Expand Up @@ -262,7 +275,12 @@ def _type_block(ordinal: int, previous: str | None) -> str:
name = f'T{ordinal}'
if previous is None:
return _plain(name, ordinal, '')
return _FORMS[ordinal % len(_FORMS)](name, ordinal, previous)
form = _FORMS[ordinal % len(_FORMS)]
# Every other derived type takes the deprecated `schema:` spelling, so the
# parse exercises the compatibility recording (docs/18 § 6).
if form is _derived and ordinal % 12 == 1:
form = _derived_schema
return form(name, ordinal, previous)


_COMMON = """#%RAML 1.0 Library
Expand All @@ -287,10 +305,13 @@ def _library(index: int, type_count: int, *, depth: int) -> str:
*same* file through a different spelling of the path. That is the diamond
the compose cache exists for (docs/12 § 1); if canonicalisation ever
regresses, this corpus goes quadratic and the linearity check catches it.
Every other library declares its whole table under the deprecated
`schemas:` alias, as a pre-1.0 document would: a fragment takes
`schemas:` or `types:`, never both (docs/18 § 6).
"""
lines = ['#%RAML 1.0 Library', f'usage: generated library {index}', 'uses:']
lines.append(f' common: {"../" * depth}common.raml')
lines.append('types:')
lines.append('schemas:' if index % 2 else 'types:')
previous: str | None = None
for ordinal in range(type_count):
lines.append(_type_block(ordinal, previous).rstrip('\n'))
Expand All @@ -307,6 +328,8 @@ def _api_using(libraries: list[str]) -> str:
'uses:',
]
lines.extend(f' lib{index}: {path}' for index, path in enumerate(libraries))
lines.append('schemas:')
lines.extend(_plain(f'Root{ordinal}', ordinal, '').rstrip('\n') for ordinal in range(3))
return '\n'.join(lines) + '\n'


Expand All @@ -327,7 +350,11 @@ def write_large(root: Path, *, type_count: int = 7000, library_count: int = 150)
"""`type_count` types spread over `library_count` libraries.

Sized after go-raml's published corpus (7124 types, 148 libraries). Halve
`type_count` and `library_count` together for the linearity check.
`type_count` and `library_count` together for the linearity check. Every
other library declares its types under the deprecated `schemas:` alias,
every other inherited type uses the deprecated `schema:` facet, and the
root carries a `schemas:` table of its own, so the parse exercises the
compatibility recording (docs/18 § 6).
"""
per_library, remainder = divmod(type_count, library_count)
files = {'common.raml': _COMMON}
Expand Down Expand Up @@ -375,12 +402,15 @@ def write_endpoints(root: Path, *, resource_count: int = 500) -> Path:

Measures the two-stage build (docs/12 § 1): 6000 method-level trait
applications, every one of them a tree merge rather than a model merge.
`Item` is declared in a deprecated `schemas:` table, and every fourth
resource types its body with the deprecated `schema:` facet, so the parse
exercises the compatibility recording (docs/18 § 6).
"""
lines = [
'#%RAML 1.0',
'title: Generated endpoint benchmark',
'baseUri: https://example.test',
'types:',
'schemas:',
' Item:',
' type: object',
' properties:',
Expand All @@ -398,7 +428,7 @@ def write_endpoints(root: Path, *, resource_count: int = 500) -> Path:
lines.append(' 200:')
lines.append(' body:')
lines.append(' application/json:')
lines.append(' type: Item')
lines.append(' schema: Item' if index % 4 == 0 else ' type: Item')
_write(root, {'api.raml': '\n'.join(lines) + '\n'})
return root / 'api.raml'

Expand Down
18 changes: 18 additions & 0 deletions docs/04-fragments-and-namespaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class ReferenceResolver(Fragment, Protocol):
def trait_definition(self, name: str) -> TraitDefinition: ...
def security_scheme_definition(self, name: str) -> SecuritySchemeDefinition: ...
def library_link(self, prefix: str) -> LibraryLink | None: ...
def visible_names(self, kind: str) -> Iterator[tuple[str, Declaration]]: ...
```

`ReferenceResolver` includes the `SecuritySchemeResolver` capability. Every
Expand All @@ -55,6 +56,17 @@ declarations, as does an Overlay or Extension in its target tree's declarations
implementation over two hooks: the local table and the `uses:` map. Resolver
capabilities are checked structurally.

`visible_names(kind)` enumerates names for one declaration table (`types`,
`annotationTypes`, `traits`, `resourceTypes`, or `securitySchemes`). It yields
local declarations first, then each directly imported library's declarations
with that import's prefix. Local dotted names take precedence. Every qualified
candidate must round-trip through ordinary resolution to the same entity;
unresolved imports and inaccessible dotted suffixes contribute no names.
It never follows a library's own imports. Annotation-type enumeration includes
the ordinary-type fallback, with annotation types taking precedence.
Extension resolvers enumerate only the declarations and imports visible at
their chain position. Literal included content delegates to its host resolver.

## 3. Name resolution

`resolve_reference()` resolves declarations in a fragment with a local table.
Expand Down Expand Up @@ -134,6 +146,12 @@ resolves every name through the includer and is located in the included file.

## 5. Fragment decoding

When a declaration table uses the accepted compatibility spelling `schemas:`,
the decoder records its name, authored URI and key position in
`Raml.syntax_aliases`, keyed by the fragment's entity ID. This is an authored
language fact, independent of source retention. The lint view decides whether
to report it and with what severity.

API decoding first collects global `mediaType`, `protocols`, and `securedBy`,
then decodes remaining declarations in source order. A lone `mediaType` may be
written as an annotated scalar, `{value: ..., (a): ...}`; its annotations
Expand Down
14 changes: 14 additions & 0 deletions docs/05-type-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ reference or inline declaration, not a bare sequence; use

## 3. Decoding and inference

An accepted `schema:` compatibility facet records its authored URI, position
and spelling in `Raml.syntax_aliases`, keyed by the shape's ID. It needs no
retained node and has no severity or diagnostic attached. A lint rule consumes
this fact rather than rediscovering it from every declaration's YAML tree.
Keys in examples, annotations or other opaque data do not declare such a facet.

`make_shape()` is the general declaration constructor. It records common facets
on the base, keeps remaining YAML key/value pairs flat, determines or defers the
kind, attaches it, builds the declaration facets (`properties`, `items`,
Expand Down Expand Up @@ -198,6 +204,14 @@ same selection, but preserves all possible structural alternatives when no
discriminator selects one; it does not validate the whole value to guess a
branch.

`types/navigation.py` exposes `branches(base, value)` and `children(base,
value)` over positioned `ValueNode` data. Branches follow aliases and recursion
markers and preserve every structural union candidate unless a discriminator
selects one. Children report the governing declaration, expected type, source
key span and field name or array index. Invalid values retain known field
types; ordinary additional fields have no invented declaration. The service
uses this API for typed-data tokens, independently of hover presentation.

RAML field names shared by fragment, endpoint, and type decoders are defined in
`facet_names.py`; diagnostic fields and JSON Schema keywords are separate.

Expand Down
21 changes: 19 additions & 2 deletions docs/12-performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ must receive a parser diagnostic rather than `RecursionError`.

## 4. Benchmark suite

`bench/` generates deterministic corpora and measures thirty-two workloads:
`bench/` generates deterministic corpora and measures thirty-three workloads:

| Bench | Primary coverage |
|---|---|
Expand Down Expand Up @@ -120,10 +120,11 @@ must receive a parser diagnostic rather than `RecursionError`.
| `hover` | a cold language-service snapshot, fourteen authoring hovers per type family and custom-facet definition queries: inherited summaries, facet explanations, built-ins, custom-facet declarations and supplied keys, nested custom-facet and annotation keys and values, property presence, methods and response statuses ([21](21-language-service.md) § 4.2) |
| `effective-types` | a cold service snapshot, code-lens enumeration and full-depth RAML rendering for every named type and annotation type, including nested arrays, unions, scalar item constraints and recursive references ([21](21-language-service.md) § 4.3) |
| `inlays` | a cold snapshot, inferred declaration types, expected types at supplied custom-facet/annotation roots and nested data keys, and compact inherited constraints anchored to type references ([21](21-language-service.md) § 4.4) |
| `source-structure` | three folding requests and twelve selection requests at scattered keys, served from the workspace's composed source tree, composed once per current text ([21](21-language-service.md) § 4) |
| `service-session`, `service-source-first` | representative mixed editor requests over three root-buffer versions: parser diagnostics, occurrences, outline, links, lens enumeration, 120-line viewport inlays, folding, three sparse hovers and warm queries; source-first additionally requests folding before each snapshot |

The six general workloads are `small`, `large`, `endpoints`, `extensions`,
`validate` and `jsonschema`. The other twenty-six are feature workloads:
`validate` and `jsonschema`. The other twenty-seven are feature workloads:
each exists because no general workload runs the code it covers. Their reach
tests (`tests/bench/test_corpus.py`) count calls or check bound results, and fail
if a corpus stops reaching that code at every size it covers.
Expand Down Expand Up @@ -156,6 +157,9 @@ The small corpus-validity tests run in the ordinary test suite.
For `hover`, `unwrap` builds a service snapshot with unwrap and validation,
then queries every generated hover site. It measures the cold indices and their
reuse; probe selection and corpus generation are outside the measured region.
For `source-structure`, `unwrap` opens the corpus as a buffer and serves the
folding and selection requests from the workspace's composed source tree;
probe selection and corpus generation are outside the measured region.
For `service-session` and `service-source-first`, `unwrap` runs the three-version
mixed request sequences of § 4.2; their other configurations keep their ordinary
parse/view meanings. Input texts and sparse probes are prepared outside measurement.
Expand Down Expand Up @@ -300,6 +304,19 @@ a change to a hot path, or to any code a claim is made about:
`python -m bench linearity --bench NAME` for time and memory instead.

Include the workload, both deltas, and the noise in the commit message.

A service workload's measured region is a cold snapshot: the parse, plus the
first query of each kind, then reuse of the built indices. Read its delta in
two parts. The parse side is where the base revision built and retained every
file's tree and the comparison does not. The first-use side is where a query
composes on demand the tree the base retained at parse time, one composition
per file per snapshot, about a third of a parse of the file; repeated
queries of the same kind are cached and show no delta. The first-use cost is
avoidable only by retaining the trees, the per-snapshot memory cost
(docs/15 § 2). When a change moves composition from parse time to first use,
name which side the delta is on before attributing it to the change's hot
path (docs/21 § 2, docs/21 § 4).

`bench compare` against the committed baseline is only a coarse check for
large regressions: the baseline and the comparison run at different times, and
the machine drifts in between. Follow § 5.1 before optimizing and § 5.2 when
Expand Down
6 changes: 6 additions & 0 deletions docs/13-public-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ returns RAML text ([20](20-join.md) § 8). It is not part of `fastraml.__all__`.
Pass `unwrap=True, validate=True` together unless you specifically need declared,
unflattened types. Validation alone must clone and unwrap declarations privately.

Default lint no longer needs whole trees merely to detect `schema:` and
`schemas:`: the decoders record their accepted spelling and position in
`Raml.syntax_aliases` (docs/04 § 5, docs/05 § 3), independent of source
retention. A consumer needing only that compact fact should read it rather
than retaining the whole source tree.

A configuration file's `parser:` section is `ParserConfig`. `limits(options)`
applies its `max_include_size`, `max_depth` and `regex_engine`; its
`workspace_root` and `remote` are left to the host, which weighs them against
Expand Down
24 changes: 13 additions & 11 deletions docs/15-implementation-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ that each extend the same master remains deferred (docs/19 § 7).
XML Schema external types are unsupported ([01](01-scope-and-coverage.md) § 3).

**Service baseline acceptance and recovery.** The simpler source-cache branch
is undergoing comparison with master using the portable mixed-request workloads.
passes the master-controlled rebuild and mixed-request checks after sharing its
query source owner. The user accepted its documented first-use trade-off;
CI verification and integration are pending.
The record-backed representation replacement remains parked; its recurring
rebuild and first-use costs are recorded in the
[service cost review](reports/2026-10-10/service-authoring-cost-review.md).
Expand Down Expand Up @@ -68,6 +70,13 @@ the media-type fixes and the type-walk work landed together:
endpoint build still holds; skip the swap where the node is retained anyway.
- `validate`: time up to +4.9% on some configurations in a 3-round run; a
5-round rerun of `unwrap+validate` was within noise.
- Text-only service snapshots pay an on-demand composition for a queried input
unless a compatible source entry is already ready (docs/21 § 4). The shared
owner removes the old duplicate hover/structural tree. The accepted baseline's
remaining first-use cost and post-query allocation trade-off are recorded in the
[integration report](reports/2026-10-10/service-baseline-integration.md).
Model-backed declaration inlays and cursor-local source lookup are the next
separately measured recovery candidates.

## 3. Potential future work

Expand All @@ -78,21 +87,14 @@ parser rules without an owning design document and tests.
The language service and its LSP are built ([21](21-language-service.md)).
The ordered plan for the rest, completion, editing features and MCP, is
[research/language-service-plan.md](research/language-service-plan.md): M1 to
M4 are done, and completion (M5) comes next. Its design records are
M4 are done, and completion (M5) comes next, with its prerequisites in: the
resolver's visible-names enumeration (docs/04 § 2) and the workspace's
composed source cache (docs/21 § 4). Its design records are
[archive/language-server.md](archive/language-server.md) and
[archive/language-service-architecture.md](archive/language-service-architecture.md);
its parser prerequisite, a trustworthy `parse_lenient` model, is analysed in
[research/partial-models.md](research/partial-models.md).

**Deprecated aliases as a model fact (proposed).** `deprecated-schemas` is on
by default and reads `source_info`, so every lint-enabled parse, the language
service's included, retains each declaration's YAML subtree: +7.66 MB on
`large`, against +0.56 MB for the text alone. It needs only where a `schema:`
or `schemas:` key was accepted, which the decoder knows. Recording those
(file, position, key) on the model would let the default rule set run on
`retain_text`; the three off-by-default rules that inspect how a declaration
was written keep `retain_source`.

**Sampled examples in the tree (undecided).** The viewer's request and response
panel (parked on `feat/viewer-request-samples`) needs a working body for each
payload, and only Python can produce a validated one. The candidate design:
Expand Down
Loading
Loading