archctx-contracts 0.8.0 → 0.9.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/fixtures/invalid/projection-pr-verification-unrendered-equivalence.json +15 -0
- package/fixtures/invalid/projection-publish-run-foreign-branch.json +19 -0
- package/fixtures/valid/archctx-capabilities.json +2 -2
- package/fixtures/valid/product-version-manifest.json +5 -5
- package/fixtures/valid/projection-pr-verification.json +15 -0
- package/fixtures/valid/projection-publish-run.json +19 -0
- package/fixtures/valid/projection-publish-status.json +29 -0
- package/package.json +1 -1
- package/schemas/runtime/archctx-capabilities.schema.json +1 -1
- package/schemas/runtime/native-model-issue.schema.json +18 -0
- package/schemas/runtime/projection-pr-verification.schema.json +34 -0
- package/schemas/runtime/projection-publish-run.schema.json +74 -0
- package/schemas/runtime/projection-publish-status.schema.json +36 -0
- package/src/failure.ts +37 -3
- package/src/index.ts +1 -0
- package/src/native-context.ts +31 -29
- package/src/ports.ts +5 -1
- package/src/product-version.ts +1 -1
- package/src/projection-writer.ts +116 -0
- package/src/projection.ts +2 -0
- package/src/schema.ts +2 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "archcontext.projection-pr-verification/v1",
|
|
3
|
+
"base": "origin/main",
|
|
4
|
+
"baseSha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
5
|
+
"headSha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
6
|
+
"mergeBase": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
7
|
+
"changedPaths": ["docs/architecture/modules/capability-app.md", "src/app.ts"],
|
|
8
|
+
"nonGeneratedPaths": ["src/app.ts"],
|
|
9
|
+
"humanEditedPaths": [],
|
|
10
|
+
"generatedOnly": false,
|
|
11
|
+
"renderEquivalent": true,
|
|
12
|
+
"differingPaths": [],
|
|
13
|
+
"projectionStatus": "noop",
|
|
14
|
+
"verdict": "touches-non-generated"
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "archcontext.projection-publish-run/v1",
|
|
3
|
+
"result": "published",
|
|
4
|
+
"remote": "origin",
|
|
5
|
+
"base": "main",
|
|
6
|
+
"branch": "main",
|
|
7
|
+
"baseSha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
8
|
+
"publishedCommit": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
9
|
+
"pushed": true,
|
|
10
|
+
"pullRequest": null,
|
|
11
|
+
"publisher": "none",
|
|
12
|
+
"files": ["docs/architecture/modules/capability-app.md"],
|
|
13
|
+
"projectionStatus": "applied",
|
|
14
|
+
"humanActions": [],
|
|
15
|
+
"error": null,
|
|
16
|
+
"startedAt": "2026-10-20T08:00:00.000Z",
|
|
17
|
+
"finishedAt": "2026-10-20T08:00:09.000Z",
|
|
18
|
+
"iterations": 1
|
|
19
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "archcontext.capabilities/v1",
|
|
3
|
-
"package": { "name": "archctx", "version": "0.
|
|
3
|
+
"package": { "name": "archctx", "version": "0.9.0" },
|
|
4
4
|
"protocols": {
|
|
5
5
|
"projectionRequest": "archcontext.projection-request/v1",
|
|
6
6
|
"projectionResult": "archcontext.projection-result/v2",
|
|
@@ -10,5 +10,5 @@
|
|
|
10
10
|
"architectureDocs": "archcontext.docs-renderer/v6",
|
|
11
11
|
"agentContext": "archcontext.agent-context-renderer/v1"
|
|
12
12
|
},
|
|
13
|
-
"features": ["architecture-change-capabilities-v1", "architecture-debt-v1", "architecture-docs-renderer-v2", "architecture-refresh-signal-v1", "capability-dependencies-declared-v1", "capability-dependencies-v1", "changeset-entity-operations-v1", "changeset-ownership-delta-v1", "code-facts-unavailable-error-v1", "docs-anchors-v1", "flow-selector-check-v1", "module-statistics-v1", "native-context-export-v1", "projection-apply-readback-v1", "projection-apply-receipt-v1", "projection-apply-recovery-v1", "projection-apply-replay-restore-v1", "projection-apply-replay-v1", "projection-architecture-debt-v1", "projection-capability-isolation-v1", "projection-change-classification-v1", "projection-check-freshness-v1", "projection-freshness-v1", "projection-manifest-contract-v3", "projection-observed-major-change-acceptance-v1", "projection-orphan-review-v1", "projection-preview-v1", "projection-prior-committed-applies-v1", "projection-protocol-v2", "projection-snapshot-v1", "recommendation-scan-decision-v1", "recommendation-v3", "refactor-assessment-v1", "refactor-observation-evidence-v1", "refactor-resolution-v1", "refactor-scan-limits-v1", "refactor-scan-untracked-count-v1"]
|
|
13
|
+
"features": ["architecture-change-capabilities-v1", "architecture-debt-v1", "architecture-docs-renderer-v2", "architecture-refresh-signal-v1", "capability-dependencies-declared-v1", "capability-dependencies-v1", "changeset-entity-operations-v1", "changeset-ownership-delta-v1", "code-facts-unavailable-error-v1", "docs-anchors-v1", "flow-selector-check-v1", "module-statistics-v1", "native-context-export-v1", "projection-apply-readback-v1", "projection-apply-receipt-v1", "projection-apply-recovery-v1", "projection-apply-replay-restore-v1", "projection-apply-replay-v1", "projection-architecture-debt-v1", "projection-capability-isolation-v1", "projection-change-classification-v1", "projection-check-freshness-v1", "projection-freshness-v1", "projection-manifest-contract-v3", "projection-observed-major-change-acceptance-v1", "projection-orphan-review-v1", "projection-preview-v1", "projection-prior-committed-applies-v1", "projection-protocol-v2", "projection-publish-v1", "projection-snapshot-v1", "projection-verify-pr-v1", "recommendation-scan-decision-v1", "recommendation-v3", "refactor-assessment-v1", "refactor-observation-evidence-v1", "refactor-resolution-v1", "refactor-scan-limits-v1", "refactor-scan-untracked-count-v1"]
|
|
14
14
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": "archcontext.product-version-manifest/v1",
|
|
3
3
|
"product": {
|
|
4
4
|
"name": "archctx",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.9.0",
|
|
6
6
|
"distribution": "one-package"
|
|
7
7
|
},
|
|
8
8
|
"packageManager": "bun@1.4.3",
|
|
@@ -12,25 +12,25 @@
|
|
|
12
12
|
"surfaces": {
|
|
13
13
|
"cli": {
|
|
14
14
|
"packageName": "@archcontext/surfaces",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.9.0",
|
|
16
16
|
"entrypoint": "archctx"
|
|
17
17
|
},
|
|
18
18
|
"daemon": {
|
|
19
19
|
"packageName": "@archcontext/local-runtime",
|
|
20
|
-
"version": "0.
|
|
20
|
+
"version": "0.9.0",
|
|
21
21
|
"entrypoint": "archctx daemon start",
|
|
22
22
|
"rpcSchemaVersion": "archcontext.runtime-rpc/v2"
|
|
23
23
|
},
|
|
24
24
|
"mcp": {
|
|
25
25
|
"packageName": "@archcontext/surfaces",
|
|
26
|
-
"version": "0.
|
|
26
|
+
"version": "0.9.0",
|
|
27
27
|
"entrypoint": "archctx mcp",
|
|
28
28
|
"transport": "stdio"
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
"schemas": {
|
|
32
32
|
"schemaSetVersion": "2026-06-25.al0-ledger",
|
|
33
|
-
"contractsPackageVersion": "0.
|
|
33
|
+
"contractsPackageVersion": "0.9.0",
|
|
34
34
|
"reviewChallenge": "archcontext.review-challenge/v2",
|
|
35
35
|
"attestation": "archcontext.attestation/v2",
|
|
36
36
|
"deviceIdentity": "archcontext.device-identity/v1",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "archcontext.projection-pr-verification/v1",
|
|
3
|
+
"base": "origin/main",
|
|
4
|
+
"baseSha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
5
|
+
"headSha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
6
|
+
"mergeBase": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
7
|
+
"changedPaths": ["docs/architecture/.projection-manifest.json", "docs/architecture/modules/capability-app.md"],
|
|
8
|
+
"nonGeneratedPaths": [],
|
|
9
|
+
"humanEditedPaths": [],
|
|
10
|
+
"generatedOnly": true,
|
|
11
|
+
"renderEquivalent": true,
|
|
12
|
+
"differingPaths": [],
|
|
13
|
+
"projectionStatus": "noop",
|
|
14
|
+
"verdict": "generated-only-render-equivalent"
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "archcontext.projection-publish-run/v1",
|
|
3
|
+
"result": "published",
|
|
4
|
+
"remote": "origin",
|
|
5
|
+
"base": "main",
|
|
6
|
+
"branch": "archctx/projection",
|
|
7
|
+
"baseSha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
8
|
+
"publishedCommit": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
9
|
+
"pushed": true,
|
|
10
|
+
"pullRequest": { "url": "https://github.com/example/repo/pull/12", "number": 12 },
|
|
11
|
+
"publisher": "gh",
|
|
12
|
+
"files": ["docs/architecture/.projection-manifest.json", "docs/architecture/modules/capability-app.md"],
|
|
13
|
+
"projectionStatus": "applied",
|
|
14
|
+
"humanActions": [],
|
|
15
|
+
"error": null,
|
|
16
|
+
"startedAt": "2026-10-20T08:00:00.000Z",
|
|
17
|
+
"finishedAt": "2026-10-20T08:00:09.000Z",
|
|
18
|
+
"iterations": 1
|
|
19
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "archcontext.projection-publish-status/v1",
|
|
3
|
+
"remote": "origin",
|
|
4
|
+
"base": "main",
|
|
5
|
+
"branch": "archctx/projection",
|
|
6
|
+
"worktreePath": "/Users/example/Library/Application Support/ArchContext/repositories/repo.0123456789abcdef/projection-writer/0123456789abcdef/worktree",
|
|
7
|
+
"running": null,
|
|
8
|
+
"lastProcessedSha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
9
|
+
"lastSuccessfulSha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
10
|
+
"lastRun": {
|
|
11
|
+
"schemaVersion": "archcontext.projection-publish-run/v1",
|
|
12
|
+
"result": "failed",
|
|
13
|
+
"remote": "origin",
|
|
14
|
+
"base": "main",
|
|
15
|
+
"branch": "archctx/projection",
|
|
16
|
+
"baseSha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
17
|
+
"publishedCommit": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
18
|
+
"pushed": true,
|
|
19
|
+
"pullRequest": null,
|
|
20
|
+
"publisher": "gh",
|
|
21
|
+
"files": ["docs/architecture/modules/capability-app.md"],
|
|
22
|
+
"projectionStatus": "applied",
|
|
23
|
+
"humanActions": [],
|
|
24
|
+
"error": { "code": "AC_PRECONDITION_FAILED", "reasonCode": "projection-publisher-failed", "message": "gh publisher failed: gh pr list failed: not logged in" },
|
|
25
|
+
"startedAt": "2026-10-20T08:00:00.000Z",
|
|
26
|
+
"finishedAt": "2026-10-20T08:00:09.000Z",
|
|
27
|
+
"iterations": 1
|
|
28
|
+
}
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"type": "string",
|
|
44
44
|
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*-v[0-9]+$",
|
|
45
45
|
"description": "One feature flag: a lowercase kebab-case name that ends in a version suffix (-v1, -v2, ...). The pattern is open, so a validator built from this schema accepts flags that a later archctx adds. A consumer must ignore a flag it does not know. examples lists the flags this schema version knows.",
|
|
46
|
-
"examples": ["architecture-change-capabilities-v1", "architecture-debt-v1", "architecture-docs-renderer-v2", "architecture-refresh-signal-v1", "capability-dependencies-declared-v1", "capability-dependencies-v1", "changeset-entity-operations-v1", "changeset-ownership-delta-v1", "code-facts-unavailable-error-v1", "docs-anchors-v1", "flow-selector-check-v1", "module-statistics-v1", "native-context-export-v1", "projection-apply-readback-v1", "projection-apply-receipt-v1", "projection-apply-recovery-v1", "projection-apply-replay-restore-v1", "projection-apply-replay-v1", "projection-architecture-debt-v1", "projection-capability-isolation-v1", "projection-change-classification-v1", "projection-check-freshness-v1", "projection-freshness-v1", "projection-manifest-contract-v3", "projection-observed-major-change-acceptance-v1", "projection-orphan-review-v1", "projection-preview-v1", "projection-prior-committed-applies-v1", "projection-protocol-v2", "projection-snapshot-v1", "recommendation-scan-decision-v1", "recommendation-v3", "refactor-assessment-v1", "refactor-observation-evidence-v1", "refactor-resolution-v1", "refactor-scan-limits-v1", "refactor-scan-untracked-count-v1"]
|
|
46
|
+
"examples": ["architecture-change-capabilities-v1", "architecture-debt-v1", "architecture-docs-renderer-v2", "architecture-refresh-signal-v1", "capability-dependencies-declared-v1", "capability-dependencies-v1", "changeset-entity-operations-v1", "changeset-ownership-delta-v1", "code-facts-unavailable-error-v1", "docs-anchors-v1", "flow-selector-check-v1", "module-statistics-v1", "native-context-export-v1", "projection-apply-readback-v1", "projection-apply-receipt-v1", "projection-apply-recovery-v1", "projection-apply-replay-restore-v1", "projection-apply-replay-v1", "projection-architecture-debt-v1", "projection-capability-isolation-v1", "projection-change-classification-v1", "projection-check-freshness-v1", "projection-freshness-v1", "projection-manifest-contract-v3", "projection-observed-major-change-acceptance-v1", "projection-orphan-review-v1", "projection-preview-v1", "projection-prior-committed-applies-v1", "projection-protocol-v2", "projection-publish-v1", "projection-snapshot-v1", "projection-verify-pr-v1", "recommendation-scan-decision-v1", "recommendation-v3", "refactor-assessment-v1", "refactor-observation-evidence-v1", "refactor-resolution-v1", "refactor-scan-limits-v1", "refactor-scan-untracked-count-v1"]
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://archctx.repoharness.com/schemas/runtime/native-model-issue.schema.json",
|
|
4
|
+
"title": "NativeModelIssue",
|
|
5
|
+
"description": "A native model schema or reference issue. Source paths are optional for callers that do not supply parsed record paths.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["entityId", "field", "invariant"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"entityId": { "type": "string" },
|
|
11
|
+
"field": { "type": "string" },
|
|
12
|
+
"invariant": { "type": "string" },
|
|
13
|
+
"path": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "The repository-relative path of the parsed source file that holds this record. It is not derived from the entity ID."
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://archcontext.dev/schemas/runtime/projection-pr-verification.schema.json",
|
|
4
|
+
"title": "ProjectionPrVerificationV1",
|
|
5
|
+
"description": "Result of `archctx projection verify-pr --base <ref>` for the checked-out pull request head. A path is generated only when it is the projection manifest, a manifest target path or an agent-context target path at HEAD or at the merge base; for mixed-ownership targets the text outside the generated regions must also be unchanged. A diff with any other change is answered from the diff alone, with no render. generated-only-render-equivalent proves that the generated files and generated regions at HEAD equal a fresh render there and that no text outside those regions changed; it says nothing about whether the rendered content is desirable. The envelope is ok for every valid evaluation. Consumers detect it through the projection-verify-pr-v1 capability.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schemaVersion", "base", "baseSha", "headSha", "mergeBase", "changedPaths", "nonGeneratedPaths", "humanEditedPaths", "generatedOnly", "renderEquivalent", "differingPaths", "projectionStatus", "verdict"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schemaVersion": { "const": "archcontext.projection-pr-verification/v1" },
|
|
11
|
+
"base": { "type": "string", "minLength": 1 },
|
|
12
|
+
"baseSha": { "$ref": "#/$defs/sha" },
|
|
13
|
+
"headSha": { "$ref": "#/$defs/sha" },
|
|
14
|
+
"mergeBase": { "$ref": "#/$defs/sha" },
|
|
15
|
+
"changedPaths": { "$ref": "#/$defs/paths", "description": "Paths changed between the merge base and HEAD, without rename detection, sorted." },
|
|
16
|
+
"nonGeneratedPaths": { "$ref": "#/$defs/paths", "description": "Changed paths that are not generated outputs: not the projection manifest, not a manifest target path and not an agent-context target path at HEAD or at the merge base, plus every humanEditedPaths entry." },
|
|
17
|
+
"humanEditedPaths": { "$ref": "#/$defs/paths", "description": "Mixed-ownership targets (manifest ownership mixed, and every agent-context target) whose text outside the generated regions differs between the merge base and HEAD, whose regions cannot be delimited, or which were added or deleted. They are also in nonGeneratedPaths and are never rendered." },
|
|
18
|
+
"generatedOnly": { "type": "boolean", "description": "True when the diff is non-empty and every changed path is a generated output with no change outside its generated regions." },
|
|
19
|
+
"renderEquivalent": { "type": ["boolean", "null"], "description": "Whether the generated files and regions at HEAD equal a fresh render (a check status of noop). Null when no render ran." },
|
|
20
|
+
"differingPaths": { "$ref": "#/$defs/paths", "description": "Paths the check would change at HEAD." },
|
|
21
|
+
"projectionStatus": { "type": ["string", "null"] },
|
|
22
|
+
"verdict": { "enum": ["generated-only-not-render-equivalent", "generated-only-render-equivalent", "no-changes", "touches-non-generated"] }
|
|
23
|
+
},
|
|
24
|
+
"allOf": [
|
|
25
|
+
{ "if": { "properties": { "verdict": { "const": "generated-only-render-equivalent" } } }, "then": { "properties": { "generatedOnly": { "const": true }, "renderEquivalent": { "const": true }, "nonGeneratedPaths": { "maxItems": 0 }, "humanEditedPaths": { "maxItems": 0 } } } },
|
|
26
|
+
{ "if": { "properties": { "verdict": { "const": "generated-only-not-render-equivalent" } } }, "then": { "properties": { "generatedOnly": { "const": true }, "renderEquivalent": { "const": false }, "nonGeneratedPaths": { "maxItems": 0 }, "humanEditedPaths": { "maxItems": 0 } } } },
|
|
27
|
+
{ "if": { "properties": { "verdict": { "const": "touches-non-generated" } } }, "then": { "properties": { "generatedOnly": { "const": false }, "renderEquivalent": { "type": "null" }, "nonGeneratedPaths": { "minItems": 1 } } } },
|
|
28
|
+
{ "if": { "properties": { "verdict": { "const": "no-changes" } } }, "then": { "properties": { "generatedOnly": { "const": false }, "renderEquivalent": { "type": "null" }, "changedPaths": { "maxItems": 0 }, "humanEditedPaths": { "maxItems": 0 } } } }
|
|
29
|
+
],
|
|
30
|
+
"$defs": {
|
|
31
|
+
"sha": { "type": "string", "pattern": "^[a-f0-9]{40}$" },
|
|
32
|
+
"paths": { "type": "array", "uniqueItems": true, "items": { "type": "string", "minLength": 1 } }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://archcontext.dev/schemas/runtime/projection-publish-run.schema.json",
|
|
4
|
+
"title": "ProjectionPublishRunV1",
|
|
5
|
+
"description": "One run of `archctx projection publish` (the envelope data, and the writer state's lastRun). The writer keeps one rolling archctx/* branch equal to the base tip plus exactly one commit that touches generated projection outputs only. Every result except failed is an ok envelope. Consumers detect it through the projection-publish-v1 capability.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schemaVersion", "result", "remote", "base", "branch", "baseSha", "publishedCommit", "pushed", "pullRequest", "publisher", "files", "projectionStatus", "humanActions", "error", "startedAt", "finishedAt", "iterations"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schemaVersion": { "const": "archcontext.projection-publish-run/v1" },
|
|
11
|
+
"result": { "enum": ["already-processed", "busy", "failed", "human-action-required", "published", "up-to-date"] },
|
|
12
|
+
"remote": { "type": "string", "minLength": 1 },
|
|
13
|
+
"base": { "type": "string", "minLength": 1 },
|
|
14
|
+
"branch": { "type": "string", "pattern": "^archctx/.+", "description": "The rolling branch. The writer never pushes to any ref outside archctx/." },
|
|
15
|
+
"baseSha": { "oneOf": [{ "$ref": "#/$defs/sha" }, { "type": "null" }], "description": "The base tip this run evaluated; null when the run stopped before resolving one." },
|
|
16
|
+
"publishedCommit": { "oneOf": [{ "$ref": "#/$defs/sha" }, { "type": "null" }], "description": "The rolling branch tip that carries this base's projection, pushed by this run or an earlier one." },
|
|
17
|
+
"pushed": { "type": "boolean", "description": "True only when this run moved the remote rolling branch." },
|
|
18
|
+
"pullRequest": {
|
|
19
|
+
"oneOf": [
|
|
20
|
+
{
|
|
21
|
+
"type": "object",
|
|
22
|
+
"additionalProperties": false,
|
|
23
|
+
"properties": {
|
|
24
|
+
"url": { "type": "string", "minLength": 1 },
|
|
25
|
+
"number": { "type": "integer", "minimum": 1 }
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{ "type": "null" }
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"publisher": { "enum": ["command", "gh", "none"] },
|
|
32
|
+
"files": { "type": "array", "uniqueItems": true, "items": { "type": "string", "minLength": 1 }, "description": "Repository-relative generated paths the published commit changes, sorted. Paths only, never contents." },
|
|
33
|
+
"projectionStatus": { "type": ["string", "null"], "description": "ProjectionResultV2.status of the apply this run made, or null without one." },
|
|
34
|
+
"humanActions": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"additionalProperties": false,
|
|
39
|
+
"required": ["reasonCode"],
|
|
40
|
+
"properties": {
|
|
41
|
+
"reasonCode": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
|
|
42
|
+
"path": { "type": "string", "minLength": 1 }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"error": {
|
|
47
|
+
"oneOf": [
|
|
48
|
+
{
|
|
49
|
+
"type": "object",
|
|
50
|
+
"additionalProperties": false,
|
|
51
|
+
"required": ["code", "message"],
|
|
52
|
+
"properties": {
|
|
53
|
+
"code": { "type": "string", "pattern": "^AC_[A-Z0-9_]+$" },
|
|
54
|
+
"reasonCode": { "type": "string", "minLength": 1 },
|
|
55
|
+
"message": { "type": "string" }
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{ "type": "null" }
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"startedAt": { "type": "string", "minLength": 1 },
|
|
62
|
+
"finishedAt": { "type": "string", "minLength": 1 },
|
|
63
|
+
"iterations": { "type": "integer", "minimum": 0, "maximum": 3, "description": "Base tips evaluated in this run; above 1 when the base moved while the run worked." },
|
|
64
|
+
"holderPid": { "type": "integer", "minimum": 1, "description": "Present on busy: the pid of the live writer that holds the lock." }
|
|
65
|
+
},
|
|
66
|
+
"allOf": [
|
|
67
|
+
{ "if": { "properties": { "result": { "const": "failed" } } }, "then": { "properties": { "error": { "type": "object" } } }, "else": { "properties": { "error": { "type": "null" } } } },
|
|
68
|
+
{ "if": { "properties": { "result": { "const": "busy" } } }, "then": { "required": ["holderPid"] }, "else": { "not": { "required": ["holderPid"] } } },
|
|
69
|
+
{ "if": { "properties": { "result": { "const": "published" } } }, "then": { "properties": { "publishedCommit": { "$ref": "#/$defs/sha" }, "baseSha": { "$ref": "#/$defs/sha" } } } }
|
|
70
|
+
],
|
|
71
|
+
"$defs": {
|
|
72
|
+
"sha": { "type": "string", "pattern": "^[a-f0-9]{40}$" }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://archcontext.dev/schemas/runtime/projection-publish-status.schema.json",
|
|
4
|
+
"title": "ProjectionPublishStatusV1",
|
|
5
|
+
"description": "Read-only result of `archctx projection publish-status`: the projection writer state for one (remote, base), shared by every checkout of the repository. Consumers detect it through the projection-publish-v1 capability.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schemaVersion", "remote", "base", "branch", "worktreePath", "running", "lastProcessedSha", "lastSuccessfulSha", "lastRun"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schemaVersion": { "const": "archcontext.projection-publish-status/v1" },
|
|
11
|
+
"remote": { "type": "string", "minLength": 1 },
|
|
12
|
+
"base": { "type": "string", "minLength": 1 },
|
|
13
|
+
"branch": { "oneOf": [{ "type": "string", "pattern": "^archctx/.+" }, { "type": "null" }], "description": "The rolling branch of the last locked run, or null before the first." },
|
|
14
|
+
"worktreePath": { "type": "string", "minLength": 1, "description": "The writer-owned worktree; run `codegraph init <worktreePath>` once when code facts are required." },
|
|
15
|
+
"running": {
|
|
16
|
+
"oneOf": [
|
|
17
|
+
{
|
|
18
|
+
"type": "object",
|
|
19
|
+
"additionalProperties": false,
|
|
20
|
+
"required": ["pid", "startedAt"],
|
|
21
|
+
"properties": {
|
|
22
|
+
"pid": { "type": "integer", "minimum": 1 },
|
|
23
|
+
"startedAt": { "type": "string" }
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{ "type": "null" }
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"lastProcessedSha": { "oneOf": [{ "$ref": "#/$defs/sha" }, { "type": "null" }], "description": "The last base tip a run evaluated." },
|
|
30
|
+
"lastSuccessfulSha": { "oneOf": [{ "$ref": "#/$defs/sha" }, { "type": "null" }], "description": "The last base tip a run published or found up to date." },
|
|
31
|
+
"lastRun": { "oneOf": [{ "$ref": "projection-publish-run.schema.json" }, { "type": "null" }] }
|
|
32
|
+
},
|
|
33
|
+
"$defs": {
|
|
34
|
+
"sha": { "type": "string", "pattern": "^[a-f0-9]{40}$" }
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/failure.ts
CHANGED
|
@@ -23,6 +23,16 @@ export interface FailureEnvelope<C extends FailureCode = FailureCode> {
|
|
|
23
23
|
error: StrictFailure<C>;
|
|
24
24
|
}
|
|
25
25
|
type Policy = { code: FailureCode; message: string; fields?: readonly string[]; action?: string; facts?: Readonly<Record<string, string>>; native?: boolean; recovery?: boolean; journalRecovery?: boolean; retryable?: boolean };
|
|
26
|
+
/** Closed repository diagnostics. Messages contain no process output or filesystem paths. */
|
|
27
|
+
export const CODE_FACTS_ISSUE_MESSAGES = Object.freeze({
|
|
28
|
+
"not-initialized": "CodeGraph repository status is not initialized",
|
|
29
|
+
"version-mismatch": "CodeGraph repository status version mismatch",
|
|
30
|
+
"project-path-mismatch": "CodeGraph repository project path mismatch",
|
|
31
|
+
"index-incomplete": "CodeGraph repository index is not complete and compatible",
|
|
32
|
+
"extraction-version-mismatch": "CodeGraph repository extraction version mismatch",
|
|
33
|
+
"worktree-mismatch": "CodeGraph repository worktree mismatch",
|
|
34
|
+
"pending-source-changes": "CodeGraph repository index has pending or unreported source changes"
|
|
35
|
+
});
|
|
26
36
|
/** Each reason has a closed fact vocabulary. No source text or recovery binding is a summary fact. */
|
|
27
37
|
const policies: Record<string, Policy> = {
|
|
28
38
|
"git-worktree-required": { code: "AC_REPO_NOT_FOUND", message: "This operation requires a Git worktree.", fields: ["checks"] },
|
|
@@ -33,6 +43,7 @@ const policies: Record<string, Policy> = {
|
|
|
33
43
|
"projection-manifest-invalid": { code: "AC_PRECONDITION_FAILED", message: "The committed projection manifest violates its contract." },
|
|
34
44
|
"projection-footprint-uncovered": { code: "AC_PRECONDITION_FAILED", message: "Declared source footprints lack a projection target." },
|
|
35
45
|
"index-missing": { code: "AC_CODE_FACTS_UNAVAILABLE", message: "Required code facts have no available index. Run codegraph init." },
|
|
46
|
+
"code-facts-not-ready": { code: "AC_CODE_FACTS_NOT_READY", message: "Repository code facts are not ready. Run archctx doctor --json and follow the reported action.", facts: { issues: "codeFactsIssues" } },
|
|
36
47
|
"checkpoint-input-invalid": { code: "AC_SCHEMA_INVALID", message: "Checkpoint arguments violate the declared input schema.", fields: ["issueCount"], action: "correct-command-arguments" },
|
|
37
48
|
"cli-input-invalid": { code: "AC_SCHEMA_INVALID", message: "Command arguments violate the declared input contract.", action: "correct-command-arguments" },
|
|
38
49
|
"complete-input-invalid": { code: "AC_SCHEMA_INVALID", message: "Completion input violates the caller authority contract.", action: "correct-command-arguments" },
|
|
@@ -41,7 +52,7 @@ const policies: Record<string, Policy> = {
|
|
|
41
52
|
"runtime-version-unsupported": { code: "AC_RUNTIME_VERSION_UNSUPPORTED", message: "The connected runtime version is unsupported." },
|
|
42
53
|
"rpc-timeout": { code: "RPC_TIMEOUT", message: "The runtime RPC deadline elapsed without a response; a mutation outcome is unknown.", fields: ["method", "timeoutMs", "elapsedMs", "mutationOutcome"], action: "reconcile-before-retry" },
|
|
43
54
|
"rpc-aborted": { code: "RPC_ABORTED", message: "The runtime RPC caller aborted without a response; a mutation outcome is unknown.", fields: ["method", "timeoutMs", "elapsedMs", "mutationOutcome"], action: "reconcile-before-retry" },
|
|
44
|
-
"failure-unclassified": { code: "AC_UNCLASSIFIED_FAILURE", message: "The operation failed without a classified cause.
|
|
55
|
+
"failure-unclassified": { code: "AC_UNCLASSIFIED_FAILURE", message: "The operation failed without a classified cause. Run archctx doctor --json before retrying.", action: "inspect-diagnostics" },
|
|
45
56
|
};
|
|
46
57
|
// C1 policies are selected at explicit producer branches, never from diagnostic text.
|
|
47
58
|
Object.assign(policies, {
|
|
@@ -278,7 +289,7 @@ for (const policy of Object.values(policies)) {
|
|
|
278
289
|
}
|
|
279
290
|
export const FAILURE_POLICIES: Readonly<Record<string, Readonly<Policy>>> = Object.freeze(policies);
|
|
280
291
|
const catalogDefaults = Object.fromEntries(Object.entries(ERROR_CATALOG).map(([code, value]) => [code, Object.freeze({ severity: value.severity, retryable: value.retryable, action: value.action })])) as Readonly<Record<ArchContextErrorCode, Readonly<Pick<StrictFailure, "severity" | "retryable" | "action">>>>;
|
|
281
|
-
const actions = new Set([...Object.values(catalogDefaults).map(value => value.action), "correct-command-arguments", "inspect-diagnostics", "reconcile-before-retry", ...Object.values(policies).flatMap(policy => policy.action ? [policy.action] : [])]);
|
|
292
|
+
const actions = new Set([...Object.values(catalogDefaults).map(value => value.action), "correct-command-arguments", "inspect-diagnostics", "reconcile-before-retry", "install-compatible-codegraph", ...Object.values(policies).flatMap(policy => policy.action ? [policy.action] : [])]);
|
|
282
293
|
const bytes = (value: string) => new TextEncoder().encode(value).byteLength;
|
|
283
294
|
const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
284
295
|
const finiteNonnegative = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
@@ -400,6 +411,20 @@ const localEnums: Readonly<Record<string, readonly string[]>> = {
|
|
|
400
411
|
]
|
|
401
412
|
};
|
|
402
413
|
function localFactValue(rule: string, value: unknown): Json {
|
|
414
|
+
if (rule === "codeFactsIssues") {
|
|
415
|
+
if (!Array.isArray(value)) throw new Error("Code facts issues must be a list");
|
|
416
|
+
const length = Object.getOwnPropertyDescriptor(value, "length")?.value;
|
|
417
|
+
if (!Number.isSafeInteger(length) || length < 1 || length > 32 || Reflect.ownKeys(value).length !== length + 1) throw new Error("Code facts issues exceed their closed budget");
|
|
418
|
+
return Array.from({ length }, (_, index) => {
|
|
419
|
+
const entry = Object.getOwnPropertyDescriptor(value, String(index));
|
|
420
|
+
if (!entry || !("value" in entry)) throw new Error("Code facts issues must use own data");
|
|
421
|
+
const fields = dataRecord(entry.value);
|
|
422
|
+
const code = fields.code?.value, message = fields.message?.value;
|
|
423
|
+
if (Object.keys(fields).length !== 2 || typeof code !== "string" || !Object.hasOwn(CODE_FACTS_ISSUE_MESSAGES, code)
|
|
424
|
+
|| message !== CODE_FACTS_ISSUE_MESSAGES[code as keyof typeof CODE_FACTS_ISSUE_MESSAGES]) throw new Error("Unknown code facts diagnostic");
|
|
425
|
+
return { code, message };
|
|
426
|
+
});
|
|
427
|
+
}
|
|
403
428
|
if (rule === "commandSuggestion" && (value === null || (typeof value === "string" && bytes(value) <= 1024 && /^archctx (recommendations |refactor verify )[^\r\n]*$/.test(value)))) return value;
|
|
404
429
|
if (rule === "publicConfirmationToken" && typeof value === "string" && bytes(value) <= 1024 && /^public:github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+:[a-f0-9]{40}:[A-Za-z0-9_.:-]+$/.test(value)) return value;
|
|
405
430
|
if (rule === "diagnosticPaths" || rule === "diagnosticCommands") {
|
|
@@ -568,7 +593,7 @@ function nativeFactsFor(policy: Policy, input: unknown): {[key: string]: Json} {
|
|
|
568
593
|
if (type === "diagnostic") { const s = text(value); if (bytes(s) > 256) { omit(s); return undefined; } return s; }
|
|
569
594
|
const item = dataRecord(value);
|
|
570
595
|
if (type === "issues") {
|
|
571
|
-
if (
|
|
596
|
+
if (!["entityId", "field", "invariant"].every(key => Object.hasOwn(item, key)) || Object.keys(item).some(key => !["entityId", "field", "invariant", "path"].includes(key))) throw new Error("Native issue schema is invalid");
|
|
572
597
|
const clean = Object.fromEntries(Object.entries(item).map(([key, data]) => [key, text(data.value)]));
|
|
573
598
|
if (Object.values(clean).some(v => bytes(v) > 256)) { omit(clean); return undefined; }
|
|
574
599
|
return clean;
|
|
@@ -616,12 +641,21 @@ function nativeFactsFor(policy: Policy, input: unknown): {[key: string]: Json} {
|
|
|
616
641
|
}
|
|
617
642
|
result[key] = values;
|
|
618
643
|
}
|
|
644
|
+
const sourceIssues = Array.isArray(result.issues) ? result.issues as Array<Record<string, Json>> : [];
|
|
645
|
+
// Legacy issues without source paths keep any explicit path supplied by the caller.
|
|
646
|
+
const summarizePath = sourceIssues.some(issue => typeof issue.path === "string");
|
|
619
647
|
const counts = () => {
|
|
620
648
|
if (!Number.isSafeInteger(omitted) || !Number.isSafeInteger(characters)) throw new Error("Native diagnostic counts exceed schema");
|
|
621
649
|
if (omitted) result.omittedDiagnosticCount = omitted;
|
|
622
650
|
if (characters) result.omittedDiagnosticCharacters = characters;
|
|
623
651
|
};
|
|
624
652
|
const fits = () => {
|
|
653
|
+
// Derive the summary from surviving whole issues, including after budget omission.
|
|
654
|
+
if (summarizePath) {
|
|
655
|
+
const path = sourceIssues[0]?.path;
|
|
656
|
+
if (typeof path === "string" && sourceIssues.every(issue => issue.path === path)) result.path = path;
|
|
657
|
+
else delete result.path;
|
|
658
|
+
}
|
|
625
659
|
counts();
|
|
626
660
|
const base = catalogDefaults[policy.code as ArchContextErrorCode];
|
|
627
661
|
const error = {code: policy.code, message: policy.message, severity: base.severity, retryable: policy.retryable ?? base.retryable, action: policy.action ?? base.action, reasonCode: Object.entries(policies).find(([, p]) => p === policy)?.[0], details: result};
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ export * from "./ports";
|
|
|
11
11
|
export * from "./practices";
|
|
12
12
|
export * from "./projection";
|
|
13
13
|
export * from "./projection-recovery";
|
|
14
|
+
export * from "./projection-writer";
|
|
14
15
|
export * from "./projection-manifest";
|
|
15
16
|
export * from "./product-version";
|
|
16
17
|
export * from "./refactor";
|
package/src/native-context.ts
CHANGED
|
@@ -149,7 +149,7 @@ export function nativeByteCompare(left: string, right: string): number {
|
|
|
149
149
|
|
|
150
150
|
type SchemaDocument = Parameters<typeof validateJsonSchema>[0];
|
|
151
151
|
export interface NativeContextSchemas { node: SchemaDocument; relation: SchemaDocument; flow: SchemaDocument }
|
|
152
|
-
export interface NativeModelIssue { entityId: string; field: string; invariant: string }
|
|
152
|
+
export interface NativeModelIssue { entityId: string; field: string; invariant: string; path?: string }
|
|
153
153
|
|
|
154
154
|
/** Fixed canonical documents are supplied by the bundled reader, never by repository input. */
|
|
155
155
|
export function nativeContextSchemaResolver(schemas: NativeContextSchemas): JsonSchemaResolver {
|
|
@@ -162,10 +162,14 @@ export function nativeAllowedNodeKinds(schemas: NativeContextSchemas): string[]
|
|
|
162
162
|
return [...kinds] as string[];
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
/** Complete native shape/reference validation;
|
|
166
|
-
export function nativeContextModelIssues(model: NativeContextModel, schemas: NativeContextSchemas): NativeModelIssue[] {
|
|
165
|
+
/** Complete native shape/reference validation; optional paths bind parsed records, never IDs. */
|
|
166
|
+
export function nativeContextModelIssues(model: NativeContextModel, schemas: NativeContextSchemas, recordPaths?: ReadonlyMap<NativeModelRecord, string>): NativeModelIssue[] {
|
|
167
167
|
const issues: NativeModelIssue[] = [];
|
|
168
|
-
const add = (
|
|
168
|
+
const add = (record: NativeModelRecord | string, field: string, invariant: string) => {
|
|
169
|
+
const entityId = typeof record === "string" ? record : typeof record.id === "string" ? record.id : "(undeclared)";
|
|
170
|
+
const path = typeof record === "string" ? undefined : recordPaths?.get(record);
|
|
171
|
+
issues.push({ entityId, field, invariant, ...(path === undefined ? {} : { path }) });
|
|
172
|
+
};
|
|
169
173
|
const resolveSchema = nativeContextSchemaResolver(schemas);
|
|
170
174
|
const ids = new Set<string>();
|
|
171
175
|
for (const [kind, records] of [["node", model.nodes], ["relation", model.relations], ["flow", model.flows]] as const) {
|
|
@@ -182,46 +186,45 @@ export function nativeContextModelIssues(model: NativeContextModel, schemas: Nat
|
|
|
182
186
|
const item = pending.pop()!;
|
|
183
187
|
const value = item.value;
|
|
184
188
|
if (item.exit) { active.delete(value as object); done.add(value as object); continue; }
|
|
185
|
-
if (typeof value === "number" && !Number.isFinite(value)) { add(
|
|
189
|
+
if (typeof value === "number" && !Number.isFinite(value)) { add(record, item.field, "non-json-number"); jsonInvalid = true; break; }
|
|
186
190
|
if (value === null || typeof value !== "object") continue;
|
|
187
|
-
if (active.has(value)) { add(
|
|
191
|
+
if (active.has(value)) { add(record, item.field, "non-json-cycle"); jsonInvalid = true; break; }
|
|
188
192
|
if (done.has(value)) continue;
|
|
189
193
|
const prototype = Object.getPrototypeOf(value);
|
|
190
|
-
if (Array.isArray(value) ? prototype !== Array.prototype : prototype !== Object.prototype) { add(
|
|
194
|
+
if (Array.isArray(value) ? prototype !== Array.prototype : prototype !== Object.prototype) { add(record, item.field, "non-json-prototype"); jsonInvalid = true; break; }
|
|
191
195
|
active.add(value); pending.push({ value, field: item.field, exit: true });
|
|
192
196
|
for (const [key, child] of Object.entries(value)) pending.push({ value: child, field: `${item.field}.${key}` });
|
|
193
197
|
}
|
|
194
198
|
if (jsonInvalid) continue;
|
|
195
199
|
const schema = kind === "node" ? schemas.node : kind === "relation" ? schemas.relation : schemas.flow;
|
|
196
|
-
for (const issue of validateJsonSchema(schema, record, { resolveSchema }).issues) add(
|
|
197
|
-
if (ids.has(id)) add(
|
|
200
|
+
for (const issue of validateJsonSchema(schema, record, { resolveSchema }).issues) add(record, issue.path, issue.message);
|
|
201
|
+
if (ids.has(id)) add(record, "id", "duplicate-entity-id");
|
|
198
202
|
ids.add(id);
|
|
199
203
|
}
|
|
200
204
|
}
|
|
201
205
|
// Invalid shapes cannot safely participate in reference validation.
|
|
202
206
|
if (issues.length > 0) return issues;
|
|
203
207
|
for (const kind of ["nodes", "relations", "flows"] as const) {
|
|
204
|
-
const expected = new
|
|
205
|
-
for (const id of expected) if (!Object.hasOwn(model.sourcePaths[kind], id)) add(
|
|
208
|
+
const expected = new Map(model[kind].map(entity => [entity.id as string, entity]));
|
|
209
|
+
for (const [id, entity] of expected) if (!Object.hasOwn(model.sourcePaths[kind], id)) add(entity, `sourcePaths.${kind}`, "source-path-index-incomplete");
|
|
206
210
|
for (const [id, path] of Object.entries(model.sourcePaths[kind])) {
|
|
207
211
|
if (!expected.has(id)) add(id, `sourcePaths.${kind}`, "source-path-index-unknown-entity");
|
|
208
|
-
if (!isNativeContextPath(path)) add(id, `sourcePaths.${kind}`, "path-unsafe");
|
|
212
|
+
if (!isNativeContextPath(path)) add(expected.get(id) ?? id, `sourcePaths.${kind}`, "path-unsafe");
|
|
209
213
|
}
|
|
210
214
|
}
|
|
211
215
|
const nodes = new Map(model.nodes.map(node => [node.id as string, node]));
|
|
212
216
|
const checked = new Set<string>();
|
|
213
217
|
for (const node of model.nodes) {
|
|
214
|
-
const id = node.id as string;
|
|
215
218
|
const chain = new Set<string>();
|
|
216
219
|
let cursor: NativeModelRecord | undefined = node;
|
|
217
220
|
while (cursor) {
|
|
218
221
|
const current = cursor.id as string;
|
|
219
|
-
if (chain.has(current)) { add(
|
|
222
|
+
if (chain.has(current)) { add(node, "parent", "parent-cycle"); break; }
|
|
220
223
|
if (checked.has(current)) break;
|
|
221
224
|
chain.add(current);
|
|
222
225
|
if (cursor.parent === undefined) break;
|
|
223
226
|
const parent = nodes.get(cursor.parent as string);
|
|
224
|
-
if (!parent) { add(
|
|
227
|
+
if (!parent) { add(cursor, "parent", "parent-node-missing"); break; }
|
|
225
228
|
cursor = parent;
|
|
226
229
|
}
|
|
227
230
|
for (const current of chain) checked.add(current);
|
|
@@ -229,48 +232,47 @@ export function nativeContextModelIssues(model: NativeContextModel, schemas: Nat
|
|
|
229
232
|
const selectors = new Set<string>();
|
|
230
233
|
for (const node of model.nodes) {
|
|
231
234
|
const source = node.source as { include?: string[]; exclude?: string[]; entrypoints?: Array<{ id: string; path: string; symbols: Array<{ name: string; sinks: Array<{ id: string; path: string; symbol: string }> }> }> } | undefined;
|
|
232
|
-
for (const path of [...source?.include ?? [], ...source?.exclude ?? []]) if (!isNativeContextPath(path)) add(node
|
|
235
|
+
for (const path of [...source?.include ?? [], ...source?.exclude ?? []]) if (!isNativeContextPath(path)) add(node, "source", "path-unsafe");
|
|
233
236
|
for (const entry of source?.entrypoints ?? []) {
|
|
234
|
-
if (!isNativeContextPath(entry.path)) add(node
|
|
237
|
+
if (!isNativeContextPath(entry.path)) add(node, "source.entrypoints.path", "path-unsafe");
|
|
235
238
|
for (const symbol of entry.symbols) {
|
|
236
239
|
for (const sink of symbol.sinks) {
|
|
237
|
-
if (!isNativeContextPath(sink.path)) add(node
|
|
240
|
+
if (!isNativeContextPath(sink.path)) add(node, "source.entrypoints.symbols.sinks.path", "path-unsafe");
|
|
238
241
|
const selector = JSON.stringify([entry.id, symbol.name, sink.id]);
|
|
239
|
-
if (selectors.has(selector)) add(node
|
|
242
|
+
if (selectors.has(selector)) add(node, "source.entrypoints.symbols.sinks.id", "selector-ambiguous");
|
|
240
243
|
selectors.add(selector);
|
|
241
244
|
}
|
|
242
245
|
}
|
|
243
246
|
}
|
|
244
247
|
}
|
|
245
248
|
for (const relation of model.relations) {
|
|
246
|
-
for (const field of ["source", "target", "via"] as const) if (relation[field] !== undefined && !nodes.has(relation[field] as string)) add(relation
|
|
249
|
+
for (const field of ["source", "target", "via"] as const) if (relation[field] !== undefined && !nodes.has(relation[field] as string)) add(relation, field, "relation-node-missing");
|
|
247
250
|
}
|
|
248
251
|
for (const flow of model.flows) {
|
|
249
|
-
|
|
250
|
-
if (nodes.get(flow.capabilityId as string)?.kind !== "capability") add(id, "capabilityId", "flow-capability-missing");
|
|
252
|
+
if (nodes.get(flow.capabilityId as string)?.kind !== "capability") add(flow, "capabilityId", "flow-capability-missing");
|
|
251
253
|
if (flow.applicability !== "required") continue;
|
|
252
254
|
const participants = flow.participants as Array<{ id: string; nodeId: string }>;
|
|
253
255
|
const participantIds = new Set<string>();
|
|
254
256
|
for (const participant of participants) {
|
|
255
|
-
if (participantIds.has(participant.id)) add(
|
|
257
|
+
if (participantIds.has(participant.id)) add(flow, "participants.id", "participant-id-ambiguous");
|
|
256
258
|
participantIds.add(participant.id);
|
|
257
|
-
if (!nodes.has(participant.nodeId)) add(
|
|
259
|
+
if (!nodes.has(participant.nodeId)) add(flow, "participants.nodeId", "participant-node-missing");
|
|
258
260
|
}
|
|
259
261
|
const definitions = new Map<string, string>();
|
|
260
262
|
const outcomes = flow.outcomes as Array<{ id: string; steps: NativeModelRecord[]; terminal: { participant: string } }>;
|
|
261
263
|
const outcomeIds = new Set<string>();
|
|
262
264
|
for (const outcome of outcomes) {
|
|
263
|
-
if (outcomeIds.has(outcome.id)) add(
|
|
265
|
+
if (outcomeIds.has(outcome.id)) add(flow, "outcomes.id", "outcome-id-ambiguous");
|
|
264
266
|
outcomeIds.add(outcome.id);
|
|
265
|
-
if (!participantIds.has(outcome.terminal.participant)) add(
|
|
267
|
+
if (!participantIds.has(outcome.terminal.participant)) add(flow, "outcomes.terminal", "terminal-participant-missing");
|
|
266
268
|
}
|
|
267
269
|
for (const step of [...flow.steps as NativeModelRecord[], ...outcomes.flatMap(outcome => outcome.steps)]) {
|
|
268
|
-
if (!participantIds.has(step.from as string) || !participantIds.has(step.to as string)) add(
|
|
270
|
+
if (!participantIds.has(step.from as string) || !participantIds.has(step.to as string)) add(flow, "steps", "step-participant-missing");
|
|
269
271
|
const definition = canonicalize(step);
|
|
270
|
-
if (definitions.has(step.id as string) && definitions.get(step.id as string) !== definition) add(
|
|
272
|
+
if (definitions.has(step.id as string) && definitions.get(step.id as string) !== definition) add(flow, "steps.id", "step-definition-ambiguous");
|
|
271
273
|
definitions.set(step.id as string, definition);
|
|
272
274
|
const selector = step.evidence as { entrypointId: string; sourceSymbol: string; sinkId: string };
|
|
273
|
-
if (!selectors.has(JSON.stringify([selector.entrypointId, selector.sourceSymbol, selector.sinkId]))) add(
|
|
275
|
+
if (!selectors.has(JSON.stringify([selector.entrypointId, selector.sourceSymbol, selector.sinkId]))) add(flow, "steps.evidence", "selector-missing");
|
|
274
276
|
}
|
|
275
277
|
}
|
|
276
278
|
return issues;
|
package/src/ports.ts
CHANGED
|
@@ -121,6 +121,8 @@ export interface SourceSelector {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
export interface CodeFactsPort {
|
|
124
|
+
/** Read-only compatibility check before the existing incremental sync. No retry or rebuild. */
|
|
125
|
+
assertCanSync?(workspaceRoot: string): void;
|
|
124
126
|
ensureReady(workspace: WorkspaceRef): Promise<CodeFactsSnapshot>;
|
|
125
127
|
sync(input: { workspace: WorkspaceRef; changedPaths?: string[] }): Promise<CodeFactsSnapshot>;
|
|
126
128
|
buildTaskContext(input: { task: string; maxSymbols: number; includeSource: boolean; changedPaths?: string[] }): Promise<NormalizedCodeContext>;
|
|
@@ -148,6 +150,8 @@ export interface ModelValidationResult {
|
|
|
148
150
|
* `appliesTo` id with no node. Omitted when empty.
|
|
149
151
|
*/
|
|
150
152
|
referenceErrors?: string[];
|
|
153
|
+
/** Node schema errors with their file paths. Only a write to that node can repair them. */
|
|
154
|
+
nodeSchemaErrors?: { path: string; error: string }[];
|
|
151
155
|
/** Problems that do not invalidate the model, such as an ignored legacy setting. Omitted when empty. */
|
|
152
156
|
warnings?: string[];
|
|
153
157
|
modelDigest: string;
|
|
@@ -811,7 +815,7 @@ export interface ReviewCheckoutGitPort {
|
|
|
811
815
|
}
|
|
812
816
|
|
|
813
817
|
export interface NonLocalEgressChannel {
|
|
814
|
-
channel: "context7" | "agent-audit" | "github-issue-publishing" | "npm-update-check" | "codegraph-telemetry";
|
|
818
|
+
channel: "context7" | "agent-audit" | "github-issue-publishing" | "npm-update-check" | "projection-publishing" | "codegraph-telemetry";
|
|
815
819
|
destination: string;
|
|
816
820
|
trigger: string;
|
|
817
821
|
data: string;
|
package/src/product-version.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const ARCHCONTEXT_PRODUCT_NAME = "archctx";
|
|
2
|
-
export const ARCHCONTEXT_PRODUCT_VERSION = "0.
|
|
2
|
+
export const ARCHCONTEXT_PRODUCT_VERSION = "0.9.0";
|
|
3
3
|
export const ARCHCONTEXT_PACKAGE_MANAGER = "bun@1.4.3";
|
|
4
4
|
export const ARCHCONTEXT_NODE_RANGE = ">=22.22 <26";
|
|
5
5
|
export const LOCAL_RUNTIME_RPC_SCHEMA_VERSION = "archcontext.runtime-rpc/v2";
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { ProjectionHumanActionReasonCode } from "./projection";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Projection writer mode (issue #379, ADR-0048): `archctx projection publish` keeps one rolling
|
|
5
|
+
* pull request branch (`archctx/...`) equal to the base tip plus exactly one commit that touches
|
|
6
|
+
* generated projection outputs only, and `archctx projection verify-pr` lets downstream CI check
|
|
7
|
+
* such a pull request without trusting its author.
|
|
8
|
+
*/
|
|
9
|
+
export const PROJECTION_PUBLISH_RUN_SCHEMA_VERSION = "archcontext.projection-publish-run/v1" as const;
|
|
10
|
+
export const PROJECTION_PUBLISH_STATUS_SCHEMA_VERSION = "archcontext.projection-publish-status/v1" as const;
|
|
11
|
+
export const PROJECTION_PR_VERIFICATION_SCHEMA_VERSION = "archcontext.projection-pr-verification/v1" as const;
|
|
12
|
+
|
|
13
|
+
/** The only ref namespace the writer ever pushes to. */
|
|
14
|
+
export const PROJECTION_WRITER_BRANCH_PREFIX = "archctx/";
|
|
15
|
+
export const PROJECTION_WRITER_DEFAULT_BRANCH = "archctx/projection";
|
|
16
|
+
|
|
17
|
+
export const PROJECTION_PUBLISH_RESULTS = [
|
|
18
|
+
"already-processed",
|
|
19
|
+
"busy",
|
|
20
|
+
"failed",
|
|
21
|
+
"human-action-required",
|
|
22
|
+
"published",
|
|
23
|
+
"up-to-date"
|
|
24
|
+
] as const;
|
|
25
|
+
export const PROJECTION_PUBLISHERS = ["command", "gh", "none"] as const;
|
|
26
|
+
export const PROJECTION_PR_VERDICTS = [
|
|
27
|
+
"generated-only-not-render-equivalent",
|
|
28
|
+
"generated-only-render-equivalent",
|
|
29
|
+
"no-changes",
|
|
30
|
+
"touches-non-generated"
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
export type ProjectionPublishResult = (typeof PROJECTION_PUBLISH_RESULTS)[number];
|
|
34
|
+
export type ProjectionPublisherKind = (typeof PROJECTION_PUBLISHERS)[number];
|
|
35
|
+
export type ProjectionPrVerdict = (typeof PROJECTION_PR_VERDICTS)[number];
|
|
36
|
+
|
|
37
|
+
export interface ProjectionPublishPullRequestV1 {
|
|
38
|
+
url?: string;
|
|
39
|
+
number?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ProjectionPublishHumanActionV1 {
|
|
43
|
+
reasonCode: ProjectionHumanActionReasonCode;
|
|
44
|
+
path?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The `data` of `projection.publish`, and the writer state's `lastRun`. */
|
|
48
|
+
export interface ProjectionPublishRunV1 {
|
|
49
|
+
schemaVersion: typeof PROJECTION_PUBLISH_RUN_SCHEMA_VERSION;
|
|
50
|
+
result: ProjectionPublishResult;
|
|
51
|
+
remote: string;
|
|
52
|
+
base: string;
|
|
53
|
+
branch: string;
|
|
54
|
+
/** The base tip this run evaluated; null when the run stopped before resolving one. */
|
|
55
|
+
baseSha: string | null;
|
|
56
|
+
/** The rolling branch tip that carries this base's projection, pushed by this run or an earlier one. */
|
|
57
|
+
publishedCommit: string | null;
|
|
58
|
+
/** True only when this run moved the remote rolling branch. */
|
|
59
|
+
pushed: boolean;
|
|
60
|
+
pullRequest: ProjectionPublishPullRequestV1 | null;
|
|
61
|
+
publisher: ProjectionPublisherKind;
|
|
62
|
+
/** Repository-relative generated paths the published commit changes, sorted. */
|
|
63
|
+
files: string[];
|
|
64
|
+
/** `ProjectionResultV2.status` of the apply this run made, or null without one. */
|
|
65
|
+
projectionStatus: string | null;
|
|
66
|
+
humanActions: ProjectionPublishHumanActionV1[];
|
|
67
|
+
error: { code: string; reasonCode?: string; message: string } | null;
|
|
68
|
+
startedAt: string;
|
|
69
|
+
finishedAt: string;
|
|
70
|
+
/** Base tips evaluated in this run; above 1 when the base moved while the run worked. */
|
|
71
|
+
iterations: number;
|
|
72
|
+
/** Present on `busy`: the pid of the live writer that holds the lock. */
|
|
73
|
+
holderPid?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The `data` of `projection.publish-status`. Read-only. */
|
|
77
|
+
export interface ProjectionPublishStatusV1 {
|
|
78
|
+
schemaVersion: typeof PROJECTION_PUBLISH_STATUS_SCHEMA_VERSION;
|
|
79
|
+
remote: string;
|
|
80
|
+
base: string;
|
|
81
|
+
branch: string | null;
|
|
82
|
+
worktreePath: string;
|
|
83
|
+
running: { pid: number; startedAt: string } | null;
|
|
84
|
+
lastProcessedSha: string | null;
|
|
85
|
+
lastSuccessfulSha: string | null;
|
|
86
|
+
lastRun: ProjectionPublishRunV1 | null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The `data` of `projection.verify-pr`. */
|
|
90
|
+
export interface ProjectionPrVerificationV1 {
|
|
91
|
+
schemaVersion: typeof PROJECTION_PR_VERIFICATION_SCHEMA_VERSION;
|
|
92
|
+
base: string;
|
|
93
|
+
baseSha: string;
|
|
94
|
+
headSha: string;
|
|
95
|
+
mergeBase: string;
|
|
96
|
+
changedPaths: string[];
|
|
97
|
+
/**
|
|
98
|
+
* Changed paths that are not generated: not the projection manifest, not a manifest target path
|
|
99
|
+
* and not an agent-context target path (at HEAD or at the merge base), plus every
|
|
100
|
+
* `humanEditedPaths` entry.
|
|
101
|
+
*/
|
|
102
|
+
nonGeneratedPaths: string[];
|
|
103
|
+
/**
|
|
104
|
+
* Mixed-ownership targets (manifest `ownership: "mixed"` and every agent-context target) whose
|
|
105
|
+
* text outside the generated regions differs between the merge base and HEAD, whose regions
|
|
106
|
+
* cannot be delimited, or which are added or deleted. Never rendered.
|
|
107
|
+
*/
|
|
108
|
+
humanEditedPaths: string[];
|
|
109
|
+
/** True when the diff is non-empty and every changed path is a generated output. */
|
|
110
|
+
generatedOnly: boolean;
|
|
111
|
+
/** Null when no render ran (an empty diff, or a diff that touches a non-generated path). */
|
|
112
|
+
renderEquivalent: boolean | null;
|
|
113
|
+
differingPaths: string[];
|
|
114
|
+
projectionStatus: string | null;
|
|
115
|
+
verdict: ProjectionPrVerdict;
|
|
116
|
+
}
|
package/src/projection.ts
CHANGED
|
@@ -108,7 +108,9 @@ export const ARCHCTX_FEATURES = [
|
|
|
108
108
|
"projection-preview-v1",
|
|
109
109
|
"projection-prior-committed-applies-v1",
|
|
110
110
|
"projection-protocol-v2",
|
|
111
|
+
"projection-publish-v1",
|
|
111
112
|
"projection-snapshot-v1",
|
|
113
|
+
"projection-verify-pr-v1",
|
|
112
114
|
"recommendation-scan-decision-v1",
|
|
113
115
|
"recommendation-v3",
|
|
114
116
|
"refactor-assessment-v1",
|
package/src/schema.ts
CHANGED
|
@@ -18,6 +18,7 @@ export type ArchContextErrorCode =
|
|
|
18
18
|
| "AC_NODE_VERSION_UNSUPPORTED"
|
|
19
19
|
| "AC_CODEGRAPH_UNAVAILABLE"
|
|
20
20
|
| "AC_CODE_FACTS_UNAVAILABLE"
|
|
21
|
+
| "AC_CODE_FACTS_NOT_READY"
|
|
21
22
|
| "AC_INDEX_STALE"
|
|
22
23
|
| "AC_CONTEXT_STALE"
|
|
23
24
|
| "AC_SCHEMA_INVALID"
|
|
@@ -68,6 +69,7 @@ export const ERROR_CATALOG: Record<ArchContextErrorCode, Omit<ArchContextError,
|
|
|
68
69
|
AC_NODE_VERSION_UNSUPPORTED: { code: "AC_NODE_VERSION_UNSUPPORTED", severity: "error", retryable: false, action: "use-supported-node-version" },
|
|
69
70
|
AC_CODEGRAPH_UNAVAILABLE: { code: "AC_CODEGRAPH_UNAVAILABLE", severity: "error", retryable: true, action: "run-diagnostics" },
|
|
70
71
|
AC_CODE_FACTS_UNAVAILABLE: { code: "AC_CODE_FACTS_UNAVAILABLE", severity: "error", retryable: true, action: "codegraph-init" },
|
|
72
|
+
AC_CODE_FACTS_NOT_READY: { code: "AC_CODE_FACTS_NOT_READY", severity: "error", retryable: false, action: "codegraph-index" },
|
|
71
73
|
AC_INDEX_STALE: { code: "AC_INDEX_STALE", severity: "warning", retryable: true, action: "sync-codefacts" },
|
|
72
74
|
AC_CONTEXT_STALE: { code: "AC_CONTEXT_STALE", severity: "warning", retryable: true, action: "prepare-task-again" },
|
|
73
75
|
AC_SCHEMA_INVALID: { code: "AC_SCHEMA_INVALID", severity: "error", retryable: false, action: "repair-model" },
|