frontend-project-context 1.0.1 → 1.3.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 (31) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +164 -8
  3. package/UPGRADING.md +44 -0
  4. package/docs/00-PRODUCT-CONSTITUTION.md +42 -10
  5. package/docs/04-PROGRAM-DESIGN.md +89 -2
  6. package/docs/05-ACCEPTANCE-CONTRACT.md +52 -3
  7. package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +27 -15
  8. package/docs/14-FORMAL-RELEASE-READINESS.md +15 -0
  9. package/docs/16-GUIDED-ONBOARDING-AND-AI-RECONCILIATION-DESIGN.md +469 -0
  10. package/docs/17-AI-EXCHANGE-BOUNDARY-DESIGN.md +270 -0
  11. package/docs/18-BRANCH-AWARE-STAGED-CONTEXT-DESIGN.md +348 -0
  12. package/docs/README.md +17 -5
  13. package/examples/README.md +15 -2
  14. package/examples/package.json +6 -2
  15. package/package.json +4 -3
  16. package/schemas/action-plan.schema.json +250 -0
  17. package/schemas/assist-bundle.schema.json +75 -0
  18. package/schemas/capabilities.schema.json +146 -0
  19. package/schemas/integration-review-bundle.schema.json +43 -0
  20. package/schemas/review-bundle.schema.json +109 -0
  21. package/schemas/stage-context-bundle.schema.json +56 -0
  22. package/schemas/stage-receipt.schema.json +55 -0
  23. package/schemas/task-context-plan.schema.json +85 -0
  24. package/src/project-context/assist.mjs +422 -0
  25. package/src/project-context/capabilities.mjs +74 -0
  26. package/src/project-context/cli.mjs +168 -13
  27. package/src/project-context/exchange-schema.mjs +528 -0
  28. package/src/project-context/exchange.mjs +565 -0
  29. package/src/project-context/project-store.mjs +22 -0
  30. package/src/project-context/task-context-schema.mjs +290 -0
  31. package/src/project-context/task-context.mjs +361 -0
@@ -1,4 +1,4 @@
1
- import path from "node:path";
1
+ import { buildSetupAssistBundle, buildSyncAssistBundle } from "./assist.mjs";
2
2
  import { approvePendingItems, approveProposal } from "./approver.mjs";
3
3
  import { buildItemProposal, registerSource } from "./authoring.mjs";
4
4
  import { blockingContextFindings, checkExitCode, checkProject } from "./checker.mjs";
@@ -8,17 +8,22 @@ import { buildDashboardModel } from "./dashboard-model.mjs";
8
8
  import { renderDashboardHtml } from "./dashboard-renderer.mjs";
9
9
  import { discoverProject } from "./discovery.mjs";
10
10
  import { ProjectContextError, fail } from "./errors.mjs";
11
+ import { buildCapabilities, preflightActionPlanFile } from "./exchange.mjs";
12
+ import { normalizeProposalPath } from "./exchange-schema.mjs";
11
13
  import { atomicCreateFileOrSame, atomicWriteFile, readJsonFile } from "./io.mjs";
12
14
  import { acceptSourceChange, deprecateItem, deprecateSource, reviewSource, reviseItem } from "./maintenance.mjs";
13
15
  import { normalizeRelativePath, resolveExistingInside, resolveProjectRoot, resolveWritableInside } from "./path-policy.mjs";
14
- import { CONTRACT_FILE, initializeProject, loadProject, PROJECTIONS_LOCK_FILE, SOURCES_LOCK_FILE } from "./project-store.mjs";
16
+ import { initializeProject, inspectProjectInitialization, loadProject } from "./project-store.mjs";
15
17
  import { publishProjection } from "./projection-store.mjs";
16
18
  import { renderContextBundle } from "./renderer.mjs";
19
+ import { buildIntegrationReviewBundleFiles, buildStageContextBundleFiles } from "./task-context.mjs";
17
20
 
