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,99 @@
|
|
|
1
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { PERMANENT_BOUNDARIES } from "./capabilities.mjs";
|
|
4
|
+
import { fail, ProjectContextError } from "./errors.mjs";
|
|
5
|
+
import { EVIDENCE_BUNDLE_SCHEMA_VERSION, finalizeEvidenceBundle, normalizeEvidenceInput } from "./evidence-schema.mjs";
|
|
6
|
+
import { EXCHANGE_PROTOCOL_VERSION, PACKAGE_VERSION } from "./exchange-schema.mjs";
|
|
7
|
+
import { normalizeRelativePath, resolveExistingInside } from "./path-policy.mjs";
|
|
8
|
+
import { buildProjectStatus } from "./project-status.mjs";
|
|
9
|
+
|
|
10
|
+
async function readEvidenceInput(root, inputPath) {
|
|
11
|
+
let normalized;
|
|
12
|
+
try {
|
|
13
|
+
normalized = normalizeRelativePath(inputPath, { label: "evidence input" });
|
|
14
|
+
} catch (error) {
|
|
15
|
+
fail("evidence-input-outside-project", "evidence input must stay inside the project", { cause: error, details: { path: inputPath } });
|
|
16
|
+
}
|
|
17
|
+
const candidate = path.join(root, normalized);
|
|
18
|
+
let info;
|
|
19
|
+
try {
|
|
20
|
+
info = await lstat(candidate);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error?.code === "ENOENT") fail("evidence-input-missing", `evidence input does not exist: ${normalized}`, { cause: error });
|
|
23
|
+
fail("evidence-input-invalid", `cannot inspect evidence input: ${normalized}`, { cause: error });
|
|
24
|
+
}
|
|
25
|
+
if (info.isSymbolicLink()) {
|
|
26
|
+
fail("evidence-input-outside-project", "evidence input cannot be a symlink", { details: { path: normalized } });
|
|
27
|
+
}
|
|
28
|
+
if (!info.isFile()) fail("evidence-input-invalid", "evidence input must be an ordinary file", { details: { path: normalized } });
|
|
29
|
+
let resolved;
|
|
30
|
+
try {
|
|
31
|
+
resolved = await resolveExistingInside(root, normalized);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error?.code === "path-outside-project") {
|
|
34
|
+
fail("evidence-input-outside-project", "evidence input must stay inside the project", { cause: error, details: { path: normalized } });
|
|
35
|
+
}
|
|
36
|
+
if (error?.code === "source-missing") fail("evidence-input-missing", `evidence input does not exist: ${normalized}`, { cause: error });
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
let source;
|
|
40
|
+
try {
|
|
41
|
+
source = await readFile(resolved.absolute, "utf8");
|
|
42
|
+
} catch (error) {
|
|
43
|
+
fail("evidence-input-invalid", `cannot read evidence input: ${normalized}`, { cause: error });
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(source);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
fail("evidence-input-invalid", "evidence input contains invalid JSON", { cause: error });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function minimalProjectContext(status) {
|
|
53
|
+
return {
|
|
54
|
+
initialization: status.initialization.state,
|
|
55
|
+
health: status.health,
|
|
56
|
+
entryState: status.entry.state,
|
|
57
|
+
findingCodes: [...new Set(status.findingCodes)].sort((left, right) => left.localeCompare(right)),
|
|
58
|
+
schemas: {
|
|
59
|
+
contract: 2,
|
|
60
|
+
sourcesLock: 1,
|
|
61
|
+
projectionsLock: 2,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function buildEvidenceBundle(root, input) {
|
|
67
|
+
const normalized = normalizeEvidenceInput(input);
|
|
68
|
+
let statusResult;
|
|
69
|
+
try {
|
|
70
|
+
statusResult = await buildProjectStatus(root, PERMANENT_BOUNDARIES);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
fail("evidence-project-state-unreadable", "cannot form minimal Project Context status", {
|
|
73
|
+
cause: error,
|
|
74
|
+
details: { reason: error instanceof ProjectContextError ? error.code : "internal-state-error" },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const { schemaVersion: _inputSchemaVersion, ...observation } = normalized;
|
|
78
|
+
return finalizeEvidenceBundle({
|
|
79
|
+
schemaVersion: EVIDENCE_BUNDLE_SCHEMA_VERSION,
|
|
80
|
+
kind: "target-project-evidence",
|
|
81
|
+
product: {
|
|
82
|
+
name: "frontend-project-context",
|
|
83
|
+
version: PACKAGE_VERSION,
|
|
84
|
+
exchangeProtocolVersion: EXCHANGE_PROTOCOL_VERSION,
|
|
85
|
+
},
|
|
86
|
+
...observation,
|
|
87
|
+
projectContext: minimalProjectContext(statusResult.status),
|
|
88
|
+
transfer: {
|
|
89
|
+
state: "human-review-required",
|
|
90
|
+
automaticUpload: false,
|
|
91
|
+
destination: null,
|
|
92
|
+
},
|
|
93
|
+
boundaries: { ...PERMANENT_BOUNDARIES },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function buildEvidenceBundleFile(root, inputPath) {
|
|
98
|
+
return buildEvidenceBundle(root, await readEvidenceInput(root, inputPath));
|
|
99
|
+
}
|
|
@@ -2,27 +2,31 @@ import { canonicalValue, digestJson, validateJsonValue } from "./canonical-json.
|
|
|
2
2
|
import { fail } from "./errors.mjs";
|
|
3
3
|
import { normalizeRelativePath } from "./path-policy.mjs";
|
|
4
4
|
|
|
5
|
-
export const PACKAGE_VERSION = "1.
|
|
6
|
-
export const EXCHANGE_PROTOCOL_VERSION =
|
|
7
|
-
export const ACTION_PLAN_SCHEMA_VERSION =
|
|
8
|
-
export const REVIEW_BUNDLE_SCHEMA_VERSION =
|
|
9
|
-
export const CAPABILITIES_SCHEMA_VERSION =
|
|
5
|
+
export const PACKAGE_VERSION = "1.7.0";
|
|
6
|
+
export const EXCHANGE_PROTOCOL_VERSION = 7;
|
|
7
|
+
export const ACTION_PLAN_SCHEMA_VERSION = 2;
|
|
8
|
+
export const REVIEW_BUNDLE_SCHEMA_VERSION = 2;
|
|
9
|
+
export const CAPABILITIES_SCHEMA_VERSION = 7;
|
|
10
10
|
|
|
11
11
|
export const ACTION_KINDS = Object.freeze([
|
|
12
12
|
"accept-source-change",
|
|
13
13
|
"deprecate-item",
|
|
14
14
|
"deprecate-source",
|
|
15
15
|
"propose-item",
|
|
16
|
+
"publish-ai-entry",
|
|
16
17
|
"publish-projection",
|
|
17
18
|
"register-source",
|
|
19
|
+
"remove-ai-entry",
|
|
18
20
|
"request-item-approval",
|
|
19
21
|
"revise-item",
|
|
20
22
|
]);
|
|
21
23
|
|
|
22
24
|
export const COMMANDS = Object.freeze([
|
|
23
|
-
"accept-source-change", "approve", "capabilities", "check", "context", "dashboard", "deprecate",
|
|
24
|
-
"deprecate-source", "discover", "init", "integration-review", "preflight", "propose", "publish", "register",
|
|
25
|
-
"review-source", "revise", "setup", "stage-context", "sync",
|
|
25
|
+
"accept-source-change", "approve", "capabilities", "check", "context", "context-query", "coverage-audit", "dashboard", "deprecate",
|
|
26
|
+
"deprecate-source", "discover", "evidence", "init", "integration-review", "preflight", "propose", "publish", "register",
|
|
27
|
+
"index-context", "publish-entry", "remove-entry", "review-source", "revise", "setup", "stage-context", "status", "sync",
|
|
28
|
+
"upgrade-apply", "upgrade-check", "upgrade-plan",
|
|
29
|
+
"reconcile-truth",
|
|
26
30
|
]);
|
|
27
31
|
|
|
28
32
|
const ACTION_KIND_SET = new Set(ACTION_KINDS);
|
|
@@ -253,13 +257,20 @@ function normalizeActionInput(kind, input, label) {
|
|
|
253
257
|
digest(input.expectedContentDigest, `${label}.expectedContentDigest`, { nullable: true });
|
|
254
258
|
return { target: input.target, output, paths: [...paths].sort(), expectedContentDigest: input.expectedContentDigest };
|
|
255
259
|
}
|
|
260
|
+
if (kind === "publish-ai-entry" || kind === "remove-ai-entry") {
|
|
261
|
+
object(input, label);
|
|
262
|
+
exactKeys(input, new Set(["output"]), label);
|
|
263
|
+
const output = projectPath(input.output, `${label}.output`);
|
|
264
|
+
if (output.split("/").at(-1) !== "AGENTS.md") invalid(`${label}.output must name AGENTS.md`);
|
|
265
|
+
return { output };
|
|
266
|
+
}
|
|
256
267
|
invalid(`${label} kind is unsupported: ${kind}`);
|
|
257
268
|
}
|
|
258
269
|
|
|
259
270
|
export function validateActionPlan(input) {
|
|
260
271
|
object(input, "action plan");
|
|
261
272
|
exactKeys(input, new Set(["schemaVersion", "projectId", "baselines", "actions"]), "action plan");
|
|
262
|
-
if (input.schemaVersion
|
|
273
|
+
if (![1, ACTION_PLAN_SCHEMA_VERSION].includes(input.schemaVersion)) invalid(`action plan schemaVersion must be 1 or ${ACTION_PLAN_SCHEMA_VERSION}`);
|
|
263
274
|
stableId(input.projectId, "action plan.projectId");
|
|
264
275
|
object(input.baselines, "action plan.baselines");
|
|
265
276
|
exactKeys(input.baselines, new Set(["contract", "sourcesLock", "projectionsLock"]), "action plan.baselines");
|
|
@@ -273,10 +284,10 @@ export function validateActionPlan(input) {
|
|
|
273
284
|
stableId(action.id, `${label}.id`);
|
|
274
285
|
if (ids.has(action.id)) invalid(`action plan contains duplicate action id: ${action.id}`);
|
|
275
286
|
ids.add(action.id);
|
|
276
|
-
if (!ACTION_KIND_SET.has(action.kind)) invalid(`${label}.kind is invalid`);
|
|
287
|
+
if (!ACTION_KIND_SET.has(action.kind) || (input.schemaVersion === 1 && ["publish-ai-entry", "remove-ai-entry"].includes(action.kind))) invalid(`${label}.kind is invalid`);
|
|
277
288
|
return { id: action.id, kind: action.kind, input: normalizeActionInput(action.kind, action.input, `${label}.input`) };
|
|
278
289
|
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
279
|
-
return canonicalValue({ schemaVersion:
|
|
290
|
+
return canonicalValue({ schemaVersion: input.schemaVersion, projectId: input.projectId, baselines: canonicalValue(input.baselines), actions });
|
|
280
291
|
}
|
|
281
292
|
|
|
282
293
|
export function actionPlanDigest(plan) {
|
|
@@ -496,7 +507,7 @@ export function assertActionPlanConflictFree(plan) {
|
|
|
496
507
|
if (["register-source", "accept-source-change", "deprecate-source"].includes(action.kind)) targets.push(`source:${action.input.id}`);
|
|
497
508
|
if (["propose-item", "revise-item", "deprecate-item"].includes(action.kind)) targets.push(`item:${action.input.id}`);
|
|
498
509
|
if (action.kind === "request-item-approval") targets.push(...action.input.ids.map((id) => `item:${id}`));
|
|
499
|
-
if (
|
|
510
|
+
if (["publish-projection", "publish-ai-entry", "remove-ai-entry"].includes(action.kind)) targets.push(`projection:${action.input.output}`);
|
|
500
511
|
for (const target of targets) {
|
|
501
512
|
const existing = owners.get(target);
|
|
502
513
|
if (existing) conflicts.push({ target, actions: [existing, action.id].sort() });
|
|
@@ -16,6 +16,7 @@ import { acceptSourceChange, deprecateItem, deprecateSource, reviewSource, revis
|
|
|
16
16
|
import { resolveExistingInside, resolveWritableInside } from "./path-policy.mjs";
|
|
17
17
|
import { loadProject } from "./project-store.mjs";
|
|
18
18
|
import { publishProjection } from "./projection-store.mjs";
|
|
19
|
+
import { publishAiEntry, removeAiEntry } from "./ai-entry.mjs";
|
|
19
20
|
export { buildCapabilities } from "./capabilities.mjs";
|
|
20
21
|
|
|
21
22
|
const INTERNAL_AUTHORITY_SENTINEL = "exchange-preflight-not-authority";
|
|
@@ -29,6 +30,8 @@ const GROUP_BY_ACTION = new Map([
|
|
|
29
30
|
["deprecate-source", "deprecation"],
|
|
30
31
|
["request-item-approval", "approval-request"],
|
|
31
32
|
["publish-projection", "projection"],
|
|
33
|
+
["publish-ai-entry", "projection"],
|
|
34
|
+
["remove-ai-entry", "projection"],
|
|
32
35
|
]);
|
|
33
36
|
|
|
34
37
|
function uniqueSorted(values) {
|
|
@@ -37,13 +40,13 @@ function uniqueSorted(values) {
|
|
|
37
40
|
|
|
38
41
|
|
|
39
42
|
function allProjectionPaths(project) {
|
|
40
|
-
return project.projectionsLock.projections.map((entry) => entry.path).sort();
|
|
43
|
+
return project.projectionsLock.projections.filter((entry) => entry.ownership !== "region").map((entry) => entry.path).sort();
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
function directProjectionPaths(project, itemIds) {
|
|
44
47
|
const ids = new Set(itemIds);
|
|
45
48
|
return project.projectionsLock.projections
|
|
46
|
-
.filter((entry) => entry.itemIds.some((id) => ids.has(id)))
|
|
49
|
+
.filter((entry) => entry.ownership !== "region" && entry.itemIds.some((id) => ids.has(id)))
|
|
47
50
|
.map((entry) => entry.path)
|
|
48
51
|
.sort();
|
|
49
52
|
}
|
|
@@ -414,6 +417,24 @@ async function previewPublish(root, project, action) {
|
|
|
414
417
|
};
|
|
415
418
|
}
|
|
416
419
|
|
|
420
|
+
async function previewEntry(root, project, action) {
|
|
421
|
+
const result = action.kind === "publish-ai-entry"
|
|
422
|
+
? await publishAiEntry(root, project, { output: action.input.output, write: false })
|
|
423
|
+
: await removeAiEntry(root, project, { output: action.input.output, write: false });
|
|
424
|
+
return {
|
|
425
|
+
current: result.current,
|
|
426
|
+
proposed: { ...result.proposed, action: result.action, migration: result.migration, afterSourceDigest: result.impact.afterSourceDigest },
|
|
427
|
+
baselines: actionBaseline(project, { currentContentDigest: result.baselines.content }),
|
|
428
|
+
impact: {
|
|
429
|
+
itemIds: result.impact.itemIds,
|
|
430
|
+
sourceIds: result.impact.sourceIds,
|
|
431
|
+
paths: result.impact.paths,
|
|
432
|
+
projectionPaths: result.impact.projectionPaths,
|
|
433
|
+
},
|
|
434
|
+
invocation: invocation(root, action.kind === "publish-ai-entry" ? "publish-entry" : "remove-entry", ["--output", action.input.output]),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
417
438
|
async function previewAction(root, project, action) {
|
|
418
439
|
if (action.kind === "register-source") return previewRegister(root, project, action);
|
|
419
440
|
if (action.kind === "propose-item") return previewPropose(root, project, action);
|
|
@@ -423,6 +444,7 @@ async function previewAction(root, project, action) {
|
|
|
423
444
|
if (action.kind === "deprecate-source") return previewDeprecateSource(root, project, action);
|
|
424
445
|
if (action.kind === "request-item-approval") return previewApproval(root, project, action);
|
|
425
446
|
if (action.kind === "publish-projection") return previewPublish(root, project, action);
|
|
447
|
+
if (action.kind === "publish-ai-entry" || action.kind === "remove-ai-entry") return previewEntry(root, project, action);
|
|
426
448
|
fail("action-plan-schema-invalid", `unsupported action kind: ${action.kind}`);
|
|
427
449
|
}
|
|
428
450
|
|
|
@@ -452,9 +474,9 @@ function blockedContext(project, action, error) {
|
|
|
452
474
|
current = action.input.ids.map((id) => structuredClone(project.contract.items.find((item) => item.id === id) ?? null));
|
|
453
475
|
impact.itemIds = [...action.input.ids];
|
|
454
476
|
}
|
|
455
|
-
if (
|
|
477
|
+
if (["publish-projection", "publish-ai-entry", "remove-ai-entry"].includes(action.kind)) {
|
|
456
478
|
current = structuredClone(project.projectionsLock.projections.find((entry) => entry.path === action.input.output) ?? null);
|
|
457
|
-
impact.paths = uniqueSorted([...action.input.paths, action.input.output]);
|
|
479
|
+
impact.paths = uniqueSorted([...(action.input.paths ?? []), action.input.output]);
|
|
458
480
|
impact.projectionPaths = [action.input.output];
|
|
459
481
|
}
|
|
460
482
|
return {
|
|
@@ -72,14 +72,14 @@ export function sourceImpact(project, sourceId) {
|
|
|
72
72
|
const relations = relationDetails(project.contract, sourceId);
|
|
73
73
|
const affectedIds = new Set(relations.items.map((item) => item.id));
|
|
74
74
|
const directProjectionPaths = project.projectionsLock.projections
|
|
75
|
-
.filter((entry) => entry.itemIds.some((id) => affectedIds.has(id)))
|
|
75
|
+
.filter((entry) => entry.ownership !== "region" && entry.itemIds.some((id) => affectedIds.has(id)))
|
|
76
76
|
.map((entry) => entry.path)
|
|
77
77
|
.sort();
|
|
78
|
-
const staleProjectionPaths = project.projectionsLock.projections.map((entry) => entry.path).sort();
|
|
78
|
+
const staleProjectionPaths = project.projectionsLock.projections.filter((entry) => entry.ownership !== "region").map((entry) => entry.path).sort();
|
|
79
79
|
return { ...relations, directProjectionPaths, staleProjectionPaths };
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
export async function reviewSource(root, project, sourceId) {
|
|
82
|
+
export async function reviewSource(root, project, sourceId, options = {}) {
|
|
83
83
|
const source = project.contract.sources.find((entry) => entry.id === sourceId);
|
|
84
84
|
if (!source) fail("source-not-found", `source is not registered: ${sourceId}`, { details: { source: sourceId } });
|
|
85
85
|
const sourceObjectDigest = digestJson(source);
|
|
@@ -104,7 +104,7 @@ export async function reviewSource(root, project, sourceId) {
|
|
|
104
104
|
let status;
|
|
105
105
|
let reason;
|
|
106
106
|
try {
|
|
107
|
-
currentDigest = await readSourceDigest(root, source);
|
|
107
|
+
currentDigest = await readSourceDigest(root, source, options.sourceReadContext);
|
|
108
108
|
if (lockedDigest === null || lockedDigest !== source.digest) {
|
|
109
109
|
status = "unreadable";
|
|
110
110
|
reason = lockedDigest === null ? "source-lock-missing" : "source-lock-mismatch";
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { digestJson } from "./canonical-json.mjs";
|
|
4
|
+
import { fail } from "./errors.mjs";
|
|
5
|
+
|
|
6
|
+
export const MIGRATION_MANIFEST_SCHEMA_VERSION = 2;
|
|
7
|
+
|
|
8
|
+
export const BUILT_IN_MIGRATIONS = Object.freeze({
|
|
9
|
+
"upgrade.republish-ai-entry.v1": "republish-ai-entry",
|
|
10
|
+
"upgrade.republish-projection.v1": "republish-projection",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const MANIFEST_FILE = fileURLToPath(new URL("../../migration-manifest.json", import.meta.url));
|
|
14
|
+
const SHA256 = /^sha256:[a-f0-9]{64}$/u;
|
|
15
|
+
const VERSION = /^\d+\.\d+\.\d+$/u;
|
|
16
|
+
const MIGRATION_KINDS = new Set([
|
|
17
|
+
"package-only",
|
|
18
|
+
"republish-ai-entry",
|
|
19
|
+
"republish-projection",
|
|
20
|
+
"built-in-store-migration",
|
|
21
|
+
"invalidate-ephemeral-protocol",
|
|
22
|
+
]);
|
|
23
|
+
const ROLLBACK_CLASSES = new Set(["package-only", "reversible-data", "forward-only"]);
|
|
24
|
+
const CONSUMER_CHANGE_KEYS = new Set([
|
|
25
|
+
"actionPlan", "adaptiveContextBundle", "capabilities", "contextQuery", "coverageAudit", "evidenceBundle", "evidenceInput", "exchange",
|
|
26
|
+
"hostPromotionEvidence", "integrationReviewBundle", "reviewBundle", "routingIndex", "stageContextBundle", "stageReceipt", "taskContextPlan",
|
|
27
|
+
"truthReconciliationInput", "truthReconciliationReviewBundle",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function invalid(message, details) {
|
|
31
|
+
fail("upgrade-manifest-invalid", message, { details });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function object(value, label) {
|
|
35
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) invalid(`${label} must be an object`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function exactKeys(value, allowed, label) {
|
|
39
|
+
object(value, label);
|
|
40
|
+
const keys = Object.keys(value).sort();
|
|
41
|
+
const expected = [...allowed].sort();
|
|
42
|
+
if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) {
|
|
43
|
+
invalid(`${label} must contain exactly: ${expected.join(", ")}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function string(value, label) {
|
|
48
|
+
if (typeof value !== "string" || value.length === 0) invalid(`${label} must be a non-empty string`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function boolean(value, label) {
|
|
52
|
+
if (typeof value !== "boolean") invalid(`${label} must be a boolean`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function integer(value, label) {
|
|
56
|
+
if (!Number.isInteger(value) || value < 1) invalid(`${label} must be a positive integer`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sortedUnique(values, label, validate = string) {
|
|
60
|
+
if (!Array.isArray(values)) invalid(`${label} must be an array`);
|
|
61
|
+
values.forEach((value) => validate(value, `${label} entry`));
|
|
62
|
+
const sorted = [...values].sort((left, right) => String(left).localeCompare(String(right)));
|
|
63
|
+
if (new Set(values).size !== values.length || values.some((value, index) => value !== sorted[index])) {
|
|
64
|
+
invalid(`${label} must be unique and sorted`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function versions(values, label) {
|
|
69
|
+
sortedUnique(values, label, (value, entryLabel) => {
|
|
70
|
+
string(value, entryLabel);
|
|
71
|
+
if (!VERSION.test(value)) invalid(`${entryLabel} must be an exact semantic version`);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function versionMatrix(value, label) {
|
|
76
|
+
exactKeys(value, new Set(["readable", "written"]), label);
|
|
77
|
+
sortedUnique(value.readable, `${label}.readable`, integer);
|
|
78
|
+
if (Array.isArray(value.written)) sortedUnique(value.written, `${label}.written`, integer);
|
|
79
|
+
else integer(value.written, `${label}.written`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function validateMigration(entry, index) {
|
|
83
|
+
const label = `builtInMigrations[${index}]`;
|
|
84
|
+
exactKeys(entry, new Set(["id", "kind", "input", "targetType", "writes", "rollbackClass", "reverseMigrationId"]), label);
|
|
85
|
+
string(entry.id, `${label}.id`);
|
|
86
|
+
if (!MIGRATION_KINDS.has(entry.kind)) invalid(`${label}.kind is unsupported`);
|
|
87
|
+
exactKeys(entry.input, new Set(["schemaVersions", "beforeDigestRequired"]), `${label}.input`);
|
|
88
|
+
sortedUnique(entry.input.schemaVersions, `${label}.input.schemaVersions`, integer);
|
|
89
|
+
boolean(entry.input.beforeDigestRequired, `${label}.input.beforeDigestRequired`);
|
|
90
|
+
if (!["none", "ai-entry", "projection", "store", "ephemeral-protocol"].includes(entry.targetType)) invalid(`${label}.targetType is invalid`);
|
|
91
|
+
boolean(entry.writes, `${label}.writes`);
|
|
92
|
+
if (!ROLLBACK_CLASSES.has(entry.rollbackClass)) invalid(`${label}.rollbackClass is invalid`);
|
|
93
|
+
if (entry.reverseMigrationId !== null) string(entry.reverseMigrationId, `${label}.reverseMigrationId`);
|
|
94
|
+
if (entry.rollbackClass === "reversible-data" && entry.reverseMigrationId === null) invalid(`${label} requires a reverse migration`);
|
|
95
|
+
if (BUILT_IN_MIGRATIONS[entry.id] !== entry.kind) invalid(`${label} does not match the runtime migration registry`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function validateMigrationManifest(input) {
|
|
99
|
+
const manifest = structuredClone(input);
|
|
100
|
+
exactKeys(manifest, new Set([
|
|
101
|
+
"schemaVersion", "package", "upgradeFrom", "stores", "renderers", "protocols", "paths",
|
|
102
|
+
"builtInMigrations", "consumerChanges", "acceptanceCommands", "rollback", "externalEffects", "manifestDigest",
|
|
103
|
+
]), "migration manifest");
|
|
104
|
+
if (manifest.schemaVersion !== MIGRATION_MANIFEST_SCHEMA_VERSION) invalid("migration manifest schemaVersion must be 2");
|
|
105
|
+
exactKeys(manifest.package, new Set(["name", "version"]), "migration manifest package");
|
|
106
|
+
if (manifest.package.name !== "frontend-project-context" || manifest.package.version !== "1.7.0") invalid("migration manifest package does not match this runtime");
|
|
107
|
+
versions(manifest.upgradeFrom, "upgradeFrom");
|
|
108
|
+
if (manifest.upgradeFrom.length === 0) invalid("upgradeFrom must not be empty");
|
|
109
|
+
exactKeys(manifest.stores, new Set(["contract", "projectionLock", "proposal", "sourceLock"]), "stores");
|
|
110
|
+
for (const name of Object.keys(manifest.stores)) versionMatrix(manifest.stores[name], `stores.${name}`);
|
|
111
|
+
exactKeys(manifest.renderers, new Set(["aiEntry", "projection"]), "renderers");
|
|
112
|
+
for (const name of Object.keys(manifest.renderers)) versionMatrix(manifest.renderers[name], `renderers.${name}`);
|
|
113
|
+
exactKeys(manifest.protocols, new Set([
|
|
114
|
+
"exchange", "actionPlan", "adaptiveContextBundle", "contextQuery", "coverageAudit", "reviewBundle", "evidenceInput", "evidenceBundle", "taskContextPlan",
|
|
115
|
+
"routingIndex", "stageReceipt", "stageContextBundle", "integrationReviewBundle", "hostPromotionEvidence",
|
|
116
|
+
"truthReconciliationInput", "truthReconciliationReviewBundle",
|
|
117
|
+
]), "protocols");
|
|
118
|
+
for (const name of Object.keys(manifest.protocols)) versionMatrix(manifest.protocols[name], `protocols.${name}`);
|
|
119
|
+
if (!Array.isArray(manifest.builtInMigrations)) invalid("builtInMigrations must be an array");
|
|
120
|
+
manifest.builtInMigrations.forEach(validateMigration);
|
|
121
|
+
const migrationIds = manifest.builtInMigrations.map((entry) => entry.id);
|
|
122
|
+
sortedUnique(migrationIds, "builtInMigrations ids");
|
|
123
|
+
const runtimeMigrationIds = Object.keys(BUILT_IN_MIGRATIONS).sort((left, right) => left.localeCompare(right));
|
|
124
|
+
if (migrationIds.length !== runtimeMigrationIds.length || migrationIds.some((id, index) => id !== runtimeMigrationIds[index])) {
|
|
125
|
+
invalid("builtInMigrations must exactly match the runtime migration registry");
|
|
126
|
+
}
|
|
127
|
+
if (!Array.isArray(manifest.paths) || manifest.paths.length !== manifest.upgradeFrom.length) invalid("paths must contain one path for every upgradeFrom version");
|
|
128
|
+
manifest.paths.forEach((entry, index) => {
|
|
129
|
+
const label = `paths[${index}]`;
|
|
130
|
+
exactKeys(entry, new Set(["fromVersion", "classification", "migrationIds", "requiresHumanReview", "rollbackClass", "acceptance"]), label);
|
|
131
|
+
if (entry.fromVersion !== manifest.upgradeFrom[index]) invalid("paths must be ordered exactly like upgradeFrom");
|
|
132
|
+
if (!VERSION.test(entry.fromVersion)) invalid(`${label}.fromVersion must be exact`);
|
|
133
|
+
if (!MIGRATION_KINDS.has(entry.classification)) invalid(`${label}.classification is invalid`);
|
|
134
|
+
sortedUnique(entry.migrationIds, `${label}.migrationIds`);
|
|
135
|
+
for (const id of entry.migrationIds) if (!migrationIds.includes(id)) invalid(`${label} references unknown migration: ${id}`);
|
|
136
|
+
boolean(entry.requiresHumanReview, `${label}.requiresHumanReview`);
|
|
137
|
+
if (!ROLLBACK_CLASSES.has(entry.rollbackClass)) invalid(`${label}.rollbackClass is invalid`);
|
|
138
|
+
sortedUnique(entry.acceptance, `${label}.acceptance`);
|
|
139
|
+
});
|
|
140
|
+
exactKeys(manifest.consumerChanges, CONSUMER_CHANGE_KEYS, "consumerChanges");
|
|
141
|
+
for (const [name, change] of Object.entries(manifest.consumerChanges)) {
|
|
142
|
+
exactKeys(change, new Set(["state", "reason"]), `consumerChanges.${name}`);
|
|
143
|
+
if (!["preserve", "regenerate", "invalidate"].includes(change.state)) invalid(`consumerChanges.${name}.state is invalid`);
|
|
144
|
+
string(change.reason, `consumerChanges.${name}.reason`);
|
|
145
|
+
}
|
|
146
|
+
sortedUnique(manifest.acceptanceCommands, "acceptanceCommands");
|
|
147
|
+
exactKeys(manifest.rollback, new Set(["defaultClass", "externalRestoreRequired", "automatic"]), "rollback");
|
|
148
|
+
if (!ROLLBACK_CLASSES.has(manifest.rollback.defaultClass)) invalid("rollback.defaultClass is invalid");
|
|
149
|
+
boolean(manifest.rollback.externalRestoreRequired, "rollback.externalRestoreRequired");
|
|
150
|
+
if (manifest.rollback.automatic !== false) invalid("rollback.automatic must be false");
|
|
151
|
+
exactKeys(manifest.externalEffects, new Set(["packageManager", "network", "git", "projectTests", "businessCode", "automaticUpgrade"]), "externalEffects");
|
|
152
|
+
for (const [name, value] of Object.entries(manifest.externalEffects)) if (value !== false) invalid(`externalEffects.${name} must be false`);
|
|
153
|
+
if (!SHA256.test(manifest.manifestDigest)) invalid("manifestDigest must be sha256");
|
|
154
|
+
const unsigned = Object.fromEntries(Object.entries(manifest).filter(([key]) => key !== "manifestDigest"));
|
|
155
|
+
if (digestJson(unsigned) !== manifest.manifestDigest) invalid("manifestDigest does not match manifest bytes");
|
|
156
|
+
return manifest;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function loadMigrationManifest(dependencies = {}) {
|
|
160
|
+
const read = dependencies.readFile ?? readFile;
|
|
161
|
+
let parsed;
|
|
162
|
+
try {
|
|
163
|
+
parsed = JSON.parse(await read(MANIFEST_FILE, "utf8"));
|
|
164
|
+
} catch (error) {
|
|
165
|
+
invalid("cannot read the package migration manifest", { reason: error.message });
|
|
166
|
+
}
|
|
167
|
+
return validateMigrationManifest(parsed);
|
|
168
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { buildSyncAssistBundle } from "./assist.mjs";
|
|
2
|
+
import { inspectAiEntry } from "./ai-entry.mjs";
|
|
3
|
+
import { checkExitCode, checkProject } from "./checker.mjs";
|
|
4
|
+
import { ProjectContextError } from "./errors.mjs";
|
|
5
|
+
import { PACKAGE_VERSION } from "./exchange-schema.mjs";
|
|
6
|
+
import { inspectProjectInitialization, loadProject } from "./project-store.mjs";
|
|
7
|
+
|
|
8
|
+
export const PROJECT_STATUS_SCHEMA_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
function uniqueSorted(values) {
|
|
11
|
+
return [...new Set(values.filter((value) => value !== undefined && value !== null))]
|
|
12
|
+
.sort((left, right) => String(left).localeCompare(String(right)));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function emptySummary() {
|
|
16
|
+
return { findings: 0, changedSources: 0, pendingItems: 0, affectedProjections: 0 };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function compactReadTarget(target) {
|
|
20
|
+
return {
|
|
21
|
+
sourceId: target.sourceId,
|
|
22
|
+
locator: structuredClone(target.locator),
|
|
23
|
+
reason: target.reason,
|
|
24
|
+
priority: target.priority,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compactWorkUnit(unit) {
|
|
29
|
+
const compact = { kind: unit.kind };
|
|
30
|
+
for (const key of ["sourceIds", "itemIds", "fallbackItemIds", "paths", "projectionPaths", "findingCodes"]) {
|
|
31
|
+
if (unit[key] !== undefined) compact[key] = [...unit[key]];
|
|
32
|
+
}
|
|
33
|
+
return compact;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function base(initialization, health, nextActions) {
|
|
37
|
+
return {
|
|
38
|
+
schemaVersion: PROJECT_STATUS_SCHEMA_VERSION,
|
|
39
|
+
package: { name: "frontend-project-context", version: PACKAGE_VERSION },
|
|
40
|
+
initialization: {
|
|
41
|
+
state: initialization,
|
|
42
|
+
present: [],
|
|
43
|
+
missing: [],
|
|
44
|
+
},
|
|
45
|
+
project: null,
|
|
46
|
+
snapshots: null,
|
|
47
|
+
health,
|
|
48
|
+
entry: { state: "absent", path: null, rendererVersion: null },
|
|
49
|
+
summary: emptySummary(),
|
|
50
|
+
findingCodes: [],
|
|
51
|
+
sourceIds: [],
|
|
52
|
+
itemIds: [],
|
|
53
|
+
projectionPaths: [],
|
|
54
|
+
readTargets: [],
|
|
55
|
+
workUnits: [],
|
|
56
|
+
nextActions,
|
|
57
|
+
boundaries: {},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function statusBoundaries(boundaries) {
|
|
62
|
+
return structuredClone(boundaries);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function buildProjectStatus(root, boundaries) {
|
|
66
|
+
const initialization = await inspectProjectInitialization(root);
|
|
67
|
+
if (initialization.status !== "initialized") {
|
|
68
|
+
const result = base(initialization.status, initialization.status, initialization.status === "uninitialized" ? ["run-setup-preview"] : ["resolve-conflict"]);
|
|
69
|
+
result.initialization.present = [...initialization.present];
|
|
70
|
+
result.initialization.missing = [...initialization.missing];
|
|
71
|
+
if (initialization.status === "partial") result.findingCodes = ["project-state-partial"];
|
|
72
|
+
result.summary.findings = result.findingCodes.length;
|
|
73
|
+
result.boundaries = statusBoundaries(boundaries);
|
|
74
|
+
return { status: result, exitCode: initialization.status === "uninitialized" ? 1 : 2 };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let project;
|
|
78
|
+
try {
|
|
79
|
+
project = await loadProject(root);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (!(error instanceof ProjectContextError)) throw error;
|
|
82
|
+
const result = base("invalid", "invalid", ["resolve-conflict"]);
|
|
83
|
+
result.initialization.present = [...initialization.present];
|
|
84
|
+
result.initialization.missing = [...initialization.missing];
|
|
85
|
+
result.findingCodes = ["project-state-invalid"];
|
|
86
|
+
result.summary.findings = 1;
|
|
87
|
+
result.boundaries = statusBoundaries(boundaries);
|
|
88
|
+
return { status: result, exitCode: 2 };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const regionEntries = project.projectionsLock.projections.filter((entry) => entry.ownership === "region" && entry.target === "ai-entry");
|
|
92
|
+
const entryPath = regionEntries[0]?.path ?? "AGENTS.md";
|
|
93
|
+
const [sync, entry] = await Promise.all([
|
|
94
|
+
buildSyncAssistBundle(root, project),
|
|
95
|
+
inspectAiEntry(root, project, entryPath),
|
|
96
|
+
]);
|
|
97
|
+
const findings = [...sync.findings];
|
|
98
|
+
if (entry.state === "absent") findings.push({ code: "ai-entry-missing", path: null });
|
|
99
|
+
if (entry.state === "conflict" && !findings.some((finding) => finding.code === "ai-entry-ownership-conflict")) {
|
|
100
|
+
findings.push({ code: "ai-entry-ownership-conflict", path: entry.path });
|
|
101
|
+
}
|
|
102
|
+
const conflict = entry.state === "conflict" || findings.some((finding) => finding.code.includes("ownership-conflict"));
|
|
103
|
+
const sourceOrContractBlocker = findings.some((finding) =>
|
|
104
|
+
finding.code.startsWith("source-") || finding.code.startsWith("verification-") || finding.code.startsWith("scope-") || finding.code === "contract-conflict",
|
|
105
|
+
);
|
|
106
|
+
const health = conflict ? "conflict" : findings.length > 0 || entry.state !== "current" ? "attention" : "clean";
|
|
107
|
+
const nextActions = [];
|
|
108
|
+
if (conflict) nextActions.push("resolve-conflict");
|
|
109
|
+
else {
|
|
110
|
+
if (sync.summary.changedSources > 0 || sourceOrContractBlocker) nextActions.push("review-source-change");
|
|
111
|
+
if (sync.summary.pendingItems > 0) nextActions.push("review-pending");
|
|
112
|
+
if (sync.summary.affectedProjections > 0) nextActions.push("review-projection");
|
|
113
|
+
if (entry.state !== "current") nextActions.push("publish-ai-entry");
|
|
114
|
+
if (findings.length > 0 && nextActions.length === 0) nextActions.push("run-sync");
|
|
115
|
+
if (health === "clean") nextActions.push("ready-for-task");
|
|
116
|
+
}
|
|
117
|
+
const sourceIds = uniqueSorted([
|
|
118
|
+
...findings.flatMap((finding) => finding.source ? [finding.source] : []),
|
|
119
|
+
...sync.workUnits.flatMap((unit) => unit.sourceIds ?? []),
|
|
120
|
+
]);
|
|
121
|
+
const itemIds = uniqueSorted([
|
|
122
|
+
...findings.flatMap((finding) => finding.items ?? (finding.item ? [finding.item] : [])),
|
|
123
|
+
...sync.workUnits.flatMap((unit) => unit.itemIds ?? []),
|
|
124
|
+
]);
|
|
125
|
+
const projectionPaths = uniqueSorted([
|
|
126
|
+
...sync.projectionPaths,
|
|
127
|
+
...findings.flatMap((finding) => finding.path ? [finding.path] : []),
|
|
128
|
+
]);
|
|
129
|
+
const status = {
|
|
130
|
+
schemaVersion: PROJECT_STATUS_SCHEMA_VERSION,
|
|
131
|
+
package: { name: "frontend-project-context", version: PACKAGE_VERSION },
|
|
132
|
+
initialization: { state: "initialized", present: [...initialization.present], missing: [] },
|
|
133
|
+
project: { id: project.contract.project.id, name: project.contract.project.name },
|
|
134
|
+
snapshots: {
|
|
135
|
+
contract: project.contractDigest,
|
|
136
|
+
sourcesLock: project.sourcesLockDigest,
|
|
137
|
+
projectionsLock: project.projectionsLockDigest,
|
|
138
|
+
},
|
|
139
|
+
health,
|
|
140
|
+
entry,
|
|
141
|
+
summary: {
|
|
142
|
+
findings: findings.length,
|
|
143
|
+
changedSources: sync.summary.changedSources,
|
|
144
|
+
pendingItems: sync.summary.pendingItems,
|
|
145
|
+
affectedProjections: projectionPaths.length,
|
|
146
|
+
},
|
|
147
|
+
findingCodes: uniqueSorted(findings.map((finding) => finding.code)),
|
|
148
|
+
sourceIds,
|
|
149
|
+
itemIds,
|
|
150
|
+
projectionPaths,
|
|
151
|
+
readTargets: sync.readTargets.map(compactReadTarget),
|
|
152
|
+
workUnits: sync.workUnits.map(compactWorkUnit),
|
|
153
|
+
nextActions: uniqueSorted(nextActions),
|
|
154
|
+
boundaries: statusBoundaries(boundaries),
|
|
155
|
+
};
|
|
156
|
+
return { status, exitCode: health === "clean" ? 0 : conflict ? 3 : Math.max(1, checkExitCode(findings)) };
|
|
157
|
+
}
|
|
@@ -28,6 +28,12 @@ export async function publishProjection(root, project, options, dependencies = {
|
|
|
28
28
|
const rendered = renderProjection(project.contract, paths, target);
|
|
29
29
|
const existing = await readContent(resolved.absolute);
|
|
30
30
|
const lockEntry = project.projectionsLock.projections.find((entry) => entry.path === resolved.normalized);
|
|
31
|
+
if (lockEntry?.ownership === "region") {
|
|
32
|
+
fail("ai-entry-path-conflict", `refusing whole-file projection over managed AI Entry: ${resolved.normalized}`, {
|
|
33
|
+
exitCode: 3,
|
|
34
|
+
details: { path: resolved.normalized },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
31
37
|
if (existing !== null) {
|
|
32
38
|
const marker = parseProjectionMarker(existing);
|
|
33
39
|
if (
|
|
@@ -46,6 +52,7 @@ export async function publishProjection(root, project, options, dependencies = {
|
|
|
46
52
|
const nextEntry = {
|
|
47
53
|
path: resolved.normalized,
|
|
48
54
|
target,
|
|
55
|
+
...(project.projectionsLock.schemaVersion === 2 ? { ownership: "file" } : {}),
|
|
49
56
|
paths: rendered.paths,
|
|
50
57
|
contractDigest: rendered.contractDigest,
|
|
51
58
|
bundleDigest: rendered.bundleDigest,
|
|
@@ -54,7 +61,7 @@ export async function publishProjection(root, project, options, dependencies = {
|
|
|
54
61
|
rendererVersion: rendered.rendererVersion,
|
|
55
62
|
};
|
|
56
63
|
const nextLock = {
|
|
57
|
-
schemaVersion:
|
|
64
|
+
schemaVersion: project.projectionsLock.schemaVersion,
|
|
58
65
|
projections: [
|
|
59
66
|
...project.projectionsLock.projections.filter((entry) => entry.path !== resolved.normalized),
|
|
60
67
|
nextEntry,
|