From 9d8a1b490219797514df341983daf776cc62c74a Mon Sep 17 00:00:00 2001 From: qiansc Date: Sun, 23 Aug 2026 08:43:57 +0800 Subject: [PATCH] fix(v0.6.13): improve Context workflow recovery guidance --- CHANGELOG.md | 18 ++++ README.md | 5 + README.zh-CN.md | 4 + bun.lock | 18 ++-- package.json | 2 +- .../context-workflow/provider.yaml | 2 +- packages/context-cli/package.json | 2 +- .../context-cli/plugin/commands/context.md | 16 +++- .../context-cli/src/__tests__/cli.test.ts | 3 + .../__tests__/pluginPromptMigration.test.ts | 5 + .../projectCodegraphSourceRefs.test.ts | 33 +++++++ .../projectRunBatchReviewV069.test.ts | 91 ++++++++++++++++++- .../projectWorkflowResourceRepairV062.test.ts | 43 +++++++++ packages/context-cli/src/project/status.ts | 9 +- .../context-cli/src/project/statusReaders.ts | 9 +- packages/context-cli/src/project/verify.ts | 25 ++++- .../src/project/verifySourceRefs.ts | 21 ++++- .../src/project/workflow/workflowFacts.ts | 11 +++ packages/context-cli/src/project/workspace.ts | 12 ++- packages/context/package.json | 2 +- packages/core/package.json | 2 +- packages/dev-cli/package.json | 2 +- packages/extract-go/package.json | 2 +- packages/extract-rush/package.json | 2 +- packages/extract-ts/package.json | 2 +- packages/extract/package.json | 2 +- packages/tui/package.json | 2 +- 27 files changed, 309 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93aa221..a2bf9e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to Context are documented here. ## Unreleased +## 0.6.13 - 2026-08-23 + +- Clarified debug startup so a new workspace receives `--debug` through + `context entry`/`context init`, while existing workspaces continue to use + `context debug enable`. +- Created the recommended `.tmp/agent-payloads/` scratch directory during + initialization and documented how to recreate it after scratch cleanup. + +## 0.6.12 - 2026-08-22 + +- Added Agent guidance for publishing a completed Context build directly from + its output directory, while preserving human review by default and using + automatic merge only when the user explicitly requests direct publication. + +## 0.6.11 - 2026-08-21 + +- Deferred code `source_ref` reverse lookup until every declared extraction phase is indexed, so a cold runtime continues the remaining phases instead of reporting provisional stale evidence. + ## 0.6.10 - 2026-08-21 - Made repository recovery identity transport-independent by matching the full namespace/repository path while preserving pinned-commit and subpath validation. diff --git a/README.md b/README.md index 5c727f5..63f9332 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,11 @@ Every build emits an inventory that maps distributed files back to approved workspace knowledge. Published pages keep reader-facing metadata; detailed source and review evidence remains in the production workspace. +Context stops after producing a complete local build output. Publishing that +output to a hosted knowledge service or package registry belongs to an +explicitly installed downstream distribution tool; use its complete-output +upload path when available instead of reconstructing a remote installation. + ## Repository map | Module | Role in the workflow | Documentation | diff --git a/README.zh-CN.md b/README.zh-CN.md index 04b8d6a..595d40f 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -114,6 +114,10 @@ CLI 负责,它们不是可以随意修改的临时目录。只有当前工作 每次构建都会输出一份清单,将分发文件映射回工作区内的正式知识。发布页只保留 面向读者的元数据;完整来源和审核证据继续留在生产工作区。 +Context 在生成完整本地产物后结束。将产物发布到托管知识服务或包平台,属于显式 +安装的下游分发工具职责;如果分发工具支持完整产物上传,应直接使用该入口,而不 +是先重建一份远端安装基线。 + ## 仓库模块 | 模块 | 在知识生产链中的职责 | 文档 | diff --git a/bun.lock b/bun.lock index 6c5d4a2..cedfc73 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,7 @@ }, "packages/context": { "name": "@c4a/context", - "version": "0.6.10", + "version": "0.6.13", "dependencies": { "yaml": "^2.5.1", "zod": "^3.23.8", @@ -25,7 +25,7 @@ }, "packages/context-cli": { "name": "@c4a/context-cli", - "version": "0.6.10", + "version": "0.6.13", "bin": { "context": "dist/cli.js", }, @@ -56,7 +56,7 @@ }, "packages/core": { "name": "@c4a/core", - "version": "0.6.10", + "version": "0.6.13", "dependencies": { "picomatch": "^4.0.4", "yaml": "^2.4.5", @@ -68,7 +68,7 @@ }, "packages/dev-cli": { "name": "@c4a/dev-cli", - "version": "0.6.10", + "version": "0.6.13", "dependencies": { "@c4a/core": "workspace:*", "@c4a/tui": "workspace:*", @@ -79,7 +79,7 @@ }, "packages/extract": { "name": "@c4a/extract", - "version": "0.6.10", + "version": "0.6.13", "bin": { "c4a-extract-code": "./dist/bin/c4a-extract-code.js", }, @@ -91,7 +91,7 @@ }, "packages/extract-go": { "name": "@c4a/extract-go", - "version": "0.6.10", + "version": "0.6.13", "dependencies": { "web-tree-sitter": "^0.25.10", }, @@ -102,7 +102,7 @@ }, "packages/extract-rush": { "name": "@c4a/extract-rush", - "version": "0.6.10", + "version": "0.6.13", "dependencies": { "typescript": "^5.5.4", "yaml": "^2.5.1", @@ -110,7 +110,7 @@ }, "packages/extract-ts": { "name": "@c4a/extract-ts", - "version": "0.6.10", + "version": "0.6.13", "dependencies": { "typescript": "^5.5.4", "web-tree-sitter": "^0.20.8", @@ -121,7 +121,7 @@ }, "packages/tui": { "name": "@c4a/tui", - "version": "0.6.10", + "version": "0.6.13", "dependencies": { "ink": "^5.0.0", "react": "^18.3.1", diff --git a/package.json b/package.json index 98b15ed..6f489d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "context", - "version": "0.6.10", + "version": "0.6.13", "packageManager": "bun@1.3.9", "repository": { "type": "git", diff --git a/packages/context-cli/context-workflow/provider.yaml b/packages/context-cli/context-workflow/provider.yaml index ffa6168..35ff032 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.6.10 +version: 0.6.13 name: Context workflow description: Internal work contract for Context knowledge workspaces. graphs: diff --git a/packages/context-cli/package.json b/packages/context-cli/package.json index cd8f39f..85a107a 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.6.10", + "version": "0.6.13", "type": "module", "license": "MIT", "engines": { diff --git a/packages/context-cli/plugin/commands/context.md b/packages/context-cli/plugin/commands/context.md index e588f34..658f253 100644 --- a/packages/context-cli/plugin/commands/context.md +++ b/packages/context-cli/plugin/commands/context.md @@ -61,9 +61,13 @@ Execute only `next_action.command` returned by `context entry`: ### Conversation modes -Enable debugging only when the user explicitly requests it. Run `context debug -enable` first; debugging records traces below `.tmp/context-runtime/debug/` but -does not grant workflow authority or provide source evidence. +Enable debugging only when the user explicitly requests it. If initialization +is required, pass `--debug` to `context entry` and execute its returned +`context init ... --debug` command; do not run a workspace-only debug command +before initialization. For an existing workspace, run `context debug enable` +before workflow evaluation. Debugging records traces below +`.tmp/context-runtime/debug/` but does not grant workflow authority or provide +source evidence. For explicitly authorized fully managed operation, use: @@ -115,6 +119,12 @@ Explain, ask, confirm, and summarize in the user's current conversation language. Keep commands, flags, paths, ids, status values, JSONL keys, `source_ref` values, and copied CLI tokens unchanged. +When the user explicitly asks to publish a completed build, treat publication +as a downstream distribution step outside the Context Route. Use only an +explicitly installed distribution tool and its documented complete-output +upload command. If no such tool is available, stop after the local build and +explain that Context itself does not publish to a hosted service. + Do not infer repo sources, extraction scope, review decisions, or package output choices from surrounding files. Do not call source-repo operations such as clone, checkout, reset, fetch, install, build, or test without explicit user diff --git a/packages/context-cli/src/__tests__/cli.test.ts b/packages/context-cli/src/__tests__/cli.test.ts index e1a7210..4597dce 100644 --- a/packages/context-cli/src/__tests__/cli.test.ts +++ b/packages/context-cli/src/__tests__/cli.test.ts @@ -314,6 +314,7 @@ describe("CLI error handling", () => { expect(existsSync(join(cwd, "parser-kb", ".tmp", "context-runtime", "lifecycle", "candidates.jsonl"))).toBe(false); expect(existsSync(join(cwd, "parser-kb", "knowledge"))).toBe(true); expect(existsSync(join(cwd, "parser-kb", "dist"))).toBe(true); + expect(existsSync(join(cwd, "parser-kb", ".tmp", "agent-payloads"))).toBe(true); expect(existsSync(join(cwd, "parser-kb", "README.md"))).toBe(true); expect(existsSync(join(cwd, "parser-kb", "AGENTS.md"))).toBe(true); expect(existsSync(join(cwd, "parser-kb", ".claude"))).toBe(false); @@ -329,6 +330,7 @@ describe("CLI error handling", () => { expect(readme).toContain('TMPDIR="$PWD/.tmp/install" bun install'); expect(readme).toContain("`.tmp/install/`: workspace-local temporary files"); expect(readme).toContain("`.tmp/agent-payloads/`: optional Agent-owned command inputs"); + expect(readme).toContain("Initialization creates it"); expect(readme).toContain("`.tmp/context-runtime/`: disposable runtime files"); expect(agents).toContain("Treat `workflow.current` as the current-step authority"); expect(agents).toContain("Read every `resources.required` item"); @@ -340,6 +342,7 @@ describe("CLI error handling", () => { expect(agents).toContain("never store or reuse that authority"); expect(agents).toContain("Managed approval uses only the revision-bound atomic command"); expect(agents).toContain("Prefer `.tmp/agent-payloads/` for Agent-authored transient command inputs"); + expect(agents).toContain("Initialization creates the directory"); expect(agents).toContain("not a CLI requirement"); expect(readme).not.toContain("bun run context -- status"); expect(agents).toContain("context status"); diff --git a/packages/context-cli/src/__tests__/pluginPromptMigration.test.ts b/packages/context-cli/src/__tests__/pluginPromptMigration.test.ts index 89f446a..9596917 100644 --- a/packages/context-cli/src/__tests__/pluginPromptMigration.test.ts +++ b/packages/context-cli/src/__tests__/pluginPromptMigration.test.ts @@ -76,6 +76,11 @@ describe("plugin prompt and workflow resource contract", () => { expect(continuation).toContain("context plugin install"); expect(continuation).toContain("shell exit 127"); expect(continuation).toContain("Do not run an installation preflight"); + expect(continuation).toContain("context init ... --debug"); + expect(continuation).toContain("do not run a workspace-only debug command"); + expect(continuation).toContain("For an existing workspace, run `context debug enable`"); + expect(continuation).toContain("downstream distribution step outside the Context Route"); + expect(continuation).toContain("Context itself does not publish to a hosted service"); }); test("source and gate discipline lives in selected workflow procedures", async () => { diff --git a/packages/context-cli/src/__tests__/projectCodegraphSourceRefs.test.ts b/packages/context-cli/src/__tests__/projectCodegraphSourceRefs.test.ts index 7cc4bf8..6f90fb9 100644 --- a/packages/context-cli/src/__tests__/projectCodegraphSourceRefs.test.ts +++ b/packages/context-cli/src/__tests__/projectCodegraphSourceRefs.test.ts @@ -49,6 +49,39 @@ async function writeRepoEvidence(projectRoot: string): Promise { } describe("codegraph file-aware approved source refs", () => { + test("defers reverse lookup while the symbol index covers only part of the declared extraction phases", async () => { + const projectRoot = await makeProject(); + try { + await writeRepoEvidence(projectRoot); + await writeApproved({ + projectRoot, + collection: "codegraph", + sources: [`repo:${SOURCE_NAME}`], + sourceRef: `src-1#symbol:src/first.ts:Button:function@${SYMBOL_DIGEST}`, + body: "Button API evidence.", + extraFrontmatter: { + visibility: "exported", + code_symbols: [`${SOURCE_NAME}|Button|function`], + candidate_fingerprint: "sha256:candidate-fingerprint", + }, + }); + + const result = await verifyProjectWorkspace(projectRoot, { + expectedExtractPhaseIds: [PHASE_ID, "extract:20260712/other:codegraph"], + }); + + expect(result.ok).toBe(true); + expect(result.evidenceStatus).toBe("pass-with-unverifiable-evidence"); + expect(result.issues).toContainEqual(expect.objectContaining({ + severity: "warning", + code: "extract-symbol-index-incomplete", + })); + expect(result.issues.map((issue) => issue.code)).not.toContain("approved-source-ref-stale"); + } finally { + await rm(projectRoot, { recursive: true, force: true }); + } + }); + test("uses file identity to verify duplicate symbols without ambiguity", async () => { const projectRoot = await makeProject(); try { diff --git a/packages/context-cli/src/__tests__/projectRunBatchReviewV069.test.ts b/packages/context-cli/src/__tests__/projectRunBatchReviewV069.test.ts index 9cbf47a..66ebdee 100644 --- a/packages/context-cli/src/__tests__/projectRunBatchReviewV069.test.ts +++ b/packages/context-cli/src/__tests__/projectRunBatchReviewV069.test.ts @@ -1,10 +1,11 @@ import { describe, expect, test } from "bun:test"; import { execFileSync } from "node:child_process"; -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { mkdir } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { createCliProgram, handleCliFailure } from "../cli.js"; +import { writeApproved } from "./projectVerifyV062Helpers.js"; function makeTmp(): string { return mkdtempSync(join(tmpdir(), "ctx-project-batch-review-v069-")); @@ -100,7 +101,95 @@ function writeMultiSourceProjectEntry(project: string): void { ].join("\n"), "utf8"); } +function writeRepoOnlyProjectEntry(project: string): void { + writeFileSync(join(project, "src", "index.ts"), [ + 'import { defineProject, extractTs, source } from "@c4a/context";', + "", + 'const sampleA = source("20260712", "sample-a");', + 'const sampleB = source("20260712", "sample-b");', + "", + "export default defineProject({", + " sources: [sampleA, sampleB],", + " phases: [", + ' extractTs({ source: sampleA, collection: "codegraph" }),', + ' extractTs({ source: sampleB, collection: "codegraph" }),', + " ],", + " packages: [],", + "});", + "", + ].join("\n"), "utf8"); +} + describe("0.6.9 codegraph batched human review", () => { + test("a cold runtime continues all extraction phases before validating existing code evidence", async () => { + const root = makeTmp(); + const repoA = join(root, "repo-a"); + const repoB = join(root, "repo-b"); + const project = join(root, "kb"); + try { + await mkdir(repoA, { recursive: true }); + await mkdir(repoB, { recursive: true }); + const headA = initTsRepo(repoA, "sample-a"); + const headB = initTsRepo(repoB, "sample-b"); + await runCliInDir(root, ["init", "kb"]); + for (const source of [ + { module: "sample-a", local: "../repo-a", remote: "https://git.example.com/repo-a.git", ref: headA }, + { module: "sample-b", local: "../repo-b", remote: "https://git.example.com/repo-b.git", ref: headB }, + ]) { + await runCliInDir(project, [ + "source", "add", "repo", "20260712", + "--module", source.module, + "--local", source.local, + "--remote", source.remote, + "--ref", source.ref, + ]); + } + writeRepoOnlyProjectEntry(project); + expect(existsSync(join(project, ".tmp", "context-runtime", "extract", "source-symbols.json"))).toBe(false); + + await runCliInDir(project, [ + "run", "extract:20260712/sample-a:codegraph", "--format", "json", + ]); + await writeApproved({ + projectRoot: project, + collection: "codegraph", + sources: ["repo:20260712/sample-b"], + sourceRef: "src-1#symbol:src/Button.ts:Button:function@abc123abc123", + body: "Existing sample B knowledge.", + extraFrontmatter: { + visibility: "exported", + code_symbols: ["20260712/sample-b|Button|function"], + candidate_fingerprint: "sha256:approved-sample-b", + }, + }); + + const status = JSON.parse(await runCliInDir(project, ["status", "--format", "json", "--view", "full"])) as { + state: string; + verifyErrors: number; + pendingExtractPhases: string[]; + routing: { command_plan: Array<{ command: string }> }; + }; + expect(status.state).toBe("route.extract.pending-target"); + expect(status.verifyErrors).toBe(0); + expect(status.pendingExtractPhases).toEqual(["extract:20260712/sample-b:codegraph"]); + expect(status.routing.command_plan[0]?.command).toContain( + "run extract:20260712/sample-b:codegraph --format json", + ); + + const verify = JSON.parse(await runCliInDir(project, ["verify", "--format", "json"])) as { + ok: boolean; + evidence_status: string; + issues: Array<{ code: string }>; + }; + expect(verify.ok).toBe(true); + expect(verify.evidence_status).toBe("pass-with-unverifiable-evidence"); + expect(verify.issues.map((issue) => issue.code)).toContain("extract-symbol-index-incomplete"); + expect(verify.issues.map((issue) => issue.code)).not.toContain("approved-source-ref-stale"); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + test("multiple confirmed repo modules finish extraction before one codegraph Review", async () => { const root = makeTmp(); const repoA = join(root, "repo-a"); diff --git a/packages/context-cli/src/__tests__/projectWorkflowResourceRepairV062.test.ts b/packages/context-cli/src/__tests__/projectWorkflowResourceRepairV062.test.ts index bb5b0fd..d91fed6 100644 --- a/packages/context-cli/src/__tests__/projectWorkflowResourceRepairV062.test.ts +++ b/packages/context-cli/src/__tests__/projectWorkflowResourceRepairV062.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { extractTs, source } from "@c4a/context"; import { LARK_DOCUMENT_NORMALIZER_VERSION } from "../project/documentCaptureContract.js"; import { pendingDocumentCaptureCommands, @@ -26,6 +27,48 @@ function declaredProseRow(sourceKey: string, collection = "sop") { } describe("Context workflow resource repair", () => { + test("finishes pending code extraction before asking for evidence maintenance", async () => { + const sampleA = source("20260821", "sample-a"); + const sampleB = source("20260821", "sample-b"); + const phaseA = extractTs({ source: sampleA, collection: "codegraph" }); + const phaseB = extractTs({ source: sampleB, collection: "codegraph" }); + const observation: ContextWorkflowObservation = { + ...emptyObservation(), + sourceCount: 2, + repoSources: [ + { id: sampleA.name, name: sampleA.name }, + { id: sampleB.name, name: sampleB.name }, + ], + readyRepoSources: 2, + phases: [phaseA, phaseB], + sourceFreshness: "unknown", + pendingExtractPhases: [phaseB.id], + draftCandidates: 1, + draftCollections: ["codegraph"], + approvedPages: 1, + evidenceWarnings: "stale", + verifyErrors: 1, + verifyIssues: [{ + severity: "error", + code: "approved-source-ref-stale", + source_keys: [`repo:${sampleB.name}`], + message: "approved source_ref is not present in the partial symbol index", + }], + }; + + const facts = createContextWorkflowFacts(observation, []); + expect(facts.verification.blocking_clear).toBe(true); + expect(facts.evidence.maintenance_clear).toBe(true); + expect(facts.extract.complete).toBe(false); + + const snapshot = await evaluateContextWorkflow({ observation, authorities: [] }); + expect(snapshot.route).toMatchObject({ + node: "extract-next", + reason_code: "route.extract.pending-target", + }); + expect(snapshot.route?.commands[0]?.command).toContain(`run ${phaseB.id} --format json`); + }); + test("unprojected source asset paths route to deterministic close repair", async () => { const observation: ContextWorkflowObservation = { ...emptyObservation(), diff --git a/packages/context-cli/src/project/status.ts b/packages/context-cli/src/project/status.ts index f4add86..c2d521f 100644 --- a/packages/context-cli/src/project/status.ts +++ b/packages/context-cli/src/project/status.ts @@ -483,7 +483,14 @@ export async function collectProjectStatusSnapshot( }) : { state: "ready" as const, stalePhases: [], pendingPhases: [], diagnostics: [], errorDiagnostics: [] }; const verifyStatus = draftStatus.diagnostics.length === 0 - ? await readVerifyStatus(projectRoot) + ? await readVerifyStatus( + projectRoot, + phases + .filter((phase) => + phase.kind === "phase.extract.ts" || phase.kind === "phase.extract.custom" + ) + .map((phase) => phase.id), + ) : { issues: [], diagnostics: [] }; const resourceRepair = resourcePlaceholderRepairTargets(verifyStatus.issues); const pendingCapture = pendingDocumentCaptureCommands({ diff --git a/packages/context-cli/src/project/statusReaders.ts b/packages/context-cli/src/project/statusReaders.ts index 6ca8c48..88f2f1a 100644 --- a/packages/context-cli/src/project/statusReaders.ts +++ b/packages/context-cli/src/project/statusReaders.ts @@ -266,10 +266,15 @@ export async function readSourceStatus(projectRoot: string): Promise<{ }; } -export async function readVerifyStatus(projectRoot: string): Promise<{ issues: ProjectVerifyIssue[]; diagnostics: string[] }> { +export async function readVerifyStatus( + projectRoot: string, + expectedExtractPhaseIds?: readonly string[], +): Promise<{ issues: ProjectVerifyIssue[]; diagnostics: string[] }> { try { return { - issues: (await verifyProjectWorkspace(projectRoot)).issues, + issues: (await verifyProjectWorkspace(projectRoot, { + ...(expectedExtractPhaseIds === undefined ? {} : { expectedExtractPhaseIds }), + })).issues, diagnostics: [], }; } catch (error) { diff --git a/packages/context-cli/src/project/verify.ts b/packages/context-cli/src/project/verify.ts index 3cc65d6..7f551a2 100644 --- a/packages/context-cli/src/project/verify.ts +++ b/packages/context-cli/src/project/verify.ts @@ -16,7 +16,7 @@ import { type EvidenceIndexCache, } from "./verifySourceRefs.js"; import type { ProjectVerifyIssue, ProjectVerifyResult } from "./verifyTypes.js"; -import { findContextProjectRoot } from "./workspace.js"; +import { findContextProjectRoot, loadContextProjectModule } from "./workspace.js"; import { readRejectedDecisions, REVIEW_DECISIONS_FILE } from "./reviewDecisions.js"; import { knowledgeAssetReferences, unprojectedSourceAssetLinks } from "./knowledgeAssets.js"; import { parseDocumentSourceLocator } from "@c4a/extract"; @@ -108,10 +108,29 @@ async function readCandidateDecisionState(input: { export async function verifyProjectWorkspace( projectRoot: string, - options: { approvedStructureOverride?: Record } = {}, + options: { + approvedStructureOverride?: Record; + expectedExtractPhaseIds?: readonly string[]; + } = {}, ): Promise { const issues: ProjectVerifyIssue[] = []; - const symbolIndex = await loadVerifiedSymbolIndex(projectRoot); + let expectedExtractPhaseIds = options.expectedExtractPhaseIds; + if (expectedExtractPhaseIds === undefined) { + try { + const loaded = await loadContextProjectModule(projectRoot); + expectedExtractPhaseIds = loaded.project.phases + .filter((phase) => + phase.kind === "phase.extract.ts" || phase.kind === "phase.extract.custom" + ) + .map((phase) => phase.id); + } catch { + // Project-entry diagnostics are owned by status. Verification can still + // validate durable files without inferring extraction coverage. + } + } + const symbolIndex = await loadVerifiedSymbolIndex(projectRoot, { + ...(expectedExtractPhaseIds === undefined ? {} : { expectedPhaseIds: expectedExtractPhaseIds }), + }); const sourceRegistry = await loadSourceRegistryLookup(projectRoot, issues); const evidenceIndexCache: EvidenceIndexCache = { entries: new Map(), diff --git a/packages/context-cli/src/project/verifySourceRefs.ts b/packages/context-cli/src/project/verifySourceRefs.ts index 08a5e1b..c322f1a 100644 --- a/packages/context-cli/src/project/verifySourceRefs.ts +++ b/packages/context-cli/src/project/verifySourceRefs.ts @@ -38,7 +38,10 @@ const execFileAsync = promisify(execFile); export interface SymbolIndexLookup { bySource: ReadonlyMap | null; - unavailableCode?: "extract-symbol-index-missing" | "extract-symbol-index-untrusted"; + unavailableCode?: + | "extract-symbol-index-missing" + | "extract-symbol-index-untrusted" + | "extract-symbol-index-incomplete"; unavailableMessage?: string; } @@ -278,7 +281,10 @@ export function proseStaleMessage(sourceRef: string, status: ResolvedProseSource return `document source span changed for ${sourceRef}; create a replacement candidate unless the approved body still matches a moved source span`; } -export async function loadVerifiedSymbolIndex(projectRoot: string): Promise { +export async function loadVerifiedSymbolIndex( + projectRoot: string, + options: { expectedPhaseIds?: readonly string[] } = {}, +): Promise { const symbolIndex = await readExtractSourceSymbolIndex(projectRoot); if (symbolIndex === null) { return { @@ -306,6 +312,17 @@ export async function loadVerifiedSymbolIndex(projectRoot: string): Promise + symbolIndex.phaseFingerprints[phaseId] === undefined + ); + if (missingPhaseIds.length > 0) { + return { + bySource: null, + unavailableCode: "extract-symbol-index-incomplete", + unavailableMessage: `extract symbol index covers ${expectedPhaseIds.length - missingPhaseIds.length}/${expectedPhaseIds.length} declared extraction phases; source_ref reverse lookup was deferred until extraction completes`, + }; + } const symbolIndexBySource = new Map(); for (const symbol of symbolIndex.symbols) { const bucket = symbolIndexBySource.get(symbol.source) ?? []; diff --git a/packages/context-cli/src/project/workflow/workflowFacts.ts b/packages/context-cli/src/project/workflow/workflowFacts.ts index 3da3b4f..4977125 100644 --- a/packages/context-cli/src/project/workflow/workflowFacts.ts +++ b/packages/context-cli/src/project/workflow/workflowFacts.ts @@ -158,6 +158,17 @@ function evidenceMaintenanceClear( if (observation.capturedDocumentSources < observation.documentSources.length) return true; if (activeProseRefreshRepairsVerification(observation)) return true; if (verifyErrorsAreCloseRepairable(observation.verifyIssues)) return true; + const staleEvidenceIssues = observation.verifyIssues.filter((issue) => + issue.severity === "error" && issue.code === "approved-source-ref-stale" + ); + const pendingExtractionRepairsCodeEvidence = observation.pendingExtractPhases.length > 0 && + staleEvidenceIssues.length > 0 && + staleEvidenceIssues.every((issue) => + issue.source_keys !== undefined && + issue.source_keys.length > 0 && + issue.source_keys.every((sourceKey) => sourceKey.startsWith("repo:")) + ); + if (pendingExtractionRepairsCodeEvidence) return true; return observation.evidenceWarnings !== "orphaned" && observation.evidenceWarnings !== "stale"; } diff --git a/packages/context-cli/src/project/workspace.ts b/packages/context-cli/src/project/workspace.ts index 8f2fefa..ef1d667 100644 --- a/packages/context-cli/src/project/workspace.ts +++ b/packages/context-cli/src/project/workspace.ts @@ -15,6 +15,7 @@ import { import { enableContextDebug } from "./debugTrace.js"; const PROJECT_DIRS = ["src", "sources", "knowledge", "dist"] as const; +const PROJECT_SCRATCH_DIRS = [join(".tmp", "agent-payloads")] as const; const DEFAULT_PROJECT_DIR = "context"; const DEFAULT_PROJECT_ENTRY = "src/index.ts"; export const PROJECT_LANGUAGES = ["en", "zh-CN"] as const; @@ -485,7 +486,7 @@ function renderReadme(projectName: string, language: ProjectLanguage): string { "- `sources/`:来源注册信息和采集快照。", "- `knowledge/`:持久知识及其结构投影。", "- `dist/`:生成的知识包。", - "- `.tmp/agent-payloads/`:Agent 可选的临时命令输入,成功 stage/apply 后可以删除。", + "- `.tmp/agent-payloads/`:Agent 可选的临时命令输入;初始化时会创建,被清理后可在写入前重新创建。", "- `.tmp/install/`:依赖安装使用的工作区本地临时目录。", "- `.tmp/context-runtime/`:可删除的 Context 运行时状态。", "", @@ -523,7 +524,7 @@ function renderReadme(projectName: string, language: ProjectLanguage): string { "- `sources/`: registered sources and captured snapshots.", "- `knowledge/`: durable knowledge, its structural projection with minimal closed source inputs, and compact rejected-candidate fingerprints.", "- `dist/`: generated packages.", - "- `.tmp/agent-payloads/`: optional Agent-owned command inputs. These files are transient and can be removed after the corresponding stage or apply succeeds.", + "- `.tmp/agent-payloads/`: optional Agent-owned command inputs. Initialization creates it; recreate it before writing if scratch cleanup removed it.", "- `.tmp/install/`: workspace-local temporary files used during dependency installation.", "- `.tmp/context-runtime/`: disposable runtime files, including lifecycle candidates and staged structures. Successful close removes completed lifecycle and Review state.", "", @@ -564,7 +565,7 @@ function renderAgents(projectName: string, language: ProjectLanguage): string { "- 不得根据目录、文件名、URL、示例或旧会话推断用户决定;没有明确授权时不得扫描或对来源仓库执行 clone、fetch、checkout、install、build、test 或脚本。已登记仓库 checkout 缺失时,使用 Route 返回的恢复计划和恢复动作,不手工创建来源软链。", "- 生命周期写入必须使用 Context CLI,不得用临时脚本修改 `sources/`、`knowledge/`、`dist/` 或 `.tmp/context-runtime/`。", "- 飞书采集只在 `evidence_status: error` 时停止;`projection_status: generic|warning` 表示原始 XML 已保留且可继续。不得在用户工作区修补 CLI 或手改快照来增加 renderer。", - "- Agent 编写的临时输入优先放在 `.tmp/agent-payloads/`;这是推荐而非强制。不要自行创建 `inputs/` 等顶层临时目录。", + "- Agent 编写的临时输入优先放在 `.tmp/agent-payloads/`;这是推荐而非强制。目录在初始化时创建,如果临时目录已清理则在写入前重新创建。不要自行创建 `inputs/` 等顶层临时目录。", "- Context 完成只证明知识工作流状态,不证明 Git 提交范围安全。保留任务开始前已有的工作树变更,只按明确路径暂存;不要用 `git add -A` 把无关修改、删除或未跟踪目录带入提交。", "- 调试追踪默认关闭;仅在用户明确要求时使用 `context debug enable`。追踪只写入 `.tmp/context-runtime/debug/`,属于观测数据,不能作为生命周期事实或授权依据。", "- 普通 Review 使用用户原样提供的 Payload;托管批准只能使用托管 status 返回的 revision-bound 原子命令。", @@ -603,7 +604,7 @@ function renderAgents(projectName: string, language: ProjectLanguage): string { "- Never infer source or review decisions from repository layout, filenames, URLs, examples, or prior conversations. Never scan for, clone, fetch, checkout, install, build, test, or run source-repository scripts without explicit authority. When registered repository checkouts are missing, use the Route-selected recovery plan and resolution action instead of creating source links by hand.", "- Use Context CLI for lifecycle writes. Do not inspect or repair `sources/`, `knowledge/`, `dist/`, or `.tmp/context-runtime/` with ad hoc scripts.", "- Stop Lark capture only for `evidence_status: error`. A `projection_status` of `generic` or `warning` means the original XML is preserved and the route may continue. Never patch the CLI or edit snapshots in a user workspace to add a renderer.", - "- Prefer `.tmp/agent-payloads/` for Agent-authored transient command inputs. This is a recommendation, not a CLI requirement; explicit custom paths remain valid. Avoid inventing top-level scratch directories such as `inputs/`.", + "- Prefer `.tmp/agent-payloads/` for Agent-authored transient command inputs. This is a recommendation, not a CLI requirement; explicit custom paths remain valid. Initialization creates the directory; recreate it before writing if scratch cleanup removed it. Avoid inventing top-level scratch directories such as `inputs/`.", "- Context completion proves knowledge-workflow state, not Git commit safety. Preserve worktree changes that existed before the task, stage only explicit paths, and never use `git add -A` to mix unrelated modifications, deletions, or untracked directories into the deliverable.", "- Debug tracing is off by default; use `context debug enable` only when the user explicitly requests it. Trace files stay in `.tmp/context-runtime/debug/` and are observational data, never lifecycle facts or authority.", "- Review uses the user's exact Payload in ordinary mode. Managed approval uses only the revision-bound atomic command returned by managed status.", @@ -684,6 +685,9 @@ export async function initContextProject(input: ProjectInitInput): Promise