frontend-project-context 1.7.0 → 1.8.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 +9 -0
- package/README.md +16 -9
- package/UPGRADING.md +8 -0
- package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +23 -6
- package/docs/24-A130-REAL-HOST-TARGET-PROJECT-COMPARISON.md +1 -1
- package/docs/26-A130-QUALITY-CLOSURE-AND-ADAPTIVE-DELIVERY-REPAIR-DESIGN.md +1 -1
- package/docs/27-TEAM-SHARED-CONTEXT-DIRECTION-DISCUSSION.md +30 -0
- package/docs/28-REAL-PROJECT-ONBOARDING-CLOSURE-DESIGN.md +534 -0
- package/docs/AI-PROJECT-INITIALIZATION.md +89 -0
- package/docs/README.md +12 -0
- package/docs/USER-AND-AI-OPERATION-MANUAL.md +17 -13
- package/examples/README.md +2 -2
- package/examples/package.json +1 -1
- package/migration-manifest.json +19 -7
- package/package.json +2 -2
- package/schemas/capabilities.schema.json +23 -8
- package/schemas/evidence-bundle.schema.json +1 -1
- package/schemas/initialization-instruction.schema.json +60 -0
- package/schemas/migration-manifest.schema.json +3 -3
- package/schemas/migration-plan.schema.json +2 -2
- package/schemas/project-status.schema.json +5 -4
- package/schemas/upgrade-assessment.schema.json +2 -2
- package/schemas/upgrade-result-bundle.schema.json +1 -1
- package/src/project-context/ai-entry.mjs +8 -6
- package/src/project-context/capabilities.mjs +14 -0
- package/src/project-context/cli.mjs +15 -0
- package/src/project-context/contract-schema.mjs +1 -1
- package/src/project-context/exchange-schema.mjs +4 -4
- package/src/project-context/initialization-instruction.mjs +42 -0
- package/src/project-context/migration-manifest.mjs +3 -3
- package/src/project-context/project-status.mjs +14 -3
- package/src/project-context/project-store.mjs +27 -2
- package/src/project-context/upgrade-schema.mjs +1 -1
|
@@ -2,11 +2,11 @@ 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 =
|
|
5
|
+
export const PACKAGE_VERSION = "1.8.0";
|
|
6
|
+
export const EXCHANGE_PROTOCOL_VERSION = 8;
|
|
7
7
|
export const ACTION_PLAN_SCHEMA_VERSION = 2;
|
|
8
8
|
export const REVIEW_BUNDLE_SCHEMA_VERSION = 2;
|
|
9
|
-
export const CAPABILITIES_SCHEMA_VERSION =
|
|
9
|
+
export const CAPABILITIES_SCHEMA_VERSION = 8;
|
|
10
10
|
|
|
11
11
|
export const ACTION_KINDS = Object.freeze([
|
|
12
12
|
"accept-source-change",
|
|
@@ -24,7 +24,7 @@ export const ACTION_KINDS = Object.freeze([
|
|
|
24
24
|
export const COMMANDS = Object.freeze([
|
|
25
25
|
"accept-source-change", "approve", "capabilities", "check", "context", "context-query", "coverage-audit", "dashboard", "deprecate",
|
|
26
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",
|
|
27
|
+
"index-context", "instructions", "publish-entry", "remove-entry", "review-source", "revise", "setup", "stage-context", "status", "sync",
|
|
28
28
|
"upgrade-apply", "upgrade-check", "upgrade-plan",
|
|
29
29
|
"reconcile-truth",
|
|
30
30
|
]);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { sha256 } from "./canonical-json.mjs";
|
|
4
|
+
import { PACKAGE_VERSION } from "./exchange-schema.mjs";
|
|
5
|
+
|
|
6
|
+
export const INITIALIZATION_INSTRUCTION_SCHEMA_VERSION = 1;
|
|
7
|
+
export const INITIALIZATION_INSTRUCTION_ID = "ai-project-initialization";
|
|
8
|
+
export const INITIALIZATION_INSTRUCTION_VERSION = 1;
|
|
9
|
+
export const INITIALIZATION_INSTRUCTION_PACKAGE_PATH = "docs/AI-PROJECT-INITIALIZATION.md";
|
|
10
|
+
export const INITIALIZATION_INSTRUCTION_COMMAND = "project-context instructions --project PATH [--json | --prompt]";
|
|
11
|
+
|
|
12
|
+
const INSTRUCTION_FILE = fileURLToPath(new URL(`../../${INITIALIZATION_INSTRUCTION_PACKAGE_PATH}`, import.meta.url));
|
|
13
|
+
|
|
14
|
+
export async function readInitializationInstruction() {
|
|
15
|
+
const content = await readFile(INSTRUCTION_FILE, "utf8");
|
|
16
|
+
return {
|
|
17
|
+
id: INITIALIZATION_INSTRUCTION_ID,
|
|
18
|
+
version: INITIALIZATION_INSTRUCTION_VERSION,
|
|
19
|
+
packagePath: INITIALIZATION_INSTRUCTION_PACKAGE_PATH,
|
|
20
|
+
digest: sha256(content),
|
|
21
|
+
content,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function buildInitializationInstruction(targetRoot, boundaries) {
|
|
26
|
+
return {
|
|
27
|
+
schemaVersion: INITIALIZATION_INSTRUCTION_SCHEMA_VERSION,
|
|
28
|
+
package: { name: "frontend-project-context", version: PACKAGE_VERSION },
|
|
29
|
+
instruction: await readInitializationInstruction(),
|
|
30
|
+
targetRoot,
|
|
31
|
+
boundaries: { ...boundaries },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function renderInitializationPrompt(result) {
|
|
36
|
+
return [
|
|
37
|
+
`<!-- frontend-project-context initialization instruction ${result.instruction.version}; ${result.instruction.digest} -->`,
|
|
38
|
+
`<!-- resolved-target-root: ${result.targetRoot} -->`,
|
|
39
|
+
result.instruction.content.trimEnd(),
|
|
40
|
+
"",
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
@@ -24,7 +24,7 @@ const ROLLBACK_CLASSES = new Set(["package-only", "reversible-data", "forward-on
|
|
|
24
24
|
const CONSUMER_CHANGE_KEYS = new Set([
|
|
25
25
|
"actionPlan", "adaptiveContextBundle", "capabilities", "contextQuery", "coverageAudit", "evidenceBundle", "evidenceInput", "exchange",
|
|
26
26
|
"hostPromotionEvidence", "integrationReviewBundle", "reviewBundle", "routingIndex", "stageContextBundle", "stageReceipt", "taskContextPlan",
|
|
27
|
-
"truthReconciliationInput", "truthReconciliationReviewBundle",
|
|
27
|
+
"truthReconciliationInput", "truthReconciliationReviewBundle", "initializationInstruction", "projectStatus",
|
|
28
28
|
]);
|
|
29
29
|
|
|
30
30
|
function invalid(message, details) {
|
|
@@ -103,7 +103,7 @@ export function validateMigrationManifest(input) {
|
|
|
103
103
|
]), "migration manifest");
|
|
104
104
|
if (manifest.schemaVersion !== MIGRATION_MANIFEST_SCHEMA_VERSION) invalid("migration manifest schemaVersion must be 2");
|
|
105
105
|
exactKeys(manifest.package, new Set(["name", "version"]), "migration manifest package");
|
|
106
|
-
if (manifest.package.name !== "frontend-project-context" || manifest.package.version !== "1.
|
|
106
|
+
if (manifest.package.name !== "frontend-project-context" || manifest.package.version !== "1.8.0") invalid("migration manifest package does not match this runtime");
|
|
107
107
|
versions(manifest.upgradeFrom, "upgradeFrom");
|
|
108
108
|
if (manifest.upgradeFrom.length === 0) invalid("upgradeFrom must not be empty");
|
|
109
109
|
exactKeys(manifest.stores, new Set(["contract", "projectionLock", "proposal", "sourceLock"]), "stores");
|
|
@@ -113,7 +113,7 @@ export function validateMigrationManifest(input) {
|
|
|
113
113
|
exactKeys(manifest.protocols, new Set([
|
|
114
114
|
"exchange", "actionPlan", "adaptiveContextBundle", "contextQuery", "coverageAudit", "reviewBundle", "evidenceInput", "evidenceBundle", "taskContextPlan",
|
|
115
115
|
"routingIndex", "stageReceipt", "stageContextBundle", "integrationReviewBundle", "hostPromotionEvidence",
|
|
116
|
-
"truthReconciliationInput", "truthReconciliationReviewBundle",
|
|
116
|
+
"truthReconciliationInput", "truthReconciliationReviewBundle", "initializationInstruction", "projectStatus",
|
|
117
117
|
]), "protocols");
|
|
118
118
|
for (const name of Object.keys(manifest.protocols)) versionMatrix(manifest.protocols[name], `protocols.${name}`);
|
|
119
119
|
if (!Array.isArray(manifest.builtInMigrations)) invalid("builtInMigrations must be an array");
|
|
@@ -4,8 +4,9 @@ import { checkExitCode, checkProject } from "./checker.mjs";
|
|
|
4
4
|
import { ProjectContextError } from "./errors.mjs";
|
|
5
5
|
import { PACKAGE_VERSION } from "./exchange-schema.mjs";
|
|
6
6
|
import { inspectProjectInitialization, loadProject } from "./project-store.mjs";
|
|
7
|
+
import { sourceStatus } from "./contract-schema.mjs";
|
|
7
8
|
|
|
8
|
-
export const PROJECT_STATUS_SCHEMA_VERSION =
|
|
9
|
+
export const PROJECT_STATUS_SCHEMA_VERSION = 2;
|
|
9
10
|
|
|
10
11
|
function uniqueSorted(values) {
|
|
11
12
|
return [...new Set(values.filter((value) => value !== undefined && value !== null))]
|
|
@@ -46,6 +47,7 @@ function base(initialization, health, nextActions) {
|
|
|
46
47
|
snapshots: null,
|
|
47
48
|
health,
|
|
48
49
|
entry: { state: "absent", path: null, rendererVersion: null },
|
|
50
|
+
contractReadiness: initialization === "uninitialized" ? "not-initialized" : "contract-incomplete",
|
|
49
51
|
summary: emptySummary(),
|
|
50
52
|
findingCodes: [],
|
|
51
53
|
sourceIds: [],
|
|
@@ -65,7 +67,7 @@ function statusBoundaries(boundaries) {
|
|
|
65
67
|
export async function buildProjectStatus(root, boundaries) {
|
|
66
68
|
const initialization = await inspectProjectInitialization(root);
|
|
67
69
|
if (initialization.status !== "initialized") {
|
|
68
|
-
const result = base(initialization.status, initialization.status, initialization.status === "uninitialized" ? ["
|
|
70
|
+
const result = base(initialization.status, initialization.status, initialization.status === "uninitialized" ? ["read-initialization-instructions"] : ["resolve-conflict"]);
|
|
69
71
|
result.initialization.present = [...initialization.present];
|
|
70
72
|
result.initialization.missing = [...initialization.missing];
|
|
71
73
|
if (initialization.status === "partial") result.findingCodes = ["project-state-partial"];
|
|
@@ -112,7 +114,11 @@ export async function buildProjectStatus(root, boundaries) {
|
|
|
112
114
|
if (sync.summary.affectedProjections > 0) nextActions.push("review-projection");
|
|
113
115
|
if (entry.state !== "current") nextActions.push("publish-ai-entry");
|
|
114
116
|
if (findings.length > 0 && nextActions.length === 0) nextActions.push("run-sync");
|
|
115
|
-
if (health === "clean")
|
|
117
|
+
if (health === "clean") {
|
|
118
|
+
const hasActiveSource = project.contract.sources.some((source) => sourceStatus(source) === "active");
|
|
119
|
+
const hasApprovedItem = project.contract.items.some((item) => item.status === "approved");
|
|
120
|
+
nextActions.push(hasActiveSource && hasApprovedItem && entry.state === "current" ? "compile-task-context" : "complete-onboarding");
|
|
121
|
+
}
|
|
116
122
|
}
|
|
117
123
|
const sourceIds = uniqueSorted([
|
|
118
124
|
...findings.flatMap((finding) => finding.source ? [finding.source] : []),
|
|
@@ -138,6 +144,11 @@ export async function buildProjectStatus(root, boundaries) {
|
|
|
138
144
|
},
|
|
139
145
|
health,
|
|
140
146
|
entry,
|
|
147
|
+
contractReadiness: health === "clean" && entry.state === "current" &&
|
|
148
|
+
project.contract.sources.some((source) => sourceStatus(source) === "active") &&
|
|
149
|
+
project.contract.items.some((item) => item.status === "approved")
|
|
150
|
+
? "contract-ready"
|
|
151
|
+
: "contract-incomplete",
|
|
141
152
|
summary: {
|
|
142
153
|
findings: findings.length,
|
|
143
154
|
changedSources: sync.summary.changedSources,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access, mkdir, rename, rm } from "node:fs/promises";
|
|
1
|
+
import { access, lstat, mkdir, readdir, rename, rm, rmdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { canonicalJson, digestJson } from "./canonical-json.mjs";
|
|
@@ -43,8 +43,20 @@ async function exists(filePath) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
async function isEmptyDirectory(filePath) {
|
|
47
|
+
try {
|
|
48
|
+
const stats = await lstat(filePath);
|
|
49
|
+
if (!stats.isDirectory()) return false;
|
|
50
|
+
return (await readdir(filePath)).length === 0;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error?.code === "ENOENT") return false;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
export async function inspectProjectInitialization(root) {
|
|
47
58
|
const files = projectFiles(root);
|
|
59
|
+
const emptyContainer = await isEmptyDirectory(files.directory);
|
|
48
60
|
const entries = [
|
|
49
61
|
[CONTEXT_DIRECTORY, files.directory],
|
|
50
62
|
[`${CONTEXT_DIRECTORY}/${CONTRACT_FILE}`, files.contract],
|
|
@@ -53,6 +65,7 @@ export async function inspectProjectInitialization(root) {
|
|
|
53
65
|
];
|
|
54
66
|
const present = [];
|
|
55
67
|
for (const [relative, absolute] of entries) {
|
|
68
|
+
if (relative === CONTEXT_DIRECTORY && emptyContainer) continue;
|
|
56
69
|
if (await exists(absolute)) present.push(relative);
|
|
57
70
|
}
|
|
58
71
|
const storePaths = entries.slice(1).map(([relative]) => relative);
|
|
@@ -71,8 +84,9 @@ export async function initializeProject(root, id, name, write) {
|
|
|
71
84
|
validateContract(initial.contract);
|
|
72
85
|
validateSourceLock(initial.sourcesLock);
|
|
73
86
|
validateProjectionLock(initial.projectionsLock);
|
|
87
|
+
const emptyContainer = await isEmptyDirectory(files.directory);
|
|
74
88
|
const existing = [];
|
|
75
|
-
if (await exists(files.directory)) existing.push(CONTEXT_DIRECTORY);
|
|
89
|
+
if ((await exists(files.directory)) && !emptyContainer) existing.push(CONTEXT_DIRECTORY);
|
|
76
90
|
for (const filePath of [files.contract, files.sourcesLock, files.projectionsLock]) {
|
|
77
91
|
if (await exists(filePath)) existing.push(path.relative(root, filePath));
|
|
78
92
|
}
|
|
@@ -80,6 +94,17 @@ export async function initializeProject(root, id, name, write) {
|
|
|
80
94
|
fail("project-already-initialized", "project context files already exist", { details: { paths: existing } });
|
|
81
95
|
}
|
|
82
96
|
if (write) {
|
|
97
|
+
if (emptyContainer) {
|
|
98
|
+
try {
|
|
99
|
+
await rmdir(files.directory);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error?.code !== "ENOENT") {
|
|
102
|
+
fail("project-already-initialized", "project context directory changed before initialization", {
|
|
103
|
+
details: { paths: [CONTEXT_DIRECTORY] },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
83
108
|
const temporaryDirectory = `${files.directory}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`;
|
|
84
109
|
try {
|
|
85
110
|
await mkdir(temporaryDirectory, { recursive: false });
|
|
@@ -123,7 +123,7 @@ export function validateUpgradeAssessment(input) {
|
|
|
123
123
|
protocols: new Set([
|
|
124
124
|
"actionPlan", "adaptiveContextBundle", "contextQuery", "coverageAudit", "evidenceBundle", "evidenceInput", "exchange",
|
|
125
125
|
"hostPromotionEvidence", "integrationReviewBundle", "reviewBundle", "routingIndex", "stageContextBundle", "stageReceipt",
|
|
126
|
-
"taskContextPlan", "truthReconciliationInput", "truthReconciliationReviewBundle",
|
|
126
|
+
"taskContextPlan", "truthReconciliationInput", "truthReconciliationReviewBundle", "initializationInstruction", "projectStatus",
|
|
127
127
|
]),
|
|
128
128
|
};
|
|
129
129
|
for (const [name, group] of Object.entries(value.compatibility)) {
|