Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

All notable changes to Context are documented here.

## 0.7.65 - 2026-10-13

- Inline code in knowledge website articles that names a cited repository file, one of its directories, or the unique name of such a directory (for example a Skill name) links to that path at the recorded revision, shown with a link icon and a dotted underline. Bare file names, uncited or ambiguous names stay plain text, as do headings and existing links.
- Writing guidance asks authors to name cited repository files, directories and Skills in inline code instead of hand-writing repository links or adding source-link columns.

## 0.7.64 - 2026-10-13

- The knowledge website homepage no longer highlights the first section in the top bar; a `site.nav` link to the current page, such as a documentation home link, is highlighted instead.
Expand Down
30 changes: 15 additions & 15 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "context",
"version": "0.7.64",
"version": "0.7.65",
"packageManager": "bun@1.3.9",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion packages/context-cli/context-workflow/provider.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema: agent-graph.provider.v1
id: c4a/context
version: 0.7.64
version: 0.7.65
name: Context workflow
description: Internal work contract for Context knowledge workspaces.
graphs:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ article identity; legacy pages without `artifact_ref` use their approved path,
so moving those legacy files changes their URL. The shared knowledge map,
KB layout and production Markdown remain unchanged.

Article pages include a small source footer from recorded article source references and the source registry. Repository entries link to the recorded revision and module (or an explicitly referenced file); document entries link to their registered HTTP(S) URL. Unrecorded associations are not inferred from prose. Source-footer changes participate in the website build fingerprint without modifying knowledge Markdown.
Article pages include a small source footer from recorded article source references and the source registry. Repository entries link to the recorded revision and module (or an explicitly referenced file); document entries link to their registered HTTP(S) URL. Unrecorded associations are not inferred from prose. Source-footer changes participate in the website build fingerprint without modifying knowledge Markdown. Inline code in article prose that spells a cited repository file exactly, a cited file's directory of at least two segments (with or without a trailing `/`), or the name of exactly one such directory when it contains `.`, `-` or `_` links to that path at the recorded revision. Headings, existing links and code blocks are left unchanged, and ambiguous or uncited spellings remain plain code.

The website reuses resource delivery already performed for the KB: bundled
resources are copied into the site's `resources/`; Git raw links remain remote,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ complete article with stable `context:section` markers. References contain
text. CLI computes source digests. A fragment may cite at most three distinct
source positions; an article may have multiple fragments.

When prose names a file, directory or Skill from a cited repository, write its
exact repository-relative path, or the unique name of its directory such as a
Skill name, in inline code and cite a file inside it. The website links such
spellings to the recorded revision; do not hand-write links to repository files
or add source-link columns.

