diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ea70a3d..0116ec32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to Context are documented here. +## 0.7.88 - 2026-10-28 + +- `context update --pending` processes registered repositories whose commit moved past the version approved knowledge was processed at, such as after `context source add repo --ref `. Only pages whose cited text changed become update candidates, listed with their sections in `changed_references`; the update still asks for one scope decision covering additions and shared dependencies, and the new commit settles, with moved citations updating their locations, only after that decision. The workspace Route runs it before the idle task gate, `source add repo` returns a `next` hint for it, and `verify` warnings point to it when a cited repository moved. +- Reading earlier settled commits works in shallow and partial checkouts: each commit is checked once and fetched once when missing, concurrent references share one reader and read one file at a time per checkout, and a commit the remote no longer has fails every read at once instead of retrying per file. + ## 0.7.87 - 2026-10-26 - Each website article now ends with its sources as a map. The article's sections sit in the middle and the documents they cite sit around them in flat blobs, grouped by where each document comes from: a repository directory, Lark documents or notes. Sections are purple; repository files are green, Skills magenta, Lark documents blue and notes orange. Hovering a section or document highlights what it is linked to and shows the cited line ranges; clicking a document opens the cited lines and clicking a section scrolls to it. A List view shows the same documents by group, and narrow screens show only the list. Other articles citing the same documents are linked below. The map is computed during `context build` from the recorded references, registered sources and committed snapshots, so it needs no agent step. Set `site.sourceMap: false` to keep the plain source list. diff --git a/bun.lock b/bun.lock index fcbbad85..2385b27b 100644 --- a/bun.lock +++ b/bun.lock @@ -18,7 +18,7 @@ }, "packages/context": { "name": "@c4a/context", - "version": "0.7.87", + "version": "0.7.88", "dependencies": { "yaml": "^2.5.1", "zod": "^3.23.8", @@ -26,7 +26,7 @@ }, "packages/context-cli": { "name": "@c4a/context-cli", - "version": "0.7.87", + "version": "0.7.88", "bin": { "context": "dist/cli.js", }, @@ -76,7 +76,7 @@ }, "packages/core": { "name": "@c4a/core", - "version": "0.7.87", + "version": "0.7.88", "dependencies": { "picomatch": "^4.0.4", "yaml": "^2.4.5", @@ -88,7 +88,7 @@ }, "packages/dev-cli": { "name": "@c4a/dev-cli", - "version": "0.7.87", + "version": "0.7.88", "dependencies": { "@c4a/context": "workspace:*", "@c4a/core": "workspace:*", @@ -100,7 +100,7 @@ }, "packages/extract": { "name": "@c4a/extract", - "version": "0.7.87", + "version": "0.7.88", "bin": { "c4a-extract-code": "./dist/bin/c4a-extract-code.js", }, @@ -115,7 +115,7 @@ }, "packages/extract-contract": { "name": "@c4a/extract-contract", - "version": "0.7.87", + "version": "0.7.88", "dependencies": { "@c4a/core": "workspace:*", "graphql": "^16.14.2", @@ -125,7 +125,7 @@ }, "packages/extract-go": { "name": "@c4a/extract-go", - "version": "0.7.87", + "version": "0.7.88", "dependencies": { "@c4a/core": "workspace:*", "@c4a/extract": "workspace:*", @@ -135,7 +135,7 @@ }, "packages/extract-mdx": { "name": "@c4a/extract-mdx", - "version": "0.7.87", + "version": "0.7.88", "dependencies": { "@c4a/core": "workspace:*", "remark-mdx": "^3.1.1", @@ -147,7 +147,7 @@ }, "packages/extract-proto": { "name": "@c4a/extract-proto", - "version": "0.7.87", + "version": "0.7.88", "dependencies": { "@c4a/core": "workspace:*", "zod": "^3.23.8", @@ -155,7 +155,7 @@ }, "packages/extract-rush": { "name": "@c4a/extract-rush", - "version": "0.7.87", + "version": "0.7.88", "dependencies": { "@c4a/core": "workspace:*", "typescript": "^5.5.4", @@ -165,7 +165,7 @@ }, "packages/extract-sql": { "name": "@c4a/extract-sql", - "version": "0.7.87", + "version": "0.7.88", "dependencies": { "@c4a/core": "workspace:*", "node-sql-parser": "^5.4.0", @@ -174,7 +174,7 @@ }, "packages/extract-style": { "name": "@c4a/extract-style", - "version": "0.7.87", + "version": "0.7.88", "dependencies": { "@c4a/core": "workspace:*", "postcss": "^8.5.26", @@ -186,7 +186,7 @@ }, "packages/extract-thrift": { "name": "@c4a/extract-thrift", - "version": "0.7.87", + "version": "0.7.88", "dependencies": { "@c4a/core": "workspace:*", "zod": "^3.23.8", @@ -194,7 +194,7 @@ }, "packages/extract-ts": { "name": "@c4a/extract-ts", - "version": "0.7.87", + "version": "0.7.88", "dependencies": { "@c4a/core": "workspace:*", "@c4a/extract": "workspace:*", @@ -205,7 +205,7 @@ }, "packages/tui": { "name": "@c4a/tui", - "version": "0.7.87", + "version": "0.7.88", "dependencies": { "ink": "^5.0.0", "react": "^18.3.1", diff --git a/package.json b/package.json index 27739f77..1c36b093 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "context", - "version": "0.7.87", + "version": "0.7.88", "packageManager": "bun@1.3.9", "repository": { "type": "git", diff --git a/packages/context-cli/README.md b/packages/context-cli/README.md index 050c3617..df5e61df 100644 --- a/packages/context-cli/README.md +++ b/packages/context-cli/README.md @@ -331,6 +331,9 @@ they do not resume an earlier managed task before the new request is understood. - `context update --input ` accepts confirmed source/module scopes and fixed, already acquired versions. The current Route asks which existing pages need changes and whether additions need new pages. +- `context update --pending` processes registered repositories whose commit moved + past the processed version. Only pages whose cited text changed become + candidates; the scope decision still covers additions before the commit settles. - `context revise --instruction ` starts from the current approved body. `--move-to ` preserves its identity and updates Markdown navigation when the reviewed move is applied. diff --git a/packages/context-cli/context-workflow/actions/process-source-update.yaml b/packages/context-cli/context-workflow/actions/process-source-update.yaml new file mode 100644 index 00000000..c26b7a96 --- /dev/null +++ b/packages/context-cli/context-workflow/actions/process-source-update.yaml @@ -0,0 +1,5 @@ +schema: agent-graph.action.v1 +id: process-source-update +runner: command +effect: write +command: context update --pending --format json diff --git a/packages/context-cli/context-workflow/codes.yaml b/packages/context-cli/context-workflow/codes.yaml index 2e3a55d9..88520a27 100644 --- a/packages/context-cli/context-workflow/codes.yaml +++ b/packages/context-cli/context-workflow/codes.yaml @@ -97,4 +97,5 @@ codes: - { code: route.indexer.candidate-review, kind: route-reason, summary: Review the exact current candidate set only after current mechanical validation passes. } - { code: route.indexer.approved-knowledge-close, kind: route-reason, summary: Atomically close approved Indexer structure after required material gaps are resolved. } - { code: route.indexer.source-update, kind: route-reason, summary: Inspect fixed source changes against current approved pages. } + - { code: route.source.update-pending, kind: route-reason, summary: A registered repository moved past the commit its approved knowledge was processed at; process the new commit before other work., document: resources/procedures/knowledge-updates.md } - { code: route.workspace.task-cleared, kind: route-reason, summary: No active production task; existing knowledge is retained and new production awaits explicit user intent., document: resources/procedures/workspace-prepare.md } diff --git a/packages/context-cli/context-workflow/graphs/workspace.yaml b/packages/context-cli/context-workflow/graphs/workspace.yaml index ce373781..5fdc4475 100644 --- a/packages/context-cli/context-workflow/graphs/workspace.yaml +++ b/packages/context-cli/context-workflow/graphs/workspace.yaml @@ -147,6 +147,17 @@ nodes: - resources/procedures/source-capture-detailed.md - resources/manuals/guides/lark-resources.md + - id: process-source-update + kind: action + reasonCode: route.source.update-pending + action: actions/process-source-update.yaml + satisfiedBy: + - path: sources.update_pending + equals: false + resources: + recommended: + - resources/procedures/knowledge-updates.md + - id: reopen-cleared-task kind: gate reasonCode: route.workspace.task-cleared @@ -355,7 +366,8 @@ edges: - { from: choose-source-boundary, to: configure-document-capture, kind: gatedBy } - { from: configure-document-capture, to: authorize-document-capture } - { from: authorize-document-capture, to: capture-next, kind: gatedBy } - - { from: capture-next, to: reopen-cleared-task } + - { from: capture-next, to: process-source-update } + - { from: process-source-update, to: reopen-cleared-task } - { from: reopen-cleared-task, to: run-indexer-lifecycle, kind: gatedBy } - { from: run-indexer-lifecycle, to: review-current-batch } - { from: review-current-batch, to: close-approved-knowledge, kind: gatedBy } diff --git a/packages/context-cli/context-workflow/provider.yaml b/packages/context-cli/context-workflow/provider.yaml index 5ce67d04..f83ca494 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.87 +version: 0.7.88 name: Context workflow description: Internal work contract for Context knowledge workspaces. graphs: 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 e036f497..ece929c0 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 @@ -425,6 +425,13 @@ also checking additions that have no old page reference. The scope is complete only after every required change is reviewed, closed and built, or after an explicit conclusion that it needs no knowledge changes. +Moving a repository with `context source add repo --ref ` only changes its +registration. When approved knowledge was processed at the previous commit, run +`context update --pending --format json`. Only pages whose cited text changed +become candidates, listed in `changed_references`; still inspect the change for +additions and shared dependencies before deciding. Moved citations update their +locations when the update settles. + Before importing a `note`, read [note source preparation](note.md). Before importing `sessions`, read [development summary preparation](sessions.md). Choose the reference for the actual input; do not read both by default. 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 9bfe8bdf..311071d0 100644 --- a/packages/context-cli/context-workflow/resources/procedures/knowledge-updates.md +++ b/packages/context-cli/context-workflow/resources/procedures/knowledge-updates.md @@ -425,6 +425,13 @@ also checking additions that have no old page reference. The scope is complete only after every required change is reviewed, closed and built, or after an explicit conclusion that it needs no knowledge changes. +Moving a repository with `context source add repo --ref ` only changes its +registration. When approved knowledge was processed at the previous commit, run +`context update --pending --format json`. Only pages whose cited text changed +become candidates, listed in `changed_references`; still inspect the change for +additions and shared dependencies before deciding. Moved citations update their +locations when the update settles. + Before importing a `note`, read [note source preparation](note.md). Before importing `sessions`, read [development summary preparation](sessions.md). Choose the reference for the actual input; do not read both by default. diff --git a/packages/context-cli/context-workflow/resources/procedures/source-update.md b/packages/context-cli/context-workflow/resources/procedures/source-update.md index 769eda4b..a7403b4e 100644 --- a/packages/context-cli/context-workflow/resources/procedures/source-update.md +++ b/packages/context-cli/context-workflow/resources/procedures/source-update.md @@ -17,6 +17,11 @@ of them. Read their current approved Markdown. Submit one decision per path: omit `instruction` if no correction is needed, or describe the concrete change to make. Preserve unrelated content and the reader's existing improvements. Include a short `scope_summary` explaining the scope-level conclusion. +When the action lists `changed_references`, start from those sections: the +cited text changed (`changed`) or could not be read (`unavailable`, such as a +renamed or deleted file). For `context update --pending`, pages whose citations +are unchanged or only moved are not candidates, so an empty list is possible; +still check the change for additions before submitting the scope decision. Also inspect additions and shared dependencies in the source change. Existing page references cannot discover every new topic. List necessary new pages in `new_topics`, each with a readable `path` in an existing diff --git a/packages/context-cli/context-workflow/tests/capture-configuration-missing.yaml b/packages/context-cli/context-workflow/tests/capture-configuration-missing.yaml index 74b7e10c..60360850 100644 --- a/packages/context-cli/context-workflow/tests/capture-configuration-missing.yaml +++ b/packages/context-cli/context-workflow/tests/capture-configuration-missing.yaml @@ -22,7 +22,7 @@ state: compile_complete: true knowledge_review_resolved: true review_gate_clear: true - sources: { registered: true, repositories_ready: true } + sources: { registered: true, repositories_ready: true, update_pending: false } capture: { declarations_complete: false, batch_digest: null, complete: false, route_satisfied: false } expect: statusCode: actionable diff --git a/packages/context-cli/context-workflow/tests/capture-next.yaml b/packages/context-cli/context-workflow/tests/capture-next.yaml index e6dc959a..aca3839d 100644 --- a/packages/context-cli/context-workflow/tests/capture-next.yaml +++ b/packages/context-cli/context-workflow/tests/capture-next.yaml @@ -18,7 +18,7 @@ state: compile_complete: true knowledge_review_resolved: true review_gate_clear: true - sources: { registered: true, repositories_ready: true } + sources: { registered: true, repositories_ready: true, update_pending: false } capture: { declarations_complete: true, batch_digest: null, complete: false, route_satisfied: false } expect: statusCode: actionable diff --git a/packages/context-cli/context-workflow/tests/close-before-package.yaml b/packages/context-cli/context-workflow/tests/close-before-package.yaml index 942e015a..c1aa10ec 100644 --- a/packages/context-cli/context-workflow/tests/close-before-package.yaml +++ b/packages/context-cli/context-workflow/tests/close-before-package.yaml @@ -22,7 +22,7 @@ state: compile_complete: true knowledge_review_resolved: true review_gate_clear: true - sources: { registered: true, repositories_ready: true } + sources: { registered: true, repositories_ready: true, update_pending: false } capture: { declarations_complete: true, batch_digest: null, complete: true, route_satisfied: true } extract: { migration_required: false, declarations_complete: true, plans_complete: true, capability_clear: true, preview_current: true, ownership_clear: true, scale_clear: true, batch_digest: null, complete: true, audit_input_required: false, audit_guidance_required: false, audit_revision_required: false, audit_resolved: true } documents: { classified: true } diff --git a/packages/context-cli/context-workflow/tests/complete.yaml b/packages/context-cli/context-workflow/tests/complete.yaml index 2bc9eb80..f651ff1b 100644 --- a/packages/context-cli/context-workflow/tests/complete.yaml +++ b/packages/context-cli/context-workflow/tests/complete.yaml @@ -23,7 +23,7 @@ state: structure_confirmation_resolved: true knowledge_review_resolved: true package_output_resolved: true - sources: { registered: true, repositories_ready: true } + sources: { registered: true, repositories_ready: true, update_pending: false } capture: { declarations_complete: true, batch_digest: null, complete: true, route_satisfied: true } extract: { migration_required: false, declarations_complete: true, plans_complete: true, capability_clear: true, preview_current: true, ownership_clear: true, scale_clear: true, batch_digest: null, complete: true, audit_input_required: false, audit_guidance_required: false, audit_revision_required: false, audit_resolved: true } documents: { classified: true } diff --git a/packages/context-cli/context-workflow/tests/configure-package-output.yaml b/packages/context-cli/context-workflow/tests/configure-package-output.yaml index af0664ac..bdd1f8be 100644 --- a/packages/context-cli/context-workflow/tests/configure-package-output.yaml +++ b/packages/context-cli/context-workflow/tests/configure-package-output.yaml @@ -22,7 +22,7 @@ state: compile_complete: true knowledge_review_resolved: true review_gate_clear: true - sources: { registered: true, repositories_ready: true } + sources: { registered: true, repositories_ready: true, update_pending: false } capture: { declarations_complete: true, batch_digest: null, complete: true, route_satisfied: true } extract: { migration_required: false, declarations_complete: true, plans_complete: true, capability_clear: true, preview_current: true, ownership_clear: true, scale_clear: true, batch_digest: null, complete: true, audit_input_required: false, audit_guidance_required: false, audit_revision_required: false, audit_resolved: true } documents: { classified: true } diff --git a/packages/context-cli/context-workflow/tests/document-acquisition-deferred.yaml b/packages/context-cli/context-workflow/tests/document-acquisition-deferred.yaml index e594480c..0e228683 100644 --- a/packages/context-cli/context-workflow/tests/document-acquisition-deferred.yaml +++ b/packages/context-cli/context-workflow/tests/document-acquisition-deferred.yaml @@ -23,7 +23,7 @@ state: structure_confirmation_resolved: true knowledge_review_resolved: true package_output_resolved: true - sources: { registered: true, repositories_ready: true } + sources: { registered: true, repositories_ready: true, update_pending: false } capture: { declarations_complete: true, batch_digest: null, complete: false, route_satisfied: true } extract: { migration_required: false, declarations_complete: true, plans_complete: true, capability_clear: true, preview_current: true, ownership_clear: true, scale_clear: true, batch_digest: null, complete: true, audit_input_required: false, audit_guidance_required: false, audit_revision_required: false, audit_resolved: true } documents: { classified: true } diff --git a/packages/context-cli/context-workflow/tests/managed-package-template-review.yaml b/packages/context-cli/context-workflow/tests/managed-package-template-review.yaml index ee5247e9..2562bbaa 100644 --- a/packages/context-cli/context-workflow/tests/managed-package-template-review.yaml +++ b/packages/context-cli/context-workflow/tests/managed-package-template-review.yaml @@ -23,7 +23,7 @@ state: compile_complete: true knowledge_review_resolved: true review_gate_clear: true - sources: { registered: true, repositories_ready: true } + sources: { registered: true, repositories_ready: true, update_pending: false } capture: { declarations_complete: true, batch_digest: null, complete: true, route_satisfied: true } extract: { migration_required: false, declarations_complete: true, plans_complete: true, capability_clear: true, preview_current: true, ownership_clear: true, scale_clear: true, batch_digest: null, complete: true, audit_input_required: false, audit_guidance_required: false, audit_revision_required: false, audit_resolved: true } documents: { classified: true } diff --git a/packages/context-cli/context-workflow/tests/managed-review.yaml b/packages/context-cli/context-workflow/tests/managed-review.yaml index 2fcf99aa..75b24a34 100644 --- a/packages/context-cli/context-workflow/tests/managed-review.yaml +++ b/packages/context-cli/context-workflow/tests/managed-review.yaml @@ -23,7 +23,7 @@ state: compile_complete: true knowledge_review_resolved: false review_gate_clear: true - sources: { registered: true, repositories_ready: true } + sources: { registered: true, repositories_ready: true, update_pending: false } capture: { declarations_complete: true, batch_digest: null, complete: true, route_satisfied: true } extract: { migration_required: false, declarations_complete: true, plans_complete: true, capability_clear: true, preview_current: true, ownership_clear: true, scale_clear: true, batch_digest: null, complete: true, audit_input_required: false, audit_guidance_required: false, audit_revision_required: false, audit_resolved: true } documents: { classified: true } diff --git a/packages/context-cli/context-workflow/tests/package-output-gate.yaml b/packages/context-cli/context-workflow/tests/package-output-gate.yaml index 71a14897..654d65f8 100644 --- a/packages/context-cli/context-workflow/tests/package-output-gate.yaml +++ b/packages/context-cli/context-workflow/tests/package-output-gate.yaml @@ -22,7 +22,7 @@ state: compile_complete: true knowledge_review_resolved: true review_gate_clear: true - sources: { registered: true, repositories_ready: true } + sources: { registered: true, repositories_ready: true, update_pending: false } capture: { declarations_complete: true, batch_digest: null, complete: true, route_satisfied: true } extract: { migration_required: false, declarations_complete: true, plans_complete: true, capability_clear: true, preview_current: true, ownership_clear: true, scale_clear: true, batch_digest: null, complete: true, audit_input_required: false, audit_guidance_required: false, audit_revision_required: false, audit_resolved: true } documents: { classified: true } diff --git a/packages/context-cli/context-workflow/tests/package-stale.yaml b/packages/context-cli/context-workflow/tests/package-stale.yaml index e94a1735..26c7bf31 100644 --- a/packages/context-cli/context-workflow/tests/package-stale.yaml +++ b/packages/context-cli/context-workflow/tests/package-stale.yaml @@ -23,7 +23,7 @@ state: compile_complete: true knowledge_review_resolved: true review_gate_clear: true - sources: { registered: true, repositories_ready: true } + sources: { registered: true, repositories_ready: true, update_pending: false } capture: { declarations_complete: true, batch_digest: null, complete: true, route_satisfied: true } extract: { migration_required: false, declarations_complete: true, plans_complete: true, capability_clear: true, preview_current: true, ownership_clear: true, scale_clear: true, batch_digest: null, complete: true, audit_input_required: false, audit_guidance_required: false, audit_revision_required: false, audit_resolved: true } documents: { classified: true } diff --git a/packages/context-cli/context-workflow/tests/package-template-review.yaml b/packages/context-cli/context-workflow/tests/package-template-review.yaml index 86cadfb9..62fd6239 100644 --- a/packages/context-cli/context-workflow/tests/package-template-review.yaml +++ b/packages/context-cli/context-workflow/tests/package-template-review.yaml @@ -22,7 +22,7 @@ state: compile_complete: true knowledge_review_resolved: true review_gate_clear: true - sources: { registered: true, repositories_ready: true } + sources: { registered: true, repositories_ready: true, update_pending: false } capture: { declarations_complete: true, batch_digest: null, complete: true, route_satisfied: true } extract: { migration_required: false, declarations_complete: true, plans_complete: true, capability_clear: true, preview_current: true, ownership_clear: true, scale_clear: true, batch_digest: null, complete: true, audit_input_required: false, audit_guidance_required: false, audit_revision_required: false, audit_resolved: true } documents: { classified: true } diff --git a/packages/context-cli/context-workflow/tests/repository-source-not-ready.yaml b/packages/context-cli/context-workflow/tests/repository-source-not-ready.yaml index d528c6f2..139ec63d 100644 --- a/packages/context-cli/context-workflow/tests/repository-source-not-ready.yaml +++ b/packages/context-cli/context-workflow/tests/repository-source-not-ready.yaml @@ -22,7 +22,7 @@ state: compile_complete: true knowledge_review_resolved: true review_gate_clear: true - sources: { registered: true, repositories_ready: false } + sources: { registered: true, repositories_ready: false, update_pending: false } capture: { declarations_complete: true, complete: false, route_satisfied: false } expect: statusCode: actionable diff --git a/packages/context-cli/context-workflow/tests/review-after-compile.yaml b/packages/context-cli/context-workflow/tests/review-after-compile.yaml index 5fe10f9a..804f4200 100644 --- a/packages/context-cli/context-workflow/tests/review-after-compile.yaml +++ b/packages/context-cli/context-workflow/tests/review-after-compile.yaml @@ -22,7 +22,7 @@ state: compile_complete: true knowledge_review_resolved: true review_gate_clear: true - sources: { registered: true, repositories_ready: true } + sources: { registered: true, repositories_ready: true, update_pending: false } capture: { declarations_complete: true, batch_digest: null, complete: true, route_satisfied: true } extract: { migration_required: false, declarations_complete: true, plans_complete: true, capability_clear: true, preview_current: true, ownership_clear: true, scale_clear: true, batch_digest: null, complete: true, audit_input_required: false, audit_guidance_required: false, audit_revision_required: false, audit_resolved: true } documents: { classified: true } diff --git a/packages/context-cli/context-workflow/tests/review-before-stale-close.yaml b/packages/context-cli/context-workflow/tests/review-before-stale-close.yaml index c95ca4bb..ff896b19 100644 --- a/packages/context-cli/context-workflow/tests/review-before-stale-close.yaml +++ b/packages/context-cli/context-workflow/tests/review-before-stale-close.yaml @@ -22,7 +22,7 @@ state: compile_complete: true knowledge_review_resolved: true review_gate_clear: true - sources: { registered: true, repositories_ready: true } + sources: { registered: true, repositories_ready: true, update_pending: false } capture: { declarations_complete: true, batch_digest: null, complete: true, route_satisfied: true } extract: { migration_required: false, declarations_complete: true, plans_complete: true, capability_clear: true, preview_current: true, ownership_clear: true, scale_clear: true, batch_digest: null, complete: true, audit_input_required: false, audit_guidance_required: false, audit_revision_required: false, audit_resolved: true } documents: { classified: true } diff --git a/packages/context-cli/context-workflow/tests/source-read-gate.yaml b/packages/context-cli/context-workflow/tests/source-read-gate.yaml index 59b31544..31d03fbd 100644 --- a/packages/context-cli/context-workflow/tests/source-read-gate.yaml +++ b/packages/context-cli/context-workflow/tests/source-read-gate.yaml @@ -23,7 +23,7 @@ state: compile_complete: true knowledge_review_resolved: true review_gate_clear: true - sources: { registered: true, repositories_ready: true } + sources: { registered: true, repositories_ready: true, update_pending: false } capture: { declarations_complete: true, batch_digest: null, complete: false, route_satisfied: false } expect: statusCode: waiting-user diff --git a/packages/context-cli/context-workflow/tests/source-update-pending.yaml b/packages/context-cli/context-workflow/tests/source-update-pending.yaml new file mode 100644 index 00000000..1c2c9806 --- /dev/null +++ b/packages/context-cli/context-workflow/tests/source-update-pending.yaml @@ -0,0 +1,40 @@ +schema: agent-graph.test.v1 +name: a moved repository commit is processed before the cleared task waits +graph: workspace +entry: context +state: + facts: + translation: { route_satisfied: true } + workspace: { task_start_authorized: false, project_entry_valid: true, state_valid: true } + verification: { blocking_clear: true } + indexer: { lifecycle_current: true, registry_state: current } + resume: + prose_declarations_complete: true + structure_confirmation_resolved: true + structure_confirmed: true + compile_complete: true + knowledge_review_resolved: true + review_gate_clear: true + gates: + source_read_resolved: true + extraction_scope_resolved: true + document_classification_resolved: true + structure_confirmation_resolved: true + knowledge_review_resolved: true + package_output_resolved: true + sources: { registered: true, repositories_ready: true, update_pending: true } + capture: { declarations_complete: true, batch_digest: null, complete: true, route_satisfied: true } + extract: { migration_required: false, declarations_complete: true, plans_complete: true, capability_clear: true, preview_current: true, ownership_clear: true, scale_clear: true, batch_digest: null, complete: true, audit_input_required: false, audit_guidance_required: false, audit_revision_required: false, audit_resolved: true } + documents: { classified: true } + prose: { declarations_complete: true } + align: { prepared: true } + structure: { confirmed: true } + compile: { complete: true } + review: { gate_clear: true, batch_resolved: true } + close: { current: true } + packages: { declared: true, templates_reviewed: true, current: true } +expect: + statusCode: actionable + primaryNode: process-source-update + primaryReasonCode: route.source.update-pending + command: context update --pending --format json diff --git a/packages/context-cli/context-workflow/tests/task-cleared.yaml b/packages/context-cli/context-workflow/tests/task-cleared.yaml index f517efce..366b5c91 100644 --- a/packages/context-cli/context-workflow/tests/task-cleared.yaml +++ b/packages/context-cli/context-workflow/tests/task-cleared.yaml @@ -22,7 +22,7 @@ state: structure_confirmation_resolved: true knowledge_review_resolved: true package_output_resolved: true - sources: { registered: true, repositories_ready: true } + sources: { registered: true, repositories_ready: true, update_pending: false } capture: { declarations_complete: true, batch_digest: null, complete: true, route_satisfied: true } extract: { migration_required: false, declarations_complete: true, plans_complete: true, capability_clear: true, preview_current: true, ownership_clear: true, scale_clear: true, batch_digest: null, complete: true, audit_input_required: false, audit_guidance_required: false, audit_revision_required: false, audit_resolved: true } documents: { classified: true } diff --git a/packages/context-cli/context-workflow/tests/translation-pending.yaml b/packages/context-cli/context-workflow/tests/translation-pending.yaml index c0c47f3e..807386bd 100644 --- a/packages/context-cli/context-workflow/tests/translation-pending.yaml +++ b/packages/context-cli/context-workflow/tests/translation-pending.yaml @@ -13,7 +13,7 @@ state: source_read_resolved: true knowledge_review_resolved: true package_output_resolved: true - sources: { registered: true, repositories_ready: true } + sources: { registered: true, repositories_ready: true, update_pending: false } capture: { declarations_complete: true, batch_digest: null, complete: true, route_satisfied: true } review: { gate_clear: true, batch_resolved: true } close: { current: true } diff --git a/packages/context-cli/context-workflow/tests/version-required.yaml b/packages/context-cli/context-workflow/tests/version-required.yaml index bbd92a45..b03deb27 100644 --- a/packages/context-cli/context-workflow/tests/version-required.yaml +++ b/packages/context-cli/context-workflow/tests/version-required.yaml @@ -13,7 +13,7 @@ state: source_read_resolved: true knowledge_review_resolved: true package_output_resolved: true - sources: { registered: true, repositories_ready: true } + sources: { registered: true, repositories_ready: true, update_pending: false } capture: { declarations_complete: true, batch_digest: null, complete: true, route_satisfied: true } review: { gate_clear: true, batch_resolved: true } close: { current: true } diff --git a/packages/context-cli/package.json b/packages/context-cli/package.json index 4cf2dffc..704679e4 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.87", + "version": "0.7.88", "type": "module", "license": "MIT", "engines": { diff --git a/packages/context-cli/src/__tests__/projectPendingSourceUpdate.test.ts b/packages/context-cli/src/__tests__/projectPendingSourceUpdate.test.ts new file mode 100644 index 00000000..f7cc9e77 --- /dev/null +++ b/packages/context-cli/src/__tests__/projectPendingSourceUpdate.test.ts @@ -0,0 +1,120 @@ +import { initialRevisionKnowledge } from "./initialRevisionKnowledge.fixture.js"; +import { afterEach, expect, test } from "bun:test"; +import { execFileSync } from "node:child_process"; +import { readFile, writeFile, rm } from "node:fs/promises"; +import { join } from "node:path"; +import YAML from "yaml"; +import { readProcessedScopes } from "@c4a/context"; +import { DOCUMENT_REVISION_SOURCE_REF } from "./projectDocumentRevisionV074.fixture.js"; +import { completeCurrentIndexerAction } from "./knowledgeMapReview.fixture.js"; +import { beginKnowledgeUpdate, readKnowledgeUpdate } from "../project/knowledgeUpdate.js"; +import { pendingSourceUpdateScopes } from "../project/pendingSourceUpdates.js"; +import { registeredRepoHistoryReader } from "../project/articleSourceReader.js"; +import { collectProjectStatus } from "../project/status.js"; + +const roots: string[] = []; +afterEach(async () => { for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true }); }); +const scope = { requirement_ref: "workspace-knowledge", source_ref: DOCUMENT_REVISION_SOURCE_REF, module_refs: ["module:app"] }; +const baseline = async (root: string) => readProcessedScopes(YAML.parse(await readFile(join(root, "knowledge/structure.yaml"), "utf8"))); + +async function settledKnowledge() { + const root = await initialRevisionKnowledge(roots); + await beginKnowledgeUpdate(root, { scopes: [scope] }); + const update = (await readKnowledgeUpdate(root))!; + const status = await collectProjectStatus(root, { managed: true }); + await completeCurrentIndexerAction({ cwd: root, revision: status.workflow.current!.revision, managed: true, + value: { stage: "source-update", decisions: update.candidates.map(({ path }) => ({ path })), + scope_summary: "Both published constants match the current source.", new_topics: [] } }); + return root; +} + +async function moveRegistration(root: string, file: string, content: string) { + const source = join(root, "fixture-source"); + await writeFile(join(source, file), content); + execFileSync("git", ["add", file], { cwd: source }); + execFileSync("git", ["commit", "-qm", `change ${file}`], { cwd: source }); + const version = execFileSync("git", ["rev-parse", "HEAD"], { cwd: source, encoding: "utf8" }).trim(); + const registry = join(root, "sources/repo/index.yaml"); + const parsed = YAML.parse(await readFile(registry, "utf8")); + parsed.sources[0].modules[0].git.ref = version; + await writeFile(registry, YAML.stringify(parsed)); + return version; +} + +async function decide(root: string, decisions: Array<{ path: string }>) { + const status = await collectProjectStatus(root, { managed: true }); + return completeCurrentIndexerAction({ cwd: root, revision: status.workflow.current!.revision, managed: true, + value: { stage: "source-update", decisions, scope_summary: "The new export needs no page.", new_topics: [] } }); +} + +async function moveCitedTextAndAddExport(root: string) { + const original = await readFile(join(root, "fixture-source/src/index.ts"), "utf8"); + return moveRegistration(root, "src/index.ts", `// moved header\n${original}\nexport const newPublicFeature = true;\n`); +} + +test("a pending update with only moved citations still requires the scope decision before settling", async () => { + const root = await settledKnowledge(); + expect(await beginKnowledgeUpdate(root, undefined, { pending: true })).toMatchObject({ outcome: "current" }); + const before = await baseline(root); + const version = await moveCitedTextAndAddExport(root); + expect(await pendingSourceUpdateScopes(root)).toEqual([scope]); + expect((await collectProjectStatus(root)).workflow.current?.node).toBe("process-source-update"); + expect(await beginKnowledgeUpdate(root, undefined, { pending: true })).toMatchObject({ outcome: "update-prepared" }); + const update = (await readKnowledgeUpdate(root))!; + expect(update.candidates).toEqual([]); + expect(update.changed_references).toBeUndefined(); + expect(await baseline(root)).toEqual(before); + expect(await pendingSourceUpdateScopes(root)).toEqual([scope]); + await decide(root, []); + expect((await baseline(root)).map(entry => entry.processed_version)).toEqual([version]); + const structure = YAML.parse(await readFile(join(root, "knowledge/structure.yaml"), "utf8")); + expect(structure.articles.map((article: { sections: Array<{ references: Array<{ locator: { start_line: number } }> }> }) => + article.sections[0]!.references[0]!.locator.start_line)).toEqual([2, 2]); + expect(await pendingSourceUpdateScopes(root)).toEqual([]); +}); + +test("concurrent history reads in a shallow checkout share one fetch and confirm every moved citation", async () => { + const root = await settledKnowledge(); + const source = join(root, "fixture-source"); + await moveCitedTextAndAddExport(root); + const checkout = join(root, "sources/repo/20260903/revision-fixture"); + await rm(checkout, { recursive: true, force: true }); + execFileSync("git", ["clone", "-q", "--depth=1", `file://${source}`, checkout]); + await rm(join(root, ".tmp/context-runtime/source-regions"), { recursive: true, force: true }); + const trace = join(root, ".tmp/git-trace.log"); + process.env.GIT_TRACE = trace; + try { await beginKnowledgeUpdate(root, undefined, { pending: true }); } + finally { delete process.env.GIT_TRACE; } + const update = (await readKnowledgeUpdate(root))!; + expect(update.changed_references).toBeUndefined(); + expect(update.candidates).toEqual([]); + const fetches = (await readFile(trace, "utf8")).split("\n").filter(line => line.includes("built-in: git fetch --quiet")); + expect(fetches).toHaveLength(1); +}); + +test("a pending update asks only about pages whose cited text changed and keeps the old baseline until decided", async () => { + const root = await settledKnowledge(); + const before = await baseline(root); + const version = await moveRegistration(root, "src/index.ts", "export const answer = 43;\n"); + expect(await beginKnowledgeUpdate(root, undefined, { pending: true })).toMatchObject({ outcome: "update-prepared" }); + const update = (await readKnowledgeUpdate(root))!; + expect(update.scopes).toEqual([{ ...scope, processed_version: version }]); + expect(update.changed_references?.length).toBeGreaterThan(0); + expect(new Set(update.candidates.map(candidate => candidate.path))) + .toEqual(new Set(update.changed_references!.map(reference => reference.path))); + expect(await baseline(root)).toEqual(before); + await expect(beginKnowledgeUpdate(root, undefined, { pending: true })).rejects.toThrow("active task"); +}); + +test("history reads fetch one missing settled commit into a shallow recovered checkout", async () => { + const root = await initialRevisionKnowledge(roots); + const source = join(root, "fixture-source"); + const first = execFileSync("git", ["rev-parse", "HEAD"], { cwd: source, encoding: "utf8" }).trim(); + const original = await readFile(join(source, "src/index.ts"), "utf8"); + await moveRegistration(root, "src/index.ts", "export const answer = 43;\n"); + const checkout = join(root, "sources/repo/20260903/revision-fixture"); + await rm(checkout, { recursive: true, force: true }); + execFileSync("git", ["clone", "-q", "--depth=1", `file://${source}`, checkout]); + const read = await registeredRepoHistoryReader(root); + expect(await read(DOCUMENT_REVISION_SOURCE_REF, "src/index.ts", first)).toBe(original); +}); diff --git a/packages/context-cli/src/commands/documentRevisionCommands.ts b/packages/context-cli/src/commands/documentRevisionCommands.ts index 3883c6b2..813b11e3 100644 --- a/packages/context-cli/src/commands/documentRevisionCommands.ts +++ b/packages/context-cli/src/commands/documentRevisionCommands.ts @@ -188,14 +188,22 @@ export function registerDocumentRevisionCommand(program: Command): void { }); program.command("update") .description("Inspect acquired source changes against approved knowledge in confirmed requirement scopes") - .requiredOption("--input ", "YAML/JSON scopes and optional change summary; - for stdin") + .option("--input ", "YAML/JSON scopes and optional change summary; - for stdin") + .option("--pending", "process registered repository commits that moved past their processed versions") .option("--format ", "output format: json", "json") - .action(async (options: { input: string; format: string }) => { + .action(async (options: { input?: string; pending?: boolean; format: string }) => { if (options.format !== "json") throw new ContextError(ExitCode.UserError, "--format must be json", { category: ErrorCategory.UserInputInvalid, }); - assertActionInputWorkspace(process.cwd(), options.input); - const value = await readYamlOrJsonInput({ path: options.input, label: "update", + if (!options.pending === !options.input) throw new ContextError(ExitCode.UserError, "Pass exactly one of --input or --pending", { + category: ErrorCategory.UserInputInvalid, + }); + if (options.pending) { + process.stdout.write(`${JSON.stringify(await beginKnowledgeUpdate(requireProjectRoot(), undefined, { pending: true }), null, 2)}\n`); + return; + } + assertActionInputWorkspace(process.cwd(), options.input!); + const value = await readYamlOrJsonInput({ path: options.input!, label: "update", missingNext: "Pass scopes with requirement_ref, source_ref and optional module_refs.", readFailureNext: "Fix the input path within the current workspace.", parseFailureNext: "Provide valid JSON or YAML scopes and an optional changes summary." }); diff --git a/packages/context-cli/src/project/approvedKnowledgeDependencyWarnings.ts b/packages/context-cli/src/project/approvedKnowledgeDependencyWarnings.ts index d665cff3..a553022e 100644 --- a/packages/context-cli/src/project/approvedKnowledgeDependencyWarnings.ts +++ b/packages/context-cli/src/project/approvedKnowledgeDependencyWarnings.ts @@ -5,6 +5,7 @@ import { readKnowledgeStructure } from "./packageBuildInventory.js"; import { approvedKnowledgeSnapshotsFromStructure } from "./approvedKnowledgeSnapshots.js"; import type { ProjectVerifyIssue } from "./verifyTypes.js"; import { repoContentImpactReader } from "./repoContentEvidence.js"; +import { pendingSourceUpdateScopes, type PendingSourceScope } from "./pendingSourceUpdates.js"; /** Read only cited captured files, once per verification. No parser invocation, * article dependency graph, or mutation of approved references. Changed regions @@ -19,6 +20,7 @@ export async function approvedKnowledgeDependencyWarnings( const read = await registeredArticleSourceReader(projectRoot); const issues: ProjectVerifyIssue[] = []; let repoImpact: ReturnType | undefined; + let pending: Promise | undefined; for (const article of articles) { const changed: string[] = []; const moved: string[] = []; @@ -54,13 +56,19 @@ export async function approvedKnowledgeDependencyWarnings( break; } } + if (!changed.length && !moved.length) continue; + // A moved registration is processed by the update, not by revising pages one at a time. + pending ??= pendingSourceUpdateScopes(projectRoot).catch(() => []); + const update = (await pending).some(scope => article.sections.some(section => section.references.some(reference => + reference.source_ref === scope.source_ref || reference.source_ref.startsWith(`${scope.source_ref}/`) || reference.source_ref.startsWith(`${scope.source_ref}#`)))) + ? " A cited repository moved past its processed commit; run context update --pending --format json first." : ""; if (changed.length) issues.push({ severity: "warning", code: "approved-source-region-changed", path: article.path, - message: `Source regions changed or are unavailable for fragments: ${changed.join(", ")}. Inspect current sources and revise this article; verification does not rewrite approved references.`, + message: `Source regions changed or are unavailable for fragments: ${changed.join(", ")}.${update || " Inspect current sources and revise this article;"} Verification does not rewrite approved references.`, }); if (moved.length) issues.push({ severity: "warning", code: "approved-source-region-moved", path: article.path, - message: `Cited regions are unchanged but their location or surrounding source changed: ${moved.join("; ")}. Check locators during the source update; do not rewrite unaffected prose.`, + message: `Cited regions are unchanged but their location or surrounding source changed: ${moved.join("; ")}.${update || " Check locators during the source update;"} Do not rewrite unaffected prose.`, }); } return issues; diff --git a/packages/context-cli/src/project/articleSourceReader.ts b/packages/context-cli/src/project/articleSourceReader.ts index 6cd18245..80156169 100644 --- a/packages/context-cli/src/project/articleSourceReader.ts +++ b/packages/context-cli/src/project/articleSourceReader.ts @@ -22,6 +22,37 @@ export async function registeredRepoHistoryReader(projectRoot: string) { const workspace = await realpath(projectRoot); const registry = await loadSourcesRegistry({ rootDir: projectRoot }); const roots = new Map>(); + const fetched = new Map>(); + const reads = new Map>(); + // Partial or shallow checkouts fetch missing objects on read; one read at a + // time per checkout keeps a large settlement from flooding the remote. + const queues = new Map>(); + const serial = (directory: string, task: () => Promise): Promise => { + const run = (queues.get(directory) ?? Promise.resolve()).then(task, task); + queues.set(directory, run.catch(() => undefined)); + return run; + }; + // Recovered checkouts are shallow; fetch a missing settled commit once. A commit + // the remote no longer has fails every path at once instead of per read. + const available = (directory: string, commit: string) => { + const key = `${directory}\0${commit}`; + let result = fetched.get(key); + if (!result) { + result = execute("git", ["-C", directory, "cat-file", "-e", `${commit}^{commit}`], + { timeout: 5000, env: { ...process.env, GIT_NO_LAZY_FETCH: "1" } }).then(() => true, () => + execute("git", ["-C", directory, "fetch", "--quiet", "--depth=1", "origin", commit], { timeout: 120_000 }) + .then(() => true, () => false)); + fetched.set(key, result); + } + return result; + }; + async function readAt(directory: string, path: string, commit: string): Promise { + if (!await available(directory, commit)) throw new TypeError(`Commit ${commit} is not available in the registered checkout`); + const blob = await execute("git", ["-C", directory, "show", `${commit}:./${path}`], { + encoding: "buffer", timeout: 60_000, maxBuffer: 16 * 1024 * 1024, + }); + return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(blob.stdout); + } return async (sourceRef: string, path: string, commit: string): Promise => { 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); @@ -32,10 +63,11 @@ export async function registeredRepoHistoryReader(projectRoot: string) { 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); + const directory = await root; + const key = JSON.stringify([directory, commit, path]); + let read = reads.get(key); + if (!read) { read = serial(directory, () => readAt(directory, path, commit)); reads.set(key, read); } + return read; }; } diff --git a/packages/context-cli/src/project/indexerBaseContracts.ts b/packages/context-cli/src/project/indexerBaseContracts.ts index f6f1a70d..afb6093c 100644 --- a/packages/context-cli/src/project/indexerBaseContracts.ts +++ b/packages/context-cli/src/project/indexerBaseContracts.ts @@ -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.87"; +export const BUNDLED_INDEXER_PARSER_PACKAGE_VERSION = "0.7.88"; const BUNDLED_PARSER_REQUIREMENTS = buildIndexerParserCapabilityRequirements( BUNDLED_INDEXER_PARSER_PACKAGE_VERSION, ); diff --git a/packages/context-cli/src/project/knowledgeUpdate.ts b/packages/context-cli/src/project/knowledgeUpdate.ts index 64dc49a2..5f4be6a8 100644 --- a/packages/context-cli/src/project/knowledgeUpdate.ts +++ b/packages/context-cli/src/project/knowledgeUpdate.ts @@ -19,6 +19,7 @@ import { repoContentScopeMatches } from "@c4a/context"; import { ensureRepoContentLinks } from "./repoContentLinks.js"; import { inspectRepoContentChanges, repoContentImpactReader } from "./repoContentEvidence.js"; import { inspectSettledReferences } from "./settledReferenceRelocation.js"; +import { pendingSourceUpdateScopes } from "./pendingSourceUpdates.js"; export const knowledgeUpdateInputSchema = z.object({ scopes: z.array(z.object({ requirement_ref: z.string().min(1), source_ref: z.string().min(1), @@ -35,6 +36,8 @@ const updateSchema = z.object({ structure_proposal: indexerCurrentActionInputDefinitions.sourceUpdate.omit({ stage: true }).optional(), previous_versions: z.array(z.string().nullable()), changes: z.string().optional(), repo_content: z.unknown().optional(), + changed_references: z.array(z.object({ path: z.string(), section_id: z.string(), state: z.enum(["changed", "unavailable"]), + source_ref: z.string(), locator: z.unknown() }).strict()).optional(), }).strict(); export type KnowledgeUpdate = z.infer; @@ -49,14 +52,23 @@ export async function readKnowledgeUpdate(projectRoot: string): Promise { - const input = knowledgeUpdateInputSchema.parse(value); await ensureRepoContentLinks(projectRoot); const { readTaskRollback } = await import("./taskRollback.js"); if (await readTaskRollback(projectRoot) || await readApprovedRevision(projectRoot) || await readKnowledgeUpdate(projectRoot) || await readProductionStage(projectRoot) || (await readCandidateRecords(projectRoot)).length > 0) { throw new TypeError("Finish or explicitly roll back the active task before starting an independent source update."); } + let input: z.infer; + if (options.pending) { + const pending = await pendingSourceUpdateScopes(projectRoot); + if (pending.length === 0) return { outcome: "current" as const, next_action: { command: "context status --format json" } }; + input = { scopes: pending }; + } else input = knowledgeUpdateInputSchema.parse(value); const versions = new Map>(); const scopes = await captureProcessedScopes(projectRoot, await Promise.all(input.scopes.map(async (scope) => { if (scope.processed_version !== undefined) return { ...scope, processed_version: scope.processed_version }; @@ -65,9 +77,13 @@ export async function beginKnowledgeUpdate(projectRoot: string, value: unknown) }))); const structure = await readKnowledgeStructure(projectRoot); if (!structure.parsed) throw new TypeError("Close the existing knowledge before checking source updates"); - const candidates = await Promise.all(validateArticleStructureEntries(structure.parsed.articles ?? []) - .filter(article => article.sections.some(section => section.references.some(reference => - scopes.some(scope => repoContentScopeMatches(scope.source_ref, reference.source_ref))))) + const settled = readProcessedScopes(structure.parsed); + const articles = validateArticleStructureEntries(structure.parsed.articles ?? []); + const cited = articles.filter(article => article.sections.some(section => section.references.some(reference => + scopes.some(scope => repoContentScopeMatches(scope.source_ref, reference.source_ref))))); + const inspected = await inspectSettledReferences(projectRoot, cited, scopes.map(scope => scope.source_ref), settled); + const changedPaths = new Set(inspected.unresolved.map(item => item.path)); + const candidates = await Promise.all(cited.filter(article => !options.pending || changedPaths.has(article.path)) .map(async article => { const title = parseFrontmatterLoose(await targetBytes(projectRoot, article.path)).title; return { path: article.path, title: typeof title === "string" ? title : article.path, @@ -75,7 +91,7 @@ export async function beginKnowledgeUpdate(projectRoot: string, value: unknown) source_refs: [...new Set(article.sections.flatMap(section => section.references.map(reference => reference.source_ref)))] }; })); const registry = await readProductionRequirements(projectRoot); - const repoReferences = validateArticleStructureEntries(structure.parsed.articles ?? []).flatMap(article => + const repoReferences = articles.flatMap(article => article.sections.flatMap(section => section.references.filter(ref => ref.source_ref.startsWith("repo-content:") && scopes.some(scope => repoContentScopeMatches(scope.source_ref, ref.source_ref))) .map(reference => ({ article: article.path, section: section.id, reference })))); @@ -88,7 +104,9 @@ export async function beginKnowledgeUpdate(projectRoot: string, value: unknown) const requirements = registry.requirements.filter((requirement) => requirementIds.has(requirement.id)); const payload = { protocol: "context.source-update/v1" as const, scopes, requirements, candidates, ...(repoContent === undefined ? {} : { repo_content: repoContent }), - previous_versions: scopes.map((scope) => processedVersionForScope(readProcessedScopes(structure.parsed), scope) ?? null), + ...(inspected.unresolved.length === 0 ? {} : { changed_references: inspected.unresolved.map(item => ({ path: item.path, + section_id: item.section_id, state: item.state, source_ref: item.reference.source_ref, locator: item.reference.locator })) }), + previous_versions: scopes.map((scope) => processedVersionForScope(settled, scope) ?? null), ...(input.changes === undefined ? {} : { changes: input.changes }) }; const request = updateSchema.parse({ ...payload, revision: indexerProtocolDigest(payload) }); await atomicWriteFile(join(projectRoot, await revisionStoragePath(projectRoot)), `${JSON.stringify(request)}\n`); diff --git a/packages/context-cli/src/project/pendingSourceUpdates.ts b/packages/context-cli/src/project/pendingSourceUpdates.ts new file mode 100644 index 00000000..2d5d8760 --- /dev/null +++ b/packages/context-cli/src/project/pendingSourceUpdates.ts @@ -0,0 +1,25 @@ +import { loadSourcesRegistry, readProcessedScopes, type ProcessedScope } from "@c4a/context"; +import { readKnowledgeStructure } from "./packageBuildInventory.js"; +import { readProductionRequirements } from "./productionRequirements.js"; + +export type PendingSourceScope = Pick; + +/** Registered repositories whose fixed commit moved past a recorded processed + * version. Moving the registration does not process knowledge; `context update` + * does, and settles the new version only after its pages are decided. */ +export async function pendingSourceUpdateScopes(projectRoot: string): Promise { + const structure = await readKnowledgeStructure(projectRoot); + if (!structure.parsed) return []; + const processed = readProcessedScopes(structure.parsed).filter(scope => scope.source_ref.startsWith("repo:")); + if (processed.length === 0) return []; + const [sources, registry] = await Promise.all([loadSourcesRegistry({ rootDir: projectRoot }), readProductionRequirements(projectRoot)]); + return processed.filter(scope => { + const name = scope.source_ref.slice("repo:".length); + const entries = sources.repos.filter(entry => entry.id === name || entry.name === name); + if (entries.length !== 1 || !/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(entries[0]!.ref) || entries[0]!.ref === scope.processed_version) return false; + const requirement = registry.requirements.find(item => item.id === scope.requirement_ref); + return !!requirement && [...requirement.target_scope.targets, ...requirement.evidence_source_scope?.targets ?? []] + .some(target => target.source_ref === scope.source_ref); + }).map(scope => ({ requirement_ref: scope.requirement_ref, source_ref: scope.source_ref, + ...(scope.module_refs === undefined ? {} : { module_refs: scope.module_refs }) })); +} diff --git a/packages/context-cli/src/project/repoSources.ts b/packages/context-cli/src/project/repoSources.ts index 5fab754f..60e7f5ea 100644 --- a/packages/context-cli/src/project/repoSources.ts +++ b/packages/context-cli/src/project/repoSources.ts @@ -66,6 +66,7 @@ export interface AddRepoSourceResult { registryPath: string; source: RepoSourceRecord; status: RepoSourceStatus; + next?: string; } function nonEmpty(value: string | null | undefined): string | null { @@ -671,10 +672,14 @@ export async function addRepoSourceUnlocked(input: AddRepoSourceInput): Promise< else registry.repos[index] = source; await writeRepoRegistry(input.projectRoot, registry); const status = await ensureRepoSource({ projectRoot: input.projectRoot, source }); + const { pendingSourceUpdateScopes } = await import("./pendingSourceUpdates.js"); + const pending = existing && existing.git.ref !== next.git.ref && (await pendingSourceUpdateScopes(input.projectRoot)) + .some((scope) => scope.source_ref === `repo:${source.name}`); return { registryPath: DEFAULT_REPO_SOURCES_REGISTRY_PATH, source, status, + ...(pending ? { next: "Approved knowledge was processed at the previous commit. Run context update --pending --format json to process this commit." } : {}), }; } diff --git a/packages/context-cli/src/project/settledReferenceRelocation.ts b/packages/context-cli/src/project/settledReferenceRelocation.ts index 384eb1b3..b6230d3c 100644 --- a/packages/context-cli/src/project/settledReferenceRelocation.ts +++ b/packages/context-cli/src/project/settledReferenceRelocation.ts @@ -33,14 +33,15 @@ const settledBy = (scopes: readonly string[], sourceRef: string) => !sourceRef.s * can stand in for it, but only when the old region still matches its digest. */ async function settledRegionHistory(root: string, previous: readonly ProcessedScope[]): Promise { if (!previous.some(scope => scope.source_ref.startsWith("repo:"))) return undefined; - let readAt: Awaited> | undefined; + // One reader per inspection: concurrent references share its cache, fetches and queue. + let reader: ReturnType | undefined; return async reference => { if (!reference.source_ref.startsWith("repo:")) return undefined; const versions = new Set(previous.filter(scope => settledBy([scope.source_ref], reference.source_ref)).map(scope => scope.processed_version)); for (const version of versions) { try { - readAt ??= await registeredRepoHistoryReader(root); - const text = await readAt(reference.source_ref, reference.locator.path, version); + reader ??= registeredRepoHistoryReader(root); + const text = await (await reader)(reference.source_ref, reference.locator.path, version); if (articleSourceRegionDigest(text, reference.locator) === reference.content_digest) return articleSourceRegion(text, reference.locator); } catch { /* Unreadable history leaves the reference unconfirmed. */ } } diff --git a/packages/context-cli/src/project/status.ts b/packages/context-cli/src/project/status.ts index e5905f73..cbcb81ef 100644 --- a/packages/context-cli/src/project/status.ts +++ b/packages/context-cli/src/project/status.ts @@ -223,6 +223,9 @@ async function collectProjectStatusSnapshotInternal( ? await inspectSiteTranslations(projectRoot, packages).then(result => result.route_satisfied, () => false) : true, taskPreparation, localRevisionActive: !!localRevision || !!localUpdate, + sourceUpdatePending: !maintenance && !localRevision && !localUpdate && !localRollback && !production && draftStatus.count === 0 && + await import("./pendingSourceUpdates.js").then(({ pendingSourceUpdateScopes }) => pendingSourceUpdateScopes(projectRoot)) + .then(scopes => scopes.length > 0, () => false), unfinishedIndexerTasks: false, ...(production ? { productionState: await productionRequirementsAreCurrent(projectRoot, production) ? dispatchProductionStage(production, productionCapabilitiesSchema.parse({})).state : "active" as const, diff --git a/packages/context-cli/src/project/workflow/workflowFacts.ts b/packages/context-cli/src/project/workflow/workflowFacts.ts index d7ba40f4..69453f8e 100644 --- a/packages/context-cli/src/project/workflow/workflowFacts.ts +++ b/packages/context-cli/src/project/workflow/workflowFacts.ts @@ -172,6 +172,7 @@ export function createContextWorkflowFacts( registered: observation.sourceCount > 0, repositories_ready: observation.readyRepoSources === observation.repoSources.length, + update_pending: observation.sourceUpdatePending === true, }, capture: { declarations_complete: diff --git a/packages/context-cli/src/project/workflow/workflowTypes.ts b/packages/context-cli/src/project/workflow/workflowTypes.ts index 828b98da..222ebbd3 100644 --- a/packages/context-cli/src/project/workflow/workflowTypes.ts +++ b/packages/context-cli/src/project/workflow/workflowTypes.ts @@ -60,6 +60,7 @@ export interface ContextWorkflowFacts extends Record { sources: { registered: boolean; repositories_ready: boolean; + update_pending: boolean; }; capture: { declarations_complete: boolean; @@ -89,6 +90,7 @@ export interface ContextWorkflowObservation { productionState?: "active" | "waiting-user" | "blocked" | "ended"; productionDelivery?: boolean; localRevisionActive?: boolean; + sourceUpdatePending?: boolean; versionCurrent?: boolean; translationRouteSatisfied?: boolean; unfinishedIndexerTasks?: boolean; diff --git a/packages/context/docs/guides/knowledge-updates.md b/packages/context/docs/guides/knowledge-updates.md index b9217f08..b7e02051 100644 --- a/packages/context/docs/guides/knowledge-updates.md +++ b/packages/context/docs/guides/knowledge-updates.md @@ -419,6 +419,13 @@ also checking additions that have no old page reference. The scope is complete only after every required change is reviewed, closed and built, or after an explicit conclusion that it needs no knowledge changes. +Moving a repository with `context source add repo --ref ` only changes its +registration. When approved knowledge was processed at the previous commit, run +`context update --pending --format json`. Only pages whose cited text changed +become candidates, listed in `changed_references`; still inspect the change for +additions and shared dependencies before deciding. Moved citations update their +locations when the update settles. + Before importing a `note`, read [note source preparation](note.md). Before importing `sessions`, read [development summary preparation](sessions.md). Choose the reference for the actual input; do not read both by default. diff --git a/packages/context/package.json b/packages/context/package.json index 146c737b..1f01bf4c 100644 --- a/packages/context/package.json +++ b/packages/context/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/context", "description": "Declarative SDK for Context knowledge sources, workflows, review, and package outputs", - "version": "0.7.87", + "version": "0.7.88", "type": "module", "license": "MIT", "engines": { diff --git a/packages/core/package.json b/packages/core/package.json index eb140b81..11b6208d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/core", "description": "Shared extraction types, schemas, and utilities for Context", - "version": "0.7.87", + "version": "0.7.88", "type": "module", "license": "MIT", "engines": { diff --git a/packages/dev-cli/package.json b/packages/dev-cli/package.json index e4b1b9be..3eea73af 100644 --- a/packages/dev-cli/package.json +++ b/packages/dev-cli/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/dev-cli", "description": "Developer menu for Context build, verification, link, and release preparation", - "version": "0.7.87", + "version": "0.7.88", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extract-contract/package.json b/packages/extract-contract/package.json index 37340e35..5dcc4cd3 100644 --- a/packages/extract-contract/package.json +++ b/packages/extract-contract/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract-contract", "description": "OpenAPI and GraphQL contract catalog adapter for Context", - "version": "0.7.87", + "version": "0.7.88", "type": "module", "license": "MIT", "engines": { diff --git a/packages/extract-go/package.json b/packages/extract-go/package.json index 31959ed0..b996e668 100644 --- a/packages/extract-go/package.json +++ b/packages/extract-go/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract-go", "description": "Go extraction plugin and structural index for Context", - "version": "0.7.87", + "version": "0.7.88", "type": "module", "license": "MIT", "engines": { diff --git a/packages/extract-mdx/package.json b/packages/extract-mdx/package.json index c65834d7..5d91d086 100644 --- a/packages/extract-mdx/package.json +++ b/packages/extract-mdx/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract-mdx", "description": "MDX component and example catalog bridge for Context", - "version": "0.7.87", + "version": "0.7.88", "type": "module", "license": "MIT", "engines": { diff --git a/packages/extract-proto/package.json b/packages/extract-proto/package.json index b098c3d4..63bdbeff 100644 --- a/packages/extract-proto/package.json +++ b/packages/extract-proto/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract-proto", "description": "Protocol Buffers IDL catalog parser for Context", - "version": "0.7.87", + "version": "0.7.88", "type": "module", "license": "MIT", "engines": { diff --git a/packages/extract-rush/package.json b/packages/extract-rush/package.json index cc4dabd7..2dfd4200 100644 --- a/packages/extract-rush/package.json +++ b/packages/extract-rush/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract-rush", "description": "Rush workspace structural index for Context", - "version": "0.7.87", + "version": "0.7.88", "type": "module", "license": "MIT", "engines": { diff --git a/packages/extract-sql/package.json b/packages/extract-sql/package.json index 19737442..9291c844 100644 --- a/packages/extract-sql/package.json +++ b/packages/extract-sql/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract-sql", "description": "Dialect-bound lightweight SQL evidence adapter for Context", - "version": "0.7.87", + "version": "0.7.88", "type": "module", "license": "MIT", "engines": { diff --git a/packages/extract-style/package.json b/packages/extract-style/package.json index 0083a232..73a51598 100644 --- a/packages/extract-style/package.json +++ b/packages/extract-style/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract-style", "description": "Lightweight CSS and SCSS evidence adapter for Context", - "version": "0.7.87", + "version": "0.7.88", "type": "module", "license": "MIT", "engines": { diff --git a/packages/extract-thrift/package.json b/packages/extract-thrift/package.json index fd467187..606f550e 100644 --- a/packages/extract-thrift/package.json +++ b/packages/extract-thrift/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract-thrift", "description": "Apache Thrift IDL catalog parser for Context", - "version": "0.7.87", + "version": "0.7.88", "type": "module", "license": "MIT", "engines": { diff --git a/packages/extract-ts/package.json b/packages/extract-ts/package.json index edd0e37d..3ad5f294 100644 --- a/packages/extract-ts/package.json +++ b/packages/extract-ts/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract-ts", "description": "TypeScript and JavaScript extraction plugin for the Context ExtractionResult v2 contract", - "version": "0.7.87", + "version": "0.7.88", "type": "module", "license": "MIT", "engines": { diff --git a/packages/extract/package.json b/packages/extract/package.json index e20d29a6..97762607 100644 --- a/packages/extract/package.json +++ b/packages/extract/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/extract", "description": "Language-plugin framework and repository runner for Context code evidence", - "version": "0.7.87", + "version": "0.7.88", "type": "module", "license": "MIT", "engines": { diff --git a/packages/tui/package.json b/packages/tui/package.json index 8b8abb51..98c9ca4a 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,7 +1,7 @@ { "name": "@c4a/tui", "description": "Shared terminal UI components (Ink + React) for Context development tools", - "version": "0.7.87", + "version": "0.7.88", "private": true, "type": "module", "license": "MIT", diff --git a/plugins/context/repo-install/claude/.claude-plugin/plugin.json b/plugins/context/repo-install/claude/.claude-plugin/plugin.json index c06834da..bd9ea168 100644 --- a/plugins/context/repo-install/claude/.claude-plugin/plugin.json +++ b/plugins/context/repo-install/claude/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "c4a", "description": "Start or continue a project-local knowledge workspace through one graph-routed entry.", - "version": "0.7.87", + "version": "0.7.88", "author": { "name": "c4a" }, diff --git a/plugins/context/repo-install/codex/.codex-plugin/plugin.json b/plugins/context/repo-install/codex/.codex-plugin/plugin.json index aac9ca1e..2ab72420 100644 --- a/plugins/context/repo-install/codex/.codex-plugin/plugin.json +++ b/plugins/context/repo-install/codex/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "c4a", - "version": "0.7.87", + "version": "0.7.88", "description": "Start or continue a project-local knowledge workspace through one graph-routed entry.", "author": { "name": "c4a" }, "homepage": "https://github.com/context4ai/c4a", @@ -10,7 +10,7 @@ "skills": "./skills/", "interface": { "displayName": "C4A Context", - "shortDescription": "Initialize and advance a local, source-linked project knowledge workspace.\nv0.7.87", + "shortDescription": "Initialize and advance a local, source-linked project knowledge workspace.\nv0.7.88", "longDescription": "Create a Context workspace and use agent-guided next steps to register sources, run extraction, review candidates, build package outputs, and verify health without silently mutating source repositories.", "developerName": "c4a", "category": "Productivity", diff --git a/plugins/context/repo-install/cursor/.cursor-plugin/plugin.json b/plugins/context/repo-install/cursor/.cursor-plugin/plugin.json index 4a862d8a..05b11b87 100644 --- a/plugins/context/repo-install/cursor/.cursor-plugin/plugin.json +++ b/plugins/context/repo-install/cursor/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "c4a", "displayName": "C4A Context", - "version": "0.7.87", + "version": "0.7.88", "description": "Start or continue a project-local knowledge workspace through one graph-routed entry.", "author": { "name": "Context4AI",