frontend-project-context 1.7.0 → 1.9.1

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +17 -10
  3. package/UPGRADING.md +18 -0
  4. package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +29 -6
  5. package/docs/14-FORMAL-RELEASE-READINESS.md +14 -0
  6. package/docs/24-A130-REAL-HOST-TARGET-PROJECT-COMPARISON.md +1 -1
  7. package/docs/26-A130-QUALITY-CLOSURE-AND-ADAPTIVE-DELIVERY-REPAIR-DESIGN.md +1 -1
  8. package/docs/27-TEAM-SHARED-CONTEXT-DIRECTION-DISCUSSION.md +30 -0
  9. package/docs/28-REAL-PROJECT-ONBOARDING-CLOSURE-DESIGN.md +544 -0
  10. package/docs/29-REAL-PROJECT-1.8.0-INITIALIZATION-OBSERVATIONS.md +228 -0
  11. package/docs/30-TASK-CONTEXT-CONSUMPTION-CLOSURE-DESIGN.md +563 -0
  12. package/docs/31-TASK-CONTEXT-INTEGRITY-REPAIR-DESIGN.md +281 -0
  13. package/docs/AI-PROJECT-INITIALIZATION.md +90 -0
  14. package/docs/PRODUCT-SHARING-AND-ADOPTION-GUIDE.md +111 -0
  15. package/docs/README.md +24 -0
  16. package/docs/USER-AND-AI-OPERATION-MANUAL.md +17 -13
  17. package/docs/assets/product-sharing-01-overview.svg +32 -0
  18. package/docs/assets/product-sharing-02-how-it-works.svg +17 -0
  19. package/docs/assets/product-sharing-03-example.svg +19 -0
  20. package/examples/README.md +2 -2
  21. package/examples/package.json +1 -1
  22. package/migration-manifest.json +40 -10
  23. package/package.json +2 -2
  24. package/schemas/capabilities.schema.json +29 -10
  25. package/schemas/context-bundle.schema.json +32 -0
  26. package/schemas/coverage-audit.schema.json +6 -4
  27. package/schemas/evidence-bundle.schema.json +1 -1
  28. package/schemas/initialization-instruction.schema.json +72 -0
  29. package/schemas/migration-manifest.schema.json +3 -3
  30. package/schemas/migration-plan.schema.json +2 -2
  31. package/schemas/project-status.schema.json +5 -4
  32. package/schemas/projection-lock.schema.json +1 -1
  33. package/schemas/upgrade-assessment.schema.json +2 -2
  34. package/schemas/upgrade-result-bundle.schema.json +1 -1
  35. package/src/project-context/adaptive-context-schema.mjs +1 -1
  36. package/src/project-context/adaptive-context.mjs +12 -23
  37. package/src/project-context/ai-entry.mjs +24 -9
  38. package/src/project-context/approver.mjs +6 -2
  39. package/src/project-context/authoring.mjs +28 -8
  40. package/src/project-context/capabilities.mjs +18 -1
  41. package/src/project-context/checker.mjs +1 -1
  42. package/src/project-context/cli.mjs +44 -16
  43. package/src/project-context/context-bundle.mjs +378 -0
  44. package/src/project-context/contract-schema.mjs +62 -9
  45. package/src/project-context/coverage-profile.mjs +127 -0
  46. package/src/project-context/exchange-schema.mjs +24 -7
  47. package/src/project-context/exchange.mjs +3 -0
  48. package/src/project-context/initialization-instruction.mjs +60 -0
  49. package/src/project-context/maintenance.mjs +5 -4
  50. package/src/project-context/migration-manifest.mjs +5 -5
  51. package/src/project-context/project-status.mjs +14 -3
  52. package/src/project-context/project-store.mjs +27 -2
  53. package/src/project-context/renderer.mjs +11 -4
  54. package/src/project-context/source-reader.mjs +27 -2
  55. package/src/project-context/upgrade-schema.mjs +2 -2
@@ -9,8 +9,8 @@
9
9
  "schemaVersion": {"const": 1},
10
10
  "kind": {"const": "upgrade-assessment"},
11
11
  "product": {"type": "object", "additionalProperties": false, "required": ["name"], "properties": {"name": {"const": "frontend-project-context"}}},
