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.
- package/CHANGELOG.md +51 -2
- package/README.md +156 -40
- package/UPGRADING.md +55 -1
- package/docs/04-PROGRAM-DESIGN.md +34 -4
- package/docs/05-ACCEPTANCE-CONTRACT.md +40 -3
- package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +67 -22
- package/docs/14-FORMAL-RELEASE-READINESS.md +30 -1
- package/docs/18-BRANCH-AWARE-STAGED-CONTEXT-DESIGN.md +2 -2
- package/docs/19-POST-1.3.1-AI-TAKEOVER-EVIDENCE-AND-UPGRADE-PLAN.md +579 -0
- package/docs/20-PHASE-A-AI-TAKEOVER-AND-HEALTH-CLOSURE-DESIGN.md +535 -0
- package/docs/21-PHASE-B-EVIDENCE-FEEDBACK-PROTOCOL-DESIGN.md +347 -0
- package/docs/22-PHASE-C-TARGET-UPGRADE-PROTOCOL-DESIGN.md +398 -0
- package/docs/23-ADAPTIVE-BOUNDED-TASK-CONTEXT-DESIGN.md +432 -0
- package/docs/24-A130-REAL-HOST-TARGET-PROJECT-COMPARISON.md +210 -0
- package/docs/25-REAL-PROJECT-SOURCE-OF-TRUTH-MAINTENANCE-DESIGN.md +409 -0
- package/docs/26-A130-QUALITY-CLOSURE-AND-ADAPTIVE-DELIVERY-REPAIR-DESIGN.md +609 -0
- package/docs/README.md +38 -6
- package/docs/USER-AND-AI-OPERATION-MANUAL.md +840 -0
- package/examples/README.md +29 -2
- package/examples/package.json +6 -2
- package/migration-manifest.json +110 -0
- package/package.json +3 -2
- package/schemas/action-plan.schema.json +31 -3
- package/schemas/adaptive-context-bundle.schema.json +70 -0
- package/schemas/capabilities.schema.json +64 -18
- package/schemas/context-query.schema.json +69 -0
- package/schemas/coverage-audit.schema.json +32 -0
- package/schemas/evidence-bundle.schema.json +64 -0
- package/schemas/evidence-input.schema.json +82 -0
- package/schemas/host-promotion-evidence.schema.json +33 -0
- package/schemas/migration-manifest.schema.json +29 -0
- package/schemas/migration-plan.schema.json +32 -0
- package/schemas/project-status.schema.json +75 -0
- package/schemas/projection-lock.schema.json +48 -0
- package/schemas/review-bundle.schema.json +3 -3
- package/schemas/routing-index.schema.json +58 -0
- package/schemas/truth-reconciliation-input.schema.json +60 -0
- package/schemas/truth-reconciliation-review-bundle.schema.json +155 -0
- package/schemas/upgrade-assessment.schema.json +48 -0
- package/schemas/upgrade-result-bundle.schema.json +35 -0
- package/src/project-context/a130-evaluation.mjs +91 -0
- package/src/project-context/adaptive-context-schema.mjs +392 -0
- package/src/project-context/adaptive-context.mjs +547 -0
- package/src/project-context/ai-entry.mjs +320 -0
- package/src/project-context/assist.mjs +4 -2
- package/src/project-context/capabilities.mjs +62 -17
- package/src/project-context/checker.mjs +24 -6
- package/src/project-context/cli.mjs +113 -3
- package/src/project-context/contract-schema.mjs +30 -16
- package/src/project-context/dashboard-model.mjs +4 -4
- package/src/project-context/dashboard-renderer.mjs +3 -3
- package/src/project-context/discovery.mjs +13 -8
- package/src/project-context/evidence-schema.mjs +209 -0
- package/src/project-context/evidence.mjs +99 -0
- package/src/project-context/exchange-schema.mjs +23 -12
- package/src/project-context/exchange.mjs +26 -4
- package/src/project-context/maintenance.mjs +4 -4
- package/src/project-context/migration-manifest.mjs +168 -0
- package/src/project-context/project-status.mjs +157 -0
- package/src/project-context/projection-store.mjs +8 -1
- package/src/project-context/renderer.mjs +75 -1
- package/src/project-context/source-reader.mjs +63 -30
- package/src/project-context/task-context.mjs +14 -2
- package/src/project-context/truth-reconciliation-schema.mjs +488 -0
- package/src/project-context/truth-reconciliation.mjs +543 -0
- package/src/project-context/upgrade-schema.mjs +219 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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" })),
|