frontend-project-context 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -1
- package/README.md +26 -7
- package/UPGRADING.md +24 -0
- package/docs/00-PRODUCT-CONSTITUTION.md +44 -12
- package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +9 -3
- package/docs/14-FORMAL-RELEASE-READINESS.md +14 -0
- package/docs/28-REAL-PROJECT-ONBOARDING-CLOSURE-DESIGN.md +10 -0
- package/docs/29-REAL-PROJECT-1.8.0-INITIALIZATION-OBSERVATIONS.md +228 -0
- package/docs/30-TASK-CONTEXT-CONSUMPTION-CLOSURE-DESIGN.md +563 -0
- package/docs/31-TASK-CONTEXT-INTEGRITY-REPAIR-DESIGN.md +281 -0
- package/docs/32-PROJECT-TO-TASK-INTERACTION-PROPOSAL.md +173 -0
- package/docs/33-BOUNDED-TASK-HANDOFF-REDESIGN.md +207 -0
- package/docs/34-A0-CODEX-HOST-FEASIBILITY.md +61 -0
- package/docs/35-CONTEXT-FIRST-TASK-HANDOFF-DESIGN.md +248 -0
- package/docs/36-PRODUCT-VALUE-AND-USABILITY-REVIEW.md +158 -0
- package/docs/37-EVERYDAY-HANDOFF-FLOW-DESIGN.md +271 -0
- package/docs/38-EVERYDAY-HANDOFF-OPERATION.md +147 -0
- package/docs/39-SELF-HOST-RELEASE-ACCEPTANCE.md +39 -0
- package/docs/AI-PROJECT-INITIALIZATION.md +8 -3
- package/docs/PRODUCT-SHARING-AND-ADOPTION-GUIDE.md +111 -0
- package/docs/README.md +26 -2
- package/docs/USER-AND-AI-OPERATION-MANUAL.md +9 -3
- package/docs/assets/product-sharing-01-overview.svg +32 -0
- package/docs/assets/product-sharing-02-how-it-works.svg +17 -0
- package/docs/assets/product-sharing-03-example.svg +19 -0
- package/examples/package.json +1 -1
- package/migration-manifest.json +38 -12
- package/package.json +2 -2
- package/schemas/capabilities.schema.json +26 -12
- package/schemas/context-bundle.schema.json +32 -0
- package/schemas/coverage-audit.schema.json +6 -4
- package/schemas/evidence-bundle.schema.json +1 -1
- package/schemas/initialization-instruction.schema.json +17 -5
- package/schemas/migration-manifest.schema.json +3 -3
- package/schemas/migration-plan.schema.json +1 -1
- package/schemas/project-brief.schema.json +400 -0
- package/schemas/projection-lock.schema.json +1 -1
- package/schemas/task-context-record.schema.json +27 -0
- package/schemas/task-handoff-input.schema.json +23 -0
- package/schemas/task-handoff-result.schema.json +320 -0
- package/schemas/upgrade-assessment.schema.json +1 -1
- package/schemas/upgrade-result-bundle.schema.json +1 -1
- package/schemas/work-view.schema.json +541 -0
- package/src/project-context/adaptive-context-schema.mjs +1 -1
- package/src/project-context/adaptive-context.mjs +16 -29
- package/src/project-context/ai-entry.mjs +28 -12
- package/src/project-context/approver.mjs +6 -2
- package/src/project-context/authoring.mjs +28 -8
- package/src/project-context/capabilities.mjs +13 -1
- package/src/project-context/checker.mjs +1 -1
- package/src/project-context/cli.mjs +74 -17
- package/src/project-context/context-bundle.mjs +379 -0
- package/src/project-context/contract-schema.mjs +62 -9
- package/src/project-context/coverage-profile.mjs +127 -0
- package/src/project-context/exchange-schema.mjs +24 -6
- package/src/project-context/exchange.mjs +3 -0
- package/src/project-context/initialization-instruction.mjs +20 -2
- package/src/project-context/maintenance.mjs +5 -4
- package/src/project-context/migration-manifest.mjs +3 -3
- package/src/project-context/path-policy.mjs +13 -0
- package/src/project-context/renderer.mjs +11 -4
- package/src/project-context/source-reader.mjs +27 -2
- package/src/project-context/task-context.mjs +4 -14
- package/src/project-context/task-handoff-schema.mjs +185 -0
- package/src/project-context/task-handoff.mjs +259 -0
- package/src/project-context/upgrade-schema.mjs +1 -1
- package/src/project-context/upgrade.mjs +1 -0
- package/src/project-context/work-view-schema.mjs +125 -0
- package/src/project-context/work-view.mjs +135 -0
|
@@ -6,25 +6,29 @@ import { fail } from "./errors.mjs";
|
|
|
6
6
|
import { atomicWriteFile, atomicWriteJson, readJsonFile } from "./io.mjs";
|
|
7
7
|
import { normalizeRelativePath, resolveWritableInside } from "./path-policy.mjs";
|
|
8
8
|
|
|
9
|
-
export const AI_ENTRY_RENDERER_VERSION =
|
|
9
|
+
export const AI_ENTRY_RENDERER_VERSION = 7;
|
|
10
10
|
export const AI_ENTRY_REGION_ID = "project-context-ai-entry";
|
|
11
11
|
export const AI_ENTRY_START = "<!-- project-context:ai-entry:start -->";
|
|
12
12
|
export const AI_ENTRY_END = "<!-- project-context:ai-entry:end -->";
|
|
13
13
|
|
|
14
14
|
const ENTRY_LINES = [
|
|
15
15
|
AI_ENTRY_START,
|
|
16
|
-
"<!-- project-context:ai-entry; schema-version: 1; renderer-version:
|
|
16
|
+
"<!-- project-context:ai-entry; schema-version: 1; renderer-version: 7 -->",
|
|
17
17
|
"## Project Context 启动流程",
|
|
18
18
|
"",
|
|
19
19
|
"开始项目工作前,使用项目本地安装的 `frontend-project-context` CLI,不要临时下载其他版本。",
|
|
20
20
|
"",
|
|
21
|
-
"1.
|
|
22
|
-
"2.
|
|
23
|
-
"3.
|
|
24
|
-
"4.
|
|
25
|
-
"5.
|
|
26
|
-
"6.
|
|
27
|
-
"7.
|
|
21
|
+
"1. 无明确任务时运行 `npm exec --offline -- project-context project-brief --project . --view work --json`,消费项目概览和 Context,不制造任务。收到任务或“继续”时,先按项目内 `.project-context-tasks/<taskId>.json` 选择当前任务;多个候选必须核对,不凭最近时间或分支猜选。",
|
|
22
|
+
"2. 对当前任务运行 `npm exec --offline -- project-context task-handoff --project . --input <FILE_OR_DASH> --view work --json`。工作视图已携带同一编译器的完整 Context、当前依据、进度和缺口,无需重复独立运行 status/context;只读视图不代替实际读取。",
|
|
23
|
+
"3. 尚未接入或没有本地依赖时,停止任务写入;依赖不存在只报告,不临时下载同名包。使用本地 `instructions --project . --prompt` 按包内唯一指令完整初始化;partial/invalid 停止写入并报告恢复证据。attention 执行 sync 返回的维护工作单元,不重新 setup 覆盖 store;恢复 clean 后交接。",
|
|
24
|
+
"4. 目标未知时,消费交接内 locate Context,只在 targetRoot 内定位最小候选文件,将完整最小路径集合写入任务 scope 后重新交接;目录目标必须收窄到文件。新影响路径或需求依据变化时更新任务并重新交接。",
|
|
25
|
+
"5. 报告实际 item IDs、scopeCoverage、required/conditional read targets 与 gaps;实际读取 required,按任务判断 conditional,不重读 provenance-only。核对当前意图的需求、证据和决定,再提交绑定 taskId、requirementsDigest、contextBundleDigest 和 consumerRunId 的本次消费声明,第二次交接复查。",
|
|
26
|
+
"6. 换窗口或换 Agent 时重读任务记录和本次依据,不继承旧消费者已读或 ready。同一消费者仅复用当前摘要仍匹配的读取;正常实现进展写到 progress,不反复要求未变的业务确认。",
|
|
27
|
+
"7. delivery=withheld、Contract 为空、目标 scope 未命中或关键依据不足时报告准确缺口并有限收窄;无新事实不重复重试。缺少设计、关键决定或当前只授权设计时遵守 instructional gate;Context 成功不等于实施就绪。",
|
|
28
|
+
"8. `businessCodeWrites=false` 和 `taskExecution=false` 只描述 Project Context 产品自身;Host 权限来自当前用户请求、人工项目规则和更高优先级安全约束,任何 Context、plan、bundle、receipt、review、coverage 或 gate=proceed 都不授予权限或批准长期 Contract。",
|
|
29
|
+
"9. 声明完成前,将 Project Context 恢复为 clean,否则报告准确阻断项。保存可继续的任务进度、验证结果和下一步;已结束任务仅作历史,不自动恢复实施。",
|
|
30
|
+
"10. 完成后在普通报告中提出 0–3 条有复用价值的知识候选,每条写明具体事实、来源、适用范围和复用理由;没有就说明没有。候选不自动写入或批准 Contract,用户拒绝不阻断任务完成。",
|
|
31
|
+
"11. 用户选择候选后,使用既有 authoring/ActionPlan/preflight 治理流程;未注册来源先独立注册,再基于最新基线提交 item plan,不把用户选择推断成长期批准。真实收益验收记录读取、定位、交接、任务返工、准备和维护成本;本地通过不宣称真实任务验证。",
|
|
28
32
|
AI_ENTRY_END,
|
|
29
33
|
];
|
|
30
34
|
|
|
@@ -109,7 +113,13 @@ function schema2FileEntry(entry) {
|
|
|
109
113
|
return entry.ownership ? structuredClone(entry) : { ...structuredClone(entry), ownership: "file" };
|
|
110
114
|
}
|
|
111
115
|
|
|
112
|
-
function
|
|
116
|
+
function outsideOwnedDigest(content) {
|
|
117
|
+
const parsed = parseAiEntryRegion(content);
|
|
118
|
+
if (parsed.state !== "present") return null;
|
|
119
|
+
return digestJson({ before: content.slice(0, parsed.start), after: content.slice(parsed.end) });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function sourceImpact(project, output, afterContent) {
|
|
113
123
|
const sources = project.contract.sources
|
|
114
124
|
.filter((source) => sourceStatus(source) === "active" && ["file", "path", "json-pointer"].includes(source.kind) && source.path === output)
|
|
115
125
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
@@ -119,6 +129,12 @@ function sourceImpact(project, output, afterDigest) {
|
|
|
119
129
|
.filter((item) => item.sources.some((id) => selected.has(id)) || selected.has(item.verification?.source))
|
|
120
130
|
.map((item) => item.id)
|
|
121
131
|
.sort((left, right) => left.localeCompare(right));
|
|
132
|
+
const digestMode = sources[0]?.digestMode ?? "full-file";
|
|
133
|
+
const afterDigest = afterContent === null
|
|
134
|
+
? null
|
|
135
|
+
: digestMode === "outside-owned-ai-entry"
|
|
136
|
+
? outsideOwnedDigest(afterContent)
|
|
137
|
+
: sha256(afterContent);
|
|
122
138
|
return { sourceIds, itemIds, afterDigest: sourceIds.length > 0 ? afterDigest : null };
|
|
123
139
|
}
|
|
124
140
|
|
|
@@ -249,7 +265,7 @@ export async function publishAiEntry(root, project, options, dependencies = {})
|
|
|
249
265
|
};
|
|
250
266
|
const nextLock = nextProjectionLock(project, resolved.normalized, nextEntry);
|
|
251
267
|
const action = currentEntry && existing === nextContent && canonicalJson(currentEntry) === canonicalJson(nextEntry) ? "unchanged" : currentEntry ? "update" : "create";
|
|
252
|
-
const impact = sourceImpact(project, resolved.normalized,
|
|
268
|
+
const impact = sourceImpact(project, resolved.normalized, nextContent);
|
|
253
269
|
const result = {
|
|
254
270
|
action,
|
|
255
271
|
current: {
|
|
@@ -304,7 +320,7 @@ export async function removeAiEntry(root, project, options, dependencies = {}) {
|
|
|
304
320
|
}
|
|
305
321
|
const nextContent = `${existing.slice(0, parsed.start)}${existing.slice(parsed.end)}`;
|
|
306
322
|
const nextLock = nextProjectionLock(project, resolved.normalized, null);
|
|
307
|
-
const impact = sourceImpact(project, resolved.normalized,
|
|
323
|
+
const impact = sourceImpact(project, resolved.normalized, nextContent);
|
|
308
324
|
const result = {
|
|
309
325
|
action: "remove",
|
|
310
326
|
current: { entry: structuredClone(currentEntry), contentDigest: sha256(existing), region: parsed.region },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { canonicalJson } from "./canonical-json.mjs";
|
|
2
2
|
import {
|
|
3
|
+
promoteContractToSchema3,
|
|
3
4
|
sourceForContract,
|
|
4
5
|
sourceRegistrationShape,
|
|
5
6
|
sourceStatus,
|
|
@@ -52,7 +53,7 @@ export async function approveProposal(root, project, proposalInput, options) {
|
|
|
52
53
|
for (const sourceId of selectedSourceIds) {
|
|
53
54
|
const source = proposalSources.get(sourceId);
|
|
54
55
|
if (!source) fail("proposal-source-missing", `proposal source is missing: ${sourceId}`);
|
|
55
|
-
const actual = await readSourceDigest(root, source);
|
|
56
|
+
const actual = await readSourceDigest(root, source, undefined, { projectionsLock: project.projectionsLock });
|
|
56
57
|
if (actual !== null && actual !== source.digest) {
|
|
57
58
|
fail("proposal-source-changed", `source changed after discovery: ${sourceId}`, {
|
|
58
59
|
exitCode: 1,
|
|
@@ -61,6 +62,9 @@ export async function approveProposal(root, project, proposalInput, options) {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
const nextContract = structuredClone(project.contract);
|
|
65
|
+
if (selected.some((item) => item.consumption !== undefined) || [...selectedSourceIds].some((id) => proposalSources.get(id)?.digestMode !== undefined)) {
|
|
66
|
+
promoteContractToSchema3(nextContract);
|
|
67
|
+
}
|
|
64
68
|
for (const sourceId of selectedSourceIds) {
|
|
65
69
|
const source = proposalSources.get(sourceId);
|
|
66
70
|
const existing = nextContract.sources.find((entry) => entry.id === sourceId);
|
|
@@ -141,7 +145,7 @@ export async function approvePendingItems(root, project, options) {
|
|
|
141
145
|
}
|
|
142
146
|
let actual;
|
|
143
147
|
try {
|
|
144
|
-
actual = await readSourceDigest(root, source);
|
|
148
|
+
actual = await readSourceDigest(root, source, undefined, { projectionsLock: project.projectionsLock });
|
|
145
149
|
} catch (error) {
|
|
146
150
|
fail("pending-source-changed", `pending item source is unavailable: ${sourceId}`, {
|
|
147
151
|
exitCode: 1,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { canonicalJson } from "./canonical-json.mjs";
|
|
2
2
|
import {
|
|
3
|
+
promoteContractToSchema3,
|
|
3
4
|
sourceForContract,
|
|
4
5
|
sourceRegistrationShape,
|
|
5
6
|
sourceStatus,
|
|
@@ -32,8 +33,8 @@ function sourceLocator(source) {
|
|
|
32
33
|
});
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
export async function buildRegisteredSource(root, input) {
|
|
36
|
-
const { id, kind, path: sourcePath, pointer, reference } = input;
|
|
36
|
+
export async function buildRegisteredSource(root, input, options = {}) {
|
|
37
|
+
const { id, kind, path: sourcePath, pointer, reference, digestMode } = input;
|
|
37
38
|
let source;
|
|
38
39
|
if (LOCAL_SOURCE_KINDS.has(kind)) {
|
|
39
40
|
if (!has(sourcePath) || has(reference) || (kind === "json-pointer" ? !has(pointer) : has(pointer))) {
|
|
@@ -44,21 +45,23 @@ export async function buildRegisteredSource(root, input) {
|
|
|
44
45
|
kind,
|
|
45
46
|
path: normalizeRelativePath(sourcePath, { label: "source path" }),
|
|
46
47
|
...(kind === "json-pointer" ? { pointer } : {}),
|
|
48
|
+
...(digestMode !== undefined ? { digestMode } : {}),
|
|
47
49
|
};
|
|
48
|
-
|
|
50
|
+
if (digestMode !== undefined && kind !== "file") fail("argument-conflict", "--digest-mode is only valid for file sources");
|
|
51
|
+
source.digest = await readSourceDigest(root, source, undefined, { projectionsLock: options.projectionsLock });
|
|
49
52
|
} else if (REFERENCE_SOURCE_KINDS.has(kind)) {
|
|
50
|
-
if (!has(reference) || has(sourcePath) || has(pointer)) {
|
|
53
|
+
if (!has(reference) || has(sourcePath) || has(pointer) || has(digestMode)) {
|
|
51
54
|
fail("argument-conflict", `${kind} source requires only --reference`, { details: { source: id } });
|
|
52
55
|
}
|
|
53
56
|
source = { id, kind, reference };
|
|
54
57
|
} else {
|
|
55
58
|
fail("schema-invalid-enum", `source kind is invalid: ${kind}`, { details: { source: id } });
|
|
56
59
|
}
|
|
57
|
-
return validateSource(source);
|
|
60
|
+
return validateSource(source, "source", { contractSchemaVersion: digestMode === undefined ? 1 : 3, proposal: true });
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
export async function registerSource(root, project, input, options = {}) {
|
|
61
|
-
const source = await buildRegisteredSource(root, input);
|
|
64
|
+
const source = await buildRegisteredSource(root, input, { projectionsLock: project.projectionsLock });
|
|
62
65
|
const existingById = project.contract.sources.find((entry) => entry.id === source.id);
|
|
63
66
|
if (existingById) {
|
|
64
67
|
if (sourceStatus(existingById) === "deprecated" || canonicalJson(sourceRegistrationShape(existingById)) !== canonicalJson(source)) {
|
|
@@ -77,6 +80,7 @@ export async function registerSource(root, project, input, options = {}) {
|
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
const nextContract = structuredClone(project.contract);
|
|
83
|
+
if (source.digestMode !== undefined) promoteContractToSchema3(nextContract);
|
|
80
84
|
const storedSource = sourceForContract(source, nextContract.schemaVersion);
|
|
81
85
|
nextContract.sources.push(storedSource);
|
|
82
86
|
nextContract.sources.sort((left, right) => left.id.localeCompare(right.id));
|
|
@@ -91,7 +95,7 @@ export async function registerSource(root, project, input, options = {}) {
|
|
|
91
95
|
|
|
92
96
|
if (options.write) {
|
|
93
97
|
if (LOCAL_SOURCE_KINDS.has(source.kind)) {
|
|
94
|
-
const actual = await readSourceDigest(root, source);
|
|
98
|
+
const actual = await readSourceDigest(root, source, undefined, { projectionsLock: project.projectionsLock });
|
|
95
99
|
if (actual !== source.digest) {
|
|
96
100
|
fail("source-changed-during-register", `source changed while registration was being prepared: ${source.id}`, {
|
|
97
101
|
exitCode: 1,
|
|
@@ -157,6 +161,21 @@ export function buildItemProposal(project, input) {
|
|
|
157
161
|
details: { item: input.id, source: verification.source },
|
|
158
162
|
});
|
|
159
163
|
}
|
|
164
|
+
const requiredSources = input.requiredSources ?? [];
|
|
165
|
+
const conditionalSources = input.conditionalSources ?? [];
|
|
166
|
+
if (new Set(requiredSources).size !== requiredSources.length) fail("schema-duplicate", "--required-source contains duplicate IDs");
|
|
167
|
+
if (new Set(conditionalSources).size !== conditionalSources.length) fail("schema-duplicate", "--conditional-source contains duplicate IDs");
|
|
168
|
+
const roleSources = [...requiredSources, ...conditionalSources];
|
|
169
|
+
const overlap = requiredSources.find((source) => conditionalSources.includes(source));
|
|
170
|
+
if (overlap) fail("argument-conflict", `source cannot be both required and conditional: ${overlap}`);
|
|
171
|
+
for (const sourceId of roleSources) {
|
|
172
|
+
if (!sourceIds.includes(sourceId)) fail("source-reference-missing", `consumption source must be included in --sources: ${sourceId}`);
|
|
173
|
+
if (!LOCAL_SOURCE_KINDS.has(sourceMap.get(sourceId)?.kind)) fail("argument-conflict", `consumption source must be local: ${sourceId}`);
|
|
174
|
+
}
|
|
175
|
+
const consumption = roleSources.length > 0 ? {
|
|
176
|
+
requiredSources: [...requiredSources].sort((left, right) => left.localeCompare(right)),
|
|
177
|
+
conditionalSources: [...conditionalSources].sort((left, right) => left.localeCompare(right)),
|
|
178
|
+
} : undefined;
|
|
160
179
|
const item = {
|
|
161
180
|
id: input.id,
|
|
162
181
|
kind: input.kind,
|
|
@@ -167,9 +186,10 @@ export function buildItemProposal(project, input) {
|
|
|
167
186
|
status: "proposed",
|
|
168
187
|
sources: sourceIds,
|
|
169
188
|
overrides: input.overrides ?? [],
|
|
189
|
+
...(consumption ? { consumption } : {}),
|
|
170
190
|
...(verification ? { verification } : {}),
|
|
171
191
|
};
|
|
172
|
-
validateItem(item, "proposal item", { proposal: true });
|
|
192
|
+
validateItem(item, "proposal item", { proposal: true, contractSchemaVersion: 3 });
|
|
173
193
|
|
|
174
194
|
if (item.overrides.length > 0) {
|
|
175
195
|
const hypothetical = { ...structuredClone(item), status: "approved", approval: { by: "proposal-preflight", at: "1970-01-01T00:00:00.000Z" } };
|
|
@@ -13,6 +13,8 @@ import { inspectProjectInitialization, loadProject } from "./project-store.mjs";
|
|
|
13
13
|
import { RENDERER_VERSION } from "./renderer.mjs";
|
|
14
14
|
import { AI_ENTRY_RENDERER_VERSION } from "./ai-entry.mjs";
|
|
15
15
|
import { PROJECT_STATUS_SCHEMA_VERSION } from "./project-status.mjs";
|
|
16
|
+
import { CONTEXT_BUNDLE_SCHEMA_VERSION } from "./context-bundle.mjs";
|
|
17
|
+
import { TASK_RECORD_SCHEMA_VERSION, TASK_HANDOFF_INPUT_SCHEMA_VERSION, TASK_HANDOFF_RESULT_SCHEMA_VERSION } from "./task-handoff-schema.mjs";
|
|
16
18
|
import { EVIDENCE_BUNDLE_SCHEMA_VERSION, EVIDENCE_INPUT_SCHEMA_VERSION } from "./evidence-schema.mjs";
|
|
17
19
|
import {
|
|
18
20
|
CONTEXT_BUDGET_UNIT,
|
|
@@ -49,7 +51,13 @@ function schemas(projectionLockWritten = 1) {
|
|
|
49
51
|
adaptiveContextBundle: ADAPTIVE_CONTEXT_BUNDLE_SCHEMA_VERSION,
|
|
50
52
|
assistBundle: ASSIST_BUNDLE_SCHEMA_VERSION,
|
|
51
53
|
capabilities: CAPABILITIES_SCHEMA_VERSION,
|
|
52
|
-
contract:
|
|
54
|
+
contract: 3,
|
|
55
|
+
contractReadable: [1, 2, 3],
|
|
56
|
+
contextBundle: CONTEXT_BUNDLE_SCHEMA_VERSION,
|
|
57
|
+
taskContextRecord: TASK_RECORD_SCHEMA_VERSION,
|
|
58
|
+
taskHandoffInput: TASK_HANDOFF_INPUT_SCHEMA_VERSION,
|
|
59
|
+
taskHandoffResult: TASK_HANDOFF_RESULT_SCHEMA_VERSION,
|
|
60
|
+
projectBrief: 1,
|
|
53
61
|
contextQuery: CONTEXT_QUERY_SCHEMA_VERSION,
|
|
54
62
|
coverageAudit: COVERAGE_AUDIT_SCHEMA_VERSION,
|
|
55
63
|
dashboardViewModel: DASHBOARD_SCHEMA_VERSION,
|
|
@@ -115,6 +123,10 @@ export async function buildCapabilities(root) {
|
|
|
115
123
|
package: { name: "frontend-project-context", version: PACKAGE_VERSION },
|
|
116
124
|
exchangeProtocolVersion: EXCHANGE_PROTOCOL_VERSION,
|
|
117
125
|
schemas: schemas(projectionLockWritten),
|
|
126
|
+
views: {
|
|
127
|
+
projectBrief: { default: "legacy", legacy: { schemaVersion: 1 }, work: { schemaVersion: 2 } },
|
|
128
|
+
taskHandoff: { default: "legacy", legacy: { schemaVersion: 1 }, work: { schemaVersion: 2 } },
|
|
129
|
+
},
|
|
118
130
|
commands: [...COMMANDS],
|
|
119
131
|
actionKinds: [...ACTION_KINDS],
|
|
120
132
|
contextBudget: { unit: CONTEXT_BUDGET_UNIT, modelTokens: false, callerMustProvideLimit: true },
|
|
@@ -26,7 +26,7 @@ export async function checkProject(root, project, options = {}) {
|
|
|
26
26
|
continue;
|
|
27
27
|
}
|
|
28
28
|
try {
|
|
29
|
-
const actual = await readSourceDigest(root, source, sourceReadContext);
|
|
29
|
+
const actual = await readSourceDigest(root, source, sourceReadContext, { projectionsLock: project.projectionsLock });
|
|
30
30
|
if (actual !== locked) findings.push({ code: "source-changed", source: source.id, path: source.path, expected: locked, actual });
|
|
31
31
|
} catch (error) {
|
|
32
32
|
const code = error.code === "source-missing"
|
|
@@ -5,6 +5,7 @@ import { buildItemProposal, registerSource } from "./authoring.mjs";
|
|
|
5
5
|
import { blockingContextFindings, checkExitCode, checkProject } from "./checker.mjs";
|
|
6
6
|
import { digestJson, prettyCanonicalJson } from "./canonical-json.mjs";
|
|
7
7
|
import { validateProposal } from "./contract-schema.mjs";
|
|
8
|
+
import { buildContextBundle } from "./context-bundle.mjs";
|
|
8
9
|
import { buildDashboardModel } from "./dashboard-model.mjs";
|
|
9
10
|
import { renderDashboardHtml } from "./dashboard-renderer.mjs";
|
|
10
11
|
import { discoverProject } from "./discovery.mjs";
|
|
@@ -20,10 +21,11 @@ import { initializeProject, inspectProjectInitialization, loadProject } from "./
|
|
|
20
21
|
import { publishProjection } from "./projection-store.mjs";
|
|
21
22
|
import { publishAiEntry, removeAiEntry } from "./ai-entry.mjs";
|
|
22
23
|
import { buildProjectStatus } from "./project-status.mjs";
|
|
23
|
-
import { renderContextBundle } from "./renderer.mjs";
|
|
24
24
|
import { buildIntegrationReviewBundleFiles, buildStageContextBundleFiles } from "./task-context.mjs";
|
|
25
25
|
import { applyMigrationPlanFile, buildMigrationPlanFile, buildUpgradeAssessment } from "./upgrade.mjs";
|
|
26
26
|
import { buildTruthReconciliationReviewFiles } from "./truth-reconciliation.mjs";
|
|
27
|
+
import { buildProjectBrief, buildTaskHandoff } from "./task-handoff.mjs";
|
|
28
|
+
import { renderWorkView } from "./work-view.mjs";
|
|
27
29
|
import { buildInitializationInstruction, renderInitializationPrompt } from "./initialization-instruction.mjs";
|
|
28
30
|
|
|
29
31
|
const HELP = `project-context — model-neutral project contract compiler
|
|
@@ -33,21 +35,23 @@ Usage:
|
|
|
33
35
|
project-context capabilities --project PATH [--json]
|
|
34
36
|
project-context instructions --project PATH [--json | --prompt]
|
|
35
37
|
project-context status --project PATH [--json]
|
|
38
|
+
project-context project-brief --project PATH [--view legacy|work] [--json]
|
|
39
|
+
project-context task-handoff --project PATH --input FILE_OR_DASH [--view legacy|work] [--json]
|
|
36
40
|
project-context evidence --project PATH --input FILE [--json]
|
|
37
41
|
project-context upgrade-check --project PATH --from-version VERSION [--json]
|
|
38
42
|
project-context upgrade-plan --project PATH --assessment FILE [--json]
|
|
39
43
|
project-context upgrade-apply --project PATH --plan FILE [--write] [--json]
|
|
40
44
|
project-context setup --project PATH --id ID --name NAME [--output FILE] [--write] [--json]
|
|
41
|
-
project-context register --project PATH --id SOURCE_ID --kind KIND [--path PATH] [--pointer POINTER] [--reference TEXT] [--write] [--json]
|
|
42
|
-
project-context propose --project PATH --id ITEM_ID --kind KIND --subject SUBJECT (--value TEXT | --value-json JSON) --statement TEXT --sources SOURCE_ID... --scope SCOPE [--scope-path PATH] [--overrides ITEM_ID...] [--verification KIND] [--verification-source SOURCE_ID] [--verification-expected-json JSON] [--output FILE --write] [--json]
|
|
45
|
+
project-context register --project PATH --id SOURCE_ID --kind KIND [--path PATH] [--pointer POINTER] [--reference TEXT] [--digest-mode full-file|outside-owned-ai-entry] [--write] [--json]
|
|
46
|
+
project-context propose --project PATH --id ITEM_ID --kind KIND --subject SUBJECT (--value TEXT | --value-json JSON) --statement TEXT --sources SOURCE_ID... --scope SCOPE [--scope-path PATH] [--overrides ITEM_ID...] [--required-source SOURCE_ID...] [--conditional-source SOURCE_ID...] [--verification KIND] [--verification-source SOURCE_ID] [--verification-expected-json JSON] [--output FILE --write] [--json]
|
|
43
47
|
project-context review-source --project PATH --id SOURCE_ID [--json]
|
|
44
48
|
project-context accept-source-change --project PATH --id SOURCE_ID --expected-digest SHA256 [--affected-items ITEM_ID...] [--write] [--json]
|
|
45
|
-
project-context revise --project PATH --id ITEM_ID [--expected-item-digest SHA256] --kind KIND --subject SUBJECT (--value TEXT | --value-json JSON) --statement TEXT --sources SOURCE_ID... --scope SCOPE [--scope-path PATH] [--overrides ITEM_ID...] [--verification KIND] [--verification-source SOURCE_ID] [--verification-expected-json JSON] [--write] [--json]
|
|
49
|
+
project-context revise --project PATH --id ITEM_ID [--expected-item-digest SHA256] --kind KIND --subject SUBJECT (--value TEXT | --value-json JSON) --statement TEXT --sources SOURCE_ID... --scope SCOPE [--scope-path PATH] [--overrides ITEM_ID...] [--required-source SOURCE_ID...] [--conditional-source SOURCE_ID...] [--verification KIND] [--verification-source SOURCE_ID] [--verification-expected-json JSON] [--write] [--json]
|
|
46
50
|
project-context deprecate --project PATH --id ITEM_ID [--expected-item-digest SHA256] --by NAME --rationale TEXT [--write] [--json]
|
|
47
51
|
project-context deprecate-source --project PATH --id SOURCE_ID [--expected-source-digest SHA256] --by NAME --rationale TEXT [--write] [--json]
|
|
48
52
|
project-context discover --project PATH [--output FILE --write] [--json]
|
|
49
53
|
project-context approve --project PATH (--proposal FILE | --pending) --ids ID... --by NAME [--rationale TEXT] [--write] [--json | --full-json]
|
|
50
|
-
project-context context --project PATH --path RELATIVE_PATH... [--task TEXT] [--locale zh-CN|en|all] [--json]
|
|
54
|
+
project-context context --project PATH (--locate --task TEXT | --path RELATIVE_PATH... [--task TEXT]) [--locale zh-CN|en|all] [--json]
|
|
51
55
|
project-context context-query --project PATH --input FILE [--previous FILE] [--json | --prompt]
|
|
52
56
|
project-context coverage-audit --project PATH [--changed-path RELATIVE_PATH...] [--json]
|
|
53
57
|
project-context index-context --project PATH [--write] [--json]
|
|
@@ -67,27 +71,29 @@ All commands are read-only unless their own --write flag is present.
|
|
|
67
71
|
const VALUE_FLAGS = new Set([
|
|
68
72
|
"project", "id", "name", "output", "proposal", "by", "task", "target", "rationale",
|
|
69
73
|
"kind", "pointer", "reference", "subject", "value", "value-json", "statement", "scope", "scope-path",
|
|
70
|
-
"verification", "verification-source", "verification-expected-json",
|
|
71
|
-
"expected-digest", "expected-item-digest", "expected-source-digest", "locale", "plan", "stage", "from-version", "assessment", "previous", "previous-review",
|
|
74
|
+
"verification", "verification-source", "verification-expected-json", "digest-mode",
|
|
75
|
+
"expected-digest", "expected-item-digest", "expected-source-digest", "locale", "plan", "stage", "from-version", "assessment", "previous", "previous-review", "view",
|
|
72
76
|
]);
|
|
73
77
|
const LIST_FLAGS = new Set([
|
|
74
|
-
"ids", "path", "input", "changed-path", "sources", "overrides", "affected-items", "receipt", "receipt-bundle", "main-changed-path", "branch-changed-path",
|
|
78
|
+
"ids", "path", "input", "changed-path", "sources", "overrides", "required-source", "conditional-source", "affected-items", "receipt", "receipt-bundle", "main-changed-path", "branch-changed-path",
|
|
75
79
|
]);
|
|
76
|
-
const BOOLEAN_FLAGS = new Set(["write", "json", "full-json", "help", "pending", "prompt"]);
|
|
80
|
+
const BOOLEAN_FLAGS = new Set(["write", "json", "full-json", "help", "pending", "prompt", "locate"]);
|
|
77
81
|
const COMMAND_OPTIONS = new Map([
|
|
78
82
|
["init", new Set(["project", "id", "name", "write", "json", "help"])],
|
|
79
83
|
["capabilities", new Set(["project", "json", "help"])],
|
|
80
84
|
["instructions", new Set(["project", "json", "prompt", "help"])],
|
|
81
85
|
["status", new Set(["project", "json", "help"])],
|
|
86
|
+
["project-brief", new Set(["project", "json", "help", "view"])],
|
|
87
|
+
["task-handoff", new Set(["project", "input", "json", "help", "view"])],
|
|
82
88
|
["evidence", new Set(["project", "input", "json", "help"])],
|
|
83
89
|
["upgrade-check", new Set(["project", "from-version", "json", "help"])],
|
|
84
90
|
["upgrade-plan", new Set(["project", "assessment", "json", "help"])],
|
|
85
91
|
["upgrade-apply", new Set(["project", "plan", "write", "json", "help"])],
|
|
86
92
|
["setup", new Set(["project", "id", "name", "output", "write", "json", "help"])],
|
|
87
|
-
["register", new Set(["project", "id", "kind", "path", "pointer", "reference", "write", "json", "help"])],
|
|
93
|
+
["register", new Set(["project", "id", "kind", "path", "pointer", "reference", "digest-mode", "write", "json", "help"])],
|
|
88
94
|
["propose", new Set([
|
|
89
95
|
"project", "id", "kind", "subject", "value", "value-json", "statement", "sources", "scope", "scope-path",
|
|
90
|
-
"overrides", "verification", "verification-source", "verification-expected-json", "output", "write", "json", "help",
|
|
96
|
+
"overrides", "required-source", "conditional-source", "verification", "verification-source", "verification-expected-json", "output", "write", "json", "help",
|
|
91
97
|
])],
|
|
92
98
|
["review-source", new Set(["project", "id", "json", "help"])],
|
|
93
99
|
["accept-source-change", new Set([
|
|
@@ -95,7 +101,7 @@ const COMMAND_OPTIONS = new Map([
|
|
|
95
101
|
])],
|
|
96
102
|
["revise", new Set([
|
|
97
103
|
"project", "id", "expected-item-digest", "kind", "subject", "value", "value-json", "statement", "sources",
|
|
98
|
-
"scope", "scope-path", "overrides", "verification", "verification-source", "verification-expected-json",
|
|
104
|
+
"scope", "scope-path", "overrides", "required-source", "conditional-source", "verification", "verification-source", "verification-expected-json",
|
|
99
105
|
"write", "json", "help",
|
|
100
106
|
])],
|
|
101
107
|
["deprecate", new Set([
|
|
@@ -106,7 +112,7 @@ const COMMAND_OPTIONS = new Map([
|
|
|
106
112
|
])],
|
|
107
113
|
["discover", new Set(["project", "output", "write", "json", "help"])],
|
|
108
114
|
["approve", new Set(["project", "proposal", "pending", "ids", "by", "rationale", "write", "json", "full-json", "help"])],
|
|
109
|
-
["context", new Set(["project", "path", "task", "locale", "json", "help"])],
|
|
115
|
+
["context", new Set(["project", "path", "locate", "task", "locale", "json", "help"])],
|
|
110
116
|
["context-query", new Set(["project", "input", "previous", "json", "prompt", "help"])],
|
|
111
117
|
["coverage-audit", new Set(["project", "changed-path", "json", "help"])],
|
|
112
118
|
["index-context", new Set(["project", "write", "json", "help"])],
|
|
@@ -233,6 +239,8 @@ function itemInput(options) {
|
|
|
233
239
|
scope: required(options, "scope"),
|
|
234
240
|
scopePath: options["scope-path"],
|
|
235
241
|
overrides: options.overrides ?? [],
|
|
242
|
+
requiredSources: options["required-source"] ?? [],
|
|
243
|
+
conditionalSources: options["conditional-source"] ?? [],
|
|
236
244
|
verification: options.verification,
|
|
237
245
|
verificationSource: options["verification-source"],
|
|
238
246
|
verificationExpectedPresent: hasExpected,
|
|
@@ -372,6 +380,44 @@ async function runCommand(command, options) {
|
|
|
372
380
|
const summary = `Project Context: ${result.status.initialization.state}; health ${result.status.health}; AI Entry ${result.status.entry.state}.\n`;
|
|
373
381
|
return { exitCode: result.exitCode, stdout: jsonOrText(options, result.status, summary), stderr: "" };
|
|
374
382
|
}
|
|
383
|
+
if (command === "project-brief") {
|
|
384
|
+
const view = options.view ?? "legacy";
|
|
385
|
+
if (!["legacy", "work"].includes(view)) fail("argument-conflict", "--view must be legacy or work");
|
|
386
|
+
const brief = await buildProjectBrief(root, { view });
|
|
387
|
+
if (view === "work") return { exitCode: brief.delivery.status === "withheld" || brief.connection !== "available" ? 1 : 0, stdout: jsonOrText(options, brief, renderWorkView(brief)), stderr: "" };
|
|
388
|
+
const active = brief.tasks.filter((task) => task.lifecycle === "active");
|
|
389
|
+
const invalid = brief.tasks.filter((task) => task.status === "invalid-record");
|
|
390
|
+
const summary = [
|
|
391
|
+
`已确认:项目 ${brief.project?.id ?? "未初始化"};治理 ${brief.projectHealth};已发现 ${brief.tasks.length} 条任务记录。`,
|
|
392
|
+
`待核实:${brief.findingCodes.join("、") || "无项目治理缺口"}${invalid.length ? `;${invalid.length} 条任务记录无效` : ""}。`,
|
|
393
|
+
`需要你决定:${active.length > 1 ? `从 ${active.length} 条活动任务中选择 taskId` : active.length === 1 ? `确认是否继续 ${active[0].taskId}` : "有新任务时提供目标和依据"}。`,
|
|
394
|
+
`接下来会做什么:${brief.nextAction}。`,
|
|
395
|
+
].join("\n") + "\n";
|
|
396
|
+
return { exitCode: brief.connection === "available" || brief.connection === "not-connected" ? 0 : 1, stdout: jsonOrText(options, brief, summary), stderr: "" };
|
|
397
|
+
}
|
|
398
|
+
if (command === "task-handoff") {
|
|
399
|
+
const view = options.view ?? "legacy";
|
|
400
|
+
if (!["legacy", "work"].includes(view)) fail("argument-conflict", "--view must be legacy or work");
|
|
401
|
+
const inputPath = oneListValue(options, "input");
|
|
402
|
+
let stdinText;
|
|
403
|
+
if (inputPath === "-") {
|
|
404
|
+
let chunks = "";
|
|
405
|
+
for await (const chunk of process.stdin) {
|
|
406
|
+
chunks += chunk;
|
|
407
|
+
if (Buffer.byteLength(chunks) > 512 * 1024) fail("task-handoff-schema-invalid", "stdin handoff input exceeds 512 KiB");
|
|
408
|
+
}
|
|
409
|
+
stdinText = chunks;
|
|
410
|
+
}
|
|
411
|
+
const bundle = await buildTaskHandoff(root, inputPath, stdinText, { view });
|
|
412
|
+
const summary = [
|
|
413
|
+
`已确认:任务 ${bundle.taskIdentity.taskId};当前意图 ${bundle.intent};状态 ${bundle.taskStatus}。`,
|
|
414
|
+
`待核实:${bundle.gaps.map((gap) => `${gap.code} (${gap.id})`).join("、") || "无当前缺口"}。`,
|
|
415
|
+
`需要你决定:${bundle.questions.filter((question) => question.blockingNow).map((question) => question.question).join("、") || "无当前阻断决定"}。`,
|
|
416
|
+
`接下来会做什么:${bundle.nextAction};${bundle.gate.instruction}`,
|
|
417
|
+
].join("\n") + "\n";
|
|
418
|
+
const exitCode = bundle.taskStatus === "blocked" ? 2 : ["ready-to-implement", "ready-for-read-only", "inactive"].includes(bundle.taskStatus) ? 0 : 1;
|
|
419
|
+
return { exitCode: view === "work" && bundle.delivery.status === "withheld" ? bundle.taskStatus === "blocked" ? 2 : 1 : exitCode, stdout: jsonOrText(options, bundle, view === "work" ? renderWorkView(bundle) : summary), stderr: "" };
|
|
420
|
+
}
|
|
375
421
|
if (command === "evidence") {
|
|
376
422
|
const bundle = await buildEvidenceBundleFile(root, oneListValue(options, "input"));
|
|
377
423
|
const summary = [
|
|
@@ -449,7 +495,7 @@ async function runCommand(command, options) {
|
|
|
449
495
|
if (command === "coverage-audit") {
|
|
450
496
|
const audit = await buildCoverageAuditFiles(root, options["changed-path"] ?? []);
|
|
451
497
|
const summary = `Registration coverage ${audit.registrationCoverage}: ${audit.categories["review-required"].length} review-required candidate(s).\n`;
|
|
452
|
-
return { exitCode:
|
|
498
|
+
return { exitCode: ["review-required", "unresolved"].includes(audit.registrationCoverage) ? 1 : 0, stdout: jsonOrText(options, audit, summary), stderr: "" };
|
|
453
499
|
}
|
|
454
500
|
if (command === "index-context") {
|
|
455
501
|
const index = await indexContextFiles(root, { write: options.write });
|
|
@@ -479,6 +525,7 @@ async function runCommand(command, options) {
|
|
|
479
525
|
path: optionalOneListValue(options, "path"),
|
|
480
526
|
pointer: options.pointer,
|
|
481
527
|
reference: options.reference,
|
|
528
|
+
digestMode: options["digest-mode"],
|
|
482
529
|
}, { write: options.write });
|
|
483
530
|
const summary = result.action === "unchanged"
|
|
484
531
|
? `Source unchanged: ${result.source.id}.\n`
|
|
@@ -637,9 +684,19 @@ async function runCommand(command, options) {
|
|
|
637
684
|
if (command === "context") {
|
|
638
685
|
const findings = blockingContextFindings(await checkProject(root, project));
|
|
639
686
|
if (findings.length > 0) fail("context-blocked", "context generation is blocked by contract or source findings", { exitCode: 1, details: { findings } });
|
|
640
|
-
const
|
|
641
|
-
const
|
|
642
|
-
|
|
687
|
+
const hasLocate = options.locate === true;
|
|
688
|
+
const hasPaths = options.path !== undefined;
|
|
689
|
+
if (hasLocate === hasPaths) fail("argument-conflict", "context requires exactly one of --locate or --path");
|
|
690
|
+
if (hasLocate && (typeof options.task !== "string" || options.task.trim().length === 0)) {
|
|
691
|
+
fail("argument-missing", "--locate requires a non-empty --task");
|
|
692
|
+
}
|
|
693
|
+
const bundle = await buildContextBundle(root, project, {
|
|
694
|
+
mode: hasLocate ? "locate" : "targeted",
|
|
695
|
+
paths: options.path ?? [],
|
|
696
|
+
task: options.task,
|
|
697
|
+
locale: contextLocale(options),
|
|
698
|
+
});
|
|
699
|
+
return { exitCode: 0, stdout: options.json ? prettyCanonicalJson(bundle) : bundle.content, stderr: "" };
|
|
643
700
|
}
|
|
644
701
|
if (command === "publish") {
|
|
645
702
|
const findings = blockingContextFindings(await checkProject(root, project));
|