frontend-project-context 1.3.1 → 1.7.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/README.md +156 -40
  3. package/UPGRADING.md +55 -1
  4. package/docs/04-PROGRAM-DESIGN.md +34 -4
  5. package/docs/05-ACCEPTANCE-CONTRACT.md +40 -3
  6. package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +67 -22
  7. package/docs/14-FORMAL-RELEASE-READINESS.md +30 -1
  8. package/docs/18-BRANCH-AWARE-STAGED-CONTEXT-DESIGN.md +2 -2
  9. package/docs/19-POST-1.3.1-AI-TAKEOVER-EVIDENCE-AND-UPGRADE-PLAN.md +579 -0
  10. package/docs/20-PHASE-A-AI-TAKEOVER-AND-HEALTH-CLOSURE-DESIGN.md +535 -0
  11. package/docs/21-PHASE-B-EVIDENCE-FEEDBACK-PROTOCOL-DESIGN.md +347 -0
  12. package/docs/22-PHASE-C-TARGET-UPGRADE-PROTOCOL-DESIGN.md +398 -0
  13. package/docs/23-ADAPTIVE-BOUNDED-TASK-CONTEXT-DESIGN.md +432 -0
  14. package/docs/24-A130-REAL-HOST-TARGET-PROJECT-COMPARISON.md +210 -0
  15. package/docs/25-REAL-PROJECT-SOURCE-OF-TRUTH-MAINTENANCE-DESIGN.md +409 -0
  16. package/docs/26-A130-QUALITY-CLOSURE-AND-ADAPTIVE-DELIVERY-REPAIR-DESIGN.md +609 -0
  17. package/docs/README.md +38 -6
  18. package/docs/USER-AND-AI-OPERATION-MANUAL.md +840 -0
  19. package/examples/README.md +29 -2
  20. package/examples/package.json +6 -2
  21. package/migration-manifest.json +110 -0
  22. package/package.json +3 -2
  23. package/schemas/action-plan.schema.json +31 -3
  24. package/schemas/adaptive-context-bundle.schema.json +70 -0
  25. package/schemas/capabilities.schema.json +64 -18
  26. package/schemas/context-query.schema.json +69 -0
  27. package/schemas/coverage-audit.schema.json +32 -0
  28. package/schemas/evidence-bundle.schema.json +64 -0
  29. package/schemas/evidence-input.schema.json +82 -0
  30. package/schemas/host-promotion-evidence.schema.json +33 -0
  31. package/schemas/migration-manifest.schema.json +29 -0
  32. package/schemas/migration-plan.schema.json +32 -0
  33. package/schemas/project-status.schema.json +75 -0
  34. package/schemas/projection-lock.schema.json +48 -0
  35. package/schemas/review-bundle.schema.json +3 -3
  36. package/schemas/routing-index.schema.json +58 -0
  37. package/schemas/truth-reconciliation-input.schema.json +60 -0
  38. package/schemas/truth-reconciliation-review-bundle.schema.json +155 -0
  39. package/schemas/upgrade-assessment.schema.json +48 -0
  40. package/schemas/upgrade-result-bundle.schema.json +35 -0
  41. package/src/project-context/a130-evaluation.mjs +91 -0
  42. package/src/project-context/adaptive-context-schema.mjs +392 -0
  43. package/src/project-context/adaptive-context.mjs +547 -0
  44. package/src/project-context/ai-entry.mjs +320 -0
  45. package/src/project-context/assist.mjs +4 -2
  46. package/src/project-context/capabilities.mjs +62 -17
  47. package/src/project-context/checker.mjs +24 -6
  48. package/src/project-context/cli.mjs +113 -3
  49. package/src/project-context/contract-schema.mjs +30 -16
  50. package/src/project-context/dashboard-model.mjs +4 -4
  51. package/src/project-context/dashboard-renderer.mjs +3 -3
  52. package/src/project-context/discovery.mjs +13 -8
  53. package/src/project-context/evidence-schema.mjs +209 -0
  54. package/src/project-context/evidence.mjs +99 -0
  55. package/src/project-context/exchange-schema.mjs +23 -12
  56. package/src/project-context/exchange.mjs +26 -4
  57. package/src/project-context/maintenance.mjs +4 -4
  58. package/src/project-context/migration-manifest.mjs +168 -0
  59. package/src/project-context/project-status.mjs +157 -0
  60. package/src/project-context/projection-store.mjs +8 -1
  61. package/src/project-context/renderer.mjs +75 -1
  62. package/src/project-context/source-reader.mjs +63 -30
  63. package/src/project-context/task-context.mjs +14 -2
  64. package/src/project-context/truth-reconciliation-schema.mjs +488 -0
  65. package/src/project-context/truth-reconciliation.mjs +543 -0
  66. package/src/project-context/upgrade-schema.mjs +219 -0
  67. package/src/project-context/upgrade.mjs +494 -0
