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,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:
|
|
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":
|
|
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
|
+
}
|