18
21
  const HELP = `project-context — model-neutral project contract compiler
19
22
 
20
23
  Usage:
21
24
  project-context init --project PATH --id ID --name NAME [--write] [--json]
25
+ project-context capabilities --project PATH [--json]
26
+ project-context setup --project PATH --id ID --name NAME [--output FILE] [--write] [--json]
22
27
  project-context register --project PATH --id SOURCE_ID --kind KIND [--path PATH] [--pointer POINTER] [--reference TEXT] [--write] [--json]
23
28
  project-context propose --project PATH --id ITEM_ID --kind KIND --subject SUBJECT (--value TEXT | --value-json JSON) --statement TEXT --sources SOURCE_ID... --scope SCOPE [--scope-path PATH] [--overrides ITEM_ID...] [--verification KIND] [--verification-source SOURCE_ID] [--verification-expected-json JSON] [--output FILE --write] [--json]
24
29
  project-context review-source --project PATH --id SOURCE_ID [--json]
@@ -32,6 +37,10 @@ Usage:
32
37
  project-context publish --project PATH --target agents|ruler --output FILE [--path RELATIVE_PATH...] [--write] [--json]
33
38
  project-context check --project PATH [--json]
34
39
  project-context dashboard --project PATH [--json]
40
+ project-context sync --project PATH [--changed-path RELATIVE_PATH...] [--json]
41
+ project-context preflight --project PATH --plan FILE [--json]
42
+ project-context stage-context --project PATH --plan FILE --stage STAGE_ID [--receipt FILE...] [--changed-path RELATIVE_PATH...] [--json]
43
+ project-context integration-review --project PATH --plan FILE [--receipt FILE...] [--main-changed-path RELATIVE_PATH...] [--branch-changed-path RELATIVE_PATH...] [--json]
35
44
 
36
45
  All commands are read-only unless their own --write flag is present.
37
46
  `;
@@ -39,12 +48,16 @@ const VALUE_FLAGS = new Set([
39
48
  "project", "id", "name", "output", "proposal", "by", "task", "target", "rationale",
40
49
  "kind", "pointer", "reference", "subject", "value", "value-json", "statement", "scope", "scope-path",
41
50
  "verification", "verification-source", "verification-expected-json",
42
- "expected-digest", "expected-item-digest", "expected-source-digest", "locale",
51
+ "expected-digest", "expected-item-digest", "expected-source-digest", "locale", "plan", "stage",
52
+ ]);
53
+ const LIST_FLAGS = new Set([
54
+ "ids", "path", "changed-path", "sources", "overrides", "affected-items", "receipt", "main-changed-path", "branch-changed-path",
43
55
  ]);
44
- const LIST_FLAGS = new Set(["ids", "path", "sources", "overrides", "affected-items"]);
45
56
  const BOOLEAN_FLAGS = new Set(["write", "json", "full-json", "help", "pending"]);
