From 8d41696d45f91feadc57e47a1d7b901b230f316c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 10 Oct 2026 08:24:56 +0000 Subject: [PATCH 1/2] Type qualified reference targets as ideas or attributes. Attribute links resolve to attribute nodes. The grammar type now includes that target so the project typecheck accepts the link tests. --- .../src/reqlan-reference-code-lens.ts | 5 ++- packages/language/src/reqlan-references.ts | 6 ++- packages/language/src/reqlan.langium | 7 +++- packages/language/test/linking.test.ts | 37 ++++++++++++------- reqlan rq/language/syntax.rq | 4 ++ 5 files changed, 41 insertions(+), 18 deletions(-) diff --git a/packages/language/src/reqlan-reference-code-lens.ts b/packages/language/src/reqlan-reference-code-lens.ts index 5cff6eb..9a21cc7 100644 --- a/packages/language/src/reqlan-reference-code-lens.ts +++ b/packages/language/src/reqlan-reference-code-lens.ts @@ -10,6 +10,7 @@ import { isFileReference, isFileSymbolReference, isIdea, + isIdeaDeclaration, isIdeaSet, isLocalReference, isOneLinerIdea, @@ -65,8 +66,10 @@ export function classifyReferenceForCodeLens( ); return classifyResolvedFileLink(link); } + // Attribute targets are not idea declarations. + // rq:["../../../reqlan rq/language/syntax.rq".attribute_namespacing] const ideaTarget = peekResolvedRef(referenceIdea(target)); - if (ideaTarget) { + if (ideaTarget && isIdeaDeclaration(ideaTarget)) { return { kind: 'idea', declaration: ideaTarget }; } if (isQualifiedReference(target)) { diff --git a/packages/language/src/reqlan-references.ts b/packages/language/src/reqlan-references.ts index 37e79d8..1b1b18f 100644 --- a/packages/language/src/reqlan-references.ts +++ b/packages/language/src/reqlan-references.ts @@ -1,5 +1,6 @@ /** * Shared helpers for the reference target union types. + * rq:["../../../reqlan rq/language/syntax.rq".attribute_namespacing] */ import { AstUtils, type Reference } from 'langium'; import { @@ -8,8 +9,8 @@ import { isLocalReference, isModel, isQualifiedReference, - type IdeaDeclaration, type Import, + type QualifiedIdeaTarget, type QualifiedReference, type ReferenceTarget } from './generated/ast.js'; @@ -26,7 +27,8 @@ export function referenceImport(target: ReferenceTarget): Reference | un return undefined; } -export function referenceIdea(target: ReferenceTarget): Reference | undefined { +/** Qualified targets include attributes. Local targets are idea declarations. */ +export function referenceIdea(target: ReferenceTarget): Reference | undefined { if (isQualifiedReference(target) || isLocalReference(target)) { return target.idea; } diff --git a/packages/language/src/reqlan.langium b/packages/language/src/reqlan.langium index a883c24..80e1de6 100644 --- a/packages/language/src/reqlan.langium +++ b/packages/language/src/reqlan.langium @@ -214,9 +214,14 @@ FileSymbolReference: WildcardReference: pathPattern=STRING '.' ideaPattern=WILDCARD_NAME; +// The second name of a qualified reference is an idea, an ideaset member, or an attribute. +// rq:["../../../reqlan rq/language/syntax.rq".attribute_namespacing] +// rq:["../../../reqlan rq/language/syntax.rq".references_to_subidea] +type QualifiedIdeaTarget = IdeaDeclaration | Attribute; + QualifiedReference: (path=[Import:STRING] | qualifier=[Import:ID]) '.' - (ideaset=[IdeaSet:ReferenceName] '.')? idea=[IdeaDeclaration:ReferenceName]; + (ideaset=[IdeaSet:ReferenceName] '.')? idea=[QualifiedIdeaTarget:ReferenceName]; LocalReference: idea=[IdeaDeclaration:ReferenceName]; diff --git a/packages/language/test/linking.test.ts b/packages/language/test/linking.test.ts index 903f1d3..39e715e 100644 --- a/packages/language/test/linking.test.ts +++ b/packages/language/test/linking.test.ts @@ -1528,14 +1528,19 @@ example_ideaset ( .filter(isQualifiedReference); expect(qualified).toHaveLength(2); + const parsedDocument = document; + if (parsedDocument === undefined) { + throw new Error('The parsed document is missing.'); + } + const attributeRef = qualified[0]; - expect(attributeRef.idea?.ref).toBeDefined(); - expect(isAttribute(attributeRef.idea?.ref)).toBe(true); - if (!isAttribute(attributeRef.idea?.ref)) { - return; + const attributeTarget = attributeRef?.idea?.ref; + expect(attributeTarget).toBeDefined(); + if (!isAttribute(attributeTarget)) { + throw new Error('The link target is not an attribute.'); } - expect(attributeRef.idea.ref.name).toBe('example'); - const owner = attributeRef.idea.ref.$container; + expect(attributeTarget.name).toBe('example'); + const owner = attributeTarget.$container; expect(isIdea(owner) && owner.name).toBe('attribute_namespacing'); const memberRef = qualified[1]; @@ -1566,11 +1571,11 @@ example_ideaset ( }); expect(targetLine).toContain('@example'); - const documentLinks = await services.Reqlan.lsp.DocumentLinkProvider?.getDocumentLinks(document, { - textDocument: { uri: document.textDocument.uri } + const documentLinks = await services.Reqlan.lsp.DocumentLinkProvider?.getDocumentLinks(parsedDocument, { + textDocument: { uri: parsedDocument.textDocument.uri } }); const attributeLink = documentLinks?.find(entry => - document.textDocument.getText(entry.range) === 'example' + parsedDocument.textDocument.getText(entry.range) === 'example' && entry.target?.includes(`#L${links![0].targetSelectionRange!.start.line + 1}`) ); expect(attributeLink?.target).toBeDefined(); @@ -1590,22 +1595,26 @@ example_ideaset ( } `); await services.shared.workspace.DocumentBuilder.build([document], { validation: true }); + const parsedDocument = document; + if (parsedDocument === undefined) { + throw new Error('The parsed document is missing.'); + } - const messages = (document.diagnostics ?? []) + const messages = (parsedDocument.diagnostics ?? []) .map(diagnostic => diagnostic.message) .filter((message): message is string => typeof message === 'string'); expect(messages).toContain("Could not resolve attribute 'missing' on idea 'attribute_namespacing'."); expect(messages.some(message => message.includes("IdeaDeclaration named 'missing'"))).toBe(false); - const qualified = [...AstUtils.streamAst(document.parseResult.value)].filter(isQualifiedReference); + const qualified = [...AstUtils.streamAst(parsedDocument.parseResult.value)].filter(isQualifiedReference); expect(qualified).toHaveLength(1); expect(isIdea(peekResolvedRef(qualified[0].idea))).toBe(false); - const documentLinks = await services.Reqlan.lsp.DocumentLinkProvider?.getDocumentLinks(document, { - textDocument: { uri: document.textDocument.uri } + const documentLinks = await services.Reqlan.lsp.DocumentLinkProvider?.getDocumentLinks(parsedDocument, { + textDocument: { uri: parsedDocument.textDocument.uri } }); const decoyLink = documentLinks?.find(entry => - document.textDocument.getText(entry.range) === 'missing' + parsedDocument.textDocument.getText(entry.range) === 'missing' ); expect(decoyLink).toBeUndefined(); }); diff --git a/reqlan rq/language/syntax.rq b/reqlan rq/language/syntax.rq index d95cee6..2f04f62 100644 --- a/reqlan rq/language/syntax.rq +++ b/reqlan rq/language/syntax.rq @@ -382,6 +382,7 @@ attribute_namespacing { A bracket link `[idea.attribute]` targets one attribute of that idea. The first name is the idea name. The second name is the attribute name. + The grammar types that name as an attribute or an idea declaration. `[attribute_namespacing.example]` targets the `example` attribute of this idea. The form `[ideaset.member]` still targets an ideaset member. See [references_to_subidea]. @@ -397,11 +398,14 @@ attribute_namespacing { Related: [reference_brackets], [attribute_marker], ["../core_analysis/html_export.rq".html_export_internal_links]. @status done @implementation ( + ["../../packages/language/src/reqlan.langium"] ["../../crates/reqlan-parse/src/lexer.rs"] ["../../crates/reqlan-parse/src/parser.rs"] ["../../packages/language/src/reqlan-scope.ts"] ["../../packages/language/src/reqlan-linker.ts"] ["../../packages/language/src/reqlan-attribute-links.ts"] + ["../../packages/language/src/reqlan-references.ts"] + ["../../packages/language/src/reqlan-reference-code-lens.ts"] ["../../packages/language/src/reqlan-document-link-provider.ts"] ["../../packages/language/src/reqlan-local-symbolic-links.ts"] ["../../packages/language/src/reqlan-outbound-presentation.ts"] From d556dcc377efd8dc0fcab8d96db34ddf99c551c9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 10 Oct 2026 08:24:56 +0000 Subject: [PATCH 2/2] Point stale requirement references at declared ideas. Check reported missing export, index, startup, and context-centre names. Each link now names an idea that the target file declares. --- reqlan rq/core_analysis/check.rq | 2 +- reqlan rq/extension/extension.rq | 2 +- reqlan rq/extension/index-host/index.rq | 2 +- reqlan rq/extension/local-context/activitybar.rq | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/reqlan rq/core_analysis/check.rq b/reqlan rq/core_analysis/check.rq index ff1e12e..ca32839 100644 --- a/reqlan rq/core_analysis/check.rq +++ b/reqlan rq/core_analysis/check.rq @@ -265,7 +265,7 @@ check_qualified_missing_file { } check_qualified_missing_idea { - A qualified [reference] names a file and an idea. Example: `["./graph-analysers.rq".index_ideas]`. + A qualified [reference] names a file and an idea. Example: `["../extension/index-host/graph-analysers.rq".not_an_idea]`. The same rule applies to a namespace alias: `[alias.idea]` ([import_namespace]). [check] reports an error when that file is an `.rq` file, the file does not declare the idea, and the file does not bind that name as an import alias. The issue kind is `references`. The label is the missing idea name. diff --git a/reqlan rq/extension/extension.rq b/reqlan rq/extension/extension.rq index 601c33c..68d99cc 100644 --- a/reqlan rq/extension/extension.rq +++ b/reqlan rq/extension/extension.rq @@ -43,7 +43,7 @@ extension_product { host_lifecycle { Install vs base, activation, first paint, onboarding, and configuration schema contribution. - [installation.installation], [activation.activation], [startup.startup_performance], + [installation.installation], [activation.activation], [startup.extension_startup_reliability], [onboarding.installation_event], [configuration.configuration_location], [command_palette_doc.command_palette]. @status done diff --git a/reqlan rq/extension/index-host/index.rq b/reqlan rq/extension/index-host/index.rq index bfe19d2..f711231 100644 --- a/reqlan rq/extension/index-host/index.rq +++ b/reqlan rq/extension/index-host/index.rq @@ -10,7 +10,7 @@ application_memory { Base-local application memory for reqlan tools lives under `/.reqlan/`. The presence of `.reqlan` **marks** a directory as a ["../../bases/base.rq".base]; each base owns its own store. Shared by the VS Code extension, ["../../cli/cli_package.rq".cli_package], and MCP ([mcp_server]) — not VS Code `globalStorageUri` / extension-private storage. - The ideas graph index (["./graph-analysers.rq".index_ideas] / ["./graph-analysers.rq".index_technology]) persists as `ideas-index.sqlite` inside that directory. + The ideas graph index ([indexer.index_ideas] and [indexer.index_technology]) persists as `ideas-index.sqlite` inside that directory. Schema: ["../../indexer/sqlite-schema.rq".ideas_index_sqlite]. Index timing diagnostics persist separately as `index-diagnostics.sqlite` ([index_diagnostics_store] / ["./index-diagnostics.rq".index_diagnostics]) so rebuilds of the ideas index do not wipe history. Click session state persists as `click-sessions.sqlite` (["../../cli/click.rq".click_session]) so agent/CLI click cursors survive ideas-index rebuilds. diff --git a/reqlan rq/extension/local-context/activitybar.rq b/reqlan rq/extension/local-context/activitybar.rq index 84a6913..47958eb 100644 --- a/reqlan rq/extension/local-context/activitybar.rq +++ b/reqlan rq/extension/local-context/activitybar.rq @@ -85,7 +85,7 @@ header_buttons { - "Sync with editor" toggle: panes follow the active editor and cursor by default; when off, stay pinned on the last focused idea. - [hop_depth_control]: global graph hop depth for context search, graph slice, references, and ancestors — not a boolean toggle. - "Open in Ideas Summary": reveal [ideas_summary_panel] with the ideas table filtered to the current file, or the graph tab centred on the current idea. - - "Export slice": JSON or CSV of the current neighbourhood only (lighter than workspace [commands.export_json] / [commands.export_csv]). + - "Export slice": JSON or CSV of the current neighbourhood only. This slice is lighter than workspace ["../export/export.rq".export_JSON] and ["../export/export.rq".export_csv]. - "Copy as markdown": copy the focused idea plus parents and reference lists for pasting into chat or PRs. - Refresh control tied to index readiness. - [info_site_link] @@ -126,7 +126,7 @@ hop_depth_control { - Minimum hop depth is 1 (direct neighbours only); **-** is disabled at minimum. - Maximum hop depth is capped (default 4 for the activity bar); **+** is disabled at maximum. - Changing global depth rebuilds [context_scope_doc.reqlan_context_model], refreshes graph/refs/ancestors panes, and updates [context_scope_doc.context_footprint]. - - When no [context_scope_doc.effective_center_id] exists, depth still updates session state; panes refresh when a centre appears. + - When [context_scope_doc.context_footprint] has no centre, depth still updates session state. Panes refresh when a centre appears. - Legacy `setIncludeIndirect` maps to depth 2/1 for backward compatibility during migration. } @@ -296,7 +296,7 @@ component_list { - IdeasetPane, FileReferencesPane, RecentPane - [graph_path_doc.graph_path_walk] UI pane - ExportSliceButton - - Full workspace export via [commands.export_json] / [commands.export_csv] + - Full workspace export via ["../export/export.rq".export_JSON] and ["../export/export.rq".export_csv] } minigraph_file_reference_links {