frontend-project-context 1.3.0 → 1.6.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 +39 -2
- package/README.md +94 -16
- package/UPGRADING.md +47 -2
- package/docs/04-PROGRAM-DESIGN.md +40 -4
- package/docs/05-ACCEPTANCE-CONTRACT.md +33 -3
- package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +36 -6
- package/docs/14-FORMAL-RELEASE-READINESS.md +46 -0
- package/docs/18-BRANCH-AWARE-STAGED-CONTEXT-DESIGN.md +62 -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/README.md +21 -5
- package/docs/USER-AND-AI-OPERATION-MANUAL.md +797 -0
- package/examples/README.md +38 -0
- package/examples/package.json +6 -2
- package/migration-manifest.json +88 -0
- package/package.json +3 -2
- package/schemas/action-plan.schema.json +31 -3
- package/schemas/capabilities.schema.json +50 -18
- package/schemas/evidence-bundle.schema.json +64 -0
- package/schemas/evidence-input.schema.json +82 -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/upgrade-assessment.schema.json +48 -0
- package/schemas/upgrade-result-bundle.schema.json +35 -0
- package/src/project-context/ai-entry.mjs +320 -0
- package/src/project-context/capabilities.mjs +44 -17
- package/src/project-context/checker.mjs +20 -3
- package/src/project-context/cli.mjs +84 -7
- 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 +6 -1
- package/src/project-context/evidence-schema.mjs +209 -0
- package/src/project-context/evidence.mjs +99 -0
- package/src/project-context/exchange-schema.mjs +21 -11
- package/src/project-context/exchange.mjs +26 -4
- package/src/project-context/maintenance.mjs +2 -2
- package/src/project-context/migration-manifest.mjs +166 -0
- package/src/project-context/project-status.mjs +157 -0
- package/src/project-context/projection-store.mjs +8 -1
- package/src/project-context/task-context-schema.mjs +237 -1
- package/src/project-context/task-context.mjs +154 -13
- package/src/project-context/upgrade-schema.mjs +215 -0
- package/src/project-context/upgrade.mjs +494 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { canonicalValue, digestJson } from "./canonical-json.mjs";
|
|
2
|
+
import { fail } from "./errors.mjs";
|
|
3
|
+
|
|
4
|
+
export const UPGRADE_ASSESSMENT_SCHEMA_VERSION = 1;
|
|
5
|
+
export const MIGRATION_PLAN_SCHEMA_VERSION = 1;
|
|
6
|
+
export const UPGRADE_RESULT_BUNDLE_SCHEMA_VERSION = 1;
|
|
7
|
+
|
|
8
|
+
const SHA256 = /^sha256:[a-f0-9]{64}$/u;
|
|
9
|
+
const AUTHORITY_FIELDS = new Set(["approved", "approval", "reviewer", "by", "shell", "command", "write"]);
|
|
10
|
+
const BOUNDARY_FIELDS = new Set([
|
|
11
|
+
"provider", "agentRuntime", "git", "network", "dependencyInstallation", "automaticApproval", "businessCodeWrites",
|
|
12
|
+
"taskExecution", "stagePathBodyReads", "applyPlan", "scheduler", "daemon", "telemetry", "selfUpdate",
|
|
13
|
+
"automaticEvidenceUpload", "packageManager", "automaticUpgrade",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function invalid(code, message) {
|
|
17
|
+
fail(code, message);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function assertJsonShape(value, label, code, required, options = {}) {
|
|
21
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) invalid(code, `${label} must be an object`);
|
|
22
|
+
const keys = Object.keys(value).sort();
|
|
23
|
+
const expected = [...required].sort();
|
|
24
|
+
if (!options.allowExtra && (keys.length !== expected.length || keys.some((key, index) => key !== expected[index]))) {
|
|
25
|
+
invalid(code, `${label} must contain exactly: ${expected.join(", ")}`);
|
|
26
|
+
}
|
|
27
|
+
for (const key of Object.keys(value)) if (AUTHORITY_FIELDS.has(key)) invalid(code, `${label} contains forbidden authority or execution field: ${key}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function digest(value, label, code, nullable = false) {
|
|
31
|
+
if (nullable && value === null) return;
|
|
32
|
+
if (typeof value !== "string" || !SHA256.test(value)) invalid(code, `${label} must be sha256`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function string(value, label, code) {
|
|
36
|
+
if (typeof value !== "string" || value.length === 0) invalid(code, `${label} must be a non-empty string`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function projectPath(value, label, code) {
|
|
40
|
+
string(value, label, code);
|
|
41
|
+
const segments = value.split("/");
|
|
42
|
+
if (
|
|
43
|
+
value.includes("\0") || value.includes("\\") || value.startsWith("/") || /^[A-Za-z]:/u.test(value)
|
|
44
|
+
|| segments.some((segment) => segment === "" || segment === "." || segment === "..")
|
|
45
|
+
) invalid(code, `${label} must be a canonical project-relative path`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sortedUnique(values, label, code) {
|
|
49
|
+
if (!Array.isArray(values)) invalid(code, `${label} must be an array`);
|
|
50
|
+
values.forEach((value) => string(value, `${label} entry`, code));
|
|
51
|
+
const sorted = [...values].sort((left, right) => left.localeCompare(right));
|
|
52
|
+
if (new Set(values).size !== values.length || values.some((value, index) => value !== sorted[index])) invalid(code, `${label} must be unique and sorted`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function validateSnapshots(value, label, code, nullable = false) {
|
|
56
|
+
if (nullable && value === null) return;
|
|
57
|
+
assertJsonShape(value, label, code, new Set(["contract", "sourcesLock", "projectionsLock"]));
|
|
58
|
+
digest(value.contract, `${label}.contract`, code);
|
|
59
|
+
digest(value.sourcesLock, `${label}.sourcesLock`, code);
|
|
60
|
+
digest(value.projectionsLock, `${label}.projectionsLock`, code);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function validateAction(action, label, code) {
|
|
64
|
+
assertJsonShape(action, label, code, new Set(["id", "kind", "migrationId", "targets", "writes", "semanticImpact"]));
|
|
65
|
+
string(action.id, `${label}.id`, code);
|
|
66
|
+
if (!["verify-complete", "republish-ai-entry", "republish-projection", "built-in-store-migration", "invalidate-ephemeral-protocol"].includes(action.kind)) invalid(code, `${label}.kind is invalid`);
|
|
67
|
+
if (action.migrationId !== null) string(action.migrationId, `${label}.migrationId`, code);
|
|
68
|
+
sortedUnique(action.targets, `${label}.targets`, code);
|
|
69
|
+
action.targets.forEach((target) => projectPath(target, `${label}.targets entry`, code));
|
|
70
|
+
if (typeof action.writes !== "boolean") invalid(code, `${label}.writes must be boolean`);
|
|
71
|
+
if (!["none", "managed-rendering", "store-structure", "protocol-artifact-invalidation"].includes(action.semanticImpact)) invalid(code, `${label}.semanticImpact is invalid`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function verifySelfDigest(value, field, code) {
|
|
75
|
+
digest(value[field], field, code);
|
|
76
|
+
const unsigned = Object.fromEntries(Object.entries(value).filter(([key]) => key !== field));
|
|
77
|
+
if (digestJson(unsigned) !== value[field]) invalid(code, `${field} does not match artifact content`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function scanForbidden(value, label, code) {
|
|
81
|
+
if (!value || typeof value !== "object") return;
|
|
82
|
+
for (const [key, child] of Object.entries(value)) {
|
|
83
|
+
if (AUTHORITY_FIELDS.has(key)) invalid(code, `${label} contains forbidden field: ${key}`);
|
|
84
|
+
scanForbidden(child, `${label}.${key}`, code);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function finalizeUpgradeAssessment(value) {
|
|
89
|
+
const unsigned = canonicalValue(value);
|
|
90
|
+
return { ...unsigned, assessmentDigest: digestJson(unsigned) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function validateUpgradeAssessment(input) {
|
|
94
|
+
const code = "upgrade-assessment-invalid";
|
|
95
|
+
const value = structuredClone(input);
|
|
96
|
+
assertJsonShape(value, "upgrade assessment", code, new Set([
|
|
97
|
+
"schemaVersion", "kind", "product", "fromVersion", "targetVersion", "fromVersionEvidence",
|
|
98
|
+
"manifestSchemaVersion", "manifestDigest", "initialization", "health", "entryState", "findingCodes",
|
|
99
|
+
"snapshots", "compatibility", "migrationPath", "rollbackClass", "requiresHumanReview", "state", "boundaries", "assessmentDigest",
|
|
100
|
+
]));
|
|
101
|
+
scanForbidden(value, "upgrade assessment", code);
|
|
102
|
+
if (value.schemaVersion !== 1 || value.kind !== "upgrade-assessment") invalid(code, "upgrade assessment schema or kind is invalid");
|
|
103
|
+
assertJsonShape(value.product, "product", code, new Set(["name"]));
|
|
104
|
+
if (value.product.name !== "frontend-project-context") invalid(code, "product name is invalid");
|
|
105
|
+
string(value.fromVersion, "fromVersion", code); string(value.targetVersion, "targetVersion", code);
|
|
106
|
+
if (value.fromVersionEvidence !== "host-asserted") invalid(code, "fromVersionEvidence must be host-asserted");
|
|
107
|
+
if (value.manifestSchemaVersion !== 2) invalid(code, "manifestSchemaVersion must be 2");
|
|
108
|
+
digest(value.manifestDigest, "manifestDigest", code);
|
|
109
|
+
assertJsonShape(value.initialization, "initialization", code, new Set(["state", "present", "missing"]));
|
|
110
|
+
sortedUnique(value.initialization.present, "initialization.present", code);
|
|
111
|
+
sortedUnique(value.initialization.missing, "initialization.missing", code);
|
|
112
|
+
if (!["uninitialized", "partial", "initialized", "invalid"].includes(value.initialization.state)) invalid(code, "initialization.state is invalid");
|
|
113
|
+
if (!["uninitialized", "partial", "invalid", "attention", "conflict", "clean"].includes(value.health)) invalid(code, "health is invalid");
|
|
114
|
+
assertJsonShape(value.entryState, "entryState", code, new Set(["state", "path", "rendererVersion"]));
|
|
115
|
+
if (value.entryState.path !== null) string(value.entryState.path, "entryState.path", code);
|
|
116
|
+
if (value.entryState.rendererVersion !== null && !Number.isInteger(value.entryState.rendererVersion)) invalid(code, "entryState.rendererVersion is invalid");
|
|
117
|
+
sortedUnique(value.findingCodes, "findingCodes", code);
|
|
118
|
+
validateSnapshots(value.snapshots, "snapshots", code, true);
|
|
119
|
+
assertJsonShape(value.compatibility, "compatibility", code, new Set(["stores", "renderers", "protocols"]));
|
|
120
|
+
const compatibilityKeys = {
|
|
121
|
+
stores: new Set(["contract", "projectionLock", "proposal", "sourceLock"]),
|
|
122
|
+
renderers: new Set(["aiEntry", "projection"]),
|
|
123
|
+
protocols: new Set(["actionPlan", "evidenceBundle", "evidenceInput", "exchange", "integrationReviewBundle", "reviewBundle", "stageContextBundle", "stageReceipt", "taskContextPlan"]),
|
|
124
|
+
};
|
|
125
|
+
for (const [name, group] of Object.entries(value.compatibility)) {
|
|
126
|
+
assertJsonShape(group, `compatibility.${name}`, code, compatibilityKeys[name]);
|
|
127
|
+
for (const state of Object.values(group)) if (!["compatible", "republish", "regenerate", "invalidate", "unsupported", "not-applicable"].includes(state)) invalid(code, "compatibility state is invalid");
|
|
128
|
+
}
|
|
129
|
+
sortedUnique(value.migrationPath, "migrationPath", code);
|
|
130
|
+
if (!["package-only", "reversible-data", "forward-only"].includes(value.rollbackClass)) invalid(code, "rollbackClass is invalid");
|
|
131
|
+
if (typeof value.requiresHumanReview !== "boolean") invalid(code, "requiresHumanReview must be boolean");
|
|
132
|
+
if (!["not-applicable", "blocked", "ready-for-plan", "core-complete"].includes(value.state)) invalid(code, "assessment state is invalid");
|
|
133
|
+
assertJsonShape(value.boundaries, "boundaries", code, BOUNDARY_FIELDS);
|
|
134
|
+
for (const boundary of Object.values(value.boundaries)) if (boundary !== false) invalid(code, "upgrade boundaries must all be false");
|
|
135
|
+
verifySelfDigest(value, "assessmentDigest", code);
|
|
136
|
+
return canonicalValue(value);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function finalizeMigrationPlan(value) {
|
|
140
|
+
const unsigned = canonicalValue(value);
|
|
141
|
+
return { ...unsigned, planDigest: digestJson(unsigned) };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function validateMigrationPlan(input) {
|
|
145
|
+
const code = "upgrade-plan-invalid";
|
|
146
|
+
const value = structuredClone(input);
|
|
147
|
+
assertJsonShape(value, "migration plan", code, new Set([
|
|
148
|
+
"schemaVersion", "kind", "fromVersion", "targetVersion", "manifestDigest", "assessmentDigest", "snapshots",
|
|
149
|
+
"targetDigests", "nextAction", "remainingMigrationIds", "acceptance", "rollback", "requiresHumanReview", "authority", "planDigest",
|
|
150
|
+
]));
|
|
151
|
+
scanForbidden(value, "migration plan", code);
|
|
152
|
+
if (value.schemaVersion !== 1 || value.kind !== "target-upgrade-plan") invalid(code, "migration plan schema or kind is invalid");
|
|
153
|
+
string(value.fromVersion, "fromVersion", code); string(value.targetVersion, "targetVersion", code);
|
|
154
|
+
digest(value.manifestDigest, "manifestDigest", code); digest(value.assessmentDigest, "assessmentDigest", code);
|
|
155
|
+
validateSnapshots(value.snapshots, "snapshots", code, true);
|
|
156
|
+
if (!Array.isArray(value.targetDigests)) invalid(code, "targetDigests must be an array");
|
|
157
|
+
value.targetDigests.forEach((target, index) => {
|
|
158
|
+
assertJsonShape(target, `targetDigests[${index}]`, code, new Set(["path", "beforeDigest"]));
|
|
159
|
+
projectPath(target.path, `targetDigests[${index}].path`, code); digest(target.beforeDigest, `targetDigests[${index}].beforeDigest`, code, true);
|
|
160
|
+
});
|
|
161
|
+
const action = value.nextAction;
|
|
162
|
+
validateAction(action, "nextAction", code);
|
|
163
|
+
sortedUnique(value.remainingMigrationIds, "remainingMigrationIds", code);
|
|
164
|
+
assertJsonShape(value.acceptance, "acceptance", code, new Set(["core", "hostRequired"]));
|
|
165
|
+
sortedUnique(value.acceptance.core, "acceptance.core", code); sortedUnique(value.acceptance.hostRequired, "acceptance.hostRequired", code);
|
|
166
|
+
assertJsonShape(value.rollback, "rollback", code, new Set(["class", "reverseMigrationId", "externalRestoreRequired"]));
|
|
167
|
+
if (!["package-only", "reversible-data", "forward-only"].includes(value.rollback.class)) invalid(code, "rollback.class is invalid");
|
|
168
|
+
if (value.rollback.reverseMigrationId !== null) string(value.rollback.reverseMigrationId, "rollback.reverseMigrationId", code);
|
|
169
|
+
if (typeof value.rollback.externalRestoreRequired !== "boolean") invalid(code, "rollback.externalRestoreRequired must be boolean");
|
|
170
|
+
if (typeof value.requiresHumanReview !== "boolean") invalid(code, "requiresHumanReview must be boolean");
|
|
171
|
+
if ((action.semanticImpact === "store-structure" || value.rollback.reverseMigrationId === null && action.writes) && !value.requiresHumanReview) invalid(code, "write without automatic reverse migration requires human review");
|
|
172
|
+
if (value.authority !== "human-explicit-write-required") invalid(code, "authority is invalid");
|
|
173
|
+
verifySelfDigest(value, "planDigest", code);
|
|
174
|
+
return canonicalValue(value);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function finalizeUpgradeResult(value) {
|
|
178
|
+
const unsigned = canonicalValue(value);
|
|
179
|
+
return { ...unsigned, resultDigest: digestJson(unsigned) };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function validateUpgradeResult(input) {
|
|
183
|
+
const code = "upgrade-result-invalid";
|
|
184
|
+
const value = structuredClone(input);
|
|
185
|
+
assertJsonShape(value, "upgrade result", code, new Set([
|
|
186
|
+
"schemaVersion", "kind", "product", "fromVersion", "targetVersion", "manifestDigest", "planDigest", "mode", "action",
|
|
187
|
+
"written", "targets", "snapshots", "actionResult", "findingCodes", "coreMigration", "hostAcceptance", "overallUpgrade", "resultDigest",
|
|
188
|
+
]));
|
|
189
|
+
scanForbidden(value, "upgrade result", code);
|
|
190
|
+
if (value.schemaVersion !== 1 || value.kind !== "upgrade-result-bundle") invalid(code, "upgrade result schema or kind is invalid");
|
|
191
|
+
assertJsonShape(value.product, "product", code, new Set(["name"]));
|
|
192
|
+
if (value.product.name !== "frontend-project-context") invalid(code, "product name is invalid");
|
|
193
|
+
string(value.fromVersion, "fromVersion", code); string(value.targetVersion, "targetVersion", code);
|
|
194
|
+
digest(value.manifestDigest, "manifestDigest", code); digest(value.planDigest, "planDigest", code);
|
|
195
|
+
if (!["preview", "write"].includes(value.mode)) invalid(code, "mode is invalid");
|
|
196
|
+
validateAction(value.action, "action", code);
|
|
197
|
+
if (typeof value.written !== "boolean") invalid(code, "written must be boolean");
|
|
198
|
+
if (!Array.isArray(value.targets)) invalid(code, "targets must be an array");
|
|
199
|
+
value.targets.forEach((target, index) => {
|
|
200
|
+
assertJsonShape(target, `targets[${index}]`, code, new Set(["path", "beforeDigest", "afterDigest"]));
|
|
201
|
+
projectPath(target.path, `targets[${index}].path`, code);
|
|
202
|
+
digest(target.beforeDigest, `targets[${index}].beforeDigest`, code, true);
|
|
203
|
+
digest(target.afterDigest, `targets[${index}].afterDigest`, code, true);
|
|
204
|
+
});
|
|
205
|
+
assertJsonShape(value.snapshots, "snapshots", code, new Set(["before", "after"]));
|
|
206
|
+
validateSnapshots(value.snapshots.before, "snapshots.before", code, true); validateSnapshots(value.snapshots.after, "snapshots.after", code, true);
|
|
207
|
+
string(value.actionResult, "actionResult", code);
|
|
208
|
+
sortedUnique(value.findingCodes, "findingCodes", code);
|
|
209
|
+
if (!["previewed", "applied", "blocked", "complete"].includes(value.coreMigration)) invalid(code, "coreMigration is invalid");
|
|
210
|
+
sortedUnique(value.hostAcceptance, "hostAcceptance", code);
|
|
211
|
+
if (value.hostAcceptance.join("|") !== ["dependency-and-lockfile", "independent-new-window", "project-tests-or-ci"].join("|")) invalid(code, "hostAcceptance is invalid");
|
|
212
|
+
if (value.overallUpgrade !== "host-validation-required") invalid(code, "overallUpgrade is invalid");
|
|
213
|
+
verifySelfDigest(value, "resultDigest", code);
|
|
214
|
+
return canonicalValue(value);
|
|
215
|
+
}
|