46
57
  const COMMAND_OPTIONS = new Map([
47
58
  ["init", new Set(["project", "id", "name", "write", "json", "help"])],
59
+ ["capabilities", new Set(["project", "json", "help"])],
60
+ ["setup", new Set(["project", "id", "name", "output", "write", "json", "help"])],
48
61
  ["register", new Set(["project", "id", "kind", "path", "pointer", "reference", "write", "json", "help"])],
49
62
  ["propose", new Set([
50
63
  "project", "id", "kind", "subject", "value", "value-json", "statement", "sources", "scope", "scope-path",
@@ -71,6 +84,10 @@ const COMMAND_OPTIONS = new Map([
71
84
  ["publish", new Set(["project", "target", "output", "path", "write", "json", "help"])],
72
85
  ["check", new Set(["project", "json", "help"])],
73
86
  ["dashboard", new Set(["project", "json", "help"])],
87
+ ["sync", new Set(["project", "changed-path", "json", "help"])],
88
+ ["preflight", new Set(["project", "plan", "json", "help"])],
89
+ ["stage-context", new Set(["project", "plan", "stage", "receipt", "changed-path", "json", "help"])],
90
+ ["integration-review", new Set(["project", "plan", "receipt", "main-changed-path", "branch-changed-path", "json", "help"])],
74
91
  ]);
75
92
 
76
93
  export function parseArgs(argv) {
@@ -135,16 +152,14 @@ function rejectUnsupportedOptions(command, options, allowed) {
135
152
  }
136
153
 
137
154
  function validateProposalOutput(output) {
138
- const normalized = normalizeRelativePath(output, { label: "proposal output" });
139
- const reserved = new Set([
140
- `.project-context/${CONTRACT_FILE}`,
141
- `.project-context/${SOURCES_LOCK_FILE}`,
142
- `.project-context/${PROJECTIONS_LOCK_FILE}`,
143
- ]);
144
- if (!normalized.startsWith(".project-context/") || path.posix.extname(normalized) !== ".json" || reserved.has(normalized)) {
145
- fail("invalid-proposal-output", "proposals must be non-store JSON files inside .project-context/");
155
+ try {
156
+ return normalizeProposalPath(output, "proposal output");
157
+ } catch (error) {
158
+ if (error?.code === "action-plan-schema-invalid") {
159
+ fail("invalid-proposal-output", "proposals must be non-store JSON files inside .project-context/");
160
+ }
161
+ throw error;
146
162
  }
147
- return normalized;
148
163
  }
149
164
 
150
165
  function jsonOrText(options, value, text) {
@@ -195,11 +210,119 @@ function itemInput(options) {
195
210
  };
196
211
  }
197
212
 
213
+ function inMemoryProject(files, initial) {
214
+ return {
215
+ files,
216
+ contract: initial.contract,
217
+ sourcesLock: initial.sourcesLock,
218
+ projectionsLock: initial.projectionsLock,
219
+ contractDigest: digestJson(initial.contract),
220
+ sourcesLockDigest: digestJson(initial.sourcesLock),
221
+ projectionsLockDigest: digestJson(initial.projectionsLock),
222
+ };
223
+ }
224
+
225
+ async function createSetupProposal(root, output, proposal) {
226
+ const normalized = validateProposalOutput(output);
227
+ const resolved = await resolveWritableInside(root, normalized);
228
+ try {
229
+ return await atomicCreateFileOrSame(resolved.absolute, prettyCanonicalJson(proposal));
230
+ } catch (error) {
231
+ if (error instanceof ProjectContextError && error.code === "proposal-output-conflict") {
232
+ fail("setup-proposal-conflict", `refusing to overwrite a different setup proposal: ${normalized}`, {
233
+ details: { path: normalized },
234
+ });
235
+ }
236
+ throw error;
237
+ }
238
+ }
239
+
240
+ async function runSetup(root, options) {
241
+ const id = required(options, "id");
242
+ const name = required(options, "name");
243
+ const output = options.output ?? ".project-context/setup.proposal.json";
244
+ if (options.write || options.output) validateProposalOutput(output);
245
+ const initialization = await inspectProjectInitialization(root);
246
+ if (initialization.status === "partial") {
247
+ fail("setup-state-partial", "project context stores are incomplete; setup will not guess or repair state", {
248
+ details: { present: initialization.present, missing: initialization.missing },
249
+ });
250
+ }
251
+
252
+ let project;
253
+ let initialized;
254
+ if (initialization.status === "initialized") {
255
+ project = await loadProject(root);
256
+ if (project.contract.project.id !== id || project.contract.project.name !== name) {
257
+ fail("setup-project-mismatch", "setup id and name must match the initialized project", {
258
+ details: {
259
+ expected: project.contract.project,
260
+ actual: { id, name },
261
+ },
262
+ });
263
+ }
264
+ initialized = true;
265
+ } else if (options.write) {
266
+ await initializeProject(root, id, name, true);
267
+ project = await loadProject(root);
268
+ initialized = true;
269
+ } else {
270
+ const preview = await initializeProject(root, id, name, false);
271
+ project = inMemoryProject(preview.files, preview.preview);
272
+ initialized = false;
273
+ }
274
+
275
+ const proposal = validateProposal(await discoverProject(root, project.contract));
276
+ let proposalAction = "preview";
277
+ if (options.write) proposalAction = await createSetupProposal(root, output, proposal);
278
+ else if (options.output) validateProposalOutput(options.output);
279
+
280
+ const bundle = buildSetupAssistBundle(project, proposal, {
281
+ initialized,
282
+ proposalPath: output,
283
+ proposalAction,
284
+ proposalPersisted: Boolean(options.write),
285
+ });
286
+ const summary = [
287
+ `Setup ${initialized ? "initialized" : "preview"}: ${bundle.project.id}.`,
288
+ `Candidate sources: ${bundle.summary.candidateSources}`,
289
+ `Candidate items: ${bundle.summary.candidateItems}`,
290
+ `Proposal: ${options.write ? `${proposalAction} ${output}` : "not written"}`,
291
+ "Approval: required before any candidate becomes normative.",
292
+ ].join("\n") + "\n";
293
+ return {
294
+ exitCode: 0,
295
+ stdout: jsonOrText(options, bundle, summary),
296
+ stderr: options.output && !options.write ? `Preview only; ${options.output} was not written.\n` : "",
297
+ };
298
+ }
299
+
300
+ function syncSummary(bundle) {
301
+ const changed = bundle.sourceChanges.filter((source) => ["changed", "missing", "unreadable"].includes(source.status));
302
+ const lines = [
303
+ `Sync: ${bundle.summary.changedSources} changed source(s), ${bundle.summary.affectedItems} affected item(s), ${bundle.summary.pendingItems} pending item(s), ${bundle.summary.affectedProjections} affected projection(s).`,
304
+ ];
305
+ for (const source of changed) lines.push(`- ${source.sourceId}: ${source.status}; ${source.affectedItemIds.join(", ") || "no affected items"}`);
306
+ if (bundle.findings.length === 0) lines.push("No contract, source, scope, or projection findings.");
307
+ else lines.push(`${bundle.findings.length} finding(s) require review.`);
308
+ return `${lines.join("\n")}\n`;
309
+ }
310
+
198
311
  async function runCommand(command, options) {
199
312
  if (!command || command === "help" || options.help) return { exitCode: 0, stdout: HELP, stderr: "" };
200
313
  if (!COMMAND_OPTIONS.has(command)) fail("command-unknown", `unknown command: ${command}`);
201
314
  rejectUnsupportedOptions(command, options, COMMAND_OPTIONS.get(command));
202
315
  const root = await resolveProjectRoot(required(options, "project"));
316
+ if (command === "capabilities") {
317
+ const capabilities = await buildCapabilities(root);
318
+ const summary = [
319
+ `Frontend Project Context ${capabilities.package.version}; exchange protocol ${capabilities.exchangeProtocolVersion}.`,
320
+ `Initialization: ${capabilities.initialization}.`,
321
+ `Action kinds: ${capabilities.actionKinds.join(", ")}.`,
322
+ "No Provider, Agent Runtime, automatic approval, Git, network, or business-code writes.",
323
+ ].join("\n") + "\n";
324
+ return { exitCode: 0, stdout: jsonOrText(options, capabilities, summary), stderr: "" };
325
+ }
203
326
  if (command === "init") {
204
327
  const result = await initializeProject(root, required(options, "id"), required(options, "name"), options.write);
205
328
  return {
@@ -208,6 +331,30 @@ async function runCommand(command, options) {
208
331
  stderr: "",
209
332
  };
210
333
  }
334
+ if (command === "setup") return runSetup(root, options);
335
+ if (command === "preflight") {
336
+ const bundle = await preflightActionPlanFile(root, required(options, "plan"));
337
+ const summary = `Preflight ${bundle.status}: ${bundle.summary.reviewable} reviewable, ${bundle.summary.blocked} blocked action(s).\n`;
338
+ return { exitCode: bundle.status === "reviewable" ? 0 : 1, stdout: jsonOrText(options, bundle, summary), stderr: "" };
339
+ }
340
+ if (command === "stage-context") {
341
+ const bundle = await buildStageContextBundleFiles(root, required(options, "plan"), {
342
+ stageId: required(options, "stage"),
343
+ receiptPaths: options.receipt ?? [],
344
+ changedPaths: options["changed-path"] ?? [],
345
+ });
346
+ const summary = `Stage context ${bundle.status}: ${bundle.stage.id}; ${bundle.contractItems.length} contract item(s), ${bundle.readTargets.length} read target(s), ${bundle.budget.usedUtf8Bytes}/${bundle.budget.maxUtf8Bytes} UTF-8 bytes.\n`;
347
+ return { exitCode: bundle.status === "ready" ? 0 : 1, stdout: jsonOrText(options, bundle, summary), stderr: "" };
348
+ }
349
+ if (command === "integration-review") {
350
+ const bundle = await buildIntegrationReviewBundleFiles(root, required(options, "plan"), {
351
+ receiptPaths: options.receipt ?? [],
352
+ mainChangedPaths: options["main-changed-path"] ?? [],
353
+ branchChangedPaths: options["branch-changed-path"] ?? [],
354
+ });
355
+ const summary = `Integration review ${bundle.status}: ${bundle.findings.length} finding(s), ${bundle.contractOverlapItemIds.length} contract overlap(s), ${bundle.decisionCandidates.length} decision candidate(s).\n`;
356
+ return { exitCode: bundle.status === "reviewable" ? 0 : 1, stdout: jsonOrText(options, bundle, summary), stderr: "" };
357
+ }
211
358
  const project = await loadProject(root);
212
359
  if (command === "register") {
213
360
  const result = await registerSource(root, project, {
@@ -363,6 +510,14 @@ async function runCommand(command, options) {
363
510
  stderr: "",
364
511
  };
365
512
  }
513
+ if (command === "sync") {
514
+ const bundle = await buildSyncAssistBundle(root, project, options["changed-path"] ?? []);
515
+ return {
516
+ exitCode: checkExitCode(bundle.findings),
517
+ stdout: jsonOrText(options, bundle, syncSummary(bundle)),
518
+ stderr: "",
519
+ };
520
+ }
366
521
  if (command === "context") {
367
522
  const findings = blockingContextFindings(await checkProject(root, project));
368
523
  if (findings.length > 0) fail("context-blocked", "context generation is blocked by contract or source findings", { exitCode: 1, details: { findings } });