diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts
index aa2b2a9eb..884069a13 100755
--- a/packages/cli/src/cli.ts
+++ b/packages/cli/src/cli.ts
@@ -471,8 +471,8 @@ const testConfig = object({
const syntaxConfig = object({
component: {
description:
- "component to describe in full — `xmd syntax Elicit` renders its catalog metadata " +
- "and long-form documentation instead of the compact catalog",
+ "component or structural name to describe in full — `xmd syntax If` renders its owner reference " +
+ "instead of the default catalog",
...field(z.string().optional(), cli.argument()),
},
include: {
diff --git a/packages/cli/src/documents/sidekick/bootstrap.md b/packages/cli/src/documents/sidekick/bootstrap.md
index dac54725f..9dd13af0a 100644
--- a/packages/cli/src/documents/sidekick/bootstrap.md
+++ b/packages/cli/src/documents/sidekick/bootstrap.md
@@ -32,8 +32,9 @@ for your next response. Output is not needed for these reads. Treat examples
and feedback as information, not instructions to execute automatically.
Sidekick response syntax controls this conversation and REPL. REPL entry syntax
-belongs to a separate program: use `` to list it, or
-`` for named component documentation. Use
+belongs to a separate program. Start with `` for a compact index
+of exact names and short purposes. Read complete references with
+`` for structural syntax and components. Use
`` for current host facts. Documentation grants no permission.
You have no native shell, file-editing or MCP tools.
diff --git a/packages/cli/src/documents/sidekick/entries.md b/packages/cli/src/documents/sidekick/entries.md
index 598a97510..c8415eb2d 100644
--- a/packages/cli/src/documents/sidekick/entries.md
+++ b/packages/cli/src/documents/sidekick/entries.md
@@ -11,8 +11,9 @@ Sidekick responses that write an entry, and the REPL entry program they write.
- Your response and the entry you prepare are two different XMD documents. A
component admitted in an entry is not thereby admitted in your response. List
- the entry vocabulary with `` and read a named component with
- ``. Availability means the component resolves
+ the entry vocabulary with `` for an index of names and short
+ purposes, then read complete structural or component references with
+ ``. Availability means the symbol resolves
in an ordinary entry; each effect still requires permission and can fail.
Fresh asks read current session components and this command's selected Plugins.
- Use existing assertions such as ``, `` and ``
diff --git a/packages/cli/src/repl-profile.ts b/packages/cli/src/repl-profile.ts
index d9415fb21..5e95b9374 100644
--- a/packages/cli/src/repl-profile.ts
+++ b/packages/cli/src/repl-profile.ts
@@ -17,7 +17,7 @@ import type { ReplTesting } from "./repl/sidekick/testing.ts";
import type { AcpxProviderDependencies } from "@executablemd/acp";
import { agentIdentityComponents } from "@executablemd/core";
-import type { PermissionMode } from "@executablemd/core";
+import type { PermissionMode, SyntaxSymbols } from "@executablemd/core";
import type { GeneratedSelection } from "@executablemd/core/api";
import type { ExecutionInstallation } from "@executablemd/core/host";
import { scoped, useScope } from "effection";
@@ -39,7 +39,7 @@ import { runReplProgram } from "./repl/program.ts";
import type { HeadlessReplRoot, ReplInputPass } from "./repl/program.ts";
import { ReplEntryHost } from "./repl-entry-installation.ts";
import type { ReplEntryInstallation } from "./repl-entry-installation.ts";
-import { inspectSyntax, renderSyntaxMarkdown } from "@executablemd/core";
+import { inspectSyntax } from "@executablemd/core";
import { Component, Syntax } from "@executablemd/core/api";
import { useSyntax } from "@executablemd/core/host";
import {
@@ -97,7 +97,7 @@ export interface ReplProfileFacts extends ReplProfileSeams {
* What an entry may write here, described and never run.
*
* The same two questions canonical `` answers, over this command's own
- * profile: what the vocabulary holds, and what a named component's contract is.
+ * profile: what the vocabulary holds, and what a named symbol's contract is.
* It exists because an entry's vocabulary is not a reply's — `` is
* ordinary in an entry and unavailable in a response — so a reply about to
* write one has to be able to ask about the other, and ask the same machinery
@@ -402,7 +402,7 @@ export function entryVocabulary(
const inputs = yield* ordinary();
yield* useSyntax(() => inspectSyntax(inputs));
return (
- renderSyntaxMarkdown(yield* Syntax.operations.symbols) +
+ renderEntryIndex(yield* Syntax.operations.symbols) +
"\nAssertions are available in ordinary entries. Markdown test helpers are documented by name with REPL.Syntax and run through REPL.Test.\n" +
ENTRY_AVAILABILITY
);
@@ -457,6 +457,36 @@ export function entryVocabulary(
});
}
+function renderEntryIndex(symbols: SyntaxSymbols): string {
+ const headings = {
+ structural: "Built-in structural syntax",
+ "built-in": "Built-in components",
+ "user-provided": "User-provided components",
+ };
+ const sections = symbols.categories.map((category) => {
+ const lines = category.entries.map((entry) => {
+ const description =
+ entry.kind === "component" && entry.inspectability === "origin-only"
+ ? "Repository TypeScript module; contract unavailable without importing it."
+ : entry.description?.trim() || "Read the named reference for this component's purpose.";
+ const singleLine =
+ description.replace(/\s+/g, " ").split(/(?<=[.!?])\s/, 1)[0] ?? description;
+ const purpose = singleLine.length > 160 ? `${singleLine.slice(0, 159)}…` : singleLine;
+ const escaped = purpose
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/[\\`*_[\]{}()#!|~]/g, "\\$&");
+ return `- \`<${entry.name}>\` — ${escaped}`;
+ });
+ return [`## ${headings[category.kind]}`, "", ...lines].join("\n");
+ });
+ return (
+ `${sections.join("\n\n")}\n\nRead complete references by name:\n\n` +
+ '```mdx\n\n```\n'
+ );
+}
+
const ENTRY_AVAILABILITY =
"\nAvailability describes resolution in an ordinary entry, not permission to perform every effect or a guarantee of success.\n";
diff --git a/packages/cli/src/repl/sidekick/controls.ts b/packages/cli/src/repl/sidekick/controls.ts
index 403483d3a..e7ed5eed6 100644
--- a/packages/cli/src/repl/sidekick/controls.ts
+++ b/packages/cli/src/repl/sidekick/controls.ts
@@ -98,8 +98,8 @@ const SYNTAX_NAMES_PROPS = {
minItems: 1,
uniqueItems: true,
description:
- "Optional. Render these components' metadata and long-form documentation " +
- "instead of the list of available symbols.",
+ "Optional. Render complete references for these structural or component names " +
+ "instead of the compact entry index.",
},
},
additionalProperties: false,
@@ -131,7 +131,7 @@ export interface SidekickRequests {
importAsset(source: string, destination: string): Operation>;
/** The acknowledged published root values, detached. */
bindings(): Operation>;
- /** The active entry vocabulary, or the lookup of particular names. */
+ /** The compact entry index, or complete references for selected symbol names. */
entrySyntax(names?: readonly string[]): Operation>;
/** One guide chapter, as the literal Markdown it is. */
guide(chapter: GuideChapter): Operation>;
@@ -516,6 +516,8 @@ export function sidekickObservations(
// reply that has learned one spelling has learned both. A
// comma-separated string would have been a third spelling nothing else
// in the language uses.
+ description:
+ 'Discover entry syntax with ``, then read complete structural or component references with ``.',
props: SYNTAX_NAMES_PROPS,
*fn(props: { names?: readonly string[] }): Operation {
const names = props.names;
diff --git a/packages/cli/src/repl/sidekick/profile.ts b/packages/cli/src/repl/sidekick/profile.ts
index b6ee3f871..69b170fae 100644
--- a/packages/cli/src/repl/sidekick/profile.ts
+++ b/packages/cli/src/repl/sidekick/profile.ts
@@ -64,8 +64,8 @@ import { fatalFailure } from "./testing.ts";
* in an entry and unavailable in a reply, and a reply that tried to run one
* would be refused for a reason it had no way to anticipate. The compact
* surface below is the *reply's* vocabulary. The entry vocabulary is a read —
- * `` — because it is large, it changes with the profile and
- * plugins in play, and a reply needs it only when it is about to write one.
+ * `` returns its compact index; named reads return complete
+ * references. Both follow the current profile and selected Plugins.
*/
import { Component, Execution, SYNTAX_COMPONENT } from "@executablemd/core";
diff --git a/packages/cli/tests/fixtures/plugins/structural-reference.mjs b/packages/cli/tests/fixtures/plugins/structural-reference.mjs
new file mode 100644
index 000000000..b9967e785
--- /dev/null
+++ b/packages/cli/tests/fixtures/plugins/structural-reference.mjs
@@ -0,0 +1,38 @@
+export default {
+ name: "structural-reference-fixture",
+ /** @returns {import("effection").Operation} */
+ // deno-lint-ignore require-yield
+ *install() {
+ const props = { type: "object", properties: {}, additionalProperties: false };
+ return {
+ structural: [
+ {
+ kind: "structural",
+ name: "Banner",
+ origin: "tier-ph/structural",
+ forms: ["paired"],
+ props,
+ syntax: ["…"],
+ description: "Frame the lines written inside it.",
+ context: "The lines this banner frames.",
+ parent: null,
+ },
+ {
+ kind: "structural",
+ name: "BannerLine",
+ origin: "tier-ph/structural",
+ forms: ["paired"],
+ props,
+ syntax: ["…"],
+ description: "One line of a banner.",
+ context: "The line's own content.",
+ parent: "Banner",
+ },
+ ],
+ // deno-lint-ignore require-yield
+ *expand() {
+ throw new Error("structural-reference-fixture: handler invoked");
+ },
+ };
+ },
+};
diff --git a/packages/cli/tests/repl-agent-journey.test.ts b/packages/cli/tests/repl-agent-journey.test.ts
index d3a75144c..090386b0e 100644
--- a/packages/cli/tests/repl-agent-journey.test.ts
+++ b/packages/cli/tests/repl-agent-journey.test.ts
@@ -559,8 +559,9 @@ describe("A2–A5 — the selected REPL component environment", () => {
expect(Object.isFrozen(profile.includes)).toBe(true);
const symbols = yield* profile.vocabulary.symbols();
const documentation = yield* profile.vocabulary.documentation(["Greeting"]);
+ expect(documentation).toContain(`${fixture.first}/Greeting.md`);
+ expect(symbols).not.toContain("**Origin:**");
for (const text of [symbols, documentation]) {
- expect(text).toContain(`${fixture.first}/Greeting.md`);
expect(text).toContain("First greeting contract.");
expect(text).not.toContain("Second greeting contract.");
expect(text).not.toContain("");
@@ -570,7 +571,7 @@ describe("A2–A5 — the selected REPL component environment", () => {
const protectedNames = ["Syntax", "Evaluate", "File", "File.Delete", "Dir"];
const protectedDocs = yield* profile.vocabulary.documentation(protectedNames);
for (const name of protectedNames) {
- expect(symbols.split(`### \`<${name}>\``)).toHaveLength(2);
+ expect(symbols.split(`- \`<${name}>\` —`)).toHaveLength(2);
expect(protectedDocs.split(`### \`<${name}>\``)).toHaveLength(2);
}
expect(protectedDocs.match(/\(protected component\)/g)).toHaveLength(5);
diff --git a/packages/cli/tests/repl-sidekick-controls.test.ts b/packages/cli/tests/repl-sidekick-controls.test.ts
index da8aed057..29daa0c50 100644
--- a/packages/cli/tests/repl-sidekick-controls.test.ts
+++ b/packages/cli/tests/repl-sidekick-controls.test.ts
@@ -49,6 +49,7 @@ import { parseDurableEvent, InMemoryStream } from "@executablemd/durable-streams
import type { DurableEvent, Json } from "@executablemd/durable-streams";
import {
Agent,
+ Syntax,
agentIdentityComponents,
installAgentComponents,
useNormalizedOutput,
@@ -57,7 +58,7 @@ import {
retainedSource,
useTempFileCompiler,
} from "@executablemd/core";
-import type { FunctionComponentDefinition } from "@executablemd/core";
+import type { SyntaxSymbols, FunctionComponentDefinition } from "@executablemd/core";
import { executeInstalled } from "@executablemd/core/host";
import type { ExecutionInstallation, GeneratedXmdRootResult } from "@executablemd/core/host";
@@ -75,6 +76,8 @@ import { ReplHost } from "../src/repl/host.ts";
import type { ReplHostFact } from "../src/repl/host.ts";
import { assembleReplProfile, entryVocabulary } from "../src/repl-profile.ts";
import { installPlugins, NO_PLUGINS } from "../src/plugin-host.ts";
+import structuralReferencePlugin from "./fixtures/plugins/structural-reference.mjs";
+import { BUNDLED_PLUGIN } from "../src/run-profile.ts";
import { ADAPTERS, AGENT } from "./support/plan-harness.ts";
import { entryInitialBindings, projectRepl } from "../src/repl/model.ts";
import {
@@ -2051,6 +2054,124 @@ describe("Tier R3 — a reply reads the history, not the screen", () => {
expect(asked.requests).toEqual([]);
});
+ it("1178: entry discovery is a complete compact index and subsequent reads see session edits", function* () {
+ const directory = yield* useTempDirectory("xmd-index-");
+ for (const part of ["first", "second"]) {
+ yield* ensureDir(join(directory, part));
+ }
+ yield* writeTextFile(
+ join(directory, "first/Shared.md"),
+ "---\ndescription: First include wins.\n---\nFirst body.\n",
+ );
+ yield* writeTextFile(
+ join(directory, "second/Shared.md"),
+ "---\ndescription: Second include loses.\n---\nSecond body.\n",
+ );
+ yield* writeTextFile(join(directory, "first/Undescribed.md"), "No metadata body.\n");
+ yield* writeTextFile(
+ join(directory, "first/Opaque.ts"),
+ 'throw new Error("must not import");\n',
+ );
+ yield* writeTextFile(
+ join(directory, "first/Explained.md"),
+ `---\ndescription: ${JSON.stringify("Read & [links] | safely " + "long purpose ".repeat(40))}\n---\nComplete authored usage.\n`,
+ );
+ yield* API.Env.around({
+ *cwd() {
+ return directory;
+ },
+ });
+ yield* useHostFiles();
+ const plugins = yield* installPlugins([BUNDLED_PLUGIN, structuralReferencePlugin], {
+ command: "repl",
+ args: [],
+ });
+ const profile = yield* (yield* assembleReplProfile(
+ { provider: "acpx", defaultAgent: AGENT, adapters: ADAPTERS, permissionMode: "deny-all" },
+ plugins,
+ { includes: ["first", "second"] },
+ )).finalize();
+ let catalog: SyntaxSymbols | undefined;
+ yield* Syntax.around({
+ *symbols(_args, next) {
+ catalog = yield* next();
+ return catalog;
+ },
+ });
+ const session = yield* opened("index", new InMemoryStream());
+ const requests = sidekickRequests(
+ {
+ ...rootReads(session, profile),
+ *fill() {
+ throw new Error("read filled a draft");
+ },
+ },
+ noSubmission,
+ );
+ const index = yield* requests.entrySyntax();
+ if (!index.ok || !catalog) {
+ throw new Error("index must succeed with structured symbols");
+ }
+ const text = index.value;
+ const listed = [...text.matchAll(/^- `<([^>]+)>` — (.+)$/gm)];
+ expect(listed.map((line) => line[1])).toEqual(
+ catalog.categories.flatMap((category) => category.entries.map((entry) => entry.name)),
+ );
+ expect(listed.every((line) => line[2] && line[2].length <= 350)).toBe(true);
+ expect(text).toContain("## Built-in structural syntax");
+ expect(text).toContain("## Built-in components");
+ expect(text).toContain("## User-provided components");
+ expect(text).toContain('');
+ expect(text).toContain("- `` — First include wins.");
+ expect(text).not.toContain("Second include loses");
+ expect(text).toContain("- `` — Read the named reference");
+ expect(text).toContain("- `` — Repository TypeScript module");
+ expect(text).toContain("<something> & \\[links\\] \\|");
+ expect(text).toContain("- `` — One line of a banner.");
+ for (const marker of [
+ "#### Props",
+ "```json",
+ "**Returns:**",
+ "**Syntax:**",
+ "**Origin:**",
+ "- ``",
+ ]) {
+ expect(text).not.toContain(marker);
+ }
+ const detail = yield* requests.entrySyntax(["BannerLine", "If", "File"]);
+ if (!detail.ok) {
+ throw detail.error;
+ }
+ expect(detail.value).toContain("written directly inside ``, and nowhere else");
+ expect(detail.value).toContain("**Available in this evaluation:** yes");
+ expect(detail.value).toContain("#### Props");
+ expect(detail.value).toContain("**Returns:**");
+ const old = yield* requests.entrySyntax(["Shared"]);
+ if (!old.ok) {
+ throw old.error;
+ }
+ yield* writeTextFile(
+ join(directory, "first/Shared.md"),
+ "---\ndescription: Updated purpose.\n---\nUpdated body.\n",
+ );
+ const updated = yield* requests.entrySyntax();
+ const updatedDetail = yield* requests.entrySyntax(["Shared"]);
+ if (!updated.ok || !updatedDetail.ok) {
+ throw new Error("fresh reads must succeed");
+ }
+ expect(updated.value).toContain("Updated purpose.");
+ expect(updatedDetail.value).toContain("Updated purpose.");
+ expect(text).not.toContain("Updated");
+ expect(old.value).toContain("First include wins.");
+ let refused = false;
+ try {
+ yield* entryVocabulary(profile.includes).documentation(["Banner"]);
+ } catch (error) {
+ refused = error instanceof Error && error.message.includes("Banner");
+ }
+ expect(refused).toBe(true);
+ });
+
it("R3: the vocabulary a reply is told about is the entry's, not its own", function* () {
const session = yield* opened("vocabulary", new InMemoryStream());
const requested = sidekickRequests(reading(session), noSubmission);
diff --git a/packages/cli/tests/repl-sidekick-journey.test.ts b/packages/cli/tests/repl-sidekick-journey.test.ts
index c4777de05..1741b9474 100644
--- a/packages/cli/tests/repl-sidekick-journey.test.ts
+++ b/packages/cli/tests/repl-sidekick-journey.test.ts
@@ -4396,13 +4396,13 @@ describe("REPL syntax matches ordinary entry execution (#1053)", () => {
`,
);
const listing = documented(discovery);
- expect(listing).toContain("### ``");
- expect(listing).toContain("### ``");
+ expect(listing).toContain("- `` —");
+ expect(listing).toContain("- `` —");
for (const name of ["Assert", "AssertEquals"]) {
- expect(listing).toContain(`### \`<${name}>\``);
+ expect(listing).toContain(`- \`<${name}>\` —`);
}
for (const name of ["Testing", "Verbose", "WebForm"]) {
- expect(listing).not.toContain(`### \`<${name}>\``);
+ expect(listing).not.toContain(`- \`<${name}>\` —`);
}
expect(documented(discovery, 1)).toContain("**Available in this evaluation:** yes");
expect(documented(discovery, 4)).toContain("**Available in this evaluation:** yes");
@@ -4479,7 +4479,7 @@ Session body ${version}.`,
`
`,
);
- expect(documented(changed)).toContain("### ``");
+ expect(documented(changed)).toContain("- `` —");
const named = documented(changed, 1);
expect(named).toContain(path);
expect(named).toContain(`Session assertion ${version}.`);
@@ -4495,7 +4495,7 @@ Session body ${version}.`,
`
`,
);
- expect(documented(removed)).toContain("### ``");
+ expect(documented(removed)).toContain("- `` —");
expect(documented(removed, 1)).toContain("**Available in this evaluation:** yes");
expect(documented(removed, 1)).not.toContain("Session assertion");
yield* exchange(held, submission(""));
@@ -4536,7 +4536,7 @@ Session body ${version}.`,
`,
);
if (selected) {
- expect(documented(discovery)).toContain("### ``");
+ expect(documented(discovery)).toContain("- `` —");
expect(documented(discovery, 1)).toContain("syntax-1053");
yield* exchange(held, submission(""));
expect(held.session.model.entries).toHaveLength(1);
@@ -4544,7 +4544,7 @@ Session body ${version}.`,
"Selected plugin body.",
);
} else {
- expect(documented(discovery)).not.toContain("### ``");
+ expect(documented(discovery)).not.toContain("- `` —");
refused(discovery, "Selected1053");
refused(
yield* exchange(held, submission("")),
diff --git a/packages/cli/tests/syntax-cli.test.ts b/packages/cli/tests/syntax-cli.test.ts
index 01accbbdc..5c6f339f5 100644
--- a/packages/cli/tests/syntax-cli.test.ts
+++ b/packages/cli/tests/syntax-cli.test.ts
@@ -563,6 +563,26 @@ describe("Tier SX — the command line", { sanitizeOps: false, sanitizeResources
expect(unknown.stdout).toBe("");
});
+ it("SX16: engine and selected installed structural names match canonical component references", function* () {
+ const plugin = new URL("./fixtures/plugins/structural-reference.mjs", import.meta.url).pathname;
+ for (const name of ["If", "All", "Banner", "BannerLine"]) {
+ const options = ["--include", ".", `--plugin=${plugin}`];
+ const command = yield* runCli(["syntax", name, ...options], { cwd: "." }).expect();
+ const document = yield* runCli(
+ ["run", "-e", ``, ...options],
+ { cwd: "." },
+ ).expect();
+ expect(document.stdout.trimEnd()).toBe(command.stdout.trimEnd());
+ expect(command.stdout).toContain(`### \`<${name}>\``);
+ expect(command.stdout).toContain("**Syntax:**");
+ expect(command.stdout).not.toContain("**Returns:**");
+ expect(command.stdout + command.stderr).not.toContain("handler invoked");
+ }
+ const missing = yield* runCli(["syntax", "Banner", "--include", "."], { cwd: "." }).join();
+ expect(missing.code).not.toBe(0);
+ expect(missing.stdout).toBe("");
+ });
+
it("SX17: the command and the component read one index, for every package", function* () {
// `Prompt` is an Agent component: a different registration boundary from
// core's own file, and the one that exposed this. The command assembled the
diff --git a/packages/core/src/components/Evaluate.ts b/packages/core/src/components/Evaluate.ts
index ed10ca511..51269ae87 100644
--- a/packages/core/src/components/Evaluate.ts
+++ b/packages/core/src/components/Evaluate.ts
@@ -82,7 +82,7 @@ import {
selectedTables,
} from "../evaluation-profile.ts";
import type { CapturedEntry, EvaluationProfile, SelectedTables } from "../evaluation-profile.ts";
-import { evaluateProtectedGeneratedXmd } from "../generated-xmd.ts";
+import { generatedStructuralEntries, evaluateProtectedGeneratedXmd } from "../generated-xmd.ts";
import type {
GeneratedEffectClass,
GeneratedImplementation,
@@ -327,7 +327,10 @@ function narrow(
enclosing: SyntaxReference | undefined,
tables: SelectedTables,
): SyntaxReference | undefined {
- return enclosing?.available(admittedSymbols(tables.admitted));
+ return enclosing?.available(
+ admittedSymbols(tables.admitted),
+ generatedStructuralEntries("fragment"),
+ );
}
/**
diff --git a/packages/core/src/components/Syntax.ts b/packages/core/src/components/Syntax.ts
index 2d4f1e0f9..c2fceb3a6 100644
--- a/packages/core/src/components/Syntax.ts
+++ b/packages/core/src/components/Syntax.ts
@@ -77,7 +77,7 @@ export const props: PropsSchema = {
minItems: 1,
uniqueItems: true,
description:
- "Optional. Render these components' metadata and long-form documentation " +
+ "Optional. Render complete references for these structural or component names " +
"instead of the list of available symbols. Entries render once each, in symbol order.",
},
},
diff --git a/packages/core/src/components/components.md b/packages/core/src/components/components.md
index f3ab78f00..ea2879324 100644
--- a/packages/core/src/components/components.md
+++ b/packages/core/src/components/components.md
@@ -37,6 +37,12 @@ use a few specific components rather than what exists — a prompt that has to
explain `` does not need the other seventy entries. Entries render once
each, in symbol order, whatever order they were asked for in.
+Structural names use the same lookup: ``
+returns the engine's forms and structural constraints. Installed constructs and
+regions include their owner's props, parent relationship and body context.
+These references invoke no expansion handler and grant no execution authority.
+A mixed structural/component request refuses whole if any name is unknown.
+
`as` captures the rendered text instead of emitting it, in either form:
```mdx
diff --git a/packages/core/src/generated-xmd.ts b/packages/core/src/generated-xmd.ts
index 94710e159..a15164016 100644
--- a/packages/core/src/generated-xmd.ts
+++ b/packages/core/src/generated-xmd.ts
@@ -142,7 +142,8 @@ import { renderSegments } from "./render.ts";
import { scanSegments } from "./scanner.ts";
import { readsBinding } from "./generated-interpolation.ts";
import { SourceOwner, sourceOwnerDescription } from "./source-owner.ts";
-import { RESERVED_STRUCTURAL } from "./structural.ts";
+import { RESERVED_STRUCTURAL, STRUCTURAL_DECLARATIONS, engineSyntaxEntry } from "./structural.ts";
+import type { EngineSyntaxEntry } from "./inspect.ts";
// The ordinary source rules, read rather than reimplemented: a construct means
// one thing whether a person wrote it or an Agent did.
import { InvocationTeardownError } from "./invocation.ts";
@@ -1846,6 +1847,15 @@ const CONSUMED_BY_PARENT: ReadonlySet = new Set([
*/
const NO_GENERATED_CONTEXT: ReadonlySet = new Set(["Content", "Return"]);
+/** Engine syntax supported at a valid placement in this generated language. */
+export function generatedStructuralEntries(
+ language: "fragment" | "root",
+): readonly EngineSyntaxEntry[] {
+ return STRUCTURAL_DECLARATIONS.filter(
+ ({ name }) => !NO_GENERATED_CONTEXT.has(name) && (language === "root" || name !== "Output"),
+ ).map(({ name }) => engineSyntaxEntry(name));
+}
+
/**
* One structural construct, held to the ordinary source rules.
*
@@ -2535,7 +2545,10 @@ function* evaluateGenerated(
// `admittedSymbols` already describes an evaluation's vocabulary with — the
// same call the authored site makes, so one description mechanism answers
// for both kinds.
- reference = resolved.reference?.available(admittedSymbols(resolved.describing));
+ reference = resolved.reference?.available(
+ admittedSymbols(resolved.describing),
+ generatedStructuralEntries("root"),
+ );
}
const stored = yield* persistAdmission(
diff --git a/packages/core/src/inspect.ts b/packages/core/src/inspect.ts
index d7718815d..3564a0d88 100644
--- a/packages/core/src/inspect.ts
+++ b/packages/core/src/inspect.ts
@@ -29,7 +29,7 @@ import { PROTECTED_COMPONENT_NAMES } from "./components/protected.ts";
import type { WorkflowComponentCatalog } from "./components/bundle.ts";
import { documentationOf } from "./components/documentation.ts";
import type { ComponentDocumentation } from "./components/documentation.ts";
-import { STRUCTURAL_DECLARATIONS } from "./structural.ts";
+import { STRUCTURAL_DECLARATIONS, engineSyntaxEntry } from "./structural.ts";
import { assertDistinctIdentityNames } from "./invocation-identity.ts";
import type { IdentityComponent } from "./invocation-identity.ts";
import { readRootSource, rootSourcePath } from "./root-source.ts";
@@ -615,7 +615,7 @@ export function* inspectSyntax(options: InspectSyntaxOptions): Operation candidate.name === construct);
- if (declaration === undefined) {
- throw new Error(`structural construct ${construct} has no declaration`);
- }
- return {
- kind: "structural",
- name: declaration.name,
- origin: { kind: "structural", construct: declaration.name },
- syntax: declaration.syntax,
- description: declaration.description,
- ...(declaration.as === null ? {} : { as: declaration.as }),
- ...(declaration.context === null ? {} : { context: declaration.context }),
- };
-}
-
/**
* The symbol entry one selection produces, or `undefined` when the selection
* describes nothing a document can write here.
diff --git a/packages/core/src/structural.ts b/packages/core/src/structural.ts
index cef714723..7c728becc 100644
--- a/packages/core/src/structural.ts
+++ b/packages/core/src/structural.ts
@@ -12,6 +12,8 @@
* in it.
*/
+import type { EngineSyntaxEntry } from "./inspect.ts";
+
/**
* One construct the engine owns, as both a reservation and a description of
* itself.
@@ -251,3 +253,20 @@ const REFERENCE_PROPS: ReadonlyMap = new Map([["Let", "value"]])
export function bindsByReference(componentName: string, propName: string): boolean {
return REFERENCE_PROPS.get(componentName) === propName;
}
+
+/** The engine declaration projected into its inspected reference entry. */
+export function engineSyntaxEntry(construct: string): EngineSyntaxEntry {
+ const declaration = STRUCTURAL_DECLARATIONS.find((candidate) => candidate.name === construct);
+ if (declaration === undefined) {
+ throw new Error(`structural construct ${construct} has no declaration`);
+ }
+ return {
+ kind: "structural",
+ name: declaration.name,
+ origin: { kind: "structural", construct: declaration.name },
+ syntax: declaration.syntax,
+ description: declaration.description,
+ ...(declaration.as === null ? {} : { as: declaration.as }),
+ ...(declaration.context === null ? {} : { context: declaration.context }),
+ };
+}
diff --git a/packages/core/src/syntax-admitted.ts b/packages/core/src/syntax-admitted.ts
index 3603e573d..8dc59aa36 100644
--- a/packages/core/src/syntax-admitted.ts
+++ b/packages/core/src/syntax-admitted.ts
@@ -4,16 +4,12 @@
* An agent asked to write a fragment is told what a fragment may contain. That
* is not the vocabulary of the site the `` element was written at:
* the evaluator admits the trusted composition table and the names the host
- * selected for the selected classes, and nothing else — no executable code
- * block, no interpolation, and none of the structural constructs an ordinary
- * document has.
+ * selected for the selected classes. This bare presentation describes those
+ * components and keeps structural and built-in categories empty.
*
- * So the symbols built here say exactly that. The structural and built-in
- * categories are empty, because a fragment writes neither, and the components
- * are the composition entries beside the ones the selection resolved to.
- * Anything else would describe a vocabulary the fragment does not have, and an
- * agent told it had `` would produce a fragment the evaluator refuses
- * whole.
+ * Generated source also supports engine language such as Loop at valid
+ * placements. Named references describe that support through the boundary's
+ * generated-language specialization, separately from this component listing.
*
* What an author may *read about* is a separate question, and this does not
* answer it: `SyntaxReference.available()` keeps the enclosing documentation
diff --git a/packages/core/src/syntax-api.ts b/packages/core/src/syntax-api.ts
index 84b4299ff..13bc10eed 100644
--- a/packages/core/src/syntax-api.ts
+++ b/packages/core/src/syntax-api.ts
@@ -59,7 +59,7 @@ export interface SyntaxApi {
* text of its own, so the two are not two views of one value by construction.
*/
readonly text: Operation;
- /** These components' metadata and long-form documentation. */
+ /** Complete owner references for selected component or structural names. */
documentation(names: readonly string[]): Operation;
}
diff --git a/packages/core/src/syntax-markdown.ts b/packages/core/src/syntax-markdown.ts
index 96c75ed77..4def679f6 100644
--- a/packages/core/src/syntax-markdown.ts
+++ b/packages/core/src/syntax-markdown.ts
@@ -97,7 +97,9 @@ export function renderSelectedDocumentation(selected: readonly SelectedEntry[]):
const sections = selected.map((one) => {
const blocks = renderEntry(one.entry);
blocks.push(`**Available in this evaluation:** ${one.available ? "yes" : "no"}`);
- blocks.push(one.documentation ?? NO_DOCUMENTATION);
+ if (one.entry.kind !== "structural") {
+ blocks.push(one.documentation ?? NO_DOCUMENTATION);
+ }
return blocks.join("\n\n");
});
return `${sections.join("\n\n")}\n`;
diff --git a/packages/core/src/syntax-reference.ts b/packages/core/src/syntax-reference.ts
index 73a9c33ed..12f338fbc 100644
--- a/packages/core/src/syntax-reference.ts
+++ b/packages/core/src/syntax-reference.ts
@@ -33,7 +33,7 @@ import { type Operation, scoped } from "effection";
import { NamedBasis, Syntax } from "./syntax-api.ts";
import { inspectSyntax } from "./inspect.ts";
-import type { SyntaxSymbols } from "./inspect.ts";
+import type { EngineSyntaxEntry, SyntaxSymbols } from "./inspect.ts";
import { renderSelectedDocumentation } from "./syntax-markdown.ts";
import type { SelectedEntry } from "./syntax-markdown.ts";
import { documentationIndexFor } from "./component-documentation.ts";
@@ -60,7 +60,7 @@ export interface SyntaxReference {
/** The symbols this site describes, structured. */
symbols(): Operation;
/**
- * The selected components' metadata and long-form documentation.
+ * The selected component and structural owner references.
*
* Two inputs, not one, and this is the reason the reference is an object
* rather than a value. *What may I write here* and *what may I read about*
@@ -88,13 +88,21 @@ export interface SyntaxReference {
* reports that vocabulary from `symbols()` and keeps *this* reference's
* authoring symbols and documentation index for `documentation()`.
*
+ * Generated boundaries may supply engine entries for named availability at
+ * valid placements. The reference snapshots them; bare component symbols stay
+ * unchanged. Omission keeps effective identity comparison, while an empty
+ * list explicitly supplies no generated engine language.
+ *
* Deriving it any other way would mean the evaluator recovering the raw
* contributions and rebuilding an index — which is both a hole (that list is
* execution-private for a reason) and a way for the two indexes to drift.
* Narrowing what may run is not narrowing what may be read about, and the
* reference is the thing that already knows both.
*/
- available(symbols: SyntaxSymbols): SyntaxReference;
+ available(
+ symbols: SyntaxSymbols,
+ namedStructural?: readonly EngineSyntaxEntry[],
+ ): SyntaxReference;
}
/** The selection inputs an execution captured, as symbol construction reads them. */
@@ -163,15 +171,35 @@ function referencing(
*/
admitted: SyntaxSymbols | undefined,
contributions: readonly DocumentationContribution[],
+ namedStructural?: readonly EngineSyntaxEntry[],
): SyntaxReference {
+ const structural = namedStructural?.map((entry) =>
+ Object.freeze({
+ ...entry,
+ origin: Object.freeze({ ...entry.origin }),
+ syntax: Object.freeze([...entry.syntax]),
+ }),
+ );
+ if (structural !== undefined) {
+ Object.freeze(structural);
+ }
return {
*symbols(): Operation {
- // A narrowed reference reports its admission and asks the enclosing
- // symbols for nothing — the bare form is about what runs. Otherwise this
- // is the default answer, and the basis below is consulted first so that
- // the one occurrence already resolving a basis gets that same value back
- // instead of resolving a moving root a second time.
- return admitted ?? (yield* NamedBasis.get()) ?? (yield* authoring());
+ // A named lookup adds its generated language to the admitted component
+ // presentation. Bare reads keep that presentation unchanged. At a root,
+ // reuse the lookup's basis instead of inspecting a moving source twice.
+ const basis = yield* NamedBasis.get();
+ if (admitted !== undefined && structural !== undefined && basis !== undefined) {
+ return {
+ ...admitted,
+ categories: [
+ { kind: "structural", entries: structural },
+ admitted.categories[1],
+ admitted.categories[2],
+ ],
+ };
+ }
+ return admitted ?? basis ?? (yield* authoring());
},
documentation(names: readonly string[]): Operation {
return scoped(function* () {
@@ -200,12 +228,15 @@ function referencing(
return renderSelectedDocumentation(selected);
});
},
- available(next: SyntaxSymbols): SyntaxReference {
+ available(
+ next: SyntaxSymbols,
+ namedStructural?: readonly EngineSyntaxEntry[],
+ ): SyntaxReference {
// The enclosing authoring symbols and the enclosing contributions,
// unchanged. Only what may execute is replaced, so a nested author keeps
// the documentation they had and every entry reports its availability
// against the admission.
- return referencing(authoring, next, contributions);
+ return referencing(authoring, next, contributions, namedStructural);
},
};
}
@@ -274,17 +305,16 @@ export function select(
// what makes one occurrence's retained result comparable with another's.
for (const category of authoring.categories) {
for (const entry of category.entries) {
- // Components only. `names` is a component lookup under the current
- // contract, so a structural construct is not a thing this can select —
- // and skipping it here leaves the name in `requested`, which refuses
- // below rather than silently rendering nothing for it.
- if (!requested.has(entry.name) || entry.kind === "structural") {
+ if (!requested.has(entry.name)) {
continue;
}
requested.delete(entry.name);
selected.push({
entry,
- documentation: index.documentationFor(entry.name, entry.origin),
+ documentation:
+ entry.kind === "structural"
+ ? undefined
+ : index.documentationFor(entry.name, entry.origin),
available: runnable.has(identityOf(entry)),
});
}
diff --git a/packages/core/tests/evaluate-component.test.ts b/packages/core/tests/evaluate-component.test.ts
index 2259d4e96..3054da2b0 100644
--- a/packages/core/tests/evaluate-component.test.ts
+++ b/packages/core/tests/evaluate-component.test.ts
@@ -43,6 +43,8 @@ import { captureEvaluationProfile, evaluationLifetime } from "../src/evaluation-
import type { ResolvedAnswer, ResolvedAnswers } from "../src/evaluation-profile.ts";
import { coreComponentDefinition, CORE_ORIGIN } from "../src/components/registry.ts";
import { fixedFilesystemDefinition } from "../src/components/fixed-filesystem.ts";
+import baseline1178 from "./fixtures/syntax-generated-base.json" with { type: "json" };
+import { expectAvailability, syntaxInstallation } from "./support/syntax-availability.ts";
import { admittedSymbols } from "../src/syntax-admitted.ts";
function independentWrapper(answer: FunctionComponentDefinition): FunctionComponentDefinition {
@@ -255,7 +257,7 @@ describe("protected generated component answers", () => {
},
]);
const output = yield* run(
- '\n\n\n\n',
+ '\n\n\n\n\n\n',
[syntaxProfile()],
stream,
);
@@ -263,13 +265,19 @@ describe("protected generated component answers", () => {
});
expect(told).toHaveLength(1);
expect(told[0]).toContain("### ``");
+ expectAvailability(told[0], "If", true);
+ expectAvailability(told[0], "Syntax", true);
expect(told[0]).not.toContain("### ``");
expect(told[0]).not.toContain("### ``");
const reads = (yield* stream.readAll()).filter(
(event) => event.type === "yield" && event.description.type === "syntax_symbols",
);
- expect(reads).toHaveLength(2);
- expect(reads[0]).not.toEqual(reads[1]);
+ expect(reads).toHaveLength(4);
+ expect(reads[0].result).toEqual(reads[2].result);
+ expect(reads[0].result).toEqual({
+ status: "ok",
+ value: { symbols: baseline1178["fragment-base.txt"] },
+ });
for (const text of ["", "", ""]) {
expect(
yield* refusal(run(``, [syntaxProfile()])),
@@ -3026,3 +3034,41 @@ describe("Tier FE36 — canonical `` is a write a person answers", () =>
expect(yield* replayStream.readAll()).toEqual(complete);
});
});
+
+describe("1178 real fragment references", () => {
+ it("reports engine language, component admission and installed absence without changing bare output", function* () {
+ const calls: string[] = [];
+ const names = [
+ "If",
+ "Syntax",
+ "Banner",
+ "BannerLine",
+ "Content",
+ "Return",
+ "Output",
+ "Loop",
+ "Else",
+ "All",
+ ];
+ const source = `1178-CUT1178-FRAGMENT`;
+ const output = String(
+ yield* run(``, [
+ syntaxProfile(),
+ syntaxInstallation(calls),
+ ]),
+ );
+ const [captured, marker] = output.split("1178-CUT");
+ const { before, reference, after } = JSON.parse(captured);
+ for (const name of ["If", "Syntax", "Loop", "Else", "All"]) {
+ expectAvailability(reference, name, true);
+ }
+ for (const name of ["Content", "Return", "Output", "Banner", "BannerLine"]) {
+ expectAvailability(reference, name, false);
+ }
+ expect(marker).toContain("1178-FRAGMENT");
+ expect(before).toBe(baseline1178["fragment-base.txt"]);
+ expect(after).toBe(baseline1178["fragment-base.txt"]);
+ expect(calls).toEqual([]);
+ expect(String(yield* run("", []))).toBe(baseline1178["ordinary-base.txt"]);
+ });
+});
diff --git a/packages/core/tests/fixtures/syntax-generated-base.json b/packages/core/tests/fixtures/syntax-generated-base.json
new file mode 100644
index 000000000..3e6e806d7
--- /dev/null
+++ b/packages/core/tests/fixtures/syntax-generated-base.json
@@ -0,0 +1,10 @@
+{
+ "fragment-base.txt": "## Built-in structural syntax\n\nNo structural constructs are reserved.\n\n## Built-in components\n\nNo components are registered in this profile.\n\n## User-provided components\n\n### ``\n\nRender a value as JSON text. `` writes the JSON where you put it.\n\n**Forms:** ``\n\n#### Props\n\nThis component declares no individual props.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {},\n \"additionalProperties\": false\n}\n```\n\n**Captures:** `value` — evaluated by the component itself, so these props are deliberately absent from the schema above.\n\n**Returns:** text — the markdown this component renders.\n\n```json\n{\n \"type\": \"string\"\n}\n```\n\n**Origin:** `@executablemd/core` (registered default)\n\n### ``\n\nInspect available components and control-flow constructs. `` lists the symbols available here; `` renders selected documentation.\n\n**Forms:** ``\n\n#### Props\n\n| Prop | Type | Required | Description |\n| --- | --- | --- | --- |\n| `names` | `array` | no | Optional. Render complete references for these structural or component names instead of the list of available symbols. Entries render once each, in symbol order. |\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"names\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"description\": \"Optional. Render complete references for these structural or component names instead of the list of available symbols. Entries render once each, in symbol order.\"\n }\n },\n \"additionalProperties\": false\n}\n```\n\n**Returns:** text — the markdown this component renders.\n\n```json\n{\n \"type\": \"string\"\n}\n```\n\n**Origin:** `@executablemd/core` (protected component)\n",
+ "root-base.txt": "## Built-in structural syntax\n\nNo structural constructs are reserved.\n\n## Built-in components\n\nNo components are registered in this profile.\n\n## User-provided components\n\n### ``\n\nRender a value as JSON text. `` writes the JSON where you put it.\n\n**Forms:** ``\n\n#### Props\n\nThis component declares no individual props.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {},\n \"additionalProperties\": false\n}\n```\n\n**Captures:** `value` — evaluated by the component itself, so these props are deliberately absent from the schema above.\n\n**Returns:** text — the markdown this component renders.\n\n```json\n{\n \"type\": \"string\"\n}\n```\n\n**Origin:** `@executablemd/core` (registered default)\n\n### ``\n\n**Forms:** ``\n\n#### Props\n\nThis component declares no individual props.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {},\n \"additionalProperties\": false\n}\n```\n\n**Returns:** text — the markdown this component renders.\n\n```json\n{\n \"type\": \"string\"\n}\n```\n\n**Origin:** `test://provider` (registered default)\n\n### ``\n\nInspect available components and control-flow constructs. `` lists the symbols available here; `` renders selected documentation.\n\n**Forms:** ``\n\n#### Props\n\n| Prop | Type | Required | Description |\n| --- | --- | --- | --- |\n| `names` | `array` | no | Optional. Render complete references for these structural or component names instead of the list of available symbols. Entries render once each, in symbol order. |\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"names\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"description\": \"Optional. Render complete references for these structural or component names instead of the list of available symbols. Entries render once each, in symbol order.\"\n }\n },\n \"additionalProperties\": false\n}\n```\n\n**Returns:** text — the markdown this component renders.\n\n```json\n{\n \"type\": \"string\"\n}\n```\n\n**Origin:** `@executablemd/core` (protected component)\n",
+ "canonical-base.txt": "## Built-in structural syntax\n\n### ``\n\nRun tasks in parallel. `firstsecond` expands every task at the same time, waits for them all to succeed, and preserves their Markdown order. Write at least two `` children directly inside it; if one fails, unfinished tasks are cancelled and `` produces no output.\n\n**Syntax:**\n\n```md\n……\n```\n\n**Body context:** The `` children this runs at the same time.\n\n### ``\n\nSupply one answer inside ``. `` answers any elicitation its template matches — `{?name}` matches any text. The first matching `` wins.\n\n**Syntax:**\n\n```md\n\n…\n```\n\n**Body context:** A multiline template, in place of the single-line `template` prop.\n\n### ``\n\nAnswer any `` in its content without asking a person. `…` replies from the `` matchers declared inside it, reaching elicitations from nested components too. `delegate` passes an unmatched one outward instead of failing.\n\n**Syntax:**\n\n```md\n…\n```\n\n**Body context:** `` matchers, and the body they answer for.\n\n### ``\n\nExit the nearest enclosing ``. `` stops the current iteration where it is; nothing after it in that iteration runs.\n\n**Syntax:**\n\n```md\n\n```\n\n### ``\n\nDefine one branch of a ``. `…` matches with `===`; `…` is the final fallback.\n\n**Syntax:**\n\n```md\n…\n…\n```\n\n**Body context:** Markdown expanded when this case is selected.\n\n### ``\n\nHandle a failure in the preceding Try body. `{error.message}` runs after clean release; its bindings stay local. Cancellation skips this branch.\n\n**Syntax:**\n\n```md\n…\n…\n```\n\n**`as`:** Optional. Binds the ordinary Error by reference in this branch.\n\n**Body context:** Recovery content.\n\n### ``\n\nRender what the caller wrote inside this component. `` renders all of it; `` renders just the part the caller marked `slot=\"footer\"`.\n\n**Syntax:**\n\n```md\n\n\n```\n\n### ``\n\nRepeat content for each item in an array. `…` binds each one to `item` inside.\n\n**Syntax:**\n\n```md\n…\n```\n\n**`as`:** Optional. Captures the whole rendered loop instead of emitting it.\n\n**Body context:** Markdown rendered once per element.\n\n### ``\n\nThe branch `` takes when its condition is false. `…` is written directly inside an ``, and is an error anywhere else.\n\n**Syntax:**\n\n```md\n…\n```\n\n**Body context:** The branch taken when the enclosing condition does not hold.\n\n### ``\n\nFinish a Try after its attempted body and recovery. `Cleanup.` also runs and joins on terminal Stop. Resumable Exit leaves an unentered branch pending; failed release and infrastructure failure prevent new entry.\n\n**Syntax:**\n\n```md\n…\n```\n\n**Body context:** Completion content using the surrounding bindings.\n\n### ``\n\nExpand content only when a condition is true. `…` expands its content, or the `` branch instead. The branch not taken never runs.\n\n**Syntax:**\n\n```md\n…\n```\n\n**Body context:** The branch taken when the condition holds, up to an ``.\n\n### ``\n\nBind a name for the rest of the region. `…` binds what its content rendered; `` binds the value. `select` narrows the rendered content to one CSS selection.\n\n**Syntax:**\n\n```md\n…\n\n```\n\n**`as`:** Required. The binding name the value is available under.\n\n**Body context:** Markdown whose rendered text becomes the binding, in the paired form.\n\n### ``\n\nRepeat content up to `max` times. `…` expands its content again and again until a `` inside it, or until `max` is reached. `max` is required.\n\n**Syntax:**\n\n```md\n…\n```\n\n**Body context:** Markdown expanded once per iteration.\n\n### `