12
- "fromVersion": {"enum": ["1.3.1", "1.4.0", "1.5.0", "1.6.0"]},
13
- "targetVersion": {"const": "1.7.0"},
12
+ "fromVersion": {"enum": ["1.3.1", "1.4.0", "1.5.0", "1.6.0", "1.7.0"]},
13
+ "targetVersion": {"const": "1.9.1"},
14
14
  "fromVersionEvidence": {"const": "host-asserted"},
15
15
  "manifestSchemaVersion": {"const": 2},
16
16
  "manifestDigest": {"$ref": "#/$defs/digest"},
@@ -10,7 +10,7 @@
10
10
  "kind": {"const": "upgrade-result-bundle"},
11
11
  "product": {"type": "object", "additionalProperties": false, "required": ["name"], "properties": {"name": {"const": "frontend-project-context"}}},
12
12
  "fromVersion": {"type": "string"},
13
- "targetVersion": {"const": "1.7.0"},
13
+ "targetVersion": {"const": "1.9.1"},
14
14
  "manifestDigest": {"$ref": "#/$defs/digest"},
15
15
  "planDigest": {"$ref": "#/$defs/digest"},
16
16
  "mode": {"enum": ["preview", "write"]},
@@ -5,7 +5,7 @@ import { normalizeRelativePath } from "./path-policy.mjs";
5
5
  export const CONTEXT_QUERY_SCHEMA_VERSION = 2;
6
6
  export const ADAPTIVE_CONTEXT_BUNDLE_SCHEMA_VERSION = 2;
7
7
  export const ROUTING_INDEX_SCHEMA_VERSION = 2;
8
- export const COVERAGE_AUDIT_SCHEMA_VERSION = 1;
8
+ export const COVERAGE_AUDIT_SCHEMA_VERSION = 2;
9
9
  export const ADAPTIVE_SELECTOR_VERSION = 2;
10
10
  export const ROUTING_INDEX_PATH = ".project-context/derived/routing-index.json";
11
11
 
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { canonicalJson, digestJson, prettyCanonicalJson, sha256 } from "./canonical-json.mjs";
4
4
  import { blockingContextFindings, checkProject } from "./checker.mjs";
5
5
  import { sourceStatus } from "./contract-schema.mjs";
6
+ import { coverageClass, coverageProfile } from "./coverage-profile.mjs";
6
7
  import { discoverProject } from "./discovery.mjs";
7
8
  import { ProjectContextError, fail } from "./errors.mjs";
8
9
  import { atomicWriteFile, readJsonFile } from "./io.mjs";
