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.
Files changed (49) hide show
  1. package/CHANGELOG.md +39 -2
  2. package/README.md +94 -16
  3. package/UPGRADING.md +47 -2
  4. package/docs/04-PROGRAM-DESIGN.md +40 -4
  5. package/docs/05-ACCEPTANCE-CONTRACT.md +33 -3
  6. package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +36 -6
  7. package/docs/14-FORMAL-RELEASE-READINESS.md +46 -0
  8. package/docs/18-BRANCH-AWARE-STAGED-CONTEXT-DESIGN.md +62 -2
  9. package/docs/19-POST-1.3.1-AI-TAKEOVER-EVIDENCE-AND-UPGRADE-PLAN.md +579 -0
  10. package/docs/20-PHASE-A-AI-TAKEOVER-AND-HEALTH-CLOSURE-DESIGN.md +535 -0
  11. package/docs/21-PHASE-B-EVIDENCE-FEEDBACK-PROTOCOL-DESIGN.md +347 -0
  12. package/docs/22-PHASE-C-TARGET-UPGRADE-PROTOCOL-DESIGN.md +398 -0
  13. package/docs/README.md +21 -5
  14. package/docs/USER-AND-AI-OPERATION-MANUAL.md +797 -0
  15. package/examples/README.md +38 -0
  16. package/examples/package.json +6 -2
  17. package/migration-manifest.json +88 -0
  18. package/package.json +3 -2
  19. package/schemas/action-plan.schema.json +31 -3
  20. package/schemas/capabilities.schema.json +50 -18
  21. package/schemas/evidence-bundle.schema.json +64 -0
  22. package/schemas/evidence-input.schema.json +82 -0
  23. package/schemas/migration-manifest.schema.json +29 -0
  24. package/schemas/migration-plan.schema.json +32 -0
  25. package/schemas/project-status.schema.json +75 -0
  26. package/schemas/projection-lock.schema.json +48 -0
  27. package/schemas/review-bundle.schema.json +3 -3
  28. package/schemas/upgrade-assessment.schema.json +48 -0
  29. package/schemas/upgrade-result-bundle.schema.json +35 -0
  30. package/src/project-context/ai-entry.mjs +320 -0
  31. package/src/project-context/capabilities.mjs +44 -17
  32. package/src/project-context/checker.mjs +20 -3
  33. package/src/project-context/cli.mjs +84 -7
  34. package/src/project-context/contract-schema.mjs +30 -16
  35. package/src/project-context/dashboard-model.mjs +4 -4
  36. package/src/project-context/dashboard-renderer.mjs +3 -3
  37. package/src/project-context/discovery.mjs +6 -1
  38. package/src/project-context/evidence-schema.mjs +209 -0
  39. package/src/project-context/evidence.mjs +99 -0
  40. package/src/project-context/exchange-schema.mjs +21 -11
  41. package/src/project-context/exchange.mjs +26 -4
  42. package/src/project-context/maintenance.mjs +2 -2
  43. package/src/project-context/migration-manifest.mjs +166 -0
  44. package/src/project-context/project-status.mjs +157 -0
  45. package/src/project-context/projection-store.mjs +8 -1
  46. package/src/project-context/task-context-schema.mjs +237 -1
  47. package/src/project-context/task-context.mjs +154 -13
  48. package/src/project-context/upgrade-schema.mjs +215 -0
  49. package/src/project-context/upgrade.mjs +494 -0
