diff --git a/bun.lock b/bun.lock index 3e421225..b6daf140 100644 --- a/bun.lock +++ b/bun.lock @@ -18,7 +18,7 @@ }, "packages/context": { "name": "@c4a/context", - "version": "0.7.50", + "version": "0.7.55", "dependencies": { "yaml": "^2.5.1", "zod": "^3.23.8", @@ -26,7 +26,7 @@ }, "packages/context-cli": { "name": "@c4a/context-cli", - "version": "0.7.50", + "version": "0.7.55", "bin": { "context": "dist/cli.js", }, @@ -73,7 +73,7 @@ }, "packages/core": { "name": "@c4a/core", - "version": "0.7.50", + "version": "0.7.55", "dependencies": { "picomatch": "^4.0.4", "yaml": "^2.4.5", @@ -85,7 +85,7 @@ }, "packages/dev-cli": { "name": "@c4a/dev-cli", - "version": "0.7.50", + "version": "0.7.55", "dependencies": { "@c4a/context": "workspace:*", "@c4a/core": "workspace:*", @@ -97,7 +97,7 @@ }, "packages/extract": { "name": "@c4a/extract", - "version": "0.7.50", + "version": "0.7.55", "bin": { "c4a-extract-code": "./dist/bin/c4a-extract-code.js", }, @@ -112,7 +112,7 @@ }, "packages/extract-contract": { "name": "@c4a/extract-contract", - "version": "0.7.50", + "version": "0.7.55", "dependencies": { "@c4a/core": "workspace:*", "graphql": "^16.14.2", @@ -122,7 +122,7 @@ }, "packages/extract-go": { "name": "@c4a/extract-go", - "version": "0.7.50", + "version": "0.7.55", "dependencies": { "@c4a/core": "workspace:*", "@c4a/extract": "workspace:*", @@ -132,7 +132,7 @@ }, "packages/extract-mdx": { "name": "@c4a/extract-mdx", - "version": "0.7.50", + "version": "0.7.55", "dependencies": { "@c4a/core": "workspace:*", "remark-mdx": "^3.1.1", @@ -144,7 +144,7 @@ }, "packages/extract-proto": { "name": "@c4a/extract-proto", - "version": "0.7.50", + "version": "0.7.55", "dependencies": { "@c4a/core": "workspace:*", "zod": "^3.23.8", @@ -152,7 +152,7 @@ }, "packages/extract-rush": { "name": "@c4a/extract-rush", - "version": "0.7.50", + "version": "0.7.55", "dependencies": { "@c4a/core": "workspace:*", "typescript": "^5.5.4", @@ -162,7 +162,7 @@ }, "packages/extract-sql": { "name": "@c4a/extract-sql", - "version": "0.7.50", + "version": "0.7.55", "dependencies": { "@c4a/core": "workspace:*", "node-sql-parser": "^5.4.0", @@ -171,7 +171,7 @@ }, "packages/extract-style": { "name": "@c4a/extract-style", - "version": "0.7.50", + "version": "0.7.55", "dependencies": { "@c4a/core": "workspace:*", "postcss": "^8.5.26", @@ -183,7 +183,7 @@ }, "packages/extract-thrift": { "name": "@c4a/extract-thrift", - "version": "0.7.50", + "version": "0.7.55", "dependencies": { "@c4a/core": "workspace:*", "zod": "^3.23.8", @@ -191,7 +191,7 @@ }, "packages/extract-ts": { "name": "@c4a/extract-ts", - "version": "0.7.50", + "version": "0.7.55", "dependencies": { "@c4a/core": "workspace:*", "@c4a/extract": "workspace:*", @@ -202,7 +202,7 @@ }, "packages/tui": { "name": "@c4a/tui", - "version": "0.7.50", + "version": "0.7.55", "dependencies": { "ink": "^5.0.0", "react": "^18.3.1", diff --git a/package.json b/package.json index 875643ad..778f2cdb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "context", - "version": "0.7.50", + "version": "0.7.55", "packageManager": "bun@1.3.9", "repository": { "type": "git", diff --git a/packages/context-cli/context-workflow/actions/repair-project-entry.yaml b/packages/context-cli/context-workflow/actions/repair-project-entry.yaml index f9c2e72c..b6815775 100644 --- a/packages/context-cli/context-workflow/actions/repair-project-entry.yaml +++ b/packages/context-cli/context-workflow/actions/repair-project-entry.yaml @@ -9,6 +9,7 @@ files: - resources/manuals/reference/code-extractors.md - resources/manuals/guides/knowledge-updates.md - resources/manuals/guides/repo-content.md + - resources/manuals/guides/imports.md - resources/manuals/guides/note.md - resources/manuals/guides/sessions.md - resources/manuals/guides/workspace-prepare.md diff --git a/packages/context-cli/context-workflow/provider.yaml b/packages/context-cli/context-workflow/provider.yaml index eaf3beec..9e9e9ab7 100644 --- a/packages/context-cli/context-workflow/provider.yaml +++ b/packages/context-cli/context-workflow/provider.yaml @@ -1,6 +1,6 @@ schema: agent-graph.provider.v1 id: c4a/context -version: 0.7.50 +version: 0.7.55 name: Context workflow description: Internal work contract for Context knowledge workspaces. graphs: diff --git a/packages/context-cli/context-workflow/resources/manuals/guides/imports.md b/packages/context-cli/context-workflow/resources/manuals/guides/imports.md new file mode 100644 index 00000000..1f4263a6 --- /dev/null +++ b/packages/context-cli/context-workflow/resources/manuals/guides/imports.md @@ -0,0 +1,90 @@ +--- +id: context.sdk.imports +kind: procedure +mediaType: text/markdown +--- + +# External associations + +Workspace-root `imports.yaml` declares external entrances without downloading, +installing or capturing their contents. Maintain it through the Context entry when +the task requests a lasting association. Temporary query reads do not register +dependencies. Same-repository authored documents and Skills use +[repo-content](repo-content.md) instead. + +```yaml +protocol: context.imports/v1 +imports: + engineering-guide: + kind: knowledge + url: https://docs.example.org/engineering/ + description: Engineering conventions + release-check: + kind: skill + url: https://github.com/example/skills/tree/v1/skills/release-check + path: skills/release-check + version: v1 +``` + +The stable ID and `url` are required. `kind`, `format`, `title`, `description`, +`path` and `version` are optional. Kind and format are open strings; versions are +opaque hints, not necessarily SemVer. Paths are source-relative without traversal. +Reader URLs must be HTTP(S), without credentials or whitespace; this is local +syntax validation, not a provider whitelist or availability check. Do not put +access tokens in query strings. The SDK exports `importsRegistrySchema` for local +validation. Unknown providers and offline operation do not cause network checks. + +Use a new ID when replacing the source identity. A declaration is neither proof +of reading nor an installation record. Do not expand transitive imports or infer +permission from a readable service. Only relevant originals are read through +existing host tools. Installed capability identity belongs to the host, not a +Context lock file. + +## Entrances and evidence + +An import is a reader entrance, not recorded evidence. It adds no entry to +`knowledge/structure.yaml` `references[]`, produces no review hint and has no +`import:` reference format. When an article's conclusion must be traceable or +tracked for change, register the needed external content as a source and capture +it; the import can remain as the reader entrance. + +The CLI never contacts providers or compares versions. When asked to update or +check imports, the Agent observes current versions with existing authorized host +tools and compares them with `version`. Unreachable, unauthorized or failed checks +are reported as "version unknown" and are never treated as a change. + +## Links and output + +`[Engineering guide](context:import/engineering-guide)` in an article projects to +the declared URL. Build never guesses a provider-specific URL, appends `path` or +rewrites the version. Use an entrance that already opens the intended target. +Missing IDs keep the reader label as non-clickable text and produce a build link +warning. An invalid or unreadable declaration skips the optional directory and +degrades affected links the same way; unrelated outputs still build. Correct +`imports.yaml` or the article link to restore navigation. Merely registering +entries emits no page. + +For an optional declaration-only directory: + +```ts +kbPackage({ + name: "handbook", + template: { path: "src/package-templates/kb" }, + importsPage: true, +}) +``` + +This generates `wikis/imports.md` and includes it in package navigation. With an +existing `site` configuration, `importsPage: { site: true }` also adds a website +navigation item. `false` or omission disables generation. Initialization enables +the package directory when a valid `imports.yaml` already exists. An invalid +declaration is preserved with a warning; initialization continues without enabling +that directory. When first configuring +a new workspace's KB later, enable it if the declaration is present; preserve +existing settings and explicit user choices. The directory contains only declared metadata, +not approved article bodies or installation commands. Check audience suitability +before exposing private entrances. + +Navigation links are not source evidence. Imports declaration support alone does +not authorize fabricated `import:` fragment references or advancement of article +evidence baselines. diff --git a/packages/context-cli/context-workflow/resources/manuals/guides/knowledge-updates.md b/packages/context-cli/context-workflow/resources/manuals/guides/knowledge-updates.md index 1b3db74d..18b2e043 100644 --- a/packages/context-cli/context-workflow/resources/manuals/guides/knowledge-updates.md +++ b/packages/context-cli/context-workflow/resources/manuals/guides/knowledge-updates.md @@ -35,6 +35,19 @@ empty diff. Inspect/edit does not advance references; delivery or an explicit no-impact outcome settles the selected scope. Historical locators always store the real repository path at the cited commit, not today's registration path. +External associations in `imports.yaml` are entrances, not recorded evidence: +they create no `references[]` and no review hints, and the CLI never checks +their versions. When the user asks to update or check external associations, +use existing authorized Host tools (for example `git ls-remote`, a package +manager or a web read) to observe each selected entry's current version and +compare it with its optional `version`. If it differs, read only what the +linked articles rely on, report whether they need revision and update +`version` once settled. Unreachable, unauthorized or failed checks are +"version unknown": report them, keep `version` unchanged, and never treat them +as changed. When an article's conclusion must trace or track external content, +register the needed part as a source and capture it; do not cite the +association itself as evidence. + Before starting production, compare the proposed content with the workspace's reader purpose. For clearly unrelated anecdotes or personal rankings, briefly recommend leaving them out of the formal manual or saving them separately because diff --git a/packages/context-cli/context-workflow/resources/manuals/guides/package-outputs.md b/packages/context-cli/context-workflow/resources/manuals/guides/package-outputs.md index eb0c3f6b..c090b18a 100644 --- a/packages/context-cli/context-workflow/resources/manuals/guides/package-outputs.md +++ b/packages/context-cli/context-workflow/resources/manuals/guides/package-outputs.md @@ -28,6 +28,14 @@ Same-repository originals have an optional [repository entrance](repo-content.md not the full document tree. It does not expose that page on a configured website; use `repoContentPage: { site: true }` to opt in. Existing declarations are kept. +External associations have an optional [directory](imports.md): `importsPage: true` +generates `wikis/imports.md` from declarations only; `{ site: true }` also exposes +it on a configured website. Neither option downloads or installs external content. + +When first declaring a KB for a new workspace, enable `repoContentPage: true` +if `repo-content.yaml` exists and `importsPage: true` if `imports.yaml` exists. +Keep existing package settings and explicit user choices; website exposure stays opt-in. + Output channels support multiple selection. In a new workspace without explicit preferences, the Agent proposes and configures KB + website as the default. Honor user feedback, session authority and existing workspace declarations; LLMS is an diff --git a/packages/context-cli/context-workflow/resources/procedures/knowledge-updates.md b/packages/context-cli/context-workflow/resources/procedures/knowledge-updates.md index a46e37f9..745f65d4 100644 --- a/packages/context-cli/context-workflow/resources/procedures/knowledge-updates.md +++ b/packages/context-cli/context-workflow/resources/procedures/knowledge-updates.md @@ -35,6 +35,19 @@ empty diff. Inspect/edit does not advance references; delivery or an explicit no-impact outcome settles the selected scope. Historical locators always store the real repository path at the cited commit, not today's registration path. +External associations in `imports.yaml` are entrances, not recorded evidence: +they create no `references[]` and no review hints, and the CLI never checks +their versions. When the user asks to update or check external associations, +use existing authorized Host tools (for example `git ls-remote`, a package +manager or a web read) to observe each selected entry's current version and +compare it with its optional `version`. If it differs, read only what the +linked articles rely on, report whether they need revision and update +`version` once settled. Unreachable, unauthorized or failed checks are +"version unknown": report them, keep `version` unchanged, and never treat them +as changed. When an article's conclusion must trace or track external content, +register the needed part as a source and capture it; do not cite the +association itself as evidence. + Before starting production, compare the proposed content with the workspace's reader purpose. For clearly unrelated anecdotes or personal rankings, briefly recommend leaving them out of the formal manual or saving them separately because diff --git a/packages/context-cli/package.json b/packages/context-cli/package.json index 3b8c82dc..1a5e7e34 100644 --- a/packages/context-cli/package.json +++ b/packages/context-cli/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/context-cli", "description": "Local runtime and Agent integration for traceable knowledge production", - "version": "0.7.50", + "version": "0.7.55", "type": "module", "license": "MIT", "engines": { diff --git a/packages/context-cli/scripts/build-plugin.ts b/packages/context-cli/scripts/build-plugin.ts index 6d9205d6..537992d6 100644 --- a/packages/context-cli/scripts/build-plugin.ts +++ b/packages/context-cli/scripts/build-plugin.ts @@ -282,6 +282,7 @@ async function writeClaudeCommands( ): Promise { const commandsRoot = join(outputRoot, "commands"); await mkdir(commandsRoot, { recursive: true }); + await copyDir(join(PLUGIN_SOURCE_ROOT, "skills/context/references"), join(outputRoot, "resources/context")); for (const command of commands) { const body = [ "---", @@ -294,7 +295,7 @@ async function writeClaudeCommands( ] : []), "---", "", - command.body.trimEnd(), + (command.slug === "context" ? command.body.replaceAll("](references/", "](../resources/context/") : command.body).trimEnd(), "", ].join("\n"); await writeFile(join(commandsRoot, `${command.slug}.md`), body, "utf8"); @@ -368,8 +369,10 @@ async function buildVercel(): Promise { async function writeCursorCommands(outRoot: string, commands: readonly CommandSource[]): Promise { const dest = join(outRoot, "commands"); await mkdir(dest, { recursive: true }); + await copyDir(join(PLUGIN_SOURCE_ROOT, "skills/context/references"), join(outRoot, "resources/context")); for (const command of commands) { - const rewrittenBody = rewriteClaudeSlashCommandsForCursor(command.body); + const rewrittenBody = rewriteClaudeSlashCommandsForCursor(command.slug === "context" + ? command.body.replaceAll("](references/", "](../resources/context/") : command.body); const body = stripHtmlComments(rewrittenBody).trimStart(); const file = `---\ndescription: ${JSON.stringify(command.description)}\n---\n\n${body.trimEnd()}\n`; await writeFile(join(dest, cursorCommandFileName(command.slug)), file, "utf8"); diff --git a/packages/context-cli/scripts/build-workflow.ts b/packages/context-cli/scripts/build-workflow.ts index e87ad54c..6c76cbcc 100644 --- a/packages/context-cli/scripts/build-workflow.ts +++ b/packages/context-cli/scripts/build-workflow.ts @@ -60,6 +60,7 @@ const sdkManuals = [ "guides/source-batches.md", "guides/knowledge-updates.md", "guides/repo-content.md", + "guides/imports.md", "guides/workspace-prepare.md", "guides/workspace-commit.md", "guides/workspace-restore.md", diff --git a/packages/context-cli/src/__tests__/imports.integration.test.ts b/packages/context-cli/src/__tests__/imports.integration.test.ts new file mode 100644 index 00000000..9893b077 --- /dev/null +++ b/packages/context-cli/src/__tests__/imports.integration.test.ts @@ -0,0 +1,128 @@ +import { afterEach, expect, test } from "bun:test"; +import { mkdir, mkdtemp, readFile, rm, writeFile, symlink } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import YAML from "yaml"; +import { importsRegistrySchema, kbPackage } from "@c4a/context"; +import { readImportsRegistry } from "../project/importsRegistry.js"; +import { importsPages, importsLinkProjector, importsFingerprint } from "../project/importsPages.js"; +import { repoContentNavigation, writeRepoContentPages } from "../project/repoContentPages.js"; +import { writeRenderedPackageTemplate } from "../project/packageBuildContent.js"; +import { writePackageSite } from "../project/packageSite.js"; +import { initContextProject, formatProjectInitResult } from "../project/workspace.js"; +import { repoContentGit } from "../project/repoContentGit.js"; + +const roots: string[] = []; +afterEach(async () => { for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true }); }); +async function fixture() { + const parent = resolve(import.meta.dir, "../../../../.tmp/imports-tests"); + await mkdir(parent, { recursive: true }); + const root = await mkdtemp(join(parent, "workspace-")); roots.push(root); + await repoContentGit(root, ["init", "-b", "main"]); + await writeFile(join(root, "package.json"), JSON.stringify({ context: { language: "zh-CN" } })); + await writeFile(join(root, "imports.yaml"), YAML.stringify({ protocol: "context.imports/v1", imports: { + tool: { kind: "plugin", url: "https://unavailable.example.org/plugin", version: "preview" }, + guide: { kind: "knowledge", url: "https://example.org/repo/tree/v1/docs", path: "docs", description: "|[bad](javascript:x)" }, + extra: { kind: "custom", url: "https://example.org/custom" }, + } })); + return root; +} + +test("imports are provider-neutral declarations with no installation commands", () => { + expect(importsRegistrySchema.parse({ protocol: "context.imports/v1", imports: { a: { url: "https://example.org", kind: "new-kind" } } }).imports.a?.kind).toBe("new-kind"); + for (const url of ["javascript:alert(1)", "file:///etc/passwd", "https://user:secret@example.org", "https://example.org/"]) { + expect(importsRegistrySchema.safeParse({ protocol: "context.imports/v1", imports: { a: { url } } }).success).toBe(false); + } +}); + +test("links preserve the declared entrance and do not guess provider routes", async () => { + const root = await fixture(); + const project = await importsLinkProjector(root); + expect(await project("[Guide](context:import/guide)")).toBe("[Guide](https://example.org/repo/tree/v1/docs)"); + expect(await project("`[example](context:import/missing)`")).toBe("`[example](context:import/missing)`"); + expect(await project("[Missing](context:import/missing)")).toBe("Missing (unresolved import: missing)"); + expect(await project("[Suffix](context:import/guide/child)")).toBe("Suffix (unresolved import: guide/child)"); +}); + +test("optional offline directory is escaped, ordered and included in navigation", async () => { + const root = await fixture(); + const pkg = kbPackage({ name: "kb", template: { path: "template" }, importsPage: true }); + expect(await importsPages(root, kbPackage({ name: "disabled", template: { path: "template" } }))).toEqual([]); + const pages = await importsPages(root, pkg); + const text = pages[0]!.content; + expect(text.indexOf("## 知识")).toBeLessThan(text.indexOf("## 插件")); + expect(text).not.toContain("