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.
Files changed (33) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +16 -9
  3. package/UPGRADING.md +8 -0
  4. package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +23 -6
  5. package/docs/24-A130-REAL-HOST-TARGET-PROJECT-COMPARISON.md +1 -1
  6. package/docs/26-A130-QUALITY-CLOSURE-AND-ADAPTIVE-DELIVERY-REPAIR-DESIGN.md +1 -1
  7. package/docs/27-TEAM-SHARED-CONTEXT-DIRECTION-DISCUSSION.md +30 -0
  8. package/docs/28-REAL-PROJECT-ONBOARDING-CLOSURE-DESIGN.md +534 -0
  9. package/docs/AI-PROJECT-INITIALIZATION.md +89 -0
  10. package/docs/README.md +12 -0
  11. package/docs/USER-AND-AI-OPERATION-MANUAL.md +17 -13
  12. package/examples/README.md +2 -2
  13. package/examples/package.json +1 -1
  14. package/migration-manifest.json +19 -7
  15. package/package.json +2 -2
  16. package/schemas/capabilities.schema.json +23 -8
  17. package/schemas/evidence-bundle.schema.json +1 -1
  18. package/schemas/initialization-instruction.schema.json +60 -0
  19. package/schemas/migration-manifest.schema.json +3 -3
  20. package/schemas/migration-plan.schema.json +2 -2
  21. package/schemas/project-status.schema.json +5 -4
  22. package/schemas/upgrade-assessment.schema.json +2 -2
  23. package/schemas/upgrade-result-bundle.schema.json +1 -1
  24. package/src/project-context/ai-entry.mjs +8 -6
  25. package/src/project-context/capabilities.mjs +14 -0
  26. package/src/project-context/cli.mjs +15 -0
  27. package/src/project-context/contract-schema.mjs +1 -1
  28. package/src/project-context/exchange-schema.mjs +4 -4
  29. package/src/project-context/initialization-instruction.mjs +42 -0
  30. package/src/project-context/migration-manifest.mjs +3 -3
  31. package/src/project-context/project-status.mjs +14 -3
  32. package/src/project-context/project-store.mjs +27 -2
  33. 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.7.0";
6
- export const EXCHANGE_PROTOCOL_VERSION = 7;
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 = 7;
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.7.0") invalid("migration manifest package does not match this runtime");
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 = 1;
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" ? ["run-setup-preview"] : ["resolve-conflict"]);
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") nextActions.push("ready-for-task");
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)) {