@@ -0,0 +1,75 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:frontend-project-context:schema:project-status:1",
4
+ "title": "Frontend Project Context Project Status",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "package", "initialization", "project", "snapshots", "health", "entry", "summary", "findingCodes", "sourceIds", "itemIds", "projectionPaths", "readTargets", "workUnits", "nextActions", "boundaries"],
8
+ "properties": {
9
+ "schemaVersion": { "const": 1 },
10
+ "package": {
11
+ "type": "object",
12
+ "additionalProperties": false,
13
+ "required": ["name", "version"],
14
+ "properties": { "name": { "const": "frontend-project-context" }, "version": { "type": "string" } }
15
+ },
16
+ "initialization": {
17
+ "type": "object",
18
+ "additionalProperties": false,
19
+ "required": ["state", "present", "missing"],
20
+ "properties": {
21
+ "state": { "enum": ["uninitialized", "partial", "invalid", "initialized"] },
22
+ "present": { "$ref": "#/$defs/strings" },
23
+ "missing": { "$ref": "#/$defs/strings" }
24
+ }
25
+ },
26
+ "project": { "anyOf": [{ "type": "null" }, { "$ref": "#/$defs/project" }] },
27
+ "snapshots": { "anyOf": [{ "type": "null" }, { "$ref": "#/$defs/snapshots" }] },
28
+ "health": { "enum": ["uninitialized", "partial", "invalid", "attention", "clean", "conflict"] },
29
+ "entry": {
30
+ "type": "object",
31
+ "additionalProperties": false,
32
+ "required": ["state", "path", "rendererVersion"],
33
+ "properties": {
34
+ "state": { "enum": ["absent", "current", "stale", "conflict"] },
35
+ "path": { "type": ["string", "null"] },
36
+ "rendererVersion": { "type": ["integer", "null"] }
37
+ }
38
+ },
39
+ "summary": {
40
+ "type": "object",
41
+ "additionalProperties": false,
42
+ "required": ["findings", "changedSources", "pendingItems", "affectedProjections"],
43
+ "properties": {
44
+ "findings": { "type": "integer", "minimum": 0 },
45
+ "changedSources": { "type": "integer", "minimum": 0 },
46
+ "pendingItems": { "type": "integer", "minimum": 0 },
47
+ "affectedProjections": { "type": "integer", "minimum": 0 }
48
+ }
49
+ },
50
+ "findingCodes": { "$ref": "#/$defs/strings" },
51
+ "sourceIds": { "$ref": "#/$defs/strings" },
52
+ "itemIds": { "$ref": "#/$defs/strings" },
53
+ "projectionPaths": { "$ref": "#/$defs/strings" },
54
+ "readTargets": { "type": "array", "items": { "type": "object" } },
55
+ "workUnits": { "type": "array", "items": { "type": "object" } },
56
+ "nextActions": {
57
+ "type": "array",
58
+ "uniqueItems": true,
59
+ "items": { "enum": ["run-setup-preview", "run-sync", "review-pending", "review-source-change", "review-projection", "publish-ai-entry", "resolve-conflict", "ready-for-task"] }
60
+ },
61
+ "boundaries": { "type": "object", "additionalProperties": { "const": false } }
62
+ },
63
+ "$defs": {
64
+ "strings": { "type": "array", "uniqueItems": true, "items": { "type": "string" } },
65
+ "digest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
66
+ "project": {
67
+ "type": "object", "additionalProperties": false, "required": ["id", "name"],
68
+ "properties": { "id": { "type": "string" }, "name": { "type": "string" } }
69
+ },
70
+ "snapshots": {
71
+ "type": "object", "additionalProperties": false, "required": ["contract", "sourcesLock", "projectionsLock"],
72
+ "properties": { "contract": { "$ref": "#/$defs/digest" }, "sourcesLock": { "$ref": "#/$defs/digest" }, "projectionsLock": { "$ref": "#/$defs/digest" } }
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:frontend-project-context:schema:projection-lock:2",
4
+ "title": "Frontend Project Context Projection Lock",
5
+ "oneOf": [
6
+ { "$ref": "#/$defs/schema1" },
7
+ { "$ref": "#/$defs/schema2" }
8
+ ],
9
+ "$defs": {
10
+ "digest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
11
+ "fileEntry1": {
12
+ "type": "object", "additionalProperties": false,
13
+ "required": ["path", "target", "paths", "contractDigest", "bundleDigest", "contentDigest", "itemIds", "rendererVersion"],
14
+ "properties": {
15
+ "path": { "type": "string" }, "target": { "enum": ["agents", "ruler"] }, "paths": { "type": "array", "items": { "type": "string" } },
16
+ "contractDigest": { "$ref": "#/$defs/digest" }, "bundleDigest": { "$ref": "#/$defs/digest" }, "contentDigest": { "$ref": "#/$defs/digest" },
17
+ "itemIds": { "type": "array", "items": { "type": "string" } }, "rendererVersion": { "enum": [1, 2, 3] }
18
+ }
19
+ },
20
+ "fileEntry2": {
21
+ "type": "object", "additionalProperties": false,
22
+ "required": ["path", "target", "ownership", "paths", "contractDigest", "bundleDigest", "contentDigest", "itemIds", "rendererVersion"],
23
+ "properties": {
24
+ "path": { "type": "string" }, "target": { "enum": ["agents", "ruler"] }, "ownership": { "const": "file" },
25
+ "paths": { "type": "array", "items": { "type": "string" } }, "contractDigest": { "$ref": "#/$defs/digest" },
26
+ "bundleDigest": { "$ref": "#/$defs/digest" }, "contentDigest": { "$ref": "#/$defs/digest" },
27
+ "itemIds": { "type": "array", "items": { "type": "string" } }, "rendererVersion": { "enum": [1, 2, 3] }
28
+ }
29
+ },
30
+ "regionEntry": {
31
+ "type": "object", "additionalProperties": false,
32
+ "required": ["path", "target", "ownership", "regionId", "regionDigest", "rendererVersion", "createdFile"],
33
+ "properties": {
34
+ "path": { "type": "string" }, "target": { "const": "ai-entry" }, "ownership": { "const": "region" },
35
+ "regionId": { "const": "project-context-ai-entry" }, "regionDigest": { "$ref": "#/$defs/digest" },
36
+ "rendererVersion": { "const": 1 }, "createdFile": { "type": "boolean" }
37
+ }
38
+ },
39
+ "schema1": {
40
+ "type": "object", "additionalProperties": false, "required": ["schemaVersion", "projections"],
41
+ "properties": { "schemaVersion": { "const": 1 }, "projections": { "type": "array", "items": { "$ref": "#/$defs/fileEntry1" } } }
42
+ },
43
+ "schema2": {
44
+ "type": "object", "additionalProperties": false, "required": ["schemaVersion", "projections"],
45
+ "properties": { "schemaVersion": { "const": 2 }, "projections": { "type": "array", "items": { "oneOf": [{ "$ref": "#/$defs/fileEntry2" }, { "$ref": "#/$defs/regionEntry" }] } } }
46
+ }
47
+ }
48
+ }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "urn:frontend-project-context:schema:review-bundle:1",
3
+ "$id": "urn:frontend-project-context:schema:review-bundle:2",
4
4
  "title": "Frontend Project Context Review Bundle",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
7
7
  "required": ["schemaVersion", "projectId", "planDigest", "baselines", "status", "summary", "actions", "groups", "findings", "invocations"],
8
8
  "properties": {
9
- "schemaVersion": { "const": 1 },
9
+ "schemaVersion": { "const": 2 },
10
10
  "projectId": { "$ref": "#/$defs/id" },
11
11
  "planDigest": { "$ref": "#/$defs/digest" },
12
12
  "baselines": { "$ref": "#/$defs/baselines" },
@@ -84,7 +84,7 @@
84
84
  }
85
85
  },