@@ -26,7 +27,6 @@ import {
26
27
  const LOCAL_SOURCE_KINDS = new Set(["file", "path", "json-pointer"]);
27
28
  const MANDATORY_KINDS = new Set(["policy", "validation-description"]);
28
29
  const ROUTED_KINDS = new Set(["fact", "reference"]);
29
- const COVERAGE_SUBJECT = "project.registration-coverage";
30
30
  const RETRIEVAL_SUBJECT = "project.context-retrieval";
31
31
  const EXCLUDED_BODIES = Object.freeze(["chat-history", "git-diffs", "source-bodies", "task-path-bodies", "verification-logs"]);
32
32
  const ROUTING_STOP_TERMS = new Set(["a", "an", "and", "apply", "change", "for", "in", "of", "on", "project", "rule", "the", "to", "update", "use"]);
@@ -145,24 +145,7 @@ export async function indexContext(root, project, options = {}) {
145
145
  return index;
146
146
  }
147
147
 
148
- function pathWithin(target, parent) { return parent === "." || target === parent || target.startsWith(`${parent}/`); }
149
- function coverageProfile(contract) {
150
- const candidates = contract.items.filter((item) => item.status === "approved" && item.kind === "policy" && item.subject === COVERAGE_SUBJECT);
151
- if (candidates.length === 0) return null;
152
- if (candidates.length > 1) fail("coverage-profile-conflict", `multiple approved ${COVERAGE_SUBJECT} items are active`);
153
- const item = candidates[0];
154
- const value = item.value;
155
- if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["excludedPaths", "roots"].includes(key)) || !Array.isArray(value.roots) || !Array.isArray(value.excludedPaths)) fail("coverage-profile-invalid", `${item.id} coverage value is invalid`);
156
- const normalize = (values, label) => uniqueSorted(values.map((entry) => normalizeRelativePath(entry, { allowRoot: true, label })));
157
- return { itemId: item.id, itemDigest: digestJson(item), sourceIds: [...item.sources].sort(), roots: normalize(value.roots, "coverage root"), excludedPaths: normalize(value.excludedPaths, "coverage excluded path") };
158
- }
159
- function coverageClass(profile, registeredPaths, candidatePath) {
160
- if (registeredPaths.has(candidatePath)) return "registered";
161
- if (!profile) return "outside-declared-coverage";
162
- if (profile.excludedPaths.some((entry) => pathWithin(candidatePath, entry))) return "excluded-approved";
163
- if (profile.roots.some((entry) => pathWithin(candidatePath, entry))) return "review-required";
164
- return "outside-declared-coverage";
165
- }
148
+ export { coverageClass, coverageProfile } from "./coverage-profile.mjs";
166
149
  export async function buildCoverageAudit(root, project, changedPaths = []) {
167
150
  const profile = coverageProfile(project.contract);
168
151
  const proposal = await discoverProject(root, project.contract);
@@ -175,10 +158,16 @@ export async function buildCoverageAudit(root, project, changedPaths = []) {
175
158
  if (!byPath.has(normalized)) byPath.set(normalized, { path: normalized, origin: "host-changed-path-signal" });
176
159
  }
177
160
  for (const registeredPath of registered) if (!byPath.has(registeredPath)) byPath.set(registeredPath, { path: registeredPath, origin: "registered-source" });
178
- const categories = { registered: [], "excluded-approved": [], "review-required": [], "outside-declared-coverage": [] };
161
+ const categories = { registered: [], "dynamic-investigation": [], "excluded-approved": [], "review-required": [], unresolved: [], "outside-declared-coverage": [] };
179
162
  for (const candidate of [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path))) categories[coverageClass(profile, registeredPaths, candidate.path)].push(candidate);
180
- const registrationCoverage = !profile ? "not-declared" : categories["review-required"].length === 0 ? "closed-for-declared-scope" : "review-required";
181
- return sealArtifact({ schemaVersion: 1, kind: "coverage-audit", project: { id: project.contract.project.id, name: project.contract.project.name }, snapshots: snapshots(project), profile, registrationCoverage, categories, guarantee: "declared-scope-only-never-all-project-truth" }, "auditDigest");
163
+ const registrationCoverage = !profile
164
+ ? "not-declared"
165
+ : categories.unresolved.length > 0
166
+ ? "unresolved"
167
+ : categories["review-required"].length === 0
168
+ ? "closed-for-declared-scope"
169
+ : "review-required";
170
+ return sealArtifact({ schemaVersion: 2, kind: "coverage-audit", project: { id: project.contract.project.id, name: project.contract.project.name }, snapshots: snapshots(project), profile, registrationCoverage, categories, guarantee: "declared-scope-only-never-all-project-truth" }, "auditDigest");
182
171
  }
183
172
 