@@ -1,5 +1,5 @@
1
1
  import { canonicalJson, digestJson, sha256 } from "./canonical-json.mjs";
2
- import { describeScope, effectiveItems } from "./scope-compiler.mjs";
2
+ import { describeScope, effectiveItems, scopeApplies } from "./scope-compiler.mjs";
3
3
  import { normalizeRelativePath } from "./path-policy.mjs";
4
4
 
5
5
  const KIND_TITLES = new Map([
@@ -107,6 +107,80 @@ export function renderContextBundle(contract, paths, task, options = {}) {
107
107
  return renderCollectedBundle(collectBundle(contract, paths), contract.sources, task, options);
108
108
  }
109
109
 
110
+ export function renderSelectedContextBundle(contract, paths, itemIds, task, options = {}) {
111
+ const selected = new Set(itemIds);
112
+ const normalizedPaths = paths.map((value) => normalizeRelativePath(value, { allowRoot: true, label: "target path" }));
113
+ const selectedItems = contract.items.filter((item) => item.status === "approved" && selected.has(item.id));
114
+ const sections = normalizedPaths.map((targetPath) => ({
115
+ path: targetPath,
116
+ items: selectedItems.filter((item) => scopeApplies(item.scope, targetPath)).sort((left, right) => left.id.localeCompare(right.id)),
117
+ }));
118
+ const bundle = {
119
+ project: contract.project,
120
+ contractDigest: digestJson(contract),
121
+ paths: normalizedPaths,
122
+ sections,
123
+ itemIds: [...selected].sort(),
124
+ };
125
+ return renderCollectedBundle(bundle, contract.sources, task, options);
126
+ }
127
+
128
+ // Task delivery deliberately has its own renderer. Managed AGENTS/Ruler projections
129
+ // keep using renderCollectedBundle and RENDERER_VERSION 3 byte-for-byte.
130
+ function renderTaskItem(item, locale) {
131
+ const sources = item.sources.map((source) => `\`${source}\``).join(", ");
132
+ const value = canonicalJson(item.value);
133
+ const fence = fenceFor(value);
134
+ const subject = item.subject === item.id ? "" : `Subject: \`${item.subject}\`; `;
135
+ return `- **${item.id}** — ${localizedStatement(item.statement, locale)}\n - ${subject}Scope: \`${describeScope(item.scope)}\`; source IDs: ${sources}\n - Value (canonical JSON):\n\n ${fence}json\n ${value}\n ${fence}`;
136
+ }
137
+
138
+ export function renderTaskContextBundle(contract, paths, itemIds, task, options = {}) {
139
+ const locale = options.locale ?? "zh-CN";
140
+ const selected = new Set(itemIds);
141
+ const normalizedPaths = paths.map((value) => normalizeRelativePath(value, { allowRoot: true, label: "target path" }));
142
+ const effectiveByPath = normalizedPaths.map((targetPath) => ({
143
+ path: targetPath,
144
+ items: effectiveItems(contract.items, targetPath)
145
+ .filter((item) => selected.has(item.id))
146
+ .sort((left, right) => left.id.localeCompare(right.id)),
147
+ }));
148
+ const commonIds = effectiveByPath.length === 0 ? new Set() : new Set(effectiveByPath[0].items.map((item) => item.id));
149
+ for (const section of effectiveByPath.slice(1)) {
150
+ const ids = new Set(section.items.map((item) => item.id));
151
+ for (const id of [...commonIds]) if (!ids.has(id)) commonIds.delete(id);
152
+ }
153
+ const common = effectiveByPath[0]?.items.filter((item) => commonIds.has(item.id)) ?? [];
154
+ const lines = [
155
+ "# Project Context Task Delivery",
156
+ "",
157
+ `Project: **${contract.project.name}** (\`${contract.project.id}\`)`,
158
+ `Contract digest: \`${digestJson(contract)}\``,
159
+ ];
160
+ const fence = fenceFor(task);
161
+ lines.push("", "## Task constraint", "", fence, String(task), fence);
162
+ const renderKinds = (items) => {
163
+ for (const [kind, title] of KIND_TITLES) {
164
+ const entries = items.filter((item) => item.kind === kind);
165
+ if (entries.length > 0) lines.push("", `### ${title}`, "", ...entries.map((item) => renderTaskItem(item, locale)));
166
+ }
167
+ };
168
+ if (common.length > 0) {
169
+ lines.push("", "## Applies to all targets");
170
+ renderKinds(common);
171
+ }
172
+ for (const section of effectiveByPath) {
173
+ const delta = section.items.filter((item) => !commonIds.has(item.id));
174
+ lines.push("", `## Target delta: \`${section.path}\``);
175
+ if (delta.length === 0) lines.push("", "No additional approved contract items apply.");
176
+ else renderKinds(delta);
177
+ }
178
+ lines.push("", "## Source IDs", "");
179
+ const sourceIds = [...new Set(effectiveByPath.flatMap((section) => section.items.flatMap((item) => item.sources)))].sort();
180
+ lines.push(sourceIds.length === 0 ? "No sources selected." : sourceIds.map((id) => `- \`${id}\``).join("\n"));
181
+ return `${lines.join("\n")}\n`;
182
+ }
183
+
110
184
  export function renderProjection(contract, paths, target) {
111
185
  const bundle = collectBundle(contract, paths);
112
186
  const body = renderCollectedBundle(bundle, contract.sources);
@@ -33,7 +33,20 @@ export function readJsonPointer(value, pointer) {
33
33
  return current;
34
34
  }
35
35
 
36
- async function digestDirectory(absolute) {
36
+ export function createSourceReadContext() {
37
+ return {
38
+ cache: new Map(),
39
+ metrics: { sourceDigestReads: 0, sourceBodyReads: 0, sourceIdentityReads: 0 },
40
+ };
41
+ }
42
+
43
+ async function cached(context, key, read) {
44
+ if (!context?.cache) return read();
45
+ if (!context.cache.has(key)) context.cache.set(key, Promise.resolve().then(read));
46
+ return context.cache.get(key);
47
+ }
48
+
49
+ async function digestDirectory(absolute, context) {
37
50
  const entries = [];
38
51
  async function visit(directory, prefix) {
39
52
  const children = (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
@@ -46,7 +59,11 @@ async function digestDirectory(absolute) {
46
59
  } else if (child.isDirectory()) {
47
60
  await visit(full, relative);
48
61
  } else if (child.isFile()) {
49
- entries.push([relative, sha256(await readFile(full))]);
62
+ const body = await cached(context, `body:${full}`, async () => {
63
+ if (context?.metrics) context.metrics.sourceBodyReads += 1;
64
+ return readFile(full);
65
+ });
66
+ entries.push([relative, sha256(body)]);
50
67
  }
51
68
  }
52
69
  }
@@ -54,42 +71,58 @@ async function digestDirectory(absolute) {
54
71
  return sha256(canonicalJson(entries));
55
72
  }
56
73
 
57
- export async function digestPath(projectRoot, relativePath) {
58
- const { absolute } = await resolveExistingInside(projectRoot, relativePath);
59
- const info = await stat(absolute);
60
- if (info.isDirectory()) return digestDirectory(absolute);
61
- if (!info.isFile()) fail("source-unsupported", `source is not a regular file or directory: ${relativePath}`);
62
- return sha256(await readFile(absolute));
74
+ export async function digestPath(projectRoot, relativePath, context) {
75
+ return cached(context, `digest:${relativePath}`, async () => {
76
+ if (context?.metrics) context.metrics.sourceDigestReads += 1;
77
+ const { absolute } = await resolveExistingInside(projectRoot, relativePath);
78
+ const info = await stat(absolute);
79
+ if (info.isDirectory()) return digestDirectory(absolute, context);
80
+ if (!info.isFile()) fail("source-unsupported", `source is not a regular file or directory: ${relativePath}`);
81
+ const body = await cached(context, `body:${absolute}`, async () => {
82
+ if (context?.metrics) context.metrics.sourceBodyReads += 1;
83
+ return readFile(absolute);
84
+ });
85
+ return sha256(body);
86
+ });
63
87
  }
64
88
 
65
- export async function digestPathIdentity(projectRoot, relativePath) {
66
- const { absolute } = await resolveExistingInside(projectRoot, relativePath);
67
- const info = await stat(absolute);
68
- const kind = info.isDirectory() ? "directory" : info.isFile() ? "file" : null;
69
- if (!kind) fail("source-unsupported", `source is not a regular file or directory: ${relativePath}`);
70
- return sha256(canonicalJson({ kind }));
89
+ export async function digestPathIdentity(projectRoot, relativePath, context) {
90
+ return cached(context, `identity:${relativePath}`, async () => {
91
+ if (context?.metrics) context.metrics.sourceIdentityReads += 1;
92
+ const { absolute } = await resolveExistingInside(projectRoot, relativePath);
93
+ const info = await stat(absolute);
94
+ const kind = info.isDirectory() ? "directory" : info.isFile() ? "file" : null;
95
+ if (!kind) fail("source-unsupported", `source is not a regular file or directory: ${relativePath}`);
96
+ return sha256(canonicalJson({ kind }));
97
+ });
71
98
  }
72
99
 
73
- export async function readSourceDigest(projectRoot, source) {
100
+ export async function readSourceDigest(projectRoot, source, context) {
74
101
  if (sourceStatus(source) === "deprecated") return null;
75
102
  if (source.kind === "human-decision" || source.kind === "external-reference") return null;
76
- if (source.kind === "path") return digestPathIdentity(projectRoot, source.path);
77
- if (source.kind === "file") return digestPath(projectRoot, source.path);
78
- return sha256(canonicalJson(await readJsonSourceValue(projectRoot, source)));
103
+ if (source.kind === "path") return digestPathIdentity(projectRoot, source.path, context);
104
+ if (source.kind === "file") return digestPath(projectRoot, source.path, context);
105
+ return sha256(canonicalJson(await readJsonSourceValue(projectRoot, source, context)));
79
106
  }
80
107
 
81
- async function readJsonSourceValue(projectRoot, source) {
82
- const { absolute } = await resolveExistingInside(projectRoot, source.path);
83
- let parsed;
84
- try {
85
- parsed = JSON.parse(await readFile(absolute, "utf8"));
86
- } catch (error) {
87
- fail("source-json-invalid", `cannot parse JSON source: ${source.path}`, { cause: error });
88
- }
89
- return readJsonPointer(parsed, source.pointer);
108
+ async function readJsonSourceValue(projectRoot, source, context) {
109
+ return cached(context, `json:${source.path}:${source.pointer}`, async () => {
110
+ const { absolute } = await resolveExistingInside(projectRoot, source.path);
111
+ let parsed;
112
+ try {
113
+ const body = await cached(context, `body:${absolute}`, async () => {
114
+ if (context?.metrics) context.metrics.sourceBodyReads += 1;
115
+ return readFile(absolute, "utf8");
116
+ });
117
+ parsed = JSON.parse(body);
118
+ } catch (error) {
119
+ fail("source-json-invalid", `cannot parse JSON source: ${source.path}`, { cause: error });
120
+ }
121
+ return readJsonPointer(parsed, source.pointer);
122
+ });
90
123
  }
91
124
 
92
- export async function verifyItem(projectRoot, item, sourceMap) {
125
+ export async function verifyItem(projectRoot, item, sourceMap, context) {
93
126
  const verification = item.verification;
94
127
  if (!verification || verification.kind === "none") return null;
95
128
  const source = sourceMap.get(verification.source);
@@ -106,7 +139,7 @@ export async function verifyItem(projectRoot, item, sourceMap) {
106
139
  if (source.kind !== "json-pointer") {
107
140
  return { code: "verification-source-incompatible", item: item.id, source: source.id };
108
141
  }
109
- const actualValue = await readJsonSourceValue(projectRoot, source);
142
+ const actualValue = await readJsonSourceValue(projectRoot, source, context);
110
143
  if (canonicalJson(actualValue) !== canonicalJson(verification.expected)) {
111
144
  return { code: "verification-failed", item: item.id, source: source.id, expected: verification.expected, actual: actualValue };
112
145
  }
@@ -115,7 +148,7 @@ export async function verifyItem(projectRoot, item, sourceMap) {
115
148
  if (verification.kind !== "path-digest") {
116
149
  return { code: "verification-kind-unsupported", item: item.id, source: source.id };
117
150
  }
118
- const actual = await digestPath(projectRoot, source.path);
151
+ const actual = await digestPath(projectRoot, source.path, context);
119
152
  const expected = verification.expected ?? source.digest;
120
153
  if (actual !== expected) return { code: "verification-failed", item: item.id, source: source.id, expected, actual };
121
154
  return null;
@@ -1,4 +1,5 @@
1
1
  import { canonicalJson, digestJson } from "./canonical-json.mjs";
2
+ import { selectAdaptiveItems } from "./adaptive-context.mjs";
2
3
  import { blockingContextFindings, checkProject } from "./checker.mjs";
3
4
  import { ProjectContextError, fail } from "./errors.mjs";
4
5
  import { readJsonFile } from "./io.mjs";
@@ -91,8 +92,18 @@ function normalizedReceipts(receiptInputs, plan) {
91
92
  return byStage;
92
93
  }
93
94
 
94
- function selectedItems(contract, targetPaths, findings) {
95
+ function selectedItems(contract, targetPaths, findings, adaptiveText) {
95
96
  const byId = new Map();
97
+ if (adaptiveText !== undefined) {
98
+ const adaptiveFindings = [];
99
+ const selection = selectAdaptiveItems(contract, null, {
100
+ task: { text: adaptiveText, topics: [], paths: targetPaths, changedPaths: [], itemIds: [] },
101
+ level: "initial",
102
+ }, { findings: adaptiveFindings });
103
+ findings.push(...adaptiveFindings);
104
+ for (const item of selection.selected) byId.set(item.id, item);
105
+ }
106
+ if (adaptiveText === undefined) {
96
107
  for (const targetPath of targetPaths) {
97
108
  try {
98
109
  for (const item of effectiveItems(contract.items, targetPath)) byId.set(item.id, item);
@@ -102,6 +113,7 @@ function selectedItems(contract, targetPaths, findings) {
102
113
  for (const item of contract.items.filter((entry) => entry.status === "approved" && scopeApplies(entry.scope, targetPath))) byId.set(item.id, item);
103
114
  }
104
115
  }
116
+ }
105
117
  return [...byId.values()].sort((left, right) => left.id.localeCompare(right.id)).map((item) => ({
106
118
  id: item.id,
107
119
  kind: item.kind,
@@ -205,7 +217,7 @@ async function buildStageContextBundleCore(root, project, plan, options, receipt
205
217
  if (receipt.status !== "completed") findings.push(finding("stage-dependency-blocked", "blocked", { stageId: stage.id, dependencyStageId: dependencyId }));
206
218
  }
207
219
  const targetPaths = uniqueSorted([...stage.paths, ...changedPaths]);
208
- const contractItems = selectedItems(project.contract, targetPaths, findings);
220
+ const contractItems = selectedItems(project.contract, targetPaths, findings, `${plan.task.goal}\n${stage.objective}`);
209
221
  const readTargets = mergeReadTargets([
210
222
  ...stage.paths.map((entry) => ({ path: entry, reason: "stage-scope" })),
211
223
  ...changedPaths.map((entry) => ({ path: entry, reason: "host-changed-path-signal" })),