86
86
  "actionKind": {
87
- "enum": ["accept-source-change", "deprecate-item", "deprecate-source", "propose-item", "publish-projection", "register-source", "request-item-approval", "revise-item"]
87
+ "enum": ["accept-source-change", "deprecate-item", "deprecate-source", "propose-item", "publish-ai-entry", "publish-projection", "register-source", "remove-ai-entry", "request-item-approval", "revise-item"]
88
88
  },
89
89
  "invocation": {
90
90
  "type": "object",
@@ -0,0 +1,48 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:frontend-project-context:schema:upgrade-assessment:1",
4
+ "title": "Frontend Project Context Upgrade Assessment",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "kind", "product", "fromVersion", "targetVersion", "fromVersionEvidence", "manifestSchemaVersion", "manifestDigest", "initialization", "health", "entryState", "findingCodes", "snapshots", "compatibility", "migrationPath", "rollbackClass", "requiresHumanReview", "state", "boundaries", "assessmentDigest"],
8
+ "properties": {
9
+ "schemaVersion": {"const": 1},
10
+ "kind": {"const": "upgrade-assessment"},
11
+ "product": {"type": "object", "additionalProperties": false, "required": ["name"], "properties": {"name": {"const": "frontend-project-context"}}},
12
+ "fromVersion": {"enum": ["1.3.1", "1.4.0", "1.5.0"]},
13
+ "targetVersion": {"const": "1.6.0"},
14
+ "fromVersionEvidence": {"const": "host-asserted"},
15
+ "manifestSchemaVersion": {"const": 2},
16
+ "manifestDigest": {"$ref": "#/$defs/digest"},
17
+ "initialization": {"type": "object", "additionalProperties": false, "required": ["state", "present", "missing"], "properties": {"state": {"enum": ["uninitialized", "partial", "initialized", "invalid"]}, "present": {"$ref": "#/$defs/strings"}, "missing": {"$ref": "#/$defs/strings"}}},
18
+ "health": {"enum": ["uninitialized", "partial", "invalid", "attention", "conflict", "clean"]},
19
+ "entryState": {"type": "object", "additionalProperties": false, "required": ["state", "path", "rendererVersion"], "properties": {"state": {"type": "string"}, "path": {"type": ["string", "null"]}, "rendererVersion": {"type": ["integer", "null"]}}},
20
+ "findingCodes": {"$ref": "#/$defs/strings"},
21
+ "snapshots": {"anyOf": [{"type": "null"}, {"$ref": "#/$defs/snapshots"}]},
22
+ "compatibility": {"type": "object", "additionalProperties": false, "required": ["stores", "renderers", "protocols"], "properties": {"stores": {"$ref": "#/$defs/compatibility"}, "renderers": {"$ref": "#/$defs/compatibility"}, "protocols": {"$ref": "#/$defs/compatibility"}}},
23
+ "migrationPath": {"$ref": "#/$defs/strings"},
24
+ "rollbackClass": {"enum": ["package-only", "reversible-data", "forward-only"]},
25
+ "requiresHumanReview": {"type": "boolean"},
26
+ "state": {"enum": ["not-applicable", "blocked", "ready-for-plan", "core-complete"]},
27
+ "boundaries": {"$ref": "#/$defs/boundaries"},
28
+ "assessmentDigest": {"$ref": "#/$defs/digest"}
29
+ },
30
+ "$defs": {
31
+ "digest": {"type": "string", "pattern": "^sha256:[a-f0-9]{64}$"},
32
+ "strings": {"type": "array", "uniqueItems": true, "items": {"type": "string"}},
33
+ "snapshots": {"type": "object", "additionalProperties": false, "required": ["contract", "sourcesLock", "projectionsLock"], "properties": {"contract": {"$ref": "#/$defs/digest"}, "sourcesLock": {"$ref": "#/$defs/digest"}, "projectionsLock": {"$ref": "#/$defs/digest"}}},
34
+ "compatibility": {"type": "object", "additionalProperties": {"enum": ["compatible", "republish", "regenerate", "invalidate", "unsupported", "not-applicable"]}},
35
+ "boundaries": {
36
+ "type": "object",
37
+ "additionalProperties": false,
38
+ "required": ["provider", "agentRuntime", "git", "network", "dependencyInstallation", "automaticApproval", "businessCodeWrites", "taskExecution", "stagePathBodyReads", "applyPlan", "scheduler", "daemon", "telemetry", "selfUpdate", "automaticEvidenceUpload", "packageManager", "automaticUpgrade"],
39
+ "properties": {
40
+ "provider": {"const": false}, "agentRuntime": {"const": false}, "git": {"const": false}, "network": {"const": false},
41
+ "dependencyInstallation": {"const": false}, "automaticApproval": {"const": false}, "businessCodeWrites": {"const": false},
42
+ "taskExecution": {"const": false}, "stagePathBodyReads": {"const": false}, "applyPlan": {"const": false},
43
+ "scheduler": {"const": false}, "daemon": {"const": false}, "telemetry": {"const": false}, "selfUpdate": {"const": false},
44
+ "automaticEvidenceUpload": {"const": false}, "packageManager": {"const": false}, "automaticUpgrade": {"const": false}
45
+ }
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:frontend-project-context:schema:upgrade-result-bundle:1",
4
+ "title": "Frontend Project Context Upgrade Result Bundle",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "kind", "product", "fromVersion", "targetVersion", "manifestDigest", "planDigest", "mode", "action", "written", "targets", "snapshots", "actionResult", "findingCodes", "coreMigration", "hostAcceptance", "overallUpgrade", "resultDigest"],
8
+ "properties": {
9
+ "schemaVersion": {"const": 1},
10
+ "kind": {"const": "upgrade-result-bundle"},
11
+ "product": {"type": "object", "additionalProperties": false, "required": ["name"], "properties": {"name": {"const": "frontend-project-context"}}},
12
+ "fromVersion": {"type": "string"},
13
+ "targetVersion": {"const": "1.6.0"},
14
+ "manifestDigest": {"$ref": "#/$defs/digest"},
15
+ "planDigest": {"$ref": "#/$defs/digest"},
16
+ "mode": {"enum": ["preview", "write"]},
17
+ "action": {"$ref": "#/$defs/action"},
18
+ "written": {"type": "boolean"},
19
+ "targets": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["path", "beforeDigest", "afterDigest"], "properties": {"path": {"$ref": "#/$defs/path"}, "beforeDigest": {"anyOf": [{"type": "null"}, {"$ref": "#/$defs/digest"}]}, "afterDigest": {"anyOf": [{"type": "null"}, {"$ref": "#/$defs/digest"}]}}}},
20
+ "snapshots": {"type": "object", "additionalProperties": false, "required": ["before", "after"], "properties": {"before": {"anyOf": [{"type": "null"}, {"$ref": "#/$defs/snapshots"}]}, "after": {"anyOf": [{"type": "null"}, {"$ref": "#/$defs/snapshots"}]}}},
21
+ "actionResult": {"type": "string"},
22
+ "findingCodes": {"type": "array", "uniqueItems": true, "items": {"type": "string"}},
23
+ "coreMigration": {"enum": ["previewed", "applied", "blocked", "complete"]},
24
+ "hostAcceptance": {"const": ["dependency-and-lockfile", "independent-new-window", "project-tests-or-ci"]},
25
+ "overallUpgrade": {"const": "host-validation-required"},
26
+ "resultDigest": {"$ref": "#/$defs/digest"}
27
+ },
28
+ "$defs": {
29
+ "digest": {"type": "string", "pattern": "^sha256:[a-f0-9]{64}$"},
30
+ "path": {"type": "string", "minLength": 1, "pattern": "^(?!/)(?![A-Za-z]:)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$"},
31
+ "strings": {"type": "array", "uniqueItems": true, "items": {"type": "string"}},
32
+ "snapshots": {"type": "object", "additionalProperties": false, "required": ["contract", "sourcesLock", "projectionsLock"], "properties": {"contract": {"$ref": "#/$defs/digest"}, "sourcesLock": {"$ref": "#/$defs/digest"}, "projectionsLock": {"$ref": "#/$defs/digest"}}},
33
+ "action": {"type": "object", "additionalProperties": false, "required": ["id", "kind", "migrationId", "targets", "writes", "semanticImpact"], "properties": {"id": {"type": "string"}, "kind": {"enum": ["verify-complete", "republish-ai-entry", "republish-projection", "built-in-store-migration", "invalidate-ephemeral-protocol"]}, "migrationId": {"type": ["string", "null"]}, "targets": {"type": "array", "uniqueItems": true, "items": {"$ref": "#/$defs/path"}}, "writes": {"type": "boolean"}, "semanticImpact": {"enum": ["none", "managed-rendering", "store-structure", "protocol-artifact-invalidation"]}}}
34
+ }
35
+ }
@@ -0,0 +1,320 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { canonicalJson, digestJson, sha256 } from "./canonical-json.mjs";
4
+ import { sourceStatus, validateContract, validateProjectionLock } from "./contract-schema.mjs";
5
+ import { fail } from "./errors.mjs";
6
+ import { atomicWriteFile, atomicWriteJson, readJsonFile } from "./io.mjs";
7
+ import { normalizeRelativePath, resolveWritableInside } from "./path-policy.mjs";
8
+
9
+ export const AI_ENTRY_RENDERER_VERSION = 1;
10
+ export const AI_ENTRY_REGION_ID = "project-context-ai-entry";
11
+ export const AI_ENTRY_START = "<!-- project-context:ai-entry:start -->";
12
+ export const AI_ENTRY_END = "<!-- project-context:ai-entry:end -->";
13
+
14
+ const ENTRY_LINES = [
15
+ AI_ENTRY_START,
16
+ "<!-- project-context:ai-entry; schema-version: 1; renderer-version: 1 -->",
17
+ "## Project Context startup",
18
+ "",
19
+ "Before project work, use the project-local `frontend-project-context` CLI without downloading another version.",
20
+ "",
21
+ "1. Run `project-context status --project . --json`.",
22
+ "2. If state is `partial` or `invalid`, stop writes and report the exact recovery evidence required.",
23
+ "3. If uninitialized, preview `setup`; if initialized with attention, use `sync` work units; if clean, continue the user task.",
24
+ "4. Never treat a plan, bundle, receipt, review, or AI suggestion as human approval.",
25
+ "5. Before declaring completion, restore Project Context to `clean` or report the exact blocker.",
26
+ AI_ENTRY_END,
27
+ ];
28
+
29
+ function entryOutput(output) {
30
+ let normalized;
31
+ try {
32
+ normalized = normalizeRelativePath(output, { label: "AI Entry output" });
33
+ } catch (error) {
34
+ fail("ai-entry-output-invalid", "AI Entry output must stay inside the project", {
35
+ exitCode: 2,
36
+ cause: error,
37
+ details: { path: output },
38
+ });
39
+ }
40
+ if (path.posix.basename(normalized) !== "AGENTS.md") {
41
+ fail("ai-entry-output-invalid", "AI Entry output must be an AGENTS.md file", {
42
+ exitCode: 2,
43
+ details: { path: normalized },
44
+ });
45
+ }
46
+ return normalized;
47
+ }
48
+
49
+ async function resolveEntryOutput(root, output, write) {
50
+ const normalized = entryOutput(output);
51
+ try {
52
+ return await resolveWritableInside(root, normalized, { createParent: write });
53
+ } catch (error) {
54
+ fail("ai-entry-output-invalid", "AI Entry output is not safely writable inside the project", {
55
+ exitCode: 2,
56
+ cause: error,
57
+ details: { path: normalized, reason: error.code ?? "invalid-output" },
58
+ });
59
+ }
60
+ }
61
+
62
+ async function readIfPresent(filePath) {
63
+ try {
64
+ return await readFile(filePath, "utf8");
65
+ } catch (error) {
66
+ if (error?.code === "ENOENT") return null;
67
+ throw error;
68
+ }
69
+ }
70
+
71
+ function count(content, needle) {
72
+ return content.split(needle).length - 1;
73
+ }
74
+
75
+ export function renderAiEntry(newline = "\n") {
76
+ return ENTRY_LINES.join(newline);
77
+ }
78
+
79
+ export function parseAiEntryRegion(content) {
80
+ if (typeof content !== "string") return { state: "absent", region: null };
81
+ const starts = count(content, AI_ENTRY_START);
82
+ const ends = count(content, AI_ENTRY_END);
83
+ if (starts === 0 && ends === 0) return { state: "absent", region: null };
84
+ if (starts !== 1 || ends !== 1) return { state: "conflict", reason: "marker-count", region: null };
85
+ const start = content.indexOf(AI_ENTRY_START);
86
+ const endStart = content.indexOf(AI_ENTRY_END);
87
+ if (endStart <= start) return { state: "conflict", reason: "marker-order", region: null };
88
+ const end = endStart + AI_ENTRY_END.length;
89
+ const between = content.slice(start + AI_ENTRY_START.length, endStart);
90
+ if (between.includes(AI_ENTRY_START) || between.includes(AI_ENTRY_END)) {
91
+ return { state: "conflict", reason: "nested-marker", region: null };
92
+ }
93
+ return { state: "present", start, end, region: content.slice(start, end) };
94
+ }
95
+
96
+ function newlineFor(content) {
97
+ return content?.includes("\r\n") ? "\r\n" : "\n";
98
+ }
99
+
100
+ function appendRegion(content, region, newline) {
101
+ if (content === null || content.length === 0) return region;
102
+ const trailingNewline = content.endsWith("\r\n") || content.endsWith("\n");
103
+ return `${content}${trailingNewline ? newline : `${newline}${newline}`}${region}`;
104
+ }
105
+
106
+ function schema2FileEntry(entry) {
107
+ return entry.ownership ? structuredClone(entry) : { ...structuredClone(entry), ownership: "file" };
108
+ }
109
+
110
+ function sourceImpact(project, output, afterDigest) {
111
+ const sources = project.contract.sources
112
+ .filter((source) => sourceStatus(source) === "active" && ["file", "path", "json-pointer"].includes(source.kind) && source.path === output)
113
+ .sort((left, right) => left.id.localeCompare(right.id));
114
+ const sourceIds = sources.map((source) => source.id);
115
+ const selected = new Set(sourceIds);
116
+ const itemIds = project.contract.items
117
+ .filter((item) => item.sources.some((id) => selected.has(id)) || selected.has(item.verification?.source))
118
+ .map((item) => item.id)
119
+ .sort((left, right) => left.localeCompare(right));
120
+ return { sourceIds, itemIds, afterDigest: sourceIds.length > 0 ? afterDigest : null };
121
+ }
122
+
123
+ function ownershipConflict(pathname, reason) {
124
+ fail("ai-entry-ownership-conflict", `AI Entry ownership is not trustworthy: ${pathname}`, {
125
+ exitCode: 3,
126
+ details: { path: pathname, reason },
127
+ });
128
+ }
129
+
130
+ function entryLock(project, output) {
131
+ return project.projectionsLock.projections.find((entry) => entry.path === output) ?? null;
132
+ }
133
+
134
+ function assertEntryOwnership(output, content, lockEntry, parsed) {
135
+ if (lockEntry && lockEntry.ownership !== "region") {
136
+ fail("ai-entry-path-conflict", `AI Entry conflicts with a whole-file projection: ${output}`, {
137
+ exitCode: 3,
138
+ details: { path: output },
139
+ });
140
+ }
141
+ if (parsed.state === "conflict") ownershipConflict(output, parsed.reason);
142
+ if (parsed.state === "present" && !lockEntry) ownershipConflict(output, "marker-without-lock");
143
+ if (parsed.state === "absent" && lockEntry) ownershipConflict(output, "lock-without-marker");
144
+ if (lockEntry && sha256(parsed.region) !== lockEntry.regionDigest) ownershipConflict(output, "region-digest-mismatch");
145
+ }
146
+
147
+ function nextProjectionLock(project, output, nextEntry) {
148
+ return validateProjectionLock({
149
+ schemaVersion: 2,
150
+ projections: [
151
+ ...project.projectionsLock.projections
152
+ .filter((entry) => entry.path !== output)
153
+ .map(schema2FileEntry),
154
+ ...(nextEntry ? [nextEntry] : []),
155
+ ].sort((left, right) => left.path.localeCompare(right.path)),
156
+ });
157
+ }
158
+
159
+ async function commitEntry(project, resolved, existing, nextContent, nextLock, dependencies) {
160
+ const readJson = dependencies.readJson ?? readJsonFile;
161
+ const readContent = dependencies.readFile ?? readIfPresent;
162
+ const writeJson = dependencies.writeJson ?? atomicWriteJson;
163
+ const writeFile = dependencies.writeFile ?? atomicWriteFile;
164
+ if (dependencies.beforeCommit) await dependencies.beforeCommit();
165
+ const currentContract = validateContract(await readJson(project.files.contract, "contract.json"));
166
+ const currentLock = validateProjectionLock(await readJson(project.files.projectionsLock, "projections.lock.json"));
167
+ if (digestJson(currentContract) !== project.contractDigest) {
168
+ fail("ai-entry-state-changed", "contract changed while the AI Entry was being prepared", { exitCode: 1 });
169
+ }
170
+ if (digestJson(currentLock) !== project.projectionsLockDigest) {
171
+ fail("ai-entry-state-changed", "projection lock changed while the AI Entry was being prepared", { exitCode: 1 });
172
+ }
173
+ if (await readContent(resolved.absolute) !== existing) {
174
+ fail("ai-entry-state-changed", "AGENTS.md changed while the AI Entry was being prepared", {
175
+ exitCode: 1,
176
+ details: { path: resolved.normalized },
177
+ });
178
+ }
179
+ await writeJson(project.files.projectionsLock, nextLock);
180
+ try {
181
+ await writeFile(resolved.absolute, nextContent);
182
+ } catch (error) {
183
+ try {
184
+ const recoveryCurrent = validateProjectionLock(await readJson(project.files.projectionsLock, "projections.lock.json"));
185
+ if (canonicalJson(recoveryCurrent) === canonicalJson(nextLock)) {
186
+ await writeJson(project.files.projectionsLock, project.projectionsLock);
187
+ } else if (error && typeof error === "object") {
188
+ error.details = { ...(error.details ?? {}), recovery: "projection-lock-restore-skipped-concurrent-change" };
189
+ }
190
+ } catch (recoveryError) {
191
+ if (error && typeof error === "object") {
192
+ error.details = { ...(error.details ?? {}), recovery: "projection-lock-restore-failed", recoveryMessage: recoveryError.message };
193
+ }
194
+ }
195
+ throw error;
196
+ }
197
+ }
198
+
199
+ export async function inspectAiEntry(root, project, output = "AGENTS.md") {
200
+ const resolved = await resolveEntryOutput(root, output, false);
201
+ const content = await readIfPresent(resolved.absolute);
202
+ const parsed = parseAiEntryRegion(content);
203
+ const lockEntry = entryLock(project, resolved.normalized);
204
+ if (lockEntry && lockEntry.ownership !== "region") {
205
+ return { state: "absent", path: null, rendererVersion: null };
206
+ }
207
+ if (content === null && lockEntry?.ownership === "region") {
208
+ return { state: "absent", path: resolved.normalized, rendererVersion: lockEntry.rendererVersion };
209
+ }
210
+ try {
211
+ assertEntryOwnership(resolved.normalized, content, lockEntry, parsed);
212
+ } catch (error) {
213
+ if (error?.code === "ai-entry-ownership-conflict" || error?.code === "ai-entry-path-conflict") {
214
+ return { state: "conflict", path: resolved.normalized, rendererVersion: lockEntry?.rendererVersion ?? null, reason: error.details?.reason ?? error.code };
215
+ }
216
+ throw error;
217
+ }
218
+ if (!lockEntry) return { state: "absent", path: null, rendererVersion: null };
219
+ return {
220
+ state: lockEntry.rendererVersion === AI_ENTRY_RENDERER_VERSION ? "current" : "stale",
221
+ path: resolved.normalized,
222
+ rendererVersion: lockEntry.rendererVersion,
223
+ };
224
+ }
225
+
226
+ export async function publishAiEntry(root, project, options, dependencies = {}) {
227
+ const write = Boolean(options.write);
228
+ const resolved = await resolveEntryOutput(root, options.output, write);
229
+ const readContent = dependencies.readFile ?? readIfPresent;
230
+ const existing = await readContent(resolved.absolute);
231
+ const parsed = parseAiEntryRegion(existing);
232
+ const currentEntry = entryLock(project, resolved.normalized);
233
+ assertEntryOwnership(resolved.normalized, existing, currentEntry, parsed);
234
+ const newline = newlineFor(existing);
235
+ const region = renderAiEntry(newline);
236
+ const nextContent = parsed.state === "present"
237
+ ? `${existing.slice(0, parsed.start)}${region}${existing.slice(parsed.end)}`
238
+ : appendRegion(existing, region, newline);
239
+ const nextEntry = {
240
+ path: resolved.normalized,
241
+ target: "ai-entry",
242
+ ownership: "region",
243
+ regionId: AI_ENTRY_REGION_ID,
244
+ regionDigest: sha256(region),
245
+ rendererVersion: AI_ENTRY_RENDERER_VERSION,
246
+ createdFile: existing === null || currentEntry?.createdFile === true,
247
+ };
248
+ const nextLock = nextProjectionLock(project, resolved.normalized, nextEntry);
249
+ const action = currentEntry && existing === nextContent && canonicalJson(currentEntry) === canonicalJson(nextEntry) ? "unchanged" : currentEntry ? "update" : "create";
250
+ const impact = sourceImpact(project, resolved.normalized, sha256(nextContent));
251
+ const result = {
252
+ action,
253
+ current: {
254
+ entry: currentEntry ? structuredClone(currentEntry) : null,
255
+ contentDigest: existing === null ? null : sha256(existing),
256
+ region: parsed.region,
257
+ },
258
+ proposed: {
259
+ entry: nextEntry,
260
+ contentDigest: sha256(nextContent),
261
+ region,
262
+ },
263
+ baselines: {
264
+ contract: project.contractDigest,
265
+ projectionsLock: project.projectionsLockDigest,
266
+ content: existing === null ? null : sha256(existing),
267
+ },
268
+ migration: {
269
+ required: project.projectionsLock.schemaVersion === 1,
270
+ from: project.projectionsLock.schemaVersion,
271
+ to: 2,
272
+ trigger: "publish-entry",
273
+ },
274
+ impact: { itemIds: impact.itemIds, sourceIds: impact.sourceIds, paths: [resolved.normalized], projectionPaths: [resolved.normalized], afterSourceDigest: impact.afterDigest },
275
+ written: false,
276
+ };
277
+ if (write && action !== "unchanged") {
278
+ await commitEntry(project, resolved, existing, nextContent, nextLock, dependencies);
279
+ result.written = true;
280
+ }
281
+ return result;
282
+ }
283
+
284
+ export async function removeAiEntry(root, project, options, dependencies = {}) {
285
+ const write = Boolean(options.write);
286
+ const resolved = await resolveEntryOutput(root, options.output, false);
287
+ const readContent = dependencies.readFile ?? readIfPresent;
288
+ const existing = await readContent(resolved.absolute);
289
+ const parsed = parseAiEntryRegion(existing);
290
+ const currentEntry = entryLock(project, resolved.normalized);
291
+ assertEntryOwnership(resolved.normalized, existing, currentEntry, parsed);
292
+ if (!currentEntry) {
293
+ return {
294
+ action: "unchanged",
295
+ current: { entry: null, contentDigest: existing === null ? null : sha256(existing), region: null },
296
+ proposed: { entry: null, contentDigest: existing === null ? null : sha256(existing), region: null },
297
+ baselines: { contract: project.contractDigest, projectionsLock: project.projectionsLockDigest, content: existing === null ? null : sha256(existing) },
298
+ migration: { required: false, from: project.projectionsLock.schemaVersion, to: project.projectionsLock.schemaVersion, trigger: null },
299
+ impact: { itemIds: [], sourceIds: [], paths: [resolved.normalized], projectionPaths: [resolved.normalized], afterSourceDigest: null },
300
+ written: false,
301
+ };
302
+ }
303
+ const nextContent = `${existing.slice(0, parsed.start)}${existing.slice(parsed.end)}`;
304
+ const nextLock = nextProjectionLock(project, resolved.normalized, null);
305
+ const impact = sourceImpact(project, resolved.normalized, sha256(nextContent));
306
+ const result = {
307
+ action: "remove",
308
+ current: { entry: structuredClone(currentEntry), contentDigest: sha256(existing), region: parsed.region },
309
+ proposed: { entry: null, contentDigest: sha256(nextContent), region: null },
310
+ baselines: { contract: project.contractDigest, projectionsLock: project.projectionsLockDigest, content: sha256(existing) },
311
+ migration: { required: false, from: project.projectionsLock.schemaVersion, to: 2, trigger: null },
312
+ impact: { itemIds: impact.itemIds, sourceIds: impact.sourceIds, paths: [resolved.normalized], projectionPaths: [resolved.normalized], afterSourceDigest: impact.afterDigest },
313
+ written: false,
314
+ };
315
+ if (write) {
316
+ await commitEntry(project, resolved, existing, nextContent, nextLock, dependencies);
317
+ result.written = true;
318
+ }
319
+ return result;
320
+ }