184
173
  function effectiveByTarget(contract, paths, findings) {
@@ -331,7 +320,7 @@ async function auditRelevantSources(root, project, items, sourceReadContext) {
331
320
  const base = { id: source.id, kind: source.kind, locator: sourceLocator(source), sourceDigest: digestJson(source) };
332
321
  if (!LOCAL_SOURCE_KINDS.has(source.kind)) { evidence.push({ ...base, freshness: "unverifiable-non-local" }); continue; }
333
322
  try {
334
- const actual = await readSourceDigest(root, source, sourceReadContext);
323
+ const actual = await readSourceDigest(root, source, sourceReadContext, { projectionsLock: project.projectionsLock });
335
324
  const expected = lock.get(source.id) ?? null;
336
325
  const freshness = expected !== null && expected === source.digest && actual === expected ? "current" : "drifted";
337
326
  evidence.push({ ...base, freshness, expectedDigest: expected, actualDigest: actual });
@@ -6,23 +6,26 @@ 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 = 3;
9
+ export const AI_ENTRY_RENDERER_VERSION = 5;
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: 3 -->",
16
+ "<!-- project-context:ai-entry; schema-version: 1; renderer-version: 5 -->",
17
17
  "## Project Context 启动流程",
18
18
  "",
19
19
  "开始项目工作前,使用项目本地安装的 `frontend-project-context` CLI,不要临时下载其他版本。",
20
20
  "",
21
21
  "1. 运行 `npm exec --offline -- project-context status --project . --json`;如果本地依赖不存在,停止并报告,不要临时下载同名包。",
22
- "2. 如果状态为 `partial` 或 `invalid`,停止写入并报告准确的恢复证据。",
23
- "3. 如果尚未初始化,先预览 `setup`;如果已初始化但需要处理,使用 `sync` 工作单元;如果为 `clean`,继续遵守本文件其余仓库规则,并执行已获授权的用户任务。",
24
- "4. 任何 plan、bundle、receipt、review 或 AI 建议都不代表人工批准。",
25
- "5. 声明完成前,将 Project Context 恢复为 `clean`,否则报告准确的阻断项。",
22
+ "2. 如果状态为 `uninitialized`,运行 `npm exec --offline -- project-context instructions --project . --prompt`,按包内唯一指令完成完整初始化;如果为 `partial` 或 `invalid`,停止写入并报告准确的恢复证据。",
23
+ "3. 如果状态为 `attention`,执行 `sync` 返回的维护工作单元;不得重新运行 `setup` 覆盖已有 store。",
24
+ "4. 如果状态为 `clean` 且目标路径未知,先运行 `npm exec --offline -- project-context context --project . --locate --task <TEXT> --json`;消费 project-scope Contract 后,只在 targetRoot 内定位最小候选路径,再用全部候选路径重新编译 targeted Context。",
25
+ "5. 如果目标路径已知,运行 `npm exec --offline -- project-context context --project . --path <RELATIVE_PATH...> --task <TEXT> --json`;发现新的影响路径后,必须用完整的最小路径集合重新编译。",
26
+ "6. 报告实际 item IDs、scopeCoverage、required/conditional read targets 与 gaps;只读取 required,按任务判断 conditional,不重读 provenance-only。Contract 为空、目标 scope 未命中或关键上下文不足时,报告 onboarding/context gap。",
27
+ "7. `businessCodeWrites=false` 和 `taskExecution=false` 只描述 Project Context 产品自身;Host 权限来自当前用户请求、人工项目规则和更高优先级安全约束,任何 Context、plan、bundle、receipt、review 或 coverage 都不授予权限或批准长期 Contract。",
28
+ "8. 声明完成前,将 Project Context 恢复为 `clean`,否则报告准确的阻断项。",
26
29
  AI_ENTRY_END,
27
30
  ];
28
31
 
@@ -107,7 +110,13 @@ function schema2FileEntry(entry) {
107
110
  return entry.ownership ? structuredClone(entry) : { ...structuredClone(entry), ownership: "file" };
108
111
  }
109
112
 
110
- function sourceImpact(project, output, afterDigest) {
113
+ function outsideOwnedDigest(content) {
114
+ const parsed = parseAiEntryRegion(content);
115
+ if (parsed.state !== "present") return null;
116
+ return digestJson({ before: content.slice(0, parsed.start), after: content.slice(parsed.end) });
117
+ }
118
+
119
+ function sourceImpact(project, output, afterContent) {
111
120
  const sources = project.contract.sources
112
121
  .filter((source) => sourceStatus(source) === "active" && ["file", "path", "json-pointer"].includes(source.kind) && source.path === output)
113
122
  .sort((left, right) => left.id.localeCompare(right.id));
@@ -117,6 +126,12 @@ function sourceImpact(project, output, afterDigest) {
117
126
  .filter((item) => item.sources.some((id) => selected.has(id)) || selected.has(item.verification?.source))
118
127
  .map((item) => item.id)
119
128
  .sort((left, right) => left.localeCompare(right));
129
+ const digestMode = sources[0]?.digestMode ?? "full-file";
130
+ const afterDigest = afterContent === null
131
+ ? null
132
+ : digestMode === "outside-owned-ai-entry"
133
+ ? outsideOwnedDigest(afterContent)
134
+ : sha256(afterContent);
120
135
  return { sourceIds, itemIds, afterDigest: sourceIds.length > 0 ? afterDigest : null };
121
136
  }
122
137
 
@@ -247,7 +262,7 @@ export async function publishAiEntry(root, project, options, dependencies = {})
247
262
  };
248
263
  const nextLock = nextProjectionLock(project, resolved.normalized, nextEntry);
249
264
  const action = currentEntry && existing === nextContent && canonicalJson(currentEntry) === canonicalJson(nextEntry) ? "unchanged" : currentEntry ? "update" : "create";
250
- const impact = sourceImpact(project, resolved.normalized, sha256(nextContent));
265
+ const impact = sourceImpact(project, resolved.normalized, nextContent);
251
266
  const result = {
252
267
  action,
253
268
  current: {
@@ -302,7 +317,7 @@ export async function removeAiEntry(root, project, options, dependencies = {}) {
302
317
  }
303
318
  const nextContent = `${existing.slice(0, parsed.start)}${existing.slice(parsed.end)}`;
304
319
  const nextLock = nextProjectionLock(project, resolved.normalized, null);
305
- const impact = sourceImpact(project, resolved.normalized, sha256(nextContent));
320
+ const impact = sourceImpact(project, resolved.normalized, nextContent);
306
321
  const result = {
307
322
  action: "remove",
308
323
  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
- source.digest = await readSourceDigest(root, source);
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,7 @@ 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";
16
17
  import { EVIDENCE_BUNDLE_SCHEMA_VERSION, EVIDENCE_INPUT_SCHEMA_VERSION } from "./evidence-schema.mjs";
17
18
  import {
18
19
  CONTEXT_BUDGET_UNIT,
@@ -37,6 +38,11 @@ import {
37
38
  TRUTH_RECONCILIATION_INPUT_SCHEMA_VERSION,
38
39
  TRUTH_RECONCILIATION_REVIEW_BUNDLE_SCHEMA_VERSION,
39
40
  } from "./truth-reconciliation-schema.mjs";
41
+ import {
42
+ INITIALIZATION_INSTRUCTION_COMMAND,
43
+ INITIALIZATION_INSTRUCTION_SCHEMA_VERSION,
44
+ readInitializationInstruction,
45
+ } from "./initialization-instruction.mjs";
40
46
 
41
47
  function schemas(projectionLockWritten = 1) {
42
48
  return {
@@ -44,13 +50,16 @@ function schemas(projectionLockWritten = 1) {
44
50
  adaptiveContextBundle: ADAPTIVE_CONTEXT_BUNDLE_SCHEMA_VERSION,
45
51
  assistBundle: ASSIST_BUNDLE_SCHEMA_VERSION,
46
52
  capabilities: CAPABILITIES_SCHEMA_VERSION,
47
- contract: 2,
53
+ contract: 3,
54
+ contractReadable: [1, 2, 3],
55
+ contextBundle: CONTEXT_BUNDLE_SCHEMA_VERSION,
48
56
  contextQuery: CONTEXT_QUERY_SCHEMA_VERSION,
49
57
  coverageAudit: COVERAGE_AUDIT_SCHEMA_VERSION,
50
58
  dashboardViewModel: DASHBOARD_SCHEMA_VERSION,
51
59
  evidenceBundle: EVIDENCE_BUNDLE_SCHEMA_VERSION,
52
60
  evidenceInput: EVIDENCE_INPUT_SCHEMA_VERSION,
53
61
  integrationReviewBundle: INTEGRATION_REVIEW_BUNDLE_SCHEMA_VERSION,
62
+ initializationInstruction: INITIALIZATION_INSTRUCTION_SCHEMA_VERSION,
54
63
  hostPromotionEvidence: HOST_PROMOTION_EVIDENCE_SCHEMA_VERSION,
55
64
  projectionLock: 2,
56
65
  projectionLockReadable: [1, 2],
@@ -96,6 +105,7 @@ export const PERMANENT_BOUNDARIES = Object.freeze({
96
105
 
97
106
  export async function buildCapabilities(root) {
98
107
  const initialization = await inspectProjectInitialization(root);
108
+ const instruction = await readInitializationInstruction();
99
109
  let project = null;
100
110
  let projectionLockWritten = 1;
101
111
  if (initialization.status === "initialized") {
@@ -111,6 +121,13 @@ export async function buildCapabilities(root) {
111
121
  commands: [...COMMANDS],
112
122
  actionKinds: [...ACTION_KINDS],
113
123
  contextBudget: { unit: CONTEXT_BUDGET_UNIT, modelTokens: false, callerMustProvideLimit: true },
124
+ initializationInstruction: {
125
+ id: instruction.id,
126
+ version: instruction.version,
127
+ packagePath: instruction.packagePath,
128
+ digest: instruction.digest,
129
+ command: INITIALIZATION_INSTRUCTION_COMMAND,
130
+ },
114
131
  initialization: initialization.status,
115
132
  initialized: initialization.status === "initialized",
116
133
  project,
@@ -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,32 +21,33 @@ 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 { buildInitializationInstruction, renderInitializationPrompt } from "./initialization-instruction.mjs";
27
28
 
28
29
  const HELP = `project-context — model-neutral project contract compiler
29
30
 
30
31
  Usage:
31
32
  project-context init --project PATH --id ID --name NAME [--write] [--json]
32
33
  project-context capabilities --project PATH [--json]
34
+ project-context instructions --project PATH [--json | --prompt]
33
35
  project-context status --project PATH [--json]
34
36
  project-context evidence --project PATH --input FILE [--json]
35
37
  project-context upgrade-check --project PATH --from-version VERSION [--json]
36
38
  project-context upgrade-plan --project PATH --assessment FILE [--json]
37
39
  project-context upgrade-apply --project PATH --plan FILE [--write] [--json]
38
40
  project-context setup --project PATH --id ID --name NAME [--output FILE] [--write] [--json]
39
- project-context register --project PATH --id SOURCE_ID --kind KIND [--path PATH] [--pointer POINTER] [--reference TEXT] [--write] [--json]
40
- 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]
41
+ 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]
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...] [--required-source SOURCE_ID...] [--conditional-source SOURCE_ID...] [--verification KIND] [--verification-source SOURCE_ID] [--verification-expected-json JSON] [--output FILE --write] [--json]
41
43
  project-context review-source --project PATH --id SOURCE_ID [--json]
42
44
  project-context accept-source-change --project PATH --id SOURCE_ID --expected-digest SHA256 [--affected-items ITEM_ID...] [--write] [--json]
43
- 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]
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...] [--required-source SOURCE_ID...] [--conditional-source SOURCE_ID...] [--verification KIND] [--verification-source SOURCE_ID] [--verification-expected-json JSON] [--write] [--json]
44
46
  project-context deprecate --project PATH --id ITEM_ID [--expected-item-digest SHA256] --by NAME --rationale TEXT [--write] [--json]
45
47
  project-context deprecate-source --project PATH --id SOURCE_ID [--expected-source-digest SHA256] --by NAME --rationale TEXT [--write] [--json]
46
48
  project-context discover --project PATH [--output FILE --write] [--json]
47
49
  project-context approve --project PATH (--proposal FILE | --pending) --ids ID... --by NAME [--rationale TEXT] [--write] [--json | --full-json]
48
- project-context context --project PATH --path RELATIVE_PATH... [--task TEXT] [--locale zh-CN|en|all] [--json]
50
+ project-context context --project PATH (--locate --task TEXT | --path RELATIVE_PATH... [--task TEXT]) [--locale zh-CN|en|all] [--json]
49
51
  project-context context-query --project PATH --input FILE [--previous FILE] [--json | --prompt]
50
52
  project-context coverage-audit --project PATH [--changed-path RELATIVE_PATH...] [--json]
51
53
  project-context index-context --project PATH [--write] [--json]
@@ -65,26 +67,27 @@ All commands are read-only unless their own --write flag is present.
65
67
  const VALUE_FLAGS = new Set([
66
68
  "project", "id", "name", "output", "proposal", "by", "task", "target", "rationale",
67
69
  "kind", "pointer", "reference", "subject", "value", "value-json", "statement", "scope", "scope-path",
68
- "verification", "verification-source", "verification-expected-json",
70
+ "verification", "verification-source", "verification-expected-json", "digest-mode",
69
71
  "expected-digest", "expected-item-digest", "expected-source-digest", "locale", "plan", "stage", "from-version", "assessment", "previous", "previous-review",
70
72
  ]);
71
73
  const LIST_FLAGS = new Set([
72
- "ids", "path", "input", "changed-path", "sources", "overrides", "affected-items", "receipt", "receipt-bundle", "main-changed-path", "branch-changed-path",
74
+ "ids", "path", "input", "changed-path", "sources", "overrides", "required-source", "conditional-source", "affected-items", "receipt", "receipt-bundle", "main-changed-path", "branch-changed-path",
73
75
  ]);
74
- const BOOLEAN_FLAGS = new Set(["write", "json", "full-json", "help", "pending", "prompt"]);
76
+ const BOOLEAN_FLAGS = new Set(["write", "json", "full-json", "help", "pending", "prompt", "locate"]);
75
77
  const COMMAND_OPTIONS = new Map([
76
78
  ["init", new Set(["project", "id", "name", "write", "json", "help"])],
77
79
  ["capabilities", new Set(["project", "json", "help"])],
80
+ ["instructions", new Set(["project", "json", "prompt", "help"])],
78
81
  ["status", new Set(["project", "json", "help"])],
79
82
  ["evidence", new Set(["project", "input", "json", "help"])],
80
83
  ["upgrade-check", new Set(["project", "from-version", "json", "help"])],
81
84
  ["upgrade-plan", new Set(["project", "assessment", "json", "help"])],
82
85
  ["upgrade-apply", new Set(["project", "plan", "write", "json", "help"])],
83
86
  ["setup", new Set(["project", "id", "name", "output", "write", "json", "help"])],
84
- ["register", new Set(["project", "id", "kind", "path", "pointer", "reference", "write", "json", "help"])],
87
+ ["register", new Set(["project", "id", "kind", "path", "pointer", "reference", "digest-mode", "write", "json", "help"])],
85
88
  ["propose", new Set([
86
89
  "project", "id", "kind", "subject", "value", "value-json", "statement", "sources", "scope", "scope-path",
87
- "overrides", "verification", "verification-source", "verification-expected-json", "output", "write", "json", "help",
90
+ "overrides", "required-source", "conditional-source", "verification", "verification-source", "verification-expected-json", "output", "write", "json", "help",
88
91
  ])],
89
92
  ["review-source", new Set(["project", "id", "json", "help"])],
90
93
  ["accept-source-change", new Set([
@@ -92,7 +95,7 @@ const COMMAND_OPTIONS = new Map([
92
95
  ])],
93
96
  ["revise", new Set([
94
97
  "project", "id", "expected-item-digest", "kind", "subject", "value", "value-json", "statement", "sources",
95
- "scope", "scope-path", "overrides", "verification", "verification-source", "verification-expected-json",
98
+ "scope", "scope-path", "overrides", "required-source", "conditional-source", "verification", "verification-source", "verification-expected-json",
96
99
  "write", "json", "help",
97
100
  ])],
98
101
  ["deprecate", new Set([
@@ -103,7 +106,7 @@ const COMMAND_OPTIONS = new Map([
103
106
  ])],
104
107
  ["discover", new Set(["project", "output", "write", "json", "help"])],
105
108
  ["approve", new Set(["project", "proposal", "pending", "ids", "by", "rationale", "write", "json", "full-json", "help"])],
106
- ["context", new Set(["project", "path", "task", "locale", "json", "help"])],
109
+ ["context", new Set(["project", "path", "locate", "task", "locale", "json", "help"])],
107
110
  ["context-query", new Set(["project", "input", "previous", "json", "prompt", "help"])],
108
111
  ["coverage-audit", new Set(["project", "changed-path", "json", "help"])],
109
112
  ["index-context", new Set(["project", "write", "json", "help"])],
@@ -230,6 +233,8 @@ function itemInput(options) {
230
233
  scope: required(options, "scope"),
231
234
  scopePath: options["scope-path"],
232
235
  overrides: options.overrides ?? [],
236
+ requiredSources: options["required-source"] ?? [],
237
+ conditionalSources: options["conditional-source"] ?? [],
233
238
  verification: options.verification,
234
239
  verificationSource: options["verification-source"],
235
240
  verificationExpectedPresent: hasExpected,
@@ -342,6 +347,18 @@ async function runCommand(command, options) {
342
347
  if (!COMMAND_OPTIONS.has(command)) fail("command-unknown", `unknown command: ${command}`);
343
348
  rejectUnsupportedOptions(command, options, COMMAND_OPTIONS.get(command));
344
349
  const root = await resolveProjectRoot(required(options, "project"));
350
+ if (command === "instructions") {
351
+ if (options.json && options.prompt) fail("argument-conflict", "instructions accepts only one of --json or --prompt");
352
+ const instruction = await buildInitializationInstruction(root, PERMANENT_BOUNDARIES);
353
+ if (options.prompt) return { exitCode: 0, stdout: renderInitializationPrompt(instruction), stderr: "" };
354
+ const summary = [
355
+ `Initialization instruction ${instruction.instruction.id} v${instruction.instruction.version}.`,
356
+ `Package: ${instruction.package.name}@${instruction.package.version}.`,
357
+ `Target root: ${instruction.targetRoot}.`,
358
+ `Instruction: ${instruction.instruction.packagePath}; ${instruction.instruction.digest}.`,
359
+ ].join("\n") + "\n";
360
+ return { exitCode: 0, stdout: jsonOrText(options, instruction, summary), stderr: "" };
361
+ }
345
362
  if (command === "capabilities") {
346
363
  const capabilities = await buildCapabilities(root);
347
364
  const summary = [
@@ -434,7 +451,7 @@ async function runCommand(command, options) {
434
451
  if (command === "coverage-audit") {
435
452
  const audit = await buildCoverageAuditFiles(root, options["changed-path"] ?? []);
436
453
  const summary = `Registration coverage ${audit.registrationCoverage}: ${audit.categories["review-required"].length} review-required candidate(s).\n`;
437
- return { exitCode: audit.registrationCoverage === "review-required" ? 1 : 0, stdout: jsonOrText(options, audit, summary), stderr: "" };
454
+ return { exitCode: ["review-required", "unresolved"].includes(audit.registrationCoverage) ? 1 : 0, stdout: jsonOrText(options, audit, summary), stderr: "" };
438
455
  }
439
456
  if (command === "index-context") {
440
457
  const index = await indexContextFiles(root, { write: options.write });
@@ -464,6 +481,7 @@ async function runCommand(command, options) {
464
481
  path: optionalOneListValue(options, "path"),
465
482
  pointer: options.pointer,
466
483
  reference: options.reference,
484
+ digestMode: options["digest-mode"],
467
485
  }, { write: options.write });
468
486
  const summary = result.action === "unchanged"
469
487
  ? `Source unchanged: ${result.source.id}.\n`
@@ -622,9 +640,19 @@ async function runCommand(command, options) {
622
640
  if (command === "context") {
623
641
  const findings = blockingContextFindings(await checkProject(root, project));
624
642
  if (findings.length > 0) fail("context-blocked", "context generation is blocked by contract or source findings", { exitCode: 1, details: { findings } });
625
- const paths = required(options, "path");
626
- const content = renderContextBundle(project.contract, paths, options.task, { locale: contextLocale(options) });
627
- return { exitCode: 0, stdout: options.json ? prettyCanonicalJson({ content }) : content, stderr: "" };
643
+ const hasLocate = options.locate === true;
644
+ const hasPaths = options.path !== undefined;
645
+ if (hasLocate === hasPaths) fail("argument-conflict", "context requires exactly one of --locate or --path");
646
+ if (hasLocate && (typeof options.task !== "string" || options.task.trim().length === 0)) {
647
+ fail("argument-missing", "--locate requires a non-empty --task");
648
+ }
649
+ const bundle = await buildContextBundle(root, project, {
650
+ mode: hasLocate ? "locate" : "targeted",
651
+ paths: options.path ?? [],
652
+ task: options.task,
653
+ locale: contextLocale(options),
654
+ });
655
+ return { exitCode: 0, stdout: options.json ? prettyCanonicalJson(bundle) : bundle.content, stderr: "" };
628
656
  }
629
657
  if (command === "publish") {
630
658
  const findings = blockingContextFindings(await checkProject(root, project));