A revision uses `edits` instead of `content`/`references`. The file contains an
`edits` list. Each item maps `replace: [old-fragment-ids]` to `with: [...]`, whose
entries contain `id` and the changed `markdown` and/or `references`.
Expand Down
2 changes: 1 addition & 1 deletion packages/context-cli/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@c4a/context-cli",
"description": "Local runtime and Agent integration for traceable knowledge production",
"version": "0.7.64",
"version": "0.7.65",
"type": "module",
"license": "MIT",
"engines": {
Expand Down
8 changes: 7 additions & 1 deletion packages/context-cli/src/__tests__/packageSite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ test("VitePress builds an independent site with search, safe prose, anchors and
for (const page of mapping.pages) {
const path = join(root, pkg.outDir, page.package_path);
await mkdir(join(path, ".."), { recursive: true });
await writeFile(path, `---\nhead: [[script, {}, "unsafe-script"]]\n---\n# ${page.title}\n\nParagraph before anchor.\n<a id="section-entry"></a>\n\n## Entry\n\n{{ window.alert('unsafe') }}\n\n<script>unsafe()</script>\n\n| A | B |\n|---|---|\n| { x?: number; y?: number; } | 2 |\n\n\`\`\`mermaid\nflowchart LR\nA-->B\n\`\`\`\n\n![Figure](../../../others/assets/example.svg)\n`);
await writeFile(path, `---\nhead: [[script, {}, "unsafe-script"]]\n---\n# ${page.title}\n\nParagraph before anchor.\n<a id="section-entry"></a>\n\n## Entry \`entry.ts\`\n\nRead \`src/entry.ts\`, \`packages/ui/src/\`, \`src\` and [\`entry.ts\`](https://example.com/x).\n\n{{ window.alert('unsafe') }}\n\n<script>unsafe()</script>\n\n| A | B |\n|---|---|\n| { x?: number; y?: number; } | 2 |\n\n\`\`\`mermaid\nflowchart LR\nA-->B\n\`\`\`\n\n![Figure](../../../others/assets/example.svg)\n`);
}
await mkdir(join(root, pkg.outDir, "others/assets"), { recursive: true });
await writeFile(join(root, pkg.outDir, "others/assets/example.svg"), '<svg xmlns="http://www.w3.org/2000/svg"><rect width="10" height="10"/></svg>');
Expand Down Expand Up @@ -169,6 +169,12 @@ test("VitePress builds an independent site with search, safe prose, anchors and
expect(article).toContain("language-mermaid");
expect(article).toContain('class="context-sources"');
expect(article).toContain("https://github.com/example/components/blob/abc123/packages/ui/src/entry.ts#L1-L1");
const sourceLinks = [...article.matchAll(/<a class="context-source-link" href="([^"]+)"[^>]*><code>([^<]+)<\/code><\/a>/gu)].map(match => [match[2], match[1]]);
expect(sourceLinks).toEqual([
["src/entry.ts", "https://github.com/example/components/blob/abc123/packages/ui/src/entry.ts"],
["packages/ui/src/", "https://github.com/example/components/tree/abc123/packages/ui/src"],
]);
expect(article).toContain('<a href="https://example.com/x"');
expect(await readFile(join(root, packageSiteOutputDir(pkg), "resources/others/assets/example.svg"), "utf8")).toContain("<svg");
const map = JSON.parse(await readFile(join(root, packageSiteOutputDir(pkg), "context-site-map.json"), "utf8"));
expect(map.site_url).toBe("https://example.com/docs/");
Expand Down
32 changes: 31 additions & 1 deletion packages/context-cli/src/__tests__/packageSiteSources.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test } from "bun:test";
import type { ArticleStructureEntry, SourcesRegistry } from "@c4a/context";
import { siteArticleSources } from "../project/packageSiteSources.js";
import { siteArticleSources, siteSourceLinks } from "../project/packageSiteSources.js";
const registry: SourcesRegistry = {
kind: "sources.registry", registryPaths: { repo: "", file: "", lark: "" }, absolutePaths: { repo: "", file: "", lark: "" },
repos: [{ id: "snapshot/ui", name: "ui", namespace: "snapshot", module: "ui", materializedAt: "", subpath: "packages/ui", remote: "git@github.com:example/widgets.git", ref: "abc123def4567890abc123def4567890abc123de" }],
Expand Down Expand Up @@ -38,3 +38,33 @@ test("same-repository provenance uses the source SHA and never labels worktree b
entry.sections[0]!.references[0]!.source_ref += "+worktree";
expect(siteArticleSources(entry, registry, "https://example.org/team/project.git")[0]!.href).toBeUndefined();
});

test("inline code links only to cited files, their directories and unambiguous cited directory names", () => {
const entry = article("skills/context-code-indexer/SKILL.md");
entry.sections[0]!.references.push({ ...entry.sections[0]!.references[0]!, locator: { path: "skills/context-note-indexer/SKILL.md", start_line: 1, end_line: 2 } });
const links = siteSourceLinks(entry, registry);
const blob = "https://github.com/example/widgets/blob/abc123def4567890abc123def4567890abc123de/packages/ui";
const tree = "https://github.com/example/widgets/tree/abc123def4567890abc123def4567890abc123de/packages/ui";
expect(links["skills/context-code-indexer/SKILL.md"]).toBe(`${blob}/skills/context-code-indexer/SKILL.md`);
expect(links["packages/ui/skills/context-code-indexer/SKILL.md"]).toBe(`${blob}/skills/context-code-indexer/SKILL.md`);
expect(links["skills/context-code-indexer/"]).toBe(`${tree}/skills/context-code-indexer`);
expect(links["context-code-indexer"]).toBe(`${tree}/skills/context-code-indexer`);
expect(links["packages/ui/"]).toBe(tree);
// Two cited SKILL.md files, a one-segment directory and plain words are not guessed.
expect(links["SKILL.md"]).toBeUndefined();
expect(siteSourceLinks(article(), registry)["button.ts"]).toBeUndefined();
expect(links["skills"]).toBeUndefined();
expect(links["skills/"]).toBeUndefined();
expect(links["packages"]).toBeUndefined();
expect(Object.keys(links).every(key => !key.startsWith("guide"))).toBe(true);
});

test("unsafe paths and worktree content never become inline source links", () => {
expect(siteSourceLinks(article("../../secret"), registry)).toEqual({});
const entry = article("docs/guide.md");
entry.sections[0]!.references = [{ ...entry.sections[0]!.references[0]!, source_ref: `repo-content:docs@${"c".repeat(40)}+worktree` }];
expect(siteSourceLinks(entry, registry, "https://example.org/team/project.git")).toEqual({});
entry.sections[0]!.references[0]!.source_ref = `repo-content:docs@${"c".repeat(40)}`;
expect(siteSourceLinks(entry, registry, "https://example.org/team/project.git")["docs/guide.md"])
.toBe(`https://example.org/team/project/blob/${"c".repeat(40)}/docs/guide.md`);
});
2 changes: 1 addition & 1 deletion packages/context-cli/src/project/indexerBaseContracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { bundledMarkdownReaderQuestionContracts } from
"./indexerBaseMarkdownAuthoringCatalog.js";

const BASE_CONTRACT_VERSION = "1.1.0";
export const BUNDLED_INDEXER_PARSER_PACKAGE_VERSION = "0.7.64";
export const BUNDLED_INDEXER_PARSER_PACKAGE_VERSION = "0.7.65";
const BUNDLED_PARSER_REQUIREMENTS = buildIndexerParserCapabilityRequirements(
BUNDLED_INDEXER_PARSER_PACKAGE_VERSION,
);
Expand Down
6 changes: 4 additions & 2 deletions packages/context-cli/src/project/packageSite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { parseKnowledgeFrontmatter } from "./packageKnowledgeProjection.js";
import { knowledgeMapSectionAnchor } from "./packageKnowledgeMap.js";
import { markdownReaderLinks } from "./markdownLinks.js";
import type { ApprovedKnowledgeFile } from "./packageIndexes.js";
import { articleProvenanceMarkdown, siteArticleSources } from "./packageSiteSources.js";
import { articleProvenanceMarkdown, siteArticleSources, siteSourceLinks } from "./packageSiteSources.js";
import { siteMarkdownConfig, siteThemeCss, siteThemeScript, siteThemeLabels } from "./packageSiteTheme.js";
import { packageSiteBranding } from "./packageSiteBranding.js";
import { writeSiteExtensions, siteExtensionTargets, invalidSiteExtension } from "./packageSiteExtensions.js";
Expand Down Expand Up @@ -261,11 +261,13 @@ export async function writePackageSite(input: {
const pageFrontmatter = (page: SitePage, content: string, titles?: Record<string, string>) => {
const original = sourceContent.get(page.package_path);
const sources = siteArticleSources(original?.article, registry, repositoryRemote);
const sourceLinks = siteSourceLinks(original?.article, registry, repositoryRemote);
const timestamp = parseKnowledgeFrontmatter(original?.content ?? content).timestamp;
const updated = typeof timestamp === "string" && Number.isFinite(Date.parse(timestamp))
? new Date(timestamp).toISOString() : null;
return `---\ntitle: ${JSON.stringify(page.title)}\n${titles ? `contextTitles: ${JSON.stringify(titles)}\n` : ""}` +
`contextSources: ${JSON.stringify(sources)}\ncontextUpdated: ${JSON.stringify(updated)}\n---\n`;
`contextSources: ${JSON.stringify(sources)}\n` +
`${Object.keys(sourceLinks).length ? `contextSourceLinks: ${JSON.stringify(sourceLinks)}\n` : ""}contextUpdated: ${JSON.stringify(updated)}\n---\n`;
};
for (const page of byPath.values()) {
const content = await readFile(join(root, page.package_path), "utf8");
Expand Down
45 changes: 45 additions & 0 deletions packages/context-cli/src/project/packageSiteSources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,48 @@ export function siteArticleSources(article: ArticleStructureEntry | undefined, r
}
return [...new Map(sources.map(source => [JSON.stringify(source), source])).values()];
}

/**
* Inline code that spells a cited repository file, a cited file's directory (at least two
* segments), or the distinctive name of exactly one such directory links to the cited revision.
* Only recorded references produce links; ambiguous spellings stay plain text.
*/
export function siteSourceLinks(article: ArticleStructureEntry | undefined, registry: SourcesRegistry, repositoryRemote?: string): Record<string, string> {
const candidates = new Map<string, Set<string>>();
const names = new Map<string, Set<string>>();
const add = (map: Map<string, Set<string>>, key: string, url: string) => map.set(key, (map.get(key) ?? new Set()).add(url));
for (const reference of article?.sections.flatMap(section => section.references) ?? []) {
const ref = reference.source_ref;
const local = parseRepoContentRef(ref);
const repo = local ? undefined : registry.repos.find(entry => ref === `repo:${entry.id}` || ref === `repo:${entry.name}`);
const prefix = repo?.subpath?.replace(/^\/+|\/+$/gu, "") ?? "";
const base = repo && repoUrl(repo.remote);
const target = (path: string, directory: boolean) => local
? local.worktree ? undefined : repoContentWebUrl(repositoryRemote, local.commit, path, directory)
: base ? `${base}/${directory ? "tree" : "blob"}/${encodeURIComponent(repo!.ref)}/${encodedPath(path)}` : undefined;
const path = reference.locator.path;
const parts = path.split("/");
if (!local && !repo || path.startsWith("/") || parts.some(part => !part || part === "." || part === "..")) continue;
// Authors may write a path relative to the registered module or to its repository root.
for (const written of new Set([path, prefix ? `${prefix}/${path}` : path])) {
const segments = written.split("/");
for (let depth = segments.length; depth >= 1; depth--) {
const spelled = segments.slice(0, depth).join("/");
const full = prefix && !written.startsWith(`${prefix}/`) ? `${prefix}/${spelled}` : spelled;
const directory = depth < segments.length;
const url = target(local ? spelled : full, directory);
if (!url) break;
if (!directory) add(candidates, spelled, url);
else if (depth >= 2) { add(candidates, spelled, url); add(candidates, `${spelled}/`, url); }
// File names such as README.md recur across projects, so only directory names stand alone.
const name = segments[depth - 1]!;
if (directory && /[._-]/u.test(name)) add(names, name, url);
}
}
}
const links: Record<string, string> = {};
for (const map of [names, candidates]) {
for (const [key, urls] of map) if (urls.size === 1) links[key] = [...urls][0]!;
}
return links;
}
Loading
Loading