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
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { canonicalJson, digestJson, prettyCanonicalJson, sha256 } from "./canonical-json.mjs";
|
|
4
|
+
import { blockingContextFindings, checkProject } from "./checker.mjs";
|
|
5
|
+
import { sourceStatus } from "./contract-schema.mjs";
|
|
6
|
+
import { discoverProject } from "./discovery.mjs";
|
|
7
|
+
import { ProjectContextError, fail } from "./errors.mjs";
|
|
8
|
+
import { atomicWriteFile, readJsonFile } from "./io.mjs";
|
|
9
|
+
import { normalizeRelativePath, resolveExistingInside, resolveWritableInside } from "./path-policy.mjs";
|
|
10
|
+
import { loadProject } from "./project-store.mjs";
|
|
11
|
+
import { renderTaskContextBundle } from "./renderer.mjs";
|
|
12
|
+
import { effectiveItems, findConflicts, scopeApplies, validateOverrides } from "./scope-compiler.mjs";
|
|
13
|
+
import { createSourceReadContext, readSourceDigest, verifyItem } from "./source-reader.mjs";
|
|
14
|
+
import {
|
|
15
|
+
ADAPTIVE_CONTEXT_BUNDLE_SCHEMA_VERSION,
|
|
16
|
+
ADAPTIVE_SELECTOR_VERSION,
|
|
17
|
+
COVERAGE_AUDIT_SCHEMA_VERSION,
|
|
18
|
+
ROUTING_INDEX_PATH,
|
|
19
|
+
ROUTING_INDEX_SCHEMA_VERSION,
|
|
20
|
+
sealArtifact,
|
|
21
|
+
validateAdaptiveContextBundle,
|
|
22
|
+
validateContextQuery,
|
|
23
|
+
validateRoutingIndex,
|
|
24
|
+
} from "./adaptive-context-schema.mjs";
|
|
25
|
+
|
|
26
|
+
const LOCAL_SOURCE_KINDS = new Set(["file", "path", "json-pointer"]);
|
|
27
|
+
const MANDATORY_KINDS = new Set(["policy", "validation-description"]);
|
|
28
|
+
const ROUTED_KINDS = new Set(["fact", "reference"]);
|
|
29
|
+
const COVERAGE_SUBJECT = "project.registration-coverage";
|
|
30
|
+
const RETRIEVAL_SUBJECT = "project.context-retrieval";
|
|
31
|
+
const EXCLUDED_BODIES = Object.freeze(["chat-history", "git-diffs", "source-bodies", "task-path-bodies", "verification-logs"]);
|
|
32
|
+
const ROUTING_STOP_TERMS = new Set(["a", "an", "and", "apply", "change", "for", "in", "of", "on", "project", "rule", "the", "to", "update", "use"]);
|
|
33
|
+
|
|
34
|
+
function uniqueSorted(values) { return [...new Set(values)].sort((left, right) => left.localeCompare(right)); }
|
|
35
|
+
function snapshots(project) {
|
|
36
|
+
return { contract: project.contractDigest, sourcesLock: project.sourcesLockDigest, projectionsLock: project.projectionsLockDigest };
|
|
37
|
+
}
|
|
38
|
+
function stableFindings(findings) {
|
|
39
|
+
const byJson = new Map();
|
|
40
|
+
for (const entry of findings) byJson.set(canonicalJson(entry), entry);
|
|
41
|
+
return [...byJson.values()].sort((left, right) => canonicalJson(left).localeCompare(canonicalJson(right)));
|
|
42
|
+
}
|
|
43
|
+
function semanticParts(value, output = []) {
|
|
44
|
+
if (typeof value === "string") output.push(value);
|
|
45
|
+
else if (Array.isArray(value)) value.forEach((entry) => semanticParts(entry, output));
|
|
46
|
+
else if (value && typeof value === "object") {
|
|
47
|
+
for (const [key, entry] of Object.entries(value)) { output.push(key); semanticParts(entry, output); }
|
|
48
|
+
}
|
|
49
|
+
return output;
|
|
50
|
+
}
|
|
51
|
+
function tokenize(...values) {
|
|
52
|
+
const terms = new Set();
|
|
53
|
+
for (const value of values.flat(Infinity)) {
|
|
54
|
+
if (value === undefined || value === null) continue;
|
|
55
|
+
const expanded = String(value).normalize("NFKC").replace(/([a-z0-9])([A-Z])/gu, "$1 $2").toLowerCase();
|
|
56
|
+
for (const term of expanded.split(/[^a-z0-9\u3400-\u9fff]+/u)) {
|
|
57
|
+
if (term.length > 1 && !ROUTING_STOP_TERMS.has(term)) terms.add(term);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return [...terms].sort((left, right) => left.localeCompare(right));
|
|
61
|
+
}
|
|
62
|
+
function sourceLocator(source) {
|
|
63
|
+
if (source.kind === "json-pointer") return { path: source.path, pointer: source.pointer };
|
|
64
|
+
if (LOCAL_SOURCE_KINDS.has(source.kind)) return { path: source.path };
|
|
65
|
+
return { reference: source.reference };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function retrievalProfile(contract) {
|
|
69
|
+
const profiles = contract.items.filter((item) => item.status === "approved" && item.kind === "policy" && item.subject === RETRIEVAL_SUBJECT);
|
|
70
|
+
if (profiles.length === 0) return { item: null, rules: [], aliases: new Map() };
|
|
71
|
+
if (profiles.length > 1) fail("retrieval-profile-conflict", `multiple approved ${RETRIEVAL_SUBJECT} items are active`);
|
|
72
|
+
const item = profiles[0];
|
|
73
|
+
const value = item.value;
|
|
74
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => key !== "rules") || !Array.isArray(value.rules)) {
|
|
75
|
+
fail("retrieval-profile-invalid", `${item.id} must contain only a rules array`);
|
|
76
|
+
}
|
|
77
|
+
const approved = new Map(contract.items.filter((entry) => entry.status === "approved").map((entry) => [entry.id, entry]));
|
|
78
|
+
const rules = value.rules.map((rule, index) => {
|
|
79
|
+
if (!rule || typeof rule !== "object" || Array.isArray(rule) || Object.keys(rule).some((key) => !["aliases", "itemId", "requires"].includes(key))) {
|
|
80
|
+
fail("retrieval-profile-invalid", `${item.id} rules[${index}] is invalid`);
|
|
81
|
+
}
|
|
82
|
+
if (!approved.has(rule.itemId) || !Array.isArray(rule.requires) || !Array.isArray(rule.aliases)) fail("retrieval-profile-invalid", `${item.id} rules[${index}] references invalid items`);
|
|
83
|
+
const requires = uniqueSorted(rule.requires);
|
|
84
|
+
const aliases = uniqueSorted(rule.aliases);
|
|
85
|
+
if (requires.length !== rule.requires.length || aliases.length !== rule.aliases.length || requires.includes(rule.itemId)) fail("retrieval-profile-invalid", `${item.id} rules[${index}] is not stable or contains a self dependency`);
|
|
86
|
+
for (const required of requires) if (!approved.has(required)) fail("retrieval-profile-invalid", `${item.id} requires unknown approved item ${required}`);
|
|
87
|
+
return { itemId: rule.itemId, requires, aliases };
|
|
88
|
+
}).sort((left, right) => left.itemId.localeCompare(right.itemId));
|
|
89
|
+
const dependencyMap = new Map(rules.map((rule) => [rule.itemId, rule.requires]));
|
|
90
|
+
const visit = (id, stack = []) => {
|
|
91
|
+
if (stack.includes(id)) fail("retrieval-profile-invalid", `${item.id} contains a dependency cycle`);
|
|
92
|
+
for (const next of dependencyMap.get(id) ?? []) visit(next, [...stack, id]);
|
|
93
|
+
};
|
|
94
|
+
for (const id of dependencyMap.keys()) visit(id);
|
|
95
|
+
return { item, rules, aliases: new Map(rules.map((rule) => [rule.itemId, rule.aliases])) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function itemIndexRecord(item, aliases = []) {
|
|
99
|
+
return {
|
|
100
|
+
id: item.id,
|
|
101
|
+
kind: item.kind,
|
|
102
|
+
subject: item.subject,
|
|
103
|
+
scope: structuredClone(item.scope),
|
|
104
|
+
sourceIds: [...item.sources].sort(),
|
|
105
|
+
terms: tokenize(item.id, item.subject, item.statement, semanticParts(item.value), aliases),
|
|
106
|
+
itemDigest: digestJson(item),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function buildRoutingIndex(project) {
|
|
110
|
+
const approved = project.contract.items.filter((item) => item.status === "approved");
|
|
111
|
+
const profile = retrievalProfile(project.contract);
|
|
112
|
+
const items = approved.map((item) => itemIndexRecord(item, profile.aliases.get(item.id) ?? [])).sort((left, right) => left.id.localeCompare(right.id));
|
|
113
|
+
const sources = project.contract.sources.filter((source) => sourceStatus(source) === "active").map((source) => ({
|
|
114
|
+
id: source.id,
|
|
115
|
+
kind: source.kind,
|
|
116
|
+
locator: sourceLocator(source),
|
|
117
|
+
itemIds: approved.filter((item) => item.sources.includes(source.id) || item.verification?.source === source.id).map((item) => item.id).sort(),
|
|
118
|
+
sourceDigest: digestJson(source),
|
|
119
|
+
})).sort((left, right) => left.id.localeCompare(right.id));
|
|
120
|
+
return sealArtifact({ schemaVersion: 2, kind: "routing-index", projectId: project.contract.project.id, selectorVersion: 2, snapshots: snapshots(project), items, sources }, "indexDigest");
|
|
121
|
+
}
|
|
122
|
+
async function routingIndexForQuery(root, project) {
|
|
123
|
+
const rebuilt = buildRoutingIndex(project);
|
|
124
|
+
try {
|
|
125
|
+
const resolved = await resolveExistingInside(root, ROUTING_INDEX_PATH);
|
|
126
|
+
let stored;
|
|
127
|
+
try { stored = validateRoutingIndex(await readJsonFile(resolved.absolute, "routing index")); }
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (!(error instanceof ProjectContextError)) throw error;
|
|
130
|
+
return { index: rebuilt, state: "invalid-rebuilt-in-memory", rejectedDigest: null };
|
|
131
|
+
}
|
|
132
|
+
if (canonicalJson(stored.snapshots) !== canonicalJson(rebuilt.snapshots) || stored.indexDigest !== rebuilt.indexDigest) return { index: rebuilt, state: "stale-rebuilt-in-memory", rejectedDigest: stored.indexDigest };
|
|
133
|
+
return { index: stored, state: "current", rejectedDigest: null };
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error?.code !== "source-missing") throw error;
|
|
136
|
+
return { index: rebuilt, state: "missing-rebuilt-in-memory", rejectedDigest: null };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export async function indexContext(root, project, options = {}) {
|
|
140
|
+
const index = buildRoutingIndex(project);
|
|
141
|
+
if (options.write) {
|
|
142
|
+
const resolved = await resolveWritableInside(root, ROUTING_INDEX_PATH, { createParent: true });
|
|
143
|
+
await atomicWriteFile(resolved.absolute, prettyCanonicalJson(index));
|
|
144
|
+
}
|
|
145
|
+
return index;
|
|
146
|
+
}
|
|
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
|
+
}
|
|
166
|
+
export async function buildCoverageAudit(root, project, changedPaths = []) {
|
|
167
|
+
const profile = coverageProfile(project.contract);
|
|
168
|
+
const proposal = await discoverProject(root, project.contract);
|
|
169
|
+
const registered = project.contract.sources.filter((source) => sourceStatus(source) === "active" && source.path).map((source) => source.path);
|
|
170
|
+
const registeredPaths = new Set(registered);
|
|
171
|
+
const byPath = new Map();
|
|
172
|
+
for (const source of proposal.sources.filter((entry) => entry.path)) byPath.set(source.path, { path: source.path, origin: "conservative-discovery", proposedSourceId: source.id });
|
|
173
|
+
for (const changedPath of changedPaths) {
|
|
174
|
+
const normalized = normalizeRelativePath(changedPath, { allowRoot: true, label: "changed path" });
|
|
175
|
+
if (!byPath.has(normalized)) byPath.set(normalized, { path: normalized, origin: "host-changed-path-signal" });
|
|
176
|
+
}
|
|
177
|
+
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": [] };
|
|
179
|
+
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");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function effectiveByTarget(contract, paths, findings) {
|
|
185
|
+
const result = new Map();
|
|
186
|
+
for (const target of paths) {
|
|
187
|
+
try { result.set(target, effectiveItems(contract.items, target)); }
|
|
188
|
+
catch (error) {
|
|
189
|
+
if (!(error instanceof ProjectContextError) || error.code !== "contract-conflict") throw error;
|
|
190
|
+
findings.push({ code: "contract-conflict", severity: "blocked", path: target, conflicts: error.details?.findings ?? [] });
|
|
191
|
+
result.set(target, contract.items.filter((item) => item.status === "approved" && scopeApplies(item.scope, target)));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
function unionEffective(byTarget) {
|
|
197
|
+
const byId = new Map();
|
|
198
|
+
for (const items of byTarget.values()) for (const item of items) byId.set(item.id, item);
|
|
199
|
+
return [...byId.values()].sort((left, right) => left.id.localeCompare(right.id));
|
|
200
|
+
}
|
|
201
|
+
function queryTerms(query) { return tokenize(query.task.text, query.task.topics); }
|
|
202
|
+
function matchReasons(itemRecord, terms) {
|
|
203
|
+
const matches = itemRecord?.terms.filter((term) => terms.includes(term)) ?? [];
|
|
204
|
+
return matches.map((term) => `term:${term}`);
|
|
205
|
+
}
|
|
206
|
+
function resolvesOverride(item, targetId, byId, seen = new Set()) {
|
|
207
|
+
if (item.overrides.includes(targetId)) return true;
|
|
208
|
+
if (seen.has(item.id)) return false;
|
|
209
|
+
seen.add(item.id);
|
|
210
|
+
return item.overrides.some((id) => byId.has(id) && resolvesOverride(byId.get(id), targetId, byId, seen));
|
|
211
|
+
}
|
|
212
|
+
function effectiveResolution(requiredId, targetItems, byId) {
|
|
213
|
+
const direct = targetItems.find((item) => item.id === requiredId);
|
|
214
|
+
if (direct) return direct;
|
|
215
|
+
const replacements = targetItems.filter((item) => resolvesOverride(item, requiredId, byId));
|
|
216
|
+
return replacements.length === 1 ? replacements[0] : null;
|
|
217
|
+
}
|
|
218
|
+
function closeDependencies(contract, profile, byTarget, selected, reasons, findings) {
|
|
219
|
+
const byId = new Map(contract.items.filter((item) => item.status === "approved").map((item) => [item.id, item]));
|
|
220
|
+
const rules = new Map(profile.rules.map((rule) => [rule.itemId, rule.requires]));
|
|
221
|
+
let changed = true;
|
|
222
|
+
while (changed) {
|
|
223
|
+
changed = false;
|
|
224
|
+
for (const requiringId of [...selected]) {
|
|
225
|
+
for (const requiredId of rules.get(requiringId) ?? []) {
|
|
226
|
+
const requiringTargets = [...byTarget.entries()].filter(([, items]) => items.some((item) => item.id === requiringId));
|
|
227
|
+
if (requiringTargets.length === 0) continue;
|
|
228
|
+
for (const [target, items] of requiringTargets) {
|
|
229
|
+
const resolved = effectiveResolution(requiredId, items, byId);
|
|
230
|
+
if (!resolved) {
|
|
231
|
+
findings.push({ code: "declared-dependency-missing", severity: "blocked", itemId: requiringId, requiredItemId: requiredId, path: target });
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (!selected.has(resolved.id)) {
|
|
235
|
+
selected.add(resolved.id);
|
|
236
|
+
reasons.set(resolved.id, uniqueSorted([...(reasons.get(resolved.id) ?? []), `dependency-of:${requiringId}`]));
|
|
237
|
+
changed = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function selectAdaptiveItems(contract, index, query, options = {}) {
|
|
246
|
+
const findings = options.findings ?? [];
|
|
247
|
+
const byTarget = effectiveByTarget(contract, query.task.paths, findings);
|
|
248
|
+
const applicable = unionEffective(byTarget);
|
|
249
|
+
const applicableIds = new Set(applicable.map((item) => item.id));
|
|
250
|
+
const approvedById = new Map(contract.items.filter((item) => item.status === "approved").map((item) => [item.id, item]));
|
|
251
|
+
const allById = new Map(contract.items.map((item) => [item.id, item]));
|
|
252
|
+
const indexById = new Map((index?.items ?? []).map((item) => [item.id, item]));
|
|
253
|
+
const profile = retrievalProfile(contract);
|
|
254
|
+
const selected = new Set();
|
|
255
|
+
const reasons = new Map();
|
|
256
|
+
for (const item of applicable) if (MANDATORY_KINDS.has(item.kind)) { selected.add(item.id); reasons.set(item.id, ["mandatory-kind"]); }
|
|
257
|
+
for (const explicitId of query.task.itemIds) {
|
|
258
|
+
const approved = approvedById.get(explicitId);
|
|
259
|
+
if (!approved) findings.push({ code: "requested-item-unavailable", severity: "blocked", itemId: explicitId });
|
|
260
|
+
else if (!applicableIds.has(explicitId)) {
|
|
261
|
+
const replaced = applicable.some((item) => resolvesOverride(item, explicitId, approvedById));
|
|
262
|
+
findings.push({ code: replaced ? "requested-item-not-effective" : "requested-item-out-of-scope", severity: "blocked", itemId: explicitId });
|
|
263
|
+
} else { selected.add(explicitId); reasons.set(explicitId, ["explicit-item-id"]); }
|
|
264
|
+
}
|
|
265
|
+
if (query.level === "complete") {
|
|
266
|
+
for (const item of applicable) { selected.add(item.id); reasons.set(item.id, uniqueSorted([...(reasons.get(item.id) ?? []), "complete-baseline"])); }
|
|
267
|
+
} else if (query.level === "expanded") {
|
|
268
|
+
const catalog = options.previousCatalog ?? [];
|
|
269
|
+
const requested = query.expansion;
|
|
270
|
+
const matches = catalog.filter((entry) => requested.requestedItemIds.includes(entry.id) || requested.requestedSubjects.includes(entry.subject) || entry.sourceIds.some((id) => requested.requestedSourceIds.includes(id)));
|
|
271
|
+
const requestedCount = requested.requestedItemIds.length + requested.requestedSubjects.length + requested.requestedSourceIds.length;
|
|
272
|
+
if (requestedCount === 0 || matches.length === 0) findings.push({ code: "expansion-request-unavailable", severity: "blocked" });
|
|
273
|
+
for (const id of requested.requestedItemIds) if (!catalog.some((entry) => entry.id === id)) findings.push({ code: "expansion-request-unavailable", severity: "blocked", itemId: id });
|
|
274
|
+
for (const subject of requested.requestedSubjects) if (!catalog.some((entry) => entry.subject === subject)) findings.push({ code: "expansion-request-unavailable", severity: "blocked", subject });
|
|
275
|
+
for (const sourceId of requested.requestedSourceIds) if (!catalog.some((entry) => entry.sourceIds.includes(sourceId))) findings.push({ code: "expansion-request-unavailable", severity: "blocked", sourceId });
|
|
276
|
+
for (const entry of matches) if (applicableIds.has(entry.id)) { selected.add(entry.id); reasons.set(entry.id, ["targeted-expansion"]); }
|
|
277
|
+
} else {
|
|
278
|
+
const terms = queryTerms(query);
|
|
279
|
+
for (const item of applicable.filter((entry) => ROUTED_KINDS.has(entry.kind))) {
|
|
280
|
+
const matches = matchReasons(indexById.get(item.id), terms);
|
|
281
|
+
if (matches.length > 0) { selected.add(item.id); reasons.set(item.id, matches); }
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
closeDependencies(contract, profile, byTarget, selected, reasons, findings);
|
|
285
|
+
for (const conflict of findConflicts(applicable)) {
|
|
286
|
+
if (!conflict.items?.some((id) => selected.has(id))) continue;
|
|
287
|
+
findings.push({ code: "contract-conflict", severity: "blocked", subject: conflict.subject, itemIds: [...conflict.items].sort() });
|
|
288
|
+
}
|
|
289
|
+
const selectedItems = [...selected].filter((id) => applicableIds.has(id)).map((id) => approvedById.get(id)).filter(Boolean).sort((left, right) => left.id.localeCompare(right.id));
|
|
290
|
+
const deferred = applicable.filter((item) => !selected.has(item.id));
|
|
291
|
+
return { selected: selectedItems, reasons, deferred, applicable, byTarget, profile, allById };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function hydratedItem(item, reasons) {
|
|
295
|
+
return { id: item.id, kind: item.kind, subject: item.subject, value: structuredClone(item.value), statement: item.statement, scope: structuredClone(item.scope), sourceIds: [...item.sources].sort(), overrides: [...item.overrides].sort(), ...(item.verification ? { verification: structuredClone(item.verification) } : {}), selectionReasons: [...reasons].sort(), itemDigest: digestJson(item) };
|
|
296
|
+
}
|
|
297
|
+
function deferredItem(item) { return { id: item.id, kind: item.kind, subject: item.subject, scope: structuredClone(item.scope), sourceIds: [...item.sources].sort(), itemDigest: digestJson(item), reason: "applicable-not-selected" }; }
|
|
298
|
+
function baselineFindings(query, project) {
|
|
299
|
+
const current = snapshots(project);
|
|
300
|
+
return Object.keys(current).filter((key) => current[key] !== query.snapshots[key]).map((key) => ({ code: `${key === "contract" ? "contract" : key === "sourcesLock" ? "sources-lock" : "projections-lock"}-baseline-stale`, severity: "blocked", expected: query.snapshots[key], actual: current[key] }));
|
|
301
|
+
}
|
|
302
|
+
function structuralFindings(project, selectedIds, targetPaths) {
|
|
303
|
+
const selected = new Set(selectedIds);
|
|
304
|
+
const relevantSourceIds = new Set(project.contract.items.filter((item) => selected.has(item.id)).flatMap((item) => [...item.sources, item.verification?.source].filter(Boolean)));
|
|
305
|
+
const lock = new Map(project.sourcesLock.sources.map((entry) => [entry.id, entry.digest]));
|
|
306
|
+
const findings = [];
|
|
307
|
+
for (const source of project.contract.sources.filter((entry) => sourceStatus(entry) === "active" && LOCAL_SOURCE_KINDS.has(entry.kind))) {
|
|
308
|
+
const severity = relevantSourceIds.has(source.id) ? "blocked" : "attention";
|
|
309
|
+
if (!lock.has(source.id)) findings.push({ code: "source-lock-missing", severity, sourceId: source.id, path: source.path });
|
|
310
|
+
else if (lock.get(source.id) !== source.digest) findings.push({ code: "source-lock-mismatch", severity, sourceId: source.id, path: source.path });
|
|
311
|
+
}
|
|
312
|
+
for (const finding of validateOverrides(project.contract.items)) {
|
|
313
|
+
const ids = [finding.item, finding.target].filter(Boolean);
|
|
314
|
+
findings.push({ code: finding.code, severity: ids.some((id) => selected.has(id)) ? "blocked" : "attention", projectFinding: finding });
|
|
315
|
+
}
|
|
316
|
+
for (const item of project.contract.items.filter((entry) => entry.status === "proposed")) {
|
|
317
|
+
const taskRelated = targetPaths.some((target) => scopeApplies(item.scope, target));
|
|
318
|
+
findings.push({ code: "item-approval-pending", severity: taskRelated && MANDATORY_KINDS.has(item.kind) ? "blocked" : "attention", itemId: item.id });
|
|
319
|
+
}
|
|
320
|
+
return findings;
|
|
321
|
+
}
|
|
322
|
+
async function auditRelevantSources(root, project, items, sourceReadContext) {
|
|
323
|
+
const sourceIds = new Set(items.flatMap((item) => [...item.sources, item.verification?.source].filter(Boolean)));
|
|
324
|
+
const sourceById = new Map(project.contract.sources.map((source) => [source.id, source]));
|
|
325
|
+
const lock = new Map(project.sourcesLock.sources.map((entry) => [entry.id, entry.digest]));
|
|
326
|
+
const evidence = [];
|
|
327
|
+
const findings = [];
|
|
328
|
+
for (const sourceId of [...sourceIds].sort()) {
|
|
329
|
+
const source = sourceById.get(sourceId);
|
|
330
|
+
if (!source || sourceStatus(source) !== "active") { findings.push({ code: "source-unavailable", severity: "blocked", sourceId }); continue; }
|
|
331
|
+
const base = { id: source.id, kind: source.kind, locator: sourceLocator(source), sourceDigest: digestJson(source) };
|
|
332
|
+
if (!LOCAL_SOURCE_KINDS.has(source.kind)) { evidence.push({ ...base, freshness: "unverifiable-non-local" }); continue; }
|
|
333
|
+
try {
|
|
334
|
+
const actual = await readSourceDigest(root, source, sourceReadContext);
|
|
335
|
+
const expected = lock.get(source.id) ?? null;
|
|
336
|
+
const freshness = expected !== null && expected === source.digest && actual === expected ? "current" : "drifted";
|
|
337
|
+
evidence.push({ ...base, freshness, expectedDigest: expected, actualDigest: actual });
|
|
338
|
+
if (freshness !== "current") findings.push({ code: "source-drift", severity: "blocked", sourceId: source.id, path: source.path, expectedDigest: expected, actualDigest: actual });
|
|
339
|
+
} catch (error) {
|
|
340
|
+
evidence.push({ ...base, freshness: "unreadable", expectedDigest: lock.get(source.id) ?? null, actualDigest: null });
|
|
341
|
+
findings.push({ code: "source-drift", severity: "blocked", sourceId: source.id, path: source.path, reason: error.code ?? "source-unreadable" });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
for (const item of items) {
|
|
345
|
+
if (!item.verification || item.verification.kind === "none") continue;
|
|
346
|
+
try { const finding = await verifyItem(root, item, sourceById, sourceReadContext); if (finding) findings.push({ code: "verification-failed", severity: "blocked", projectFinding: finding }); }
|
|
347
|
+
catch (error) { findings.push({ code: "verification-failed", severity: "blocked", itemId: item.id, reason: error.code ?? "unreadable" }); }
|
|
348
|
+
}
|
|
349
|
+
return { evidence, findings };
|
|
350
|
+
}
|
|
351
|
+
function readTargets(query, sourceEvidence) {
|
|
352
|
+
const byPath = new Map();
|
|
353
|
+
const add = (entry) => { if (entry.path && !byPath.has(entry.path)) byPath.set(entry.path, entry); };
|
|
354
|
+
query.task.paths.forEach((entry) => add({ path: entry, reason: "task-target" }));
|
|
355
|
+
query.task.changedPaths.forEach((entry) => add({ path: entry, reason: "host-changed-path-signal" }));
|
|
356
|
+
sourceEvidence.forEach((source) => add(source.locator.path ? { path: source.locator.path, reason: "contract-source", sourceId: source.id } : {}));
|
|
357
|
+
return [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
358
|
+
}
|
|
359
|
+
function taskHealth(findings) {
|
|
360
|
+
if (findings.some((entry) => entry.severity === "blocked")) return "blocked";
|
|
361
|
+
if (findings.some((entry) => entry.severity === "needs-expansion")) return "needs-expansion";
|
|
362
|
+
return "ready";
|
|
363
|
+
}
|
|
364
|
+
function checkerFinding(entry) { return { code: entry.code.startsWith("source-") ? "source-drift" : entry.code, severity: blockingContextFindings([entry]).length > 0 ? "blocked" : "attention", projectFinding: entry }; }
|
|
365
|
+
function taskDigest(query) { return digestJson({ projectId: query.projectId, task: query.task, freshness: query.freshness }); }
|
|
366
|
+
function monotonicBudget(previous, query, findings) {
|
|
367
|
+
const before = previous.budget;
|
|
368
|
+
const after = query.budget;
|
|
369
|
+
const shrunk = after.audit.targetUtf8Bytes < before.audit.targetUtf8Bytes || after.audit.maxUtf8Bytes < before.audit.maxUtf8Bytes || after.delivery.completeBelowUtf8Bytes < before.delivery.completeBelowUtf8Bytes || after.delivery.maxUtf8Bytes < before.delivery.maxUtf8Bytes || after.readTargets.target < before.readTargets.target || after.readTargets.max < before.readTargets.max;
|
|
370
|
+
if (shrunk) findings.push({ code: "context-query-budget-shrunk", severity: "blocked" });
|
|
371
|
+
}
|
|
372
|
+
async function targetStates(root, paths) {
|
|
373
|
+
const result = [];
|
|
374
|
+
for (const entry of paths) {
|
|
375
|
+
try { await access(path.resolve(root, entry)); result.push({ path: entry, state: "existing" }); }
|
|
376
|
+
catch (error) { if (error?.code !== "ENOENT") throw error; result.push({ path: entry, state: "prospective-or-deleted" }); }
|
|
377
|
+
}
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
function withAuditBytesAndDigest(input) {
|
|
381
|
+
const value = structuredClone(input);
|
|
382
|
+
let used = 0;
|
|
383
|
+
for (let iteration = 0; iteration < 12; iteration += 1) {
|
|
384
|
+
value.budget.audit.usedUtf8Bytes = used;
|
|
385
|
+
delete value.bundleDigest;
|
|
386
|
+
value.bundleDigest = digestJson(value);
|
|
387
|
+
const next = Buffer.byteLength(canonicalJson(value), "utf8");
|
|
388
|
+
if (next === used) break;
|
|
389
|
+
used = next;
|
|
390
|
+
}
|
|
391
|
+
value.budget.audit.usedUtf8Bytes = used;
|
|
392
|
+
delete value.bundleDigest;
|
|
393
|
+
value.bundleDigest = digestJson(value);
|
|
394
|
+
return value;
|
|
395
|
+
}
|
|
396
|
+
function validatePreviousAgainstProject(previous, project, query, findings) {
|
|
397
|
+
if (canonicalJson(previous.snapshots) !== canonicalJson(snapshots(project))) findings.push({ code: "context-query-previous-stale", severity: "blocked" });
|
|
398
|
+
if (previous.taskDigest !== taskDigest(query)) findings.push({ code: "context-query-previous-task-mismatch", severity: "blocked" });
|
|
399
|
+
if (query.expansion?.previousBundleDigest !== previous.bundleDigest) findings.push({ code: "context-query-previous-digest-mismatch", severity: "blocked" });
|
|
400
|
+
if (previous.expansionDepth !== 0) findings.push({ code: "context-query-expansion-depth-exceeded", severity: "blocked" });
|
|
401
|
+
if (canonicalJson(previous.task.itemIds) !== canonicalJson(query.task.itemIds)) findings.push({ code: "context-query-previous-task-mismatch", severity: "blocked" });
|
|
402
|
+
const effective = new Set(unionEffective(effectiveByTarget(project.contract, query.task.paths, findings)).map((item) => item.id));
|
|
403
|
+
const liveById = new Map(project.contract.items.filter((item) => item.status === "approved").map((item) => [item.id, item]));
|
|
404
|
+
for (const artifactItem of previous.hydratedItems) {
|
|
405
|
+
const live = liveById.get(artifactItem.id);
|
|
406
|
+
if (!live || !effective.has(artifactItem.id) || digestJson(live) !== artifactItem.itemDigest) findings.push({ code: "context-query-previous-item-invalid", severity: "blocked", itemId: artifactItem.id });
|
|
407
|
+
}
|
|
408
|
+
monotonicBudget(previous, query, findings);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export async function buildAdaptiveContextBundle(root, project, queryInput, options = {}) {
|
|
412
|
+
const query = validateContextQuery(queryInput);
|
|
413
|
+
if (query.projectId !== project.contract.project.id) fail("context-query-project-mismatch", "context query belongs to a different project");
|
|
414
|
+
if (query.level === "expanded" && !options.previous) fail("context-query-previous-required", "expanded context requires --previous");
|
|
415
|
+
if (query.level !== "expanded" && options.previous) fail("context-query-previous-not-allowed", `${query.level} context cannot use --previous`);
|
|
416
|
+
const previous = options.previous ?? null;
|
|
417
|
+
const indexResult = await routingIndexForQuery(root, project);
|
|
418
|
+
const findings = baselineFindings(query, project);
|
|
419
|
+
if (previous) validatePreviousAgainstProject(previous, project, query, findings);
|
|
420
|
+
const selection = selectAdaptiveItems(project.contract, indexResult.index, query, { findings, previousCatalog: previous?.deferredItems ?? [] });
|
|
421
|
+
const previousIds = new Set(previous?.delivery.itemIds ?? []);
|
|
422
|
+
const selectedIds = new Set(selection.selected.map((item) => item.id));
|
|
423
|
+
if (previous) for (const id of previousIds) selectedIds.add(id);
|
|
424
|
+
const approvedById = new Map(project.contract.items.filter((item) => item.status === "approved").map((item) => [item.id, item]));
|
|
425
|
+
const applicableIds = new Set(selection.applicable.map((item) => item.id));
|
|
426
|
+
const selectedItems = [...selectedIds].filter((id) => applicableIds.has(id)).map((id) => approvedById.get(id)).filter(Boolean).sort((left, right) => left.id.localeCompare(right.id));
|
|
427
|
+
findings.push(...structuralFindings(project, selectedItems.map((item) => item.id), query.task.paths));
|
|
428
|
+
if (query.level === "initial" && uniqueSorted(query.task.paths.filter((entry) => entry !== ".").map((entry) => entry.split("/")[0])).length > 1) findings.push({ code: "cross-module-context", severity: "needs-expansion" });
|
|
429
|
+
|
|
430
|
+
const completeIds = selection.applicable.map((item) => item.id).sort();
|
|
431
|
+
const adaptiveIds = selectedItems.map((item) => item.id).sort();
|
|
432
|
+
const renderBlocked = findings.some((entry) => entry.code === "contract-conflict" && entry.severity === "blocked");
|
|
433
|
+
const completeContent = renderBlocked ? "" : renderTaskContextBundle(project.contract, query.task.paths, completeIds, query.task.text);
|
|
434
|
+
const adaptiveContent = renderBlocked ? "" : renderTaskContextBundle(project.contract, query.task.paths, adaptiveIds, query.task.text);
|
|
435
|
+
const completeBytes = Buffer.byteLength(completeContent, "utf8");
|
|
436
|
+
const adaptiveBytes = Buffer.byteLength(adaptiveContent, "utf8");
|
|
437
|
+
let mode = "adaptive";
|
|
438
|
+
if (query.level === "complete" || completeBytes <= query.budget.delivery.completeBelowUtf8Bytes || (adaptiveBytes >= completeBytes && completeBytes <= query.budget.delivery.maxUtf8Bytes)) mode = "complete";
|
|
439
|
+
if (mode === "complete") {
|
|
440
|
+
for (let index = findings.length - 1; index >= 0; index -= 1) if (["task-candidate-miss", "cross-module-context"].includes(findings[index].code)) findings.splice(index, 1);
|
|
441
|
+
} else if (query.level === "initial" && selection.deferred.some((item) => ROUTED_KINDS.has(item.kind)) && !selection.selected.some((item) => ROUTED_KINDS.has(item.kind))) {
|
|
442
|
+
findings.push({ code: "task-candidate-miss", severity: "needs-expansion" });
|
|
443
|
+
}
|
|
444
|
+
const finalItems = (mode === "complete" ? selection.applicable : selectedItems).sort((left, right) => left.id.localeCompare(right.id));
|
|
445
|
+
const finalIds = finalItems.map((item) => item.id);
|
|
446
|
+
const finalContent = mode === "complete" ? completeContent : adaptiveContent;
|
|
447
|
+
const finalBytes = mode === "complete" ? completeBytes : adaptiveBytes;
|
|
448
|
+
if (finalBytes > query.budget.delivery.maxUtf8Bytes) findings.push({ code: "delivery-budget-insufficient", severity: "blocked", limit: query.budget.delivery.maxUtf8Bytes, required: finalBytes });
|
|
449
|
+
|
|
450
|
+
const sourceReadContext = createSourceReadContext();
|
|
451
|
+
const relevantAudit = await auditRelevantSources(root, project, finalItems, sourceReadContext);
|
|
452
|
+
findings.push(...relevantAudit.findings);
|
|
453
|
+
let globalHealth = findings.some((entry) => entry.code.endsWith("baseline-stale")) ? "snapshot-stale" : "not-checked";
|
|
454
|
+
let registrationCoverage = "not-checked";
|
|
455
|
+
const strictMode = query.freshness === "strict-current" || query.level === "complete";
|
|
456
|
+
if (strictMode) {
|
|
457
|
+
const strict = await checkProject(root, project, { sourceReadContext });
|
|
458
|
+
findings.push(...strict.map(checkerFinding));
|
|
459
|
+
globalHealth = strict.length === 0 ? "clean" : blockingContextFindings(strict).length > 0 ? "conflict" : "attention";
|
|
460
|
+
const coverage = await buildCoverageAudit(root, project, query.task.changedPaths);
|
|
461
|
+
registrationCoverage = coverage.registrationCoverage;
|
|
462
|
+
if (registrationCoverage === "review-required") findings.push({ code: "registration-coverage-review-required", severity: "blocked", paths: coverage.categories["review-required"].map((entry) => entry.path) });
|
|
463
|
+
}
|
|
464
|
+
const targets = readTargets(query, relevantAudit.evidence);
|
|
465
|
+
if (targets.length > query.budget.readTargets.target) findings.push({ code: "soft-read-target-exceeded", severity: "attention", target: query.budget.readTargets.target, required: targets.length });
|
|
466
|
+
if (targets.length > query.budget.readTargets.max) findings.push({ code: "read-target-budget-insufficient", severity: "blocked", limit: query.budget.readTargets.max, required: targets.length });
|
|
467
|
+
if (indexResult.state !== "current") findings.push({ code: "routing-index-fallback", severity: "attention", state: indexResult.state });
|
|
468
|
+
const normalizedFindings = stableFindings(findings);
|
|
469
|
+
const health = taskHealth(normalizedFindings);
|
|
470
|
+
const ready = health === "ready";
|
|
471
|
+
const retainedItemIds = previous ? finalIds.filter((id) => previousIds.has(id)).sort() : [];
|
|
472
|
+
const hydratedItems = finalItems.filter((item) => !previousIds.has(item.id)).map((item) => hydratedItem(item, selection.reasons.get(item.id) ?? [mode === "complete" ? "complete-baseline" : "dependency-or-retained-closure"]));
|
|
473
|
+
const hasDependencies = selection.profile.rules.some((rule) => finalIds.includes(rule.itemId));
|
|
474
|
+
let bundle = withAuditBytesAndDigest({
|
|
475
|
+
schemaVersion: ADAPTIVE_CONTEXT_BUNDLE_SCHEMA_VERSION,
|
|
476
|
+
kind: "adaptive-context-bundle",
|
|
477
|
+
project: { id: project.contract.project.id, name: project.contract.project.name },
|
|
478
|
+
task: structuredClone(query.task),
|
|
479
|
+
queryDigest: digestJson(query),
|
|
480
|
+
taskDigest: taskDigest(query),
|
|
481
|
+
...(previous ? { previousBundleDigest: previous.bundleDigest } : {}),
|
|
482
|
+
level: query.level,
|
|
483
|
+
expansionDepth: previous ? 1 : 0,
|
|
484
|
+
snapshots: snapshots(project),
|
|
485
|
+
routing: { indexDigest: indexResult.index.indexDigest, indexState: indexResult.state, ...(indexResult.rejectedDigest ? { rejectedDigest: indexResult.rejectedDigest } : {}) },
|
|
486
|
+
guarantees: {
|
|
487
|
+
registrationCoverage,
|
|
488
|
+
mandatoryCoverage: normalizedFindings.some((entry) => entry.severity === "blocked" && ["contract-conflict", "requested-item-out-of-scope", "requested-item-not-effective"].includes(entry.code)) ? "blocked" : "complete",
|
|
489
|
+
declaredDependencyCoverage: normalizedFindings.some((entry) => entry.code === "declared-dependency-missing") ? "missing" : hasDependencies ? "complete" : "not-declared",
|
|
490
|
+
retrievalStatus: mode === "complete" ? "complete" : normalizedFindings.some((entry) => entry.code === "task-candidate-miss") ? "no-candidate" : "matched",
|
|
491
|
+
semanticCompleteness: "not-claimed",
|
|
492
|
+
freshness: strictMode ? "strict-current" : "snapshot-and-signal-bound",
|
|
493
|
+
},
|
|
494
|
+
globalHealth,
|
|
495
|
+
taskHealth: health,
|
|
496
|
+
targetStates: await targetStates(root, query.task.paths),
|
|
497
|
+
hydratedItems,
|
|
498
|
+
retainedItemIds,
|
|
499
|
+
deferredItems: selection.deferred.filter((item) => !finalIds.includes(item.id)).map(deferredItem),
|
|
500
|
+
sources: relevantAudit.evidence,
|
|
501
|
+
evidenceEdges: finalItems.flatMap((item) => item.sources.map((sourceId) => ({ itemId: item.id, sourceId }))).sort((left, right) => canonicalJson(left).localeCompare(canonicalJson(right))),
|
|
502
|
+
readTargets: targets,
|
|
503
|
+
findings: normalizedFindings,
|
|
504
|
+
excluded: [...EXCLUDED_BODIES],
|
|
505
|
+
metrics: { sourceDigestReads: sourceReadContext.metrics.sourceDigestReads, sourceBodyReads: sourceReadContext.metrics.sourceBodyReads, sourceIdentityReads: sourceReadContext.metrics.sourceIdentityReads, hydratedItemCount: hydratedItems.length, retainedItemCount: retainedItemIds.length, deferredItemCount: selection.deferred.length, hostToolCalls: 1 },
|
|
506
|
+
budget: {
|
|
507
|
+
unit: "canonical-utf8-bytes",
|
|
508
|
+
audit: { targetUtf8Bytes: query.budget.audit.targetUtf8Bytes, maxUtf8Bytes: query.budget.audit.maxUtf8Bytes, usedUtf8Bytes: 0 },
|
|
509
|
+
delivery: { completeBelowUtf8Bytes: query.budget.delivery.completeBelowUtf8Bytes, maxUtf8Bytes: query.budget.delivery.maxUtf8Bytes, usedUtf8Bytes: ready ? finalBytes : 0 },
|
|
510
|
+
readTargets: { target: query.budget.readTargets.target, max: query.budget.readTargets.max, count: targets.length },
|
|
511
|
+
},
|
|
512
|
+
delivery: ready ? { status: "ready", format: "project-context-markdown", mode, itemIds: finalIds, contentDigest: sha256(finalContent), utf8Bytes: finalBytes } : { status: "withheld", format: "project-context-markdown", mode: null, itemIds: [], contentDigest: null, utf8Bytes: 0 },
|
|
513
|
+
});
|
|
514
|
+
if (bundle.budget.audit.usedUtf8Bytes > query.budget.audit.targetUtf8Bytes) {
|
|
515
|
+
bundle.findings = stableFindings([...bundle.findings, { code: "soft-audit-budget-exceeded", severity: "attention", target: query.budget.audit.targetUtf8Bytes, required: bundle.budget.audit.usedUtf8Bytes }]);
|
|
516
|
+
bundle = withAuditBytesAndDigest(bundle);
|
|
517
|
+
}
|
|
518
|
+
return validateAdaptiveContextBundle(bundle);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function loadPrevious(root, previousPath) {
|
|
522
|
+
if (!previousPath) return null;
|
|
523
|
+
const resolved = await resolveExistingInside(root, previousPath);
|
|
524
|
+
return validateAdaptiveContextBundle(await readJsonFile(resolved.absolute, "previous adaptive context bundle"));
|
|
525
|
+
}
|
|
526
|
+
export async function buildAdaptiveContextBundleFiles(root, inputPath, options = {}) {
|
|
527
|
+
const resolved = await resolveExistingInside(root, inputPath);
|
|
528
|
+
const input = await readJsonFile(resolved.absolute, "context query");
|
|
529
|
+
const project = await loadProject(root);
|
|
530
|
+
const previous = await loadPrevious(root, options.previousPath);
|
|
531
|
+
const bundle = await buildAdaptiveContextBundle(root, project, input, { previous });
|
|
532
|
+
if (!options.forPrompt && bundle.budget.audit.usedUtf8Bytes > bundle.budget.audit.maxUtf8Bytes) fail("audit-budget-insufficient", "adaptive context audit exceeds its hard byte budget", { exitCode: 1, details: { limit: bundle.budget.audit.maxUtf8Bytes, required: bundle.budget.audit.usedUtf8Bytes } });
|
|
533
|
+
return bundle;
|
|
534
|
+
}
|
|
535
|
+
export async function buildAdaptiveContextDeliveryFiles(root, inputPath, options = {}) {
|
|
536
|
+
const resolved = await resolveExistingInside(root, inputPath);
|
|
537
|
+
const input = validateContextQuery(await readJsonFile(resolved.absolute, "context query"));
|
|
538
|
+
const project = await loadProject(root);
|
|
539
|
+
const previous = await loadPrevious(root, options.previousPath);
|
|
540
|
+
const bundle = await buildAdaptiveContextBundle(root, project, input, { previous });
|
|
541
|
+
if (bundle.taskHealth !== "ready" || bundle.delivery.status !== "ready") fail("adaptive-context-delivery-withheld", "adaptive context delivery is not ready; inspect --json", { exitCode: 1, details: { taskHealth: bundle.taskHealth } });
|
|
542
|
+
const content = renderTaskContextBundle(project.contract, input.task.paths, bundle.delivery.itemIds, input.task.text);
|
|
543
|
+
if (sha256(content) !== bundle.delivery.contentDigest || Buffer.byteLength(content, "utf8") !== bundle.delivery.utf8Bytes) fail("adaptive-context-delivery-invalid", "adaptive context delivery projection does not match its digest or byte count");
|
|
544
|
+
return { bundle, content };
|
|
545
|
+
}
|
|
546
|
+
export async function buildCoverageAuditFiles(root, changedPaths = []) { return buildCoverageAudit(root, await loadProject(root), changedPaths); }
|
|
547
|
+
export async function indexContextFiles(root, options = {}) { return indexContext(root, await loadProject(root), options); }
|