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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

All notable changes to Context are documented here.

## 0.7.72 - 2026-10-15

- Settling a source update or a revision queue no longer leaves `knowledge/structure.yaml` stale. When citations were relocated or repository content references advanced, the structure kept its previous input hash, so `verify` reported "Structure is stale; run Close" until Close ran again. The hash is now recomputed with the rewritten references; a structure that was already stale stays stale for Close.
- Settlement can relocate citations on a machine without the local source region cache, such as a fresh clone. For repository sources it reads the cited region from the previously settled commit and uses it only when it still matches the recorded digest; otherwise the citation is kept and reported as before.
- A source update whose scopes name `processed_version` no longer leaves the workspace write lock behind when the repository checkout differs from its pinned ref. The error is returned and the lock is released.
- Kept pages whose `repo-content:` citations changed or cannot be confirmed are now listed in the source update's `reference_warnings`. Settlement still re-signs these citations to the current repository text, so the receipt is the only place they are reported.
- The package output guide no longer says the website theme follows the operating system; websites open in light mode until the reader switches.

## 0.7.71 - 2026-10-14

- `translate prepare` uses the stale translation itself as the draft and, when Git history holds the approved version it was translated from, writes a `source_diff` from that version. Without history, `source_diff` is `null`. Translations no longer store `source_parts`; existing ones are read as before. Drafts are never overwritten and are removed after a successful submission. A kept draft (`draft: kept`) has no `source_diff` and must be aligned with the whole current article, because the source may have changed again since it was written. The draft generation records, `rebase-required`, `*.unrecorded.md`, `*.previous.md` and the per-article `source_digest` submission field are removed.
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.71",
"version": "0.7.72",
"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.71
version: 0.7.72
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 @@ -113,8 +113,8 @@ top bar, homepage and articles share a centered 1440px container with 32px
margins (16px on phones); on wide screens the sidebar sits at its left edge and
articles keep a wide reading area and a smaller outline. Navigation is centered
in the top bar, which ends with icon buttons for search, theme, social links and
reader language; search, or Cmd/Ctrl+K, opens a dialog over a blurred page. The theme follows the
operating system until the reader switches it; that choice is remembered in
reader language; search, or Cmd/Ctrl+K, opens a dialog over a blurred page. The theme opens
in light mode until the reader switches it; that choice is remembered in
browser storage. Search runs locally, without a search service.
Mermaid renders in the browser using the website theme, with fit, original-size,
zoom, full-screen and source controls. Controls appear on hover or keyboard focus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ review process as revisions; a new topic is not a no-change decision.
A kept page's citations whose text only moved in the updated source are
relocated when the update settles. When cited text changed or cannot be
confirmed, the completion result lists it in `reference_warnings`; those
citations keep their previous locations. The warning is a receipt for the
citations keep their previous locations. Listed `repo-content:` citations are
still re-signed to the current repository text, so `verify` will not report
them later; check those pages from the receipt. The warning is a receipt for the
accepted decision, not a request to resubmit it: after the current update queue
finishes, run `context revise` on each listed page that needs a correction.

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.71",
"version": "0.7.72",
"type": "module",
"license": "MIT",
"engines": {
Expand Down
11 changes: 11 additions & 0 deletions packages/context-cli/src/__tests__/projectKnowledgeUpdate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ async function commitSource(root: string, content = "export const answer = 43;\n
return version;
}

test("source update with an explicit target releases the write lock when the checkout differs from the pinned ref", async () => {
const root = await initialRevisionKnowledge(roots);
const source = join(root, "fixture-source");
await writeFile(join(source, "src/index.ts"), "export const answer = 43;\n");
execFileSync("git", ["commit", "-qam", "move checkout"], { cwd: source });
const moved = execFileSync("git", ["rev-parse", "HEAD"], { cwd: source, encoding: "utf8" }).trim();
await expect(beginKnowledgeUpdate(root, { scopes: [{ ...scope, processed_version: moved }] })).rejects.toThrow();
await expect(readFile(join(root, ".tmp/context-runtime/locks/project-write.lock/owner.json"))).rejects.toThrow();
expect(await readKnowledgeUpdate(root)).toBeUndefined();
});

test("source update preserves old baseline until every revised page is built and resumes after temporary loss", async () => {
const root = await initialRevisionKnowledge(roots);
await beginKnowledgeUpdate(root, { scopes: [scope] });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,13 +236,34 @@ describe("repository content", () => {
await expect(completeKnowledgeUpdate({ projectRoot: root, revision: update.revision, decisions: [{ path: "faq/other.md" }],
scope_summary: "Wrong page", new_topics: [] })).rejects.toMatchObject({ path: ["decisions"],
message: expect.stringMatching(/Missing: faq\/guide\.md\. Not candidates .*: faq\/other\.md\./u) });
await completeKnowledgeUpdate({ projectRoot: root, revision: update.revision, decisions: [{ path: "faq/guide.md" }],
const receipt: Record<string, unknown> = await completeKnowledgeUpdate({ projectRoot: root, revision: update.revision, decisions: [{ path: "faq/guide.md" }],
scope_summary: "The clarification does not change the published claim.", new_topics: [] });
expect(receipt.reference_warnings).toEqual([{ path: "faq/guide.md", section_id: "contract", state: "changed",
source_ref: reference.source_ref, locator }]);
expect(receipt.reference_warning_instruction).toContain("re-signs");
const settled = YAML.parse(await readFile(map, "utf8"));
expect(settled.processed_scopes).toHaveLength(1);
expect(settled.articles[0].sections[0].references[0].source_ref).toBe(`repo-content:docs@${sha}+worktree`);
expect(settled.articles[0].sections[0].references[0].content_digest).not.toBe(reference.content_digest);
});
test("keeping a page after an unrelated repository edit returns no citation warning", async () => {
const root = await fixture(); const sha = await commit(root);
const locator = { path: "docs/guide.md", start_line: 2, end_line: 2 };
const reference = { source_ref: `repo-content:docs@${sha}`, locator,
content_digest: articleSourceRegionDigest(await readFile(join(root, locator.path), "utf8"), locator) };
await mkdir(join(root, "src")); await mkdir(join(root, "knowledge/faq"), { recursive: true });
await writeFile(join(root, "src/indexers.yaml"), YAML.stringify({ requirements: [{ id: "docs", purpose: "Explain the contract",
target_scope: { targets: [{ source_ref: "repo-content:docs" }] } }] }));
await writeFile(join(root, "knowledge/faq/guide.md"), "---\ntitle: Guide\n---\n\n<!-- context:section id=\"contract\" -->\nThe contract.\n<!-- /context:section -->\n");
await writeFile(join(root, "knowledge/structure.yaml"), YAML.stringify({ schema_version: "context.approved-structure.v1", articles: [{ article_id: "guide",
path: "faq/guide.md", collection: "faq", visibility: "public", sections: [{ id: "contract", references: [reference] }] }] }));
await writeFile(join(root, locator.path), "# Guide\nStable contract.\nChanged other detail.\n");
await beginKnowledgeUpdate(root, { scopes: [{ requirement_ref: "docs", source_ref: "repo-content:docs" }] });
const update = (await readKnowledgeUpdate(root))!;
const receipt: Record<string, unknown> = await completeKnowledgeUpdate({ projectRoot: root, revision: update.revision, decisions: [{ path: "faq/guide.md" }],
scope_summary: "Only an unrelated detail changed.", new_topics: [] });
expect(receipt.reference_warnings).toBeUndefined();
});
test("Skill script changes and untracked resources are candidates even with unchanged SKILL.md", async () => {
const root = await fixture({ skills: { kind: "skills", path: ".agents/skills" } });
await mkdir(join(root, ".agents/skills/check/scripts"), { recursive: true });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { buildFixturePackages as buildProjectPackages } from "./workspaceVersion
import { beginKnowledgeUpdate, readKnowledgeUpdate } from "../project/knowledgeUpdate.js";
import { readApprovedRevision } from "../project/approvedRevision.js";
import { collectProjectStatus } from "../project/status.js";
import { validateApprovedStructure } from "../project/verifyApprovedStructure.js";
import type { ProjectVerifyIssue } from "../project/verifyTypes.js";

const roots: string[] = [];
afterEach(async () => { for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true }); });
Expand Down Expand Up @@ -41,6 +43,12 @@ async function citedLines(root: string) {
.map(article => [article.path, article.sections[0]!.references[0]!]));
}

async function structureIssues(root: string) {
const issues: ProjectVerifyIssue[] = [];
await validateApprovedStructure({ projectRoot: root, issues });
return issues;
}

async function keepAll(root: string) {
await beginKnowledgeUpdate(root, { scopes: [scope] });
const update = (await readKnowledgeUpdate(root))!;
Expand All @@ -60,6 +68,7 @@ test("keeping pages relocates uniquely moved unchanged citations at settlement",
expect(reference.locator.start_line).toBe(2);
expect(reference.content_digest).toBe(before[path]!.content_digest);
}
expect(await structureIssues(root)).toEqual([]);
}, 60_000);

test("keeping pages whose cited text changed returns a receipt and retains the old citation", async () => {
Expand All @@ -84,6 +93,32 @@ test("a missing region baseline is reported, never treated as unchanged", async
expect(await citedLines(root)).toEqual(before);
}, 60_000);

test("a missing region baseline is recovered from the previously settled commit", async () => {
const root = await initialRevisionKnowledge(roots);
await keepAll(root);
const before = await citedLines(root);
await commitSource(root, `// header\n${await readFile(join(root, "fixture-source/src/index.ts"), "utf8")}`);
await rm(join(root, ".tmp/context-runtime/source-regions"), { recursive: true, force: true });
const result = await keepAll(root);
expect(result.reference_warnings).toBeUndefined();
for (const [path, reference] of Object.entries(await citedLines(root))) {
expect(reference.locator.start_line).toBe(2);
expect(reference.content_digest).toBe(before[path]!.content_digest);
}
expect(await structureIssues(root)).toEqual([]);
}, 60_000);

test("a recovered baseline still reports cited text that changed", async () => {
const root = await initialRevisionKnowledge(roots);
await keepAll(root);
const before = await citedLines(root);
await commitSource(root, "export const answer = 43;\n");
await rm(join(root, ".tmp/context-runtime/source-regions"), { recursive: true, force: true });
const result = await keepAll(root);
expect((result.reference_warnings as Array<{ state: string }>).map(item => item.state)).toEqual(["changed", "changed"]);
expect(await citedLines(root)).toEqual(before);
}, 60_000);

test("a mixed batch relocates the kept page when the revision queue settles", async () => {
const root = await initialRevisionKnowledge(roots);
await commitSource(root, `// header\n${await readFile(join(root, "fixture-source/src/index.ts"), "utf8")}`);
Expand All @@ -103,4 +138,5 @@ test("a mixed batch relocates the kept page when the revision queue settles", as
const after = await citedLines(root);
expect(after[kept!.path]!.locator.start_line).toBe(2);
expect(after[revised!.path]!.locator.start_line).toBe(2);
expect(await structureIssues(root)).toEqual([]);
}, 60_000);
23 changes: 23 additions & 0 deletions packages/context-cli/src/project/articleSourceReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@ function inside(root: string, path: string): void {
}
}

/** Reads a registered repository file at an earlier commit; callers must verify
* the result against a recorded digest before trusting it. */
export async function registeredRepoHistoryReader(projectRoot: string) {
const workspace = await realpath(projectRoot);
const registry = await loadSourcesRegistry({ rootDir: projectRoot });
const roots = new Map<string, Promise<string>>();
return async (sourceRef: string, path: string, commit: string): Promise<string> => {
if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(commit)) throw new TypeError("History reads require a fixed commit");
let root = roots.get(sourceRef);
if (!root) {
const name = sourceRef.startsWith("repo:") ? sourceRef.slice("repo:".length) : undefined;
const entry = registry.repos.find(item => item.id === name || item.name === name);
if (!entry) throw new TypeError(`Source is not a registered repository: ${sourceRef}`);
root = realpath(resolve(workspace, entry.materializedAt)).then(actual => { inside(workspace, actual); return actual; });
roots.set(sourceRef, root);
}
const blob = await execute("git", ["-C", await root, "show", `${commit}:./${path}`], {
encoding: "buffer", timeout: 5000, maxBuffer: 16 * 1024 * 1024,
});
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(blob.stdout);
};
}

/** One submission/check, one read per cited file. Registration identifies the
* source; a document manifest identifies its files even in a shared date folder.
* This never prepares parsers or loads uncited document bodies. */
Expand Down
10 changes: 10 additions & 0 deletions packages/context-cli/src/project/close.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ export async function approvedKnowledgeInputHash(projectRoot: string): Promise<s
return inputHash;
}

/** Input hash of the current approved Markdown with the given article metadata, as `verify` computes it. */
export async function approvedStructureInputHashFor(projectRoot: string, articles: readonly Record<string, unknown>[]): Promise<string> {
const files = await approvedKnowledgeFiles(projectRoot);
return approvedStructureInputHash({
schemaVersion: STRUCTURE_SCHEMA_VERSION,
files: approvedStructureInputFiles(files.map(file => ({ ...file, content: compactApprovedKnowledgeMarkdown(file.content) }))),
metadata: articles,
});
}

function approvedStructureInputFiles(files: readonly ApprovedKnowledgeFile[]): ApprovedStructureInputFile[] {
return files.map((file) => ({
path: file.relPath,
Expand Down
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.71";
export const BUNDLED_INDEXER_PARSER_PACKAGE_VERSION = "0.7.72";
const BUNDLED_PARSER_REQUIREMENTS = buildIndexerParserCapabilityRequirements(
BUNDLED_INDEXER_PARSER_PACKAGE_VERSION,
);
Expand Down
Loading
Loading