archctx-contracts 0.4.8 → 0.5.2
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/valid/archctx-capabilities.json +2 -2
- package/fixtures/valid/product-version-manifest.json +5 -5
- package/package.json +1 -1
- package/schemas/runtime/archctx-capabilities.schema.json +1 -1
- package/src/index.ts +1 -0
- package/src/ledger.ts +101 -5
- package/src/product-version.ts +1 -1
- package/src/projection.ts +5 -1
- package/src/refactor.ts +994 -0
- package/src/schema.ts +10 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "archcontext.capabilities/v1",
|
|
3
|
-
"package": { "name": "archctx", "version": "0.
|
|
3
|
+
"package": { "name": "archctx", "version": "0.5.2" },
|
|
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/v4",
|
|
11
11
|
"agentContext": "archcontext.agent-context-renderer/v1"
|
|
12
12
|
},
|
|
13
|
-
"features": ["architecture-docs-renderer-v2", "architecture-refresh-signal-v1", "projection-apply-receipt-v1", "projection-apply-recovery-v1", "projection-protocol-v2"]
|
|
13
|
+
"features": ["architecture-docs-renderer-v2", "architecture-refresh-signal-v1", "module-statistics-v1", "projection-apply-receipt-v1", "projection-apply-recovery-v1", "projection-protocol-v2", "recommendation-v3", "refactor-assessment-v1", "refactor-resolution-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.5.2",
|
|
6
6
|
"distribution": "one-package"
|
|
7
7
|
},
|
|
8
8
|
"packageManager": "bun@1.4.0",
|
|
@@ -12,25 +12,25 @@
|
|
|
12
12
|
"surfaces": {
|
|
13
13
|
"cli": {
|
|
14
14
|
"packageName": "@archcontext/surfaces",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.5.2",
|
|
16
16
|
"entrypoint": "archctx"
|
|
17
17
|
},
|
|
18
18
|
"daemon": {
|
|
19
19
|
"packageName": "@archcontext/local-runtime",
|
|
20
|
-
"version": "0.
|
|
20
|
+
"version": "0.5.2",
|
|
21
21
|
"entrypoint": "archctx daemon start",
|
|
22
22
|
"rpcSchemaVersion": "archcontext.runtime-rpc/v1"
|
|
23
23
|
},
|
|
24
24
|
"mcp": {
|
|
25
25
|
"packageName": "@archcontext/surfaces",
|
|
26
|
-
"version": "0.
|
|
26
|
+
"version": "0.5.2",
|
|
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.5.2",
|
|
34
34
|
"reviewChallenge": "archcontext.review-challenge/v2",
|
|
35
35
|
"attestation": "archcontext.attestation/v2",
|
|
36
36
|
"deviceIdentity": "archcontext.device-identity/v1",
|
package/package.json
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"type": "array",
|
|
40
40
|
"minItems": 3,
|
|
41
41
|
"uniqueItems": true,
|
|
42
|
-
"items": { "enum": ["architecture-docs-renderer-v2", "architecture-refresh-signal-v1", "projection-apply-receipt-v1", "projection-apply-recovery-v1", "projection-protocol-v2"] }
|
|
42
|
+
"items": { "enum": ["architecture-docs-renderer-v2", "architecture-refresh-signal-v1", "module-statistics-v1", "projection-apply-receipt-v1", "projection-apply-recovery-v1", "projection-protocol-v2", "recommendation-v3", "refactor-assessment-v1", "refactor-resolution-v1"] }
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
}
|
package/src/index.ts
CHANGED
package/src/ledger.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import type { ArchitectureMajorChangeReasonCode } from "./projection";
|
|
2
|
+
import type {
|
|
3
|
+
ArchitectureTargetDeltaV1,
|
|
4
|
+
RefactorKillListEntryV1,
|
|
5
|
+
RefactorObservationKind,
|
|
6
|
+
RefactorScale,
|
|
7
|
+
RefactorTargetOutcomeV1
|
|
8
|
+
} from "./refactor";
|
|
1
9
|
import { digestJson, type Json } from "./schema";
|
|
2
10
|
|
|
3
11
|
export const ARCHITECTURE_EVENT_SCHEMA_VERSION = "archcontext.architecture-event/v1" as const;
|
|
@@ -6,6 +14,7 @@ export const EVIDENCE_ITEM_SCHEMA_VERSION = "archcontext.evidence-item/v2" as co
|
|
|
6
14
|
export const EVIDENCE_BINDING_SCHEMA_VERSION = "archcontext.evidence-binding/v1" as const;
|
|
7
15
|
export const RECOMMENDATION_RUN_SCHEMA_VERSION = "archcontext.recommendation-run/v1" as const;
|
|
8
16
|
export const RECOMMENDATION_SCHEMA_VERSION = "archcontext.recommendation/v2" as const;
|
|
17
|
+
export const RECOMMENDATION_V3_SCHEMA_VERSION = "archcontext.recommendation/v3" as const;
|
|
9
18
|
export const RECOMMENDATION_FEEDBACK_SCHEMA_VERSION = "archcontext.recommendation-feedback/v1" as const;
|
|
10
19
|
export const AGENT_JOB_SCHEMA_VERSION = "archcontext.agent-job/v1" as const;
|
|
11
20
|
export const INVESTIGATION_REPORT_SCHEMA_VERSION = "archcontext.investigation-report/v1" as const;
|
|
@@ -14,9 +23,23 @@ export const ARCHITECTURE_CANDIDATE_DELTA_SCHEMA_VERSION = "archcontext.architec
|
|
|
14
23
|
export const ARCHITECTURE_CANDIDATE_DELTA_POLICY_SCHEMA_VERSION = "archcontext.architecture-candidate-delta-policy/v1" as const;
|
|
15
24
|
export const PROJECTION_TARGET_SCHEMA_VERSION = "archcontext.projection-target/v1" as const;
|
|
16
25
|
|
|
26
|
+
export const RECOMMENDATION_CATEGORIES = ["practice", "refactor_proposal", "structural_observation"] as const;
|
|
27
|
+
|
|
17
28
|
export type ArchitectureFactAuthority = "declared" | "observed" | "verified" | "proposed" | "projected";
|
|
18
29
|
export type ArchitectureLedgerMode = "yaml" | "dual" | "dual-compare" | "ledger-shadow" | "ledger" | "ledger-authoritative";
|
|
19
30
|
export type ArchitectureActorKind = "developer" | "daemon" | "hook" | "cli" | "mcp" | "subagent" | "migration" | "system";
|
|
31
|
+
export type ArchitectureActorSource = "cli" | "mcp" | "manual" | "daemon" | "system" | "subagent";
|
|
32
|
+
export type RecommendationCategory = (typeof RECOMMENDATION_CATEGORIES)[number];
|
|
33
|
+
export type RecommendationStatus =
|
|
34
|
+
| "open"
|
|
35
|
+
| "acknowledged"
|
|
36
|
+
| "accepted"
|
|
37
|
+
| "rejected"
|
|
38
|
+
| "deferred"
|
|
39
|
+
| "waived"
|
|
40
|
+
| "resolved"
|
|
41
|
+
| "superseded"
|
|
42
|
+
| "expired";
|
|
20
43
|
export type ArchitectureEventSource =
|
|
21
44
|
| "prepare_task"
|
|
22
45
|
| "checkpoint"
|
|
@@ -28,7 +51,8 @@ export type ArchitectureEventSource =
|
|
|
28
51
|
| "projection_reconcile"
|
|
29
52
|
| "migration"
|
|
30
53
|
| "manual"
|
|
31
|
-
| "agent_audit"
|
|
54
|
+
| "agent_audit"
|
|
55
|
+
| "refactor_scan";
|
|
32
56
|
|
|
33
57
|
export type EvidenceStrengthV2 = "heuristic" | "declared" | "observed" | "verified";
|
|
34
58
|
export type EvidencePolarityV2 = "positive" | "absence" | "declaration";
|
|
@@ -621,7 +645,7 @@ export interface RecommendationV2 {
|
|
|
621
645
|
fingerprint: string;
|
|
622
646
|
subject: string;
|
|
623
647
|
practiceId?: string;
|
|
624
|
-
status:
|
|
648
|
+
status: RecommendationStatus;
|
|
625
649
|
confidence: "low" | "medium" | "high";
|
|
626
650
|
enforcement: "advisory" | "checkpoint" | "complete";
|
|
627
651
|
risk: "low" | "medium" | "high";
|
|
@@ -639,12 +663,12 @@ export interface RecommendationFeedbackV1 {
|
|
|
639
663
|
recommendationId: string;
|
|
640
664
|
runId: string;
|
|
641
665
|
action: "acknowledge" | "accept" | "reject" | "defer" | "waive" | "resolve";
|
|
642
|
-
previousStatus:
|
|
643
|
-
nextStatus:
|
|
666
|
+
previousStatus: RecommendationStatus;
|
|
667
|
+
nextStatus: RecommendationStatus;
|
|
644
668
|
actor: {
|
|
645
669
|
kind: ArchitectureActorKind;
|
|
646
670
|
id: string;
|
|
647
|
-
source:
|
|
671
|
+
source: ArchitectureActorSource;
|
|
648
672
|
};
|
|
649
673
|
reason: string;
|
|
650
674
|
explicit: true;
|
|
@@ -655,6 +679,78 @@ export interface RecommendationFeedbackV1 {
|
|
|
655
679
|
extensions?: Record<string, Json>;
|
|
656
680
|
}
|
|
657
681
|
|
|
682
|
+
export interface RecommendationAuthorV1 {
|
|
683
|
+
kind: ArchitectureActorKind;
|
|
684
|
+
id: string;
|
|
685
|
+
source: ArchitectureActorSource;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export interface RecommendationRelationsV1 {
|
|
689
|
+
supersedes?: string;
|
|
690
|
+
regressesFrom?: string;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export interface PracticeRecommendationPayloadV1 {
|
|
694
|
+
practiceId: string;
|
|
695
|
+
baselineDigest: string | null;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
export interface StructuralObservationPayloadV1 {
|
|
699
|
+
assessmentDigest: string;
|
|
700
|
+
kind: RefactorObservationKind;
|
|
701
|
+
affectedNodeIds: string[];
|
|
702
|
+
baselineSnapshotDigest: string;
|
|
703
|
+
derivedOutcomes: RefactorTargetOutcomeV1[];
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
export interface RefactorProposalPayloadV1 {
|
|
707
|
+
assessmentDigest: string;
|
|
708
|
+
proposalDigest: string;
|
|
709
|
+
scale: RefactorScale;
|
|
710
|
+
affectedNodeIds: string[];
|
|
711
|
+
majorChangeReasons: ArchitectureMajorChangeReasonCode[];
|
|
712
|
+
baselineSnapshotDigest: string;
|
|
713
|
+
targetDelta?: ArchitectureTargetDeltaV1;
|
|
714
|
+
targetOutcomes: RefactorTargetOutcomeV1[];
|
|
715
|
+
killList: RefactorKillListEntryV1[];
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export type RecommendationPayloadV1 =
|
|
719
|
+
| PracticeRecommendationPayloadV1
|
|
720
|
+
| StructuralObservationPayloadV1
|
|
721
|
+
| RefactorProposalPayloadV1;
|
|
722
|
+
|
|
723
|
+
/** Strict superset of RecommendationV2: every v2 field is kept verbatim. */
|
|
724
|
+
export interface RecommendationV3Base {
|
|
725
|
+
schemaVersion: typeof RECOMMENDATION_V3_SCHEMA_VERSION;
|
|
726
|
+
recommendationId: string;
|
|
727
|
+
runId: string;
|
|
728
|
+
fingerprint: string;
|
|
729
|
+
subject: string;
|
|
730
|
+
practiceId?: string;
|
|
731
|
+
status: RecommendationStatus;
|
|
732
|
+
confidence: "low" | "medium" | "high";
|
|
733
|
+
enforcement: "advisory" | "checkpoint" | "complete";
|
|
734
|
+
risk: "low" | "medium" | "high";
|
|
735
|
+
uncertainty: "low" | "medium" | "high";
|
|
736
|
+
evidenceBindingIds: string[];
|
|
737
|
+
explanation: string[];
|
|
738
|
+
authoredBy: RecommendationAuthorV1;
|
|
739
|
+
subjectSelectorId: string;
|
|
740
|
+
relations: RecommendationRelationsV1;
|
|
741
|
+
createdAt: string;
|
|
742
|
+
updatedAt: string;
|
|
743
|
+
extensions?: Record<string, Json>;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/** `category` discriminates `payload`; no other combination is representable. */
|
|
747
|
+
export type RecommendationV3CategoryPayloadV1 =
|
|
748
|
+
| { category: "practice"; payload: PracticeRecommendationPayloadV1 }
|
|
749
|
+
| { category: "structural_observation"; payload: StructuralObservationPayloadV1 }
|
|
750
|
+
| { category: "refactor_proposal"; payload: RefactorProposalPayloadV1 };
|
|
751
|
+
|
|
752
|
+
export type RecommendationV3 = RecommendationV3Base & RecommendationV3CategoryPayloadV1;
|
|
753
|
+
|
|
658
754
|
export interface AgentJobV1 {
|
|
659
755
|
schemaVersion: typeof AGENT_JOB_SCHEMA_VERSION;
|
|
660
756
|
jobId: 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.5.2";
|
|
3
3
|
export const ARCHCONTEXT_PACKAGE_MANAGER = "bun@1.4.0";
|
|
4
4
|
export const ARCHCONTEXT_NODE_RANGE = ">=22.22 <26";
|
|
5
5
|
export const LOCAL_RUNTIME_RPC_SCHEMA_VERSION = "archcontext.runtime-rpc/v1";
|
package/src/projection.ts
CHANGED
|
@@ -58,9 +58,13 @@ export const ARCHITECTURE_REFRESH_TARGETS = [
|
|
|
58
58
|
export const ARCHCTX_FEATURES = [
|
|
59
59
|
"architecture-docs-renderer-v2",
|
|
60
60
|
"architecture-refresh-signal-v1",
|
|
61
|
+
"module-statistics-v1",
|
|
61
62
|
"projection-apply-receipt-v1",
|
|
62
63
|
"projection-apply-recovery-v1",
|
|
63
|
-
"projection-protocol-v2"
|
|
64
|
+
"projection-protocol-v2",
|
|
65
|
+
"recommendation-v3",
|
|
66
|
+
"refactor-assessment-v1",
|
|
67
|
+
"refactor-resolution-v1"
|
|
64
68
|
] as const;
|
|
65
69
|
|
|
66
70
|
export type ProjectionMode = (typeof PROJECTION_MODES)[number];
|
package/src/refactor.ts
ADDED
|
@@ -0,0 +1,994 @@
|
|
|
1
|
+
import { RECOMMENDATION_CATEGORIES, RECOMMENDATION_V3_SCHEMA_VERSION } from "./ledger";
|
|
2
|
+
import type {
|
|
3
|
+
ArchitectureActorKind,
|
|
4
|
+
ArchitectureRepositoryIdentityV1,
|
|
5
|
+
ArchitectureWorktreeIdentityV1,
|
|
6
|
+
EvidenceCoverageLevelV2,
|
|
7
|
+
PracticeRecommendationPayloadV1,
|
|
8
|
+
RecommendationAuthorV1,
|
|
9
|
+
RecommendationCategory,
|
|
10
|
+
RecommendationPayloadV1,
|
|
11
|
+
RecommendationV3,
|
|
12
|
+
RecommendationV3Base,
|
|
13
|
+
RefactorProposalPayloadV1,
|
|
14
|
+
StructuralObservationPayloadV1
|
|
15
|
+
} from "./ledger";
|
|
16
|
+
import { ARCHITECTURE_MAJOR_CHANGE_REASON_CODES, type ArchitectureMajorChangeReasonCode } from "./projection";
|
|
17
|
+
import { digestJson, isRepoRelativePosixPath, type Json, type Severity } from "./schema";
|
|
18
|
+
|
|
19
|
+
const DIGEST_PREFIX_LENGTH = "sha256:".length;
|
|
20
|
+
const SHA256_DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
21
|
+
const BARE_SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
22
|
+
const GIT_HEAD_SHA_PATTERN = /^[a-f0-9]{40}$/;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The bounded grammar an execution evidence `locator` must fit: 1..256 characters drawn from the
|
|
26
|
+
* unreserved plus sub-delims/gen-delims set that repo-relative paths and URLs already live in.
|
|
27
|
+
*
|
|
28
|
+
* A locator is a *reference*, not a body. Left unbounded it is a string field on a record whose
|
|
29
|
+
* envelope promises `privacy.rawDiffPersisted: false`, so a caller could park a whole diff, a
|
|
30
|
+
* credential, or a multi-line prompt in it and the digest check would still pass. Whitespace,
|
|
31
|
+
* newlines and control characters are the shapes that only a body has, and the length ceiling is
|
|
32
|
+
* what stops a single-line body from riding through anyway.
|
|
33
|
+
*/
|
|
34
|
+
export const REFACTOR_EXECUTION_EVIDENCE_LOCATOR_PATTERN = /^[A-Za-z0-9._~:\/?#@!$&'()*+,;=%-]{1,256}$/;
|
|
35
|
+
|
|
36
|
+
/** The single sentence every ingress uses to name the locator rule, so they cannot drift apart. */
|
|
37
|
+
export const REFACTOR_EXECUTION_EVIDENCE_LOCATOR_RULE =
|
|
38
|
+
"must be a bounded reference: 1-256 characters from [A-Za-z0-9._~:/?#@!$&'()*+,;=%-], no whitespace or control characters";
|
|
39
|
+
|
|
40
|
+
/** The only top-level keys a verification request may carry; anything else is a caller typo. */
|
|
41
|
+
export const REFACTOR_VERIFICATION_REQUEST_KEYS = [
|
|
42
|
+
"executionEvidenceRefs",
|
|
43
|
+
"expectedHeadSha",
|
|
44
|
+
"expectedWorktreeDigest",
|
|
45
|
+
"recommendationId",
|
|
46
|
+
"schemaVersion"
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
export const REFACTOR_REQUEST_SCHEMA_VERSION = "archcontext.refactor-request/v1" as const;
|
|
50
|
+
export const REFACTOR_PROPOSAL_SCHEMA_VERSION = "archcontext.refactor-proposal/v1" as const;
|
|
51
|
+
export const MODULE_STATISTICS_SCHEMA_VERSION = "archcontext.module-statistics/v1" as const;
|
|
52
|
+
export const REFACTOR_ASSESSMENT_SCHEMA_VERSION = "archcontext.refactor-assessment/v1" as const;
|
|
53
|
+
export const REFACTOR_RESOLUTION_EVIDENCE_SCHEMA_VERSION = "archcontext.refactor-resolution-evidence/v1" as const;
|
|
54
|
+
export const REFACTOR_VERIFICATION_REQUEST_SCHEMA_VERSION = "archcontext.refactor-verification-request/v1" as const;
|
|
55
|
+
|
|
56
|
+
export const REFACTOR_SCALES = [
|
|
57
|
+
"architecture",
|
|
58
|
+
"cross_module",
|
|
59
|
+
"insufficient_evidence",
|
|
60
|
+
"model_adoption_required",
|
|
61
|
+
"module"
|
|
62
|
+
] as const;
|
|
63
|
+
export const REFACTOR_SCALE_REASON_CODES = [
|
|
64
|
+
"caller-coverage-unknown",
|
|
65
|
+
"code-facts-missing",
|
|
66
|
+
"code-facts-truncated",
|
|
67
|
+
"major-change-detected",
|
|
68
|
+
"multi-node-scope",
|
|
69
|
+
"node-footprint-undeclared",
|
|
70
|
+
"ownership-ambiguous",
|
|
71
|
+
"single-node-scope",
|
|
72
|
+
"target-unresolved",
|
|
73
|
+
"unowned-paths"
|
|
74
|
+
] as const;
|
|
75
|
+
export const REFACTOR_OBSERVATION_KINDS = [
|
|
76
|
+
"cycle",
|
|
77
|
+
"direction-violation",
|
|
78
|
+
"evidence-gap",
|
|
79
|
+
"ownership-ambiguous",
|
|
80
|
+
"undeclared-footprint",
|
|
81
|
+
"unowned-paths"
|
|
82
|
+
] as const;
|
|
83
|
+
export const REFACTOR_RESOLUTION_DISPOSITIONS = [
|
|
84
|
+
"not_improved",
|
|
85
|
+
"partially_resolved",
|
|
86
|
+
"regressed",
|
|
87
|
+
"resolved",
|
|
88
|
+
"stale"
|
|
89
|
+
] as const;
|
|
90
|
+
export const REFACTOR_OUTCOME_OPERATORS = ["absent", "equals", "greater_than", "less_than", "present"] as const;
|
|
91
|
+
export const REFACTOR_OUTCOME_DIRECTIONS = ["improved", "regressed", "unchanged", "unknown"] as const;
|
|
92
|
+
export const REFACTOR_KILL_LIST_KINDS = ["path", "relation", "symbol"] as const;
|
|
93
|
+
export const REFACTOR_EXECUTION_EVIDENCE_KINDS = [
|
|
94
|
+
"acceptance_receipt",
|
|
95
|
+
"cutover_closure",
|
|
96
|
+
"merge_receipt",
|
|
97
|
+
"task_contract"
|
|
98
|
+
] as const;
|
|
99
|
+
export const REFACTOR_PROPOSAL_AUTHOR_SOURCES = ["cli", "manual", "mcp", "subagent"] as const;
|
|
100
|
+
export const REFACTOR_PROPOSAL_AUTHOR_KINDS = ["cli", "developer", "mcp", "subagent"] as const;
|
|
101
|
+
/**
|
|
102
|
+
* A refactor proposal is authored by an agent surface acting for itself, or by
|
|
103
|
+
* a human acting manually. `daemon`, `system`, `hook`, and `migration` kinds
|
|
104
|
+
* are ArchContext acting on its own behalf and can never author a proposal.
|
|
105
|
+
*/
|
|
106
|
+
export const REFACTOR_PROPOSAL_AUTHOR_PAIRS: Readonly<Record<RefactorProposalAuthorKind, readonly RefactorProposalAuthorSource[]>> = {
|
|
107
|
+
cli: ["cli"],
|
|
108
|
+
developer: ["manual"],
|
|
109
|
+
mcp: ["mcp"],
|
|
110
|
+
subagent: ["subagent"]
|
|
111
|
+
};
|
|
112
|
+
export const MODULE_DYNAMIC_INVOCATION_LEVELS = ["known", "none_observed", "possible", "unknown"] as const;
|
|
113
|
+
export const MODULE_TESTS_COVERAGE_STATUSES = ["measured", "partial", "unknown"] as const;
|
|
114
|
+
|
|
115
|
+
export type RefactorScale = (typeof REFACTOR_SCALES)[number];
|
|
116
|
+
export type RefactorScaleReasonCode = (typeof REFACTOR_SCALE_REASON_CODES)[number];
|
|
117
|
+
export type RefactorObservationKind = (typeof REFACTOR_OBSERVATION_KINDS)[number];
|
|
118
|
+
export type RefactorResolutionDisposition = (typeof REFACTOR_RESOLUTION_DISPOSITIONS)[number];
|
|
119
|
+
export type RefactorOutcomeOperator = (typeof REFACTOR_OUTCOME_OPERATORS)[number];
|
|
120
|
+
export type RefactorOutcomeDirection = (typeof REFACTOR_OUTCOME_DIRECTIONS)[number];
|
|
121
|
+
export type RefactorKillListKind = (typeof REFACTOR_KILL_LIST_KINDS)[number];
|
|
122
|
+
export type RefactorExecutionEvidenceKind = (typeof REFACTOR_EXECUTION_EVIDENCE_KINDS)[number];
|
|
123
|
+
export type RefactorProposalAuthorSource = (typeof REFACTOR_PROPOSAL_AUTHOR_SOURCES)[number];
|
|
124
|
+
export type RefactorProposalAuthorKind = (typeof REFACTOR_PROPOSAL_AUTHOR_KINDS)[number];
|
|
125
|
+
export type ModuleDynamicInvocationLevel = (typeof MODULE_DYNAMIC_INVOCATION_LEVELS)[number];
|
|
126
|
+
export type ModuleTestsCoverageStatus = (typeof MODULE_TESTS_COVERAGE_STATUSES)[number];
|
|
127
|
+
|
|
128
|
+
export type RefactorScopeV1 =
|
|
129
|
+
| { kind: "repository" }
|
|
130
|
+
| { kind: "node"; nodeId: string }
|
|
131
|
+
| { kind: "paths"; paths: string[] };
|
|
132
|
+
|
|
133
|
+
export interface RefactorTargetOutcomeV1 {
|
|
134
|
+
outcomeId: string;
|
|
135
|
+
metric: string;
|
|
136
|
+
subjectSelectorId: string;
|
|
137
|
+
nodeId: string | null;
|
|
138
|
+
operator: RefactorOutcomeOperator;
|
|
139
|
+
value: number | null;
|
|
140
|
+
required: boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface RefactorObservedOutcomeV1 {
|
|
144
|
+
outcomeId: string;
|
|
145
|
+
observedValue: number | null;
|
|
146
|
+
satisfied: boolean;
|
|
147
|
+
direction: RefactorOutcomeDirection;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface RefactorKillListEntryV1 {
|
|
151
|
+
kind: RefactorKillListKind;
|
|
152
|
+
selectorId: string;
|
|
153
|
+
required: boolean;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface ArchitectureTargetDeltaV1 {
|
|
157
|
+
interventionId: string;
|
|
158
|
+
trigger: string[];
|
|
159
|
+
thesis: string;
|
|
160
|
+
targetState: {
|
|
161
|
+
owners: Record<string, string>;
|
|
162
|
+
requiredRelations: string[];
|
|
163
|
+
removedConcepts: string[];
|
|
164
|
+
};
|
|
165
|
+
migrationState: {
|
|
166
|
+
active: boolean;
|
|
167
|
+
compatibilityContracts: string[];
|
|
168
|
+
cleanupBy?: string;
|
|
169
|
+
temporaryRelations: string[];
|
|
170
|
+
};
|
|
171
|
+
completionCriteria: RefactorTargetOutcomeV1[];
|
|
172
|
+
falsifiers: string[];
|
|
173
|
+
benefitLedger: {
|
|
174
|
+
benefits: string[];
|
|
175
|
+
costs: string[];
|
|
176
|
+
rollbackPoint: string;
|
|
177
|
+
};
|
|
178
|
+
unresolvedTargets: string[];
|
|
179
|
+
extensions?: Record<string, Json>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface RefactorProposalV1 {
|
|
183
|
+
schemaVersion: typeof REFACTOR_PROPOSAL_SCHEMA_VERSION;
|
|
184
|
+
authoredBy: RecommendationAuthorV1;
|
|
185
|
+
intent: string;
|
|
186
|
+
scopePaths: string[];
|
|
187
|
+
targetDelta?: ArchitectureTargetDeltaV1;
|
|
188
|
+
targetOutcomes: RefactorTargetOutcomeV1[];
|
|
189
|
+
killList: RefactorKillListEntryV1[];
|
|
190
|
+
proposalDigest: string;
|
|
191
|
+
extensions?: Record<string, Json>;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface RefactorRequestV1 {
|
|
195
|
+
schemaVersion: typeof REFACTOR_REQUEST_SCHEMA_VERSION;
|
|
196
|
+
scope: RefactorScopeV1;
|
|
197
|
+
proposal?: RefactorProposalV1;
|
|
198
|
+
expectedHeadSha?: string;
|
|
199
|
+
expectedWorktreeDigest?: string;
|
|
200
|
+
task?: string;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The JSON ingress for `refactor verify`. `recommendationId` is the whole subject: a verification
|
|
205
|
+
* always re-measures whatever is at HEAD now, so there is no caller-supplied AFTER state and no
|
|
206
|
+
* outcome override — the recorded recommendation already carries the acceptance test, and letting
|
|
207
|
+
* a caller restate it would let the requester steer its own verdict.
|
|
208
|
+
*
|
|
209
|
+
* `expectedHeadSha` and `expectedWorktreeDigest` are claims about the state the caller believes it
|
|
210
|
+
* is verifying. A claim that no longer holds is refused rather than answered with fresh numbers
|
|
211
|
+
* under the old identity.
|
|
212
|
+
*/
|
|
213
|
+
export interface RefactorVerificationRequestV1 {
|
|
214
|
+
schemaVersion: typeof REFACTOR_VERIFICATION_REQUEST_SCHEMA_VERSION;
|
|
215
|
+
recommendationId: string;
|
|
216
|
+
expectedHeadSha?: string;
|
|
217
|
+
expectedWorktreeDigest?: string;
|
|
218
|
+
executionEvidenceRefs?: RefactorExecutionEvidenceRefV1[];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface ModuleStatisticsV1 {
|
|
222
|
+
nodeId: string;
|
|
223
|
+
nodeDigest: string;
|
|
224
|
+
parentNodeId: string | null;
|
|
225
|
+
footprintDeclared: boolean;
|
|
226
|
+
footprint: {
|
|
227
|
+
fileCount: number;
|
|
228
|
+
lineCount: number;
|
|
229
|
+
sourceFilesDigest: string;
|
|
230
|
+
includePatterns: string[];
|
|
231
|
+
excludePatterns: string[];
|
|
232
|
+
} | null;
|
|
233
|
+
surfaces: {
|
|
234
|
+
declaredEntrypoints: string[];
|
|
235
|
+
observedEntrypoints: string[];
|
|
236
|
+
lifecycleOwners: string[];
|
|
237
|
+
datastoreSubjects: string[];
|
|
238
|
+
};
|
|
239
|
+
dependencyGraph: {
|
|
240
|
+
internalEdgeCount: number;
|
|
241
|
+
inboundModuleEdges: number;
|
|
242
|
+
outboundModuleEdges: number;
|
|
243
|
+
fanIn: number;
|
|
244
|
+
fanOut: number;
|
|
245
|
+
stronglyConnectedComponentId: string | null;
|
|
246
|
+
cycleCount: number;
|
|
247
|
+
instability: number | null;
|
|
248
|
+
directionViolationCount: number | null;
|
|
249
|
+
} | null;
|
|
250
|
+
/**
|
|
251
|
+
* `callerCoverage` is a graph-boundary resolution ratio (the share of the
|
|
252
|
+
* module's inbound call boundary the index could resolve), not a test
|
|
253
|
+
* measurement, so it is independent of `coverageStatus`: a producer may know
|
|
254
|
+
* the boundary ratio while observing no test evidence at all, and it may know
|
|
255
|
+
* test files while the boundary stays unresolved. Only the 0-1 ratio bound is
|
|
256
|
+
* enforced; the two fields are never coupled (PRD 0.3-16).
|
|
257
|
+
*/
|
|
258
|
+
tests: {
|
|
259
|
+
testFileCount: number | null;
|
|
260
|
+
observedTestEdges: number | null;
|
|
261
|
+
callerCoverage: number | null;
|
|
262
|
+
coverageStatus: ModuleTestsCoverageStatus;
|
|
263
|
+
};
|
|
264
|
+
uncertainty: {
|
|
265
|
+
unresolvedImports: number;
|
|
266
|
+
dynamicInvocation: ModuleDynamicInvocationLevel;
|
|
267
|
+
ambiguousOwnership: boolean;
|
|
268
|
+
};
|
|
269
|
+
moduleDigest: string;
|
|
270
|
+
extensions?: Record<string, Json>;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface ModuleStatisticsSnapshotV1 {
|
|
274
|
+
schemaVersion: typeof MODULE_STATISTICS_SCHEMA_VERSION;
|
|
275
|
+
repository: ArchitectureRepositoryIdentityV1;
|
|
276
|
+
worktree: ArchitectureWorktreeIdentityV1;
|
|
277
|
+
modelDigest: string;
|
|
278
|
+
codeFacts: {
|
|
279
|
+
provider: "codegraph";
|
|
280
|
+
version: string;
|
|
281
|
+
binaryDigest: string;
|
|
282
|
+
indexedWorktreeDigest: string | null;
|
|
283
|
+
coverage: EvidenceCoverageLevelV2;
|
|
284
|
+
truncated: boolean;
|
|
285
|
+
edgeLimit: number | null;
|
|
286
|
+
reasonCodes: RefactorScaleReasonCode[];
|
|
287
|
+
};
|
|
288
|
+
modules: ModuleStatisticsV1[];
|
|
289
|
+
repositorySummary: {
|
|
290
|
+
moduleCount: number;
|
|
291
|
+
undeclaredFootprintNodeCount: number;
|
|
292
|
+
ownedFileCount: number;
|
|
293
|
+
unownedFileCount: number;
|
|
294
|
+
multiplyOwnedFileCount: number;
|
|
295
|
+
crossModuleEdgeCount: number;
|
|
296
|
+
crossModuleCycleCount: number;
|
|
297
|
+
stronglyConnectedComponentCount: number;
|
|
298
|
+
unresolvedImportCount: number;
|
|
299
|
+
dynamicInvocationRiskCount: number;
|
|
300
|
+
};
|
|
301
|
+
createdAt: string;
|
|
302
|
+
snapshotDigest: string;
|
|
303
|
+
extensions?: Record<string, Json>;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export interface RefactorObservationV1 {
|
|
307
|
+
kind: RefactorObservationKind;
|
|
308
|
+
subjectSelectorId: string;
|
|
309
|
+
signalIds: string[];
|
|
310
|
+
metrics: Record<string, number | null>;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface RefactorAssessmentV1 {
|
|
314
|
+
schemaVersion: typeof REFACTOR_ASSESSMENT_SCHEMA_VERSION;
|
|
315
|
+
requestId: string;
|
|
316
|
+
statisticsSnapshotDigest: string;
|
|
317
|
+
modelDigest: string;
|
|
318
|
+
codeFactsDigest: string;
|
|
319
|
+
requestedScope: RefactorScopeV1;
|
|
320
|
+
proposalDigest: string | null;
|
|
321
|
+
observations: RefactorObservationV1[];
|
|
322
|
+
scale: RefactorScale | null;
|
|
323
|
+
scaleReasonCodes: RefactorScaleReasonCode[];
|
|
324
|
+
affectedNodeIds: string[];
|
|
325
|
+
majorChangeReasons: ArchitectureMajorChangeReasonCode[];
|
|
326
|
+
pressure: {
|
|
327
|
+
level: "low" | "medium" | "high";
|
|
328
|
+
score: number;
|
|
329
|
+
signalIds: string[];
|
|
330
|
+
};
|
|
331
|
+
confidence: {
|
|
332
|
+
level: "low" | "medium" | "high";
|
|
333
|
+
callerCoverage: number | null;
|
|
334
|
+
testsObserved: boolean | null;
|
|
335
|
+
rollbackObserved: boolean | null;
|
|
336
|
+
unresolvedEvidence: string[];
|
|
337
|
+
};
|
|
338
|
+
createdAt: string;
|
|
339
|
+
assessmentDigest: string;
|
|
340
|
+
extensions?: Record<string, Json>;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export interface RefactorResidualV1 {
|
|
344
|
+
code: string;
|
|
345
|
+
subject: string;
|
|
346
|
+
severity: Severity;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export interface RefactorExecutionEvidenceRefV1 {
|
|
350
|
+
kind: RefactorExecutionEvidenceKind;
|
|
351
|
+
locator: string;
|
|
352
|
+
sha256: string;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export interface RefactorResolutionEvidenceV1 {
|
|
356
|
+
schemaVersion: typeof REFACTOR_RESOLUTION_EVIDENCE_SCHEMA_VERSION;
|
|
357
|
+
recommendationId: string;
|
|
358
|
+
recommendationDigest: string;
|
|
359
|
+
beforeSnapshotDigest: string;
|
|
360
|
+
afterSnapshotDigest: string;
|
|
361
|
+
verifiedHeadSha: string;
|
|
362
|
+
verifiedWorktreeDigest: string;
|
|
363
|
+
expectedOutcomes: RefactorTargetOutcomeV1[];
|
|
364
|
+
observedOutcomes: RefactorObservedOutcomeV1[];
|
|
365
|
+
residuals: RefactorResidualV1[];
|
|
366
|
+
executionEvidenceRefs: RefactorExecutionEvidenceRefV1[];
|
|
367
|
+
disposition: RefactorResolutionDisposition;
|
|
368
|
+
verifiedAt: string;
|
|
369
|
+
resolutionDigest: string;
|
|
370
|
+
extensions?: Record<string, Json>;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export interface RecommendationV3FingerprintInputV1 {
|
|
374
|
+
category: RecommendationCategory;
|
|
375
|
+
subjectSelectorId: string;
|
|
376
|
+
practiceId: string | null;
|
|
377
|
+
payload: Record<string, Json>;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Digest exclusion rule for every helper below: drop the record's own digest
|
|
382
|
+
* field, its top-level `extensions`, and its timestamps. Nested `extensions`
|
|
383
|
+
* (for example `modules[i].extensions`) stay inside the hashed content, so a
|
|
384
|
+
* nested annotation changes the digest. The single exception is the derived
|
|
385
|
+
* `targetDelta` identity below: ArchContext fills `unresolvedTargets` after the
|
|
386
|
+
* agent authored the proposal, so `refactorProposalDigest` hashes the delta
|
|
387
|
+
* under the same exclusions as `architectureTargetDeltaInterventionId`
|
|
388
|
+
* (`interventionId`, `unresolvedTargets`, `extensions`) and the agent-authored
|
|
389
|
+
* identity survives assessment. `authoredBy` stays hashed at the top level:
|
|
390
|
+
* the no-self-authored gate must be digest-bound.
|
|
391
|
+
*/
|
|
392
|
+
export function refactorProposalDigest(proposal: RefactorProposalV1): string {
|
|
393
|
+
const { proposalDigest: _proposalDigest, extensions: _extensions, ...hashable } = proposal;
|
|
394
|
+
return digestJson({
|
|
395
|
+
...hashable,
|
|
396
|
+
...(hashable.targetDelta ? { targetDelta: authoredTargetDelta(hashable.targetDelta) } : {})
|
|
397
|
+
} as unknown as Json);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function architectureTargetDeltaInterventionId(delta: ArchitectureTargetDeltaV1): string {
|
|
401
|
+
const digest = digestJson(authoredTargetDelta(delta) as unknown as Json);
|
|
402
|
+
return `intervention.${digest.slice(DIGEST_PREFIX_LENGTH, DIGEST_PREFIX_LENGTH + 16)}`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function moduleStatisticsDigest(module: ModuleStatisticsV1): string {
|
|
406
|
+
const { moduleDigest: _moduleDigest, extensions: _extensions, ...hashable } = module;
|
|
407
|
+
return digestJson(hashable as unknown as Json);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function moduleStatisticsSnapshotDigest(snapshot: ModuleStatisticsSnapshotV1): string {
|
|
411
|
+
const { snapshotDigest: _snapshotDigest, createdAt: _createdAt, extensions: _extensions, ...hashable } = snapshot;
|
|
412
|
+
return digestJson(hashable as unknown as Json);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function refactorAssessmentDigest(assessment: RefactorAssessmentV1): string {
|
|
416
|
+
const {
|
|
417
|
+
assessmentDigest: _assessmentDigest,
|
|
418
|
+
requestId: _requestId,
|
|
419
|
+
createdAt: _createdAt,
|
|
420
|
+
extensions: _extensions,
|
|
421
|
+
...hashable
|
|
422
|
+
} = assessment;
|
|
423
|
+
return digestJson(hashable as unknown as Json);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function refactorResolutionEvidenceDigest(evidence: RefactorResolutionEvidenceV1): string {
|
|
427
|
+
const {
|
|
428
|
+
resolutionDigest: _resolutionDigest,
|
|
429
|
+
verifiedAt: _verifiedAt,
|
|
430
|
+
extensions: _extensions,
|
|
431
|
+
...hashable
|
|
432
|
+
} = evidence;
|
|
433
|
+
return digestJson(hashable as unknown as Json);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Canonical fingerprint input for a v3 recommendation. It deliberately drops
|
|
438
|
+
* `assessmentDigest` and `baselineSnapshotDigest` so that re-detecting the same
|
|
439
|
+
* structural fact at a new HEAD dedups against the previous recommendation and
|
|
440
|
+
* drives `relations.regressesFrom` instead of creating an unrelated record.
|
|
441
|
+
*/
|
|
442
|
+
export function recommendationV3FingerprintInput(
|
|
443
|
+
recommendation: Pick<RecommendationV3, "category" | "subjectSelectorId" | "practiceId" | "payload">
|
|
444
|
+
): RecommendationV3FingerprintInputV1 {
|
|
445
|
+
return {
|
|
446
|
+
category: recommendation.category,
|
|
447
|
+
subjectSelectorId: recommendation.subjectSelectorId,
|
|
448
|
+
practiceId: recommendation.practiceId ?? null,
|
|
449
|
+
payload: fingerprintPayloadSubset(recommendation.category, recommendation.payload)
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function refactorProposalInvariantIssues(proposal: RefactorProposalV1, prefix = "proposal"): string[] {
|
|
454
|
+
const issues = [
|
|
455
|
+
...sortedUniqueIssues(`${prefix}.scopePaths`, proposal.scopePaths),
|
|
456
|
+
...digestIssues(`${prefix}.proposalDigest`, proposal.proposalDigest),
|
|
457
|
+
...outcomeIssues(`${prefix}.targetOutcomes`, proposal.targetOutcomes)
|
|
458
|
+
];
|
|
459
|
+
if (proposal.schemaVersion !== REFACTOR_PROPOSAL_SCHEMA_VERSION) issues.push(`${prefix}.schemaVersion is invalid`);
|
|
460
|
+
if (proposal.intent.trim() === "") issues.push(`${prefix}.intent must not be empty`);
|
|
461
|
+
if (proposal.scopePaths.length === 0) issues.push(`${prefix}.scopePaths must contain at least one path`);
|
|
462
|
+
for (const path of proposal.scopePaths) {
|
|
463
|
+
if (!isRepoRelativePosixPath(path)) issues.push(`${prefix}.scopePaths must be repo-relative POSIX paths: ${path}`);
|
|
464
|
+
}
|
|
465
|
+
issues.push(...refactorProposalAuthorIssues(proposal.authoredBy, prefix));
|
|
466
|
+
issues.push(...sortedUniqueIssues(`${prefix}.killList.selectorId`, proposal.killList.map((entry) => entry.selectorId)));
|
|
467
|
+
if (refactorProposalDigest(proposal) !== proposal.proposalDigest) {
|
|
468
|
+
issues.push(`${prefix}.proposalDigest must bind the authored proposal payload`);
|
|
469
|
+
}
|
|
470
|
+
if (proposal.targetDelta) issues.push(...architectureTargetDeltaInvariantIssues(proposal.targetDelta, `${prefix}.targetDelta`));
|
|
471
|
+
return issues;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function architectureTargetDeltaInvariantIssues(delta: ArchitectureTargetDeltaV1, prefix = "targetDelta"): string[] {
|
|
475
|
+
const issues = [
|
|
476
|
+
...sortedUniqueIssues(`${prefix}.unresolvedTargets`, delta.unresolvedTargets),
|
|
477
|
+
...outcomeIssues(`${prefix}.completionCriteria`, delta.completionCriteria)
|
|
478
|
+
];
|
|
479
|
+
if (delta.thesis.trim() === "") issues.push(`${prefix}.thesis must not be empty`);
|
|
480
|
+
if (delta.trigger.length === 0) issues.push(`${prefix}.trigger must state at least one trigger`);
|
|
481
|
+
if (delta.falsifiers.length === 0) issues.push(`${prefix}.falsifiers must state at least one falsifier`);
|
|
482
|
+
if (delta.completionCriteria.length === 0) issues.push(`${prefix}.completionCriteria must contain at least one outcome`);
|
|
483
|
+
if (delta.benefitLedger.rollbackPoint.trim() === "") issues.push(`${prefix}.benefitLedger.rollbackPoint must not be empty`);
|
|
484
|
+
if (architectureTargetDeltaInterventionId(delta) !== delta.interventionId) {
|
|
485
|
+
issues.push(`${prefix}.interventionId must be derived from the authored delta`);
|
|
486
|
+
}
|
|
487
|
+
return issues;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function refactorRequestInvariantIssues(request: RefactorRequestV1, prefix = "request"): string[] {
|
|
491
|
+
const issues: string[] = [];
|
|
492
|
+
if (request.schemaVersion !== REFACTOR_REQUEST_SCHEMA_VERSION) issues.push(`${prefix}.schemaVersion is invalid`);
|
|
493
|
+
if (request.scope.kind === "node" && request.scope.nodeId.trim() === "") issues.push(`${prefix}.scope.nodeId must not be empty`);
|
|
494
|
+
if (request.scope.kind === "paths") {
|
|
495
|
+
issues.push(...sortedUniqueIssues(`${prefix}.scope.paths`, request.scope.paths));
|
|
496
|
+
if (request.scope.paths.length === 0) issues.push(`${prefix}.scope.paths must contain at least one path`);
|
|
497
|
+
for (const path of request.scope.paths) {
|
|
498
|
+
if (!isRepoRelativePosixPath(path)) issues.push(`${prefix}.scope.paths must be repo-relative POSIX paths: ${path}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (request.proposal) issues.push(...refactorProposalInvariantIssues(request.proposal, `${prefix}.proposal`));
|
|
502
|
+
return issues;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Validates a `refactor verify` request before anything measures on its behalf.
|
|
507
|
+
*
|
|
508
|
+
* Top-level keys are closed to `REFACTOR_VERIFICATION_REQUEST_KEYS`. Every ingress rebuilds the
|
|
509
|
+
* request from the declared fields, so a typo (`expectedWorktreeDigset`) would otherwise be
|
|
510
|
+
* dropped in silence and the caller's freshness claim would simply cease to exist — the request
|
|
511
|
+
* would be answered under a weaker precondition than the one it stated.
|
|
512
|
+
*
|
|
513
|
+
* `executionEvidenceRefs` are checked under exactly the rules that bind them into the ledger:
|
|
514
|
+
* three declared keys and nothing else, a kind from the frozen vocabulary, and a bare SHA-256.
|
|
515
|
+
* An extra key would ride a cast into a record whose envelope promises no raw bodies, and the
|
|
516
|
+
* resolution-evidence validator only ever inspects `locator` and `sha256`, so it would never see
|
|
517
|
+
* it. Refusing here is the only place that key is still visible.
|
|
518
|
+
*/
|
|
519
|
+
export function refactorVerificationRequestInvariantIssues(
|
|
520
|
+
request: RefactorVerificationRequestV1,
|
|
521
|
+
prefix = "request"
|
|
522
|
+
): string[] {
|
|
523
|
+
const issues: string[] = [];
|
|
524
|
+
const unknownKeys = Object.keys(request as unknown as Record<string, unknown>)
|
|
525
|
+
.filter((key) => !(REFACTOR_VERIFICATION_REQUEST_KEYS as readonly string[]).includes(key))
|
|
526
|
+
.sort();
|
|
527
|
+
if (unknownKeys.length > 0) issues.push(`${prefix} has unsupported key(s): ${unknownKeys.join(", ")}`);
|
|
528
|
+
if (request.schemaVersion !== REFACTOR_VERIFICATION_REQUEST_SCHEMA_VERSION) issues.push(`${prefix}.schemaVersion is invalid`);
|
|
529
|
+
if (typeof request.recommendationId !== "string" || request.recommendationId.trim() === "") {
|
|
530
|
+
issues.push(`${prefix}.recommendationId must be a non-empty string`);
|
|
531
|
+
}
|
|
532
|
+
if (request.expectedHeadSha !== undefined && !GIT_HEAD_SHA_PATTERN.test(request.expectedHeadSha)) {
|
|
533
|
+
issues.push(`${prefix}.expectedHeadSha must be a 40-hex Git commit sha`);
|
|
534
|
+
}
|
|
535
|
+
if (request.expectedWorktreeDigest !== undefined) {
|
|
536
|
+
issues.push(...digestIssues(`${prefix}.expectedWorktreeDigest`, request.expectedWorktreeDigest));
|
|
537
|
+
}
|
|
538
|
+
if (request.executionEvidenceRefs !== undefined) {
|
|
539
|
+
if (!Array.isArray(request.executionEvidenceRefs)) {
|
|
540
|
+
issues.push(`${prefix}.executionEvidenceRefs must be an array`);
|
|
541
|
+
return issues;
|
|
542
|
+
}
|
|
543
|
+
request.executionEvidenceRefs.forEach((ref, index) => {
|
|
544
|
+
const label = `${prefix}.executionEvidenceRefs[${index}]`;
|
|
545
|
+
if (!ref || typeof ref !== "object" || Array.isArray(ref)) {
|
|
546
|
+
issues.push(`${label} must be an object`);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const extras = Object.keys(ref).filter((key) => key !== "kind" && key !== "locator" && key !== "sha256");
|
|
550
|
+
if (extras.length > 0) issues.push(`${label} has unsupported key(s): ${[...extras].sort().join(", ")}`);
|
|
551
|
+
if (!(REFACTOR_EXECUTION_EVIDENCE_KINDS as readonly string[]).includes(ref.kind)) {
|
|
552
|
+
issues.push(`${label}.kind must be one of ${REFACTOR_EXECUTION_EVIDENCE_KINDS.join(", ")}`);
|
|
553
|
+
}
|
|
554
|
+
if (typeof ref.locator !== "string" || ref.locator.trim() === "") {
|
|
555
|
+
issues.push(`${label}.locator must be a non-empty string`);
|
|
556
|
+
} else if (!REFACTOR_EXECUTION_EVIDENCE_LOCATOR_PATTERN.test(ref.locator)) {
|
|
557
|
+
issues.push(`${label}.locator ${REFACTOR_EXECUTION_EVIDENCE_LOCATOR_RULE}`);
|
|
558
|
+
}
|
|
559
|
+
if (typeof ref.sha256 !== "string" || !BARE_SHA256_PATTERN.test(ref.sha256)) {
|
|
560
|
+
issues.push(`${label}.sha256 must be a bare SHA-256 hex digest`);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return issues;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function moduleStatisticsInvariantIssues(module: ModuleStatisticsV1, prefix = "module"): string[] {
|
|
568
|
+
const issues = [...digestIssues(`${prefix}.moduleDigest`, module.moduleDigest), ...digestIssues(`${prefix}.nodeDigest`, module.nodeDigest)];
|
|
569
|
+
if (module.nodeId.trim() === "") issues.push(`${prefix}.nodeId must not be empty`);
|
|
570
|
+
if (module.footprintDeclared === (module.footprint === null)) {
|
|
571
|
+
issues.push(`${prefix}.footprint must be present exactly when footprintDeclared is true`);
|
|
572
|
+
}
|
|
573
|
+
if (module.footprint) {
|
|
574
|
+
issues.push(
|
|
575
|
+
...digestIssues(`${prefix}.footprint.sourceFilesDigest`, module.footprint.sourceFilesDigest),
|
|
576
|
+
...nonNegativeIntegerIssues(`${prefix}.footprint.fileCount`, module.footprint.fileCount),
|
|
577
|
+
...nonNegativeIntegerIssues(`${prefix}.footprint.lineCount`, module.footprint.lineCount)
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
if (module.dependencyGraph) {
|
|
581
|
+
const graph = module.dependencyGraph;
|
|
582
|
+
issues.push(
|
|
583
|
+
...nonNegativeIntegerIssues(`${prefix}.dependencyGraph.internalEdgeCount`, graph.internalEdgeCount),
|
|
584
|
+
...nonNegativeIntegerIssues(`${prefix}.dependencyGraph.inboundModuleEdges`, graph.inboundModuleEdges),
|
|
585
|
+
...nonNegativeIntegerIssues(`${prefix}.dependencyGraph.outboundModuleEdges`, graph.outboundModuleEdges),
|
|
586
|
+
...nonNegativeIntegerIssues(`${prefix}.dependencyGraph.fanIn`, graph.fanIn),
|
|
587
|
+
...nonNegativeIntegerIssues(`${prefix}.dependencyGraph.fanOut`, graph.fanOut),
|
|
588
|
+
...nonNegativeIntegerIssues(`${prefix}.dependencyGraph.cycleCount`, graph.cycleCount),
|
|
589
|
+
...nonNegativeIntegerIssues(`${prefix}.dependencyGraph.directionViolationCount`, graph.directionViolationCount),
|
|
590
|
+
...ratioIssues(`${prefix}.dependencyGraph.instability`, graph.instability)
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
issues.push(
|
|
594
|
+
...nonNegativeIntegerIssues(`${prefix}.tests.testFileCount`, module.tests.testFileCount),
|
|
595
|
+
...nonNegativeIntegerIssues(`${prefix}.tests.observedTestEdges`, module.tests.observedTestEdges),
|
|
596
|
+
...ratioIssues(`${prefix}.tests.callerCoverage`, module.tests.callerCoverage),
|
|
597
|
+
...nonNegativeIntegerIssues(`${prefix}.uncertainty.unresolvedImports`, module.uncertainty.unresolvedImports)
|
|
598
|
+
);
|
|
599
|
+
if (moduleStatisticsDigest(module) !== module.moduleDigest) {
|
|
600
|
+
issues.push(`${prefix}.moduleDigest must bind the measured module payload`);
|
|
601
|
+
}
|
|
602
|
+
return issues;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export function moduleStatisticsSnapshotInvariantIssues(snapshot: ModuleStatisticsSnapshotV1, prefix = "snapshot"): string[] {
|
|
606
|
+
const issues = [
|
|
607
|
+
...digestIssues(`${prefix}.snapshotDigest`, snapshot.snapshotDigest),
|
|
608
|
+
...digestIssues(`${prefix}.modelDigest`, snapshot.modelDigest),
|
|
609
|
+
...sortedUniqueIssues(`${prefix}.codeFacts.reasonCodes`, snapshot.codeFacts.reasonCodes),
|
|
610
|
+
...sortedUniqueIssues(`${prefix}.modules.nodeId`, snapshot.modules.map((module) => module.nodeId)),
|
|
611
|
+
...snapshot.modules.flatMap((module, index) => moduleStatisticsInvariantIssues(module, `${prefix}.modules[${index}]`))
|
|
612
|
+
];
|
|
613
|
+
if (snapshot.schemaVersion !== MODULE_STATISTICS_SCHEMA_VERSION) issues.push(`${prefix}.schemaVersion is invalid`);
|
|
614
|
+
if (snapshot.codeFacts.coverage === "unknown") {
|
|
615
|
+
if (!snapshot.codeFacts.truncated) issues.push(`${prefix}.codeFacts.truncated must be true when coverage is unknown`);
|
|
616
|
+
for (const [index, module] of snapshot.modules.entries()) {
|
|
617
|
+
if (module.dependencyGraph !== null) {
|
|
618
|
+
issues.push(`${prefix}.modules[${index}].dependencyGraph must be null when codeFacts.coverage is unknown`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (snapshot.codeFacts.coverage !== "unknown") {
|
|
623
|
+
if (snapshot.codeFacts.indexedWorktreeDigest === null) {
|
|
624
|
+
issues.push(`${prefix}.codeFacts.indexedWorktreeDigest must be present when coverage is ${snapshot.codeFacts.coverage}`);
|
|
625
|
+
} else if (snapshot.codeFacts.indexedWorktreeDigest !== snapshot.worktree.worktreeDigest) {
|
|
626
|
+
issues.push(`${prefix}.codeFacts.indexedWorktreeDigest must match the measured worktreeDigest`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
issues.push(...nonNegativeIntegerIssues(`${prefix}.codeFacts.edgeLimit`, snapshot.codeFacts.edgeLimit));
|
|
630
|
+
for (const [field, value] of Object.entries(snapshot.repositorySummary)) {
|
|
631
|
+
issues.push(...nonNegativeIntegerIssues(`${prefix}.repositorySummary.${field}`, value));
|
|
632
|
+
}
|
|
633
|
+
if (snapshot.repositorySummary.moduleCount !== snapshot.modules.length) {
|
|
634
|
+
issues.push(`${prefix}.repositorySummary.moduleCount must equal the module count`);
|
|
635
|
+
}
|
|
636
|
+
const undeclared = snapshot.modules.filter((module) => !module.footprintDeclared).length;
|
|
637
|
+
if (snapshot.repositorySummary.undeclaredFootprintNodeCount !== undeclared) {
|
|
638
|
+
issues.push(`${prefix}.repositorySummary.undeclaredFootprintNodeCount must equal the undeclared footprint module count`);
|
|
639
|
+
}
|
|
640
|
+
if (moduleStatisticsSnapshotDigest(snapshot) !== snapshot.snapshotDigest) {
|
|
641
|
+
issues.push(`${prefix}.snapshotDigest must bind the measured snapshot payload`);
|
|
642
|
+
}
|
|
643
|
+
return issues;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export function refactorAssessmentInvariantIssues(assessment: RefactorAssessmentV1, prefix = "assessment"): string[] {
|
|
647
|
+
const issues = [
|
|
648
|
+
...digestIssues(`${prefix}.assessmentDigest`, assessment.assessmentDigest),
|
|
649
|
+
...digestIssues(`${prefix}.statisticsSnapshotDigest`, assessment.statisticsSnapshotDigest),
|
|
650
|
+
...digestIssues(`${prefix}.modelDigest`, assessment.modelDigest),
|
|
651
|
+
...digestIssues(`${prefix}.codeFactsDigest`, assessment.codeFactsDigest),
|
|
652
|
+
...sortedUniqueIssues(`${prefix}.affectedNodeIds`, assessment.affectedNodeIds),
|
|
653
|
+
...sortedUniqueIssues(`${prefix}.scaleReasonCodes`, assessment.scaleReasonCodes),
|
|
654
|
+
...sortedUniqueIssues(`${prefix}.majorChangeReasons`, assessment.majorChangeReasons)
|
|
655
|
+
];
|
|
656
|
+
if (assessment.schemaVersion !== REFACTOR_ASSESSMENT_SCHEMA_VERSION) issues.push(`${prefix}.schemaVersion is invalid`);
|
|
657
|
+
if (assessment.proposalDigest !== null) issues.push(...digestIssues(`${prefix}.proposalDigest`, assessment.proposalDigest));
|
|
658
|
+
if ((assessment.scale === null) !== (assessment.proposalDigest === null)) {
|
|
659
|
+
issues.push(`${prefix}.scale must be null exactly when proposalDigest is null`);
|
|
660
|
+
}
|
|
661
|
+
if (assessment.scale === "architecture" && assessment.majorChangeReasons.length === 0) {
|
|
662
|
+
issues.push(`${prefix}.architecture scale requires at least one majorChangeReason`);
|
|
663
|
+
}
|
|
664
|
+
const allowedReasons = new Set<string>(ARCHITECTURE_MAJOR_CHANGE_REASON_CODES);
|
|
665
|
+
for (const reason of assessment.majorChangeReasons) {
|
|
666
|
+
if (!allowedReasons.has(reason)) issues.push(`${prefix}.majorChangeReasons contains unsupported reason: ${reason}`);
|
|
667
|
+
}
|
|
668
|
+
for (const [index, observation] of assessment.observations.entries()) {
|
|
669
|
+
if (observation.subjectSelectorId.trim() === "") issues.push(`${prefix}.observations[${index}].subjectSelectorId must not be empty`);
|
|
670
|
+
issues.push(...sortedUniqueIssues(`${prefix}.observations[${index}].signalIds`, observation.signalIds));
|
|
671
|
+
}
|
|
672
|
+
issues.push(...ratioIssues(`${prefix}.confidence.callerCoverage`, assessment.confidence.callerCoverage));
|
|
673
|
+
if (!Number.isInteger(assessment.pressure.score) || assessment.pressure.score < 0 || assessment.pressure.score > 100) {
|
|
674
|
+
issues.push(`${prefix}.pressure.score must be an integer between 0 and 100`);
|
|
675
|
+
}
|
|
676
|
+
issues.push(...sortedUniqueIssues(`${prefix}.pressure.signalIds`, assessment.pressure.signalIds));
|
|
677
|
+
if (refactorAssessmentDigest(assessment) !== assessment.assessmentDigest) {
|
|
678
|
+
issues.push(`${prefix}.assessmentDigest must bind the assessed payload`);
|
|
679
|
+
}
|
|
680
|
+
return issues;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export function refactorResolutionEvidenceInvariantIssues(
|
|
684
|
+
evidence: RefactorResolutionEvidenceV1,
|
|
685
|
+
prefix = "resolutionEvidence"
|
|
686
|
+
): string[] {
|
|
687
|
+
const issues = [
|
|
688
|
+
...digestIssues(`${prefix}.resolutionDigest`, evidence.resolutionDigest),
|
|
689
|
+
...digestIssues(`${prefix}.recommendationDigest`, evidence.recommendationDigest),
|
|
690
|
+
...digestIssues(`${prefix}.beforeSnapshotDigest`, evidence.beforeSnapshotDigest),
|
|
691
|
+
...digestIssues(`${prefix}.afterSnapshotDigest`, evidence.afterSnapshotDigest),
|
|
692
|
+
...outcomeIssues(`${prefix}.expectedOutcomes`, evidence.expectedOutcomes),
|
|
693
|
+
...sortedUniqueIssues(`${prefix}.observedOutcomes.outcomeId`, evidence.observedOutcomes.map((outcome) => outcome.outcomeId))
|
|
694
|
+
];
|
|
695
|
+
if (evidence.schemaVersion !== REFACTOR_RESOLUTION_EVIDENCE_SCHEMA_VERSION) issues.push(`${prefix}.schemaVersion is invalid`);
|
|
696
|
+
const observedById = new Map(evidence.observedOutcomes.map((outcome) => [outcome.outcomeId, outcome]));
|
|
697
|
+
for (const expected of evidence.expectedOutcomes) {
|
|
698
|
+
if (!observedById.has(expected.outcomeId)) issues.push(`${prefix}.observedOutcomes is missing outcome ${expected.outcomeId}`);
|
|
699
|
+
}
|
|
700
|
+
for (const ref of evidence.executionEvidenceRefs) {
|
|
701
|
+
if (!/^[a-f0-9]{64}$/.test(ref.sha256)) issues.push(`${prefix}.executionEvidenceRefs.sha256 must be a bare SHA-256 hex digest`);
|
|
702
|
+
if (ref.locator.trim() === "") issues.push(`${prefix}.executionEvidenceRefs.locator must not be empty`);
|
|
703
|
+
}
|
|
704
|
+
for (const expected of evidence.expectedOutcomes) {
|
|
705
|
+
const observed = observedById.get(expected.outcomeId);
|
|
706
|
+
if (!observed) continue;
|
|
707
|
+
const recomputed = outcomeSatisfied(expected, observed.observedValue);
|
|
708
|
+
if (observed.satisfied !== recomputed) {
|
|
709
|
+
issues.push(
|
|
710
|
+
`${prefix}.observedOutcomes ${expected.outcomeId} claims satisfied=${observed.satisfied} but ${expected.metric} ${expected.operator} ${expected.value} against ${observed.observedValue} is ${recomputed}`
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (evidence.disposition !== "stale") {
|
|
715
|
+
const required = evidence.expectedOutcomes.filter((outcome) => outcome.required);
|
|
716
|
+
const observedRequired = required
|
|
717
|
+
.map((outcome) => {
|
|
718
|
+
const observed = observedById.get(outcome.outcomeId);
|
|
719
|
+
return observed ? { outcome, satisfied: outcomeSatisfied(outcome, observed.observedValue) } : undefined;
|
|
720
|
+
})
|
|
721
|
+
.filter(isPresent);
|
|
722
|
+
const regressed = evidence.observedOutcomes.some((outcome) => outcome.direction === "regressed");
|
|
723
|
+
const satisfiedCount = observedRequired.filter((entry) => entry.satisfied).length;
|
|
724
|
+
if (regressed && evidence.disposition !== "regressed") {
|
|
725
|
+
issues.push(`${prefix}.disposition must be regressed when any observed outcome regressed`);
|
|
726
|
+
}
|
|
727
|
+
if (!regressed && required.length > 0) {
|
|
728
|
+
if (satisfiedCount === required.length && evidence.disposition !== "resolved") {
|
|
729
|
+
issues.push(`${prefix}.disposition must be resolved when every required outcome is satisfied`);
|
|
730
|
+
}
|
|
731
|
+
if (satisfiedCount === 0 && evidence.disposition !== "not_improved") {
|
|
732
|
+
issues.push(`${prefix}.disposition must be not_improved when no required outcome is satisfied`);
|
|
733
|
+
}
|
|
734
|
+
if (satisfiedCount > 0 && satisfiedCount < required.length && evidence.disposition !== "partially_resolved") {
|
|
735
|
+
issues.push(`${prefix}.disposition must be partially_resolved when only some required outcomes are satisfied`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (refactorResolutionEvidenceDigest(evidence) !== evidence.resolutionDigest) {
|
|
740
|
+
issues.push(`${prefix}.resolutionDigest must bind the verified payload`);
|
|
741
|
+
}
|
|
742
|
+
return issues;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Guards the one invariant the `RecommendationV3` union cannot enforce at
|
|
747
|
+
* runtime: that `payload` matches `category`. A non-object or mismatched
|
|
748
|
+
* payload yields issues instead of throwing. Every other field is trusted to
|
|
749
|
+
* match its declared type; structural validation of untrusted ingress is the
|
|
750
|
+
* job of the JSON Schemas, not of this validator.
|
|
751
|
+
*
|
|
752
|
+
* The fingerprint hash itself stays owned by `recommendationFingerprint()` in
|
|
753
|
+
* `packages/core/recommendation-engine`: contracts must not import core, and
|
|
754
|
+
* re-deriving the hash here would fork it into two definitions. This validator
|
|
755
|
+
* therefore checks the fingerprint's shape, not its value.
|
|
756
|
+
*/
|
|
757
|
+
export function recommendationV3InvariantIssues(recommendation: RecommendationV3, prefix = "recommendation"): string[] {
|
|
758
|
+
const issues = [
|
|
759
|
+
...sortedUniqueIssues(`${prefix}.evidenceBindingIds`, recommendation.evidenceBindingIds),
|
|
760
|
+
...digestIssues(`${prefix}.fingerprint`, recommendation.fingerprint)
|
|
761
|
+
];
|
|
762
|
+
if (recommendation.schemaVersion !== RECOMMENDATION_V3_SCHEMA_VERSION) issues.push(`${prefix}.schemaVersion is invalid`);
|
|
763
|
+
if (!(RECOMMENDATION_CATEGORIES as readonly string[]).includes(recommendation.category)) {
|
|
764
|
+
issues.push(`${prefix}.category is unsupported: ${recommendation.category}`);
|
|
765
|
+
return issues;
|
|
766
|
+
}
|
|
767
|
+
if (recommendation.subjectSelectorId.trim() === "") issues.push(`${prefix}.subjectSelectorId must not be empty`);
|
|
768
|
+
if (recommendation.authoredBy.id.trim() === "") issues.push(`${prefix}.authoredBy.id must not be empty`);
|
|
769
|
+
const shapeIssues = recommendationPayloadShapeIssues(recommendation.category, recommendation.payload, prefix);
|
|
770
|
+
issues.push(...shapeIssues);
|
|
771
|
+
if (recommendation.category === "practice" && !recommendation.practiceId) {
|
|
772
|
+
issues.push(`${prefix}.practiceId is required for practice recommendations`);
|
|
773
|
+
}
|
|
774
|
+
if (recommendation.category === "structural_observation") {
|
|
775
|
+
if (recommendation.authoredBy.source !== "daemon") {
|
|
776
|
+
issues.push(`${prefix}.structural_observation must be authored by the daemon`);
|
|
777
|
+
}
|
|
778
|
+
if (recommendation.authoredBy.kind !== "daemon") {
|
|
779
|
+
issues.push(`${prefix}.structural_observation must be authored by the daemon actor kind`);
|
|
780
|
+
}
|
|
781
|
+
if (recommendation.enforcement !== "advisory") {
|
|
782
|
+
issues.push(`${prefix}.structural_observation enforcement must be advisory`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (recommendation.category === "refactor_proposal") {
|
|
786
|
+
if (!isRefactorProposalAuthorSource(recommendation.authoredBy.source)) {
|
|
787
|
+
issues.push(`${prefix}.refactor_proposal must not be authored by ${recommendation.authoredBy.source}`);
|
|
788
|
+
}
|
|
789
|
+
issues.push(...refactorProposalAuthorIssues(recommendation.authoredBy, prefix));
|
|
790
|
+
if (shapeIssues.length === 0) {
|
|
791
|
+
const payload = recommendation.payload as RefactorProposalPayloadV1;
|
|
792
|
+
const expected = payload.scale === "architecture" ? "complete" : "checkpoint";
|
|
793
|
+
if (recommendation.enforcement !== expected) {
|
|
794
|
+
issues.push(`${prefix}.refactor_proposal with scale ${payload.scale} requires ${expected} enforcement`);
|
|
795
|
+
}
|
|
796
|
+
issues.push(
|
|
797
|
+
...digestIssues(`${prefix}.payload.proposalDigest`, payload.proposalDigest),
|
|
798
|
+
...sortedUniqueIssues(`${prefix}.payload.affectedNodeIds`, payload.affectedNodeIds),
|
|
799
|
+
...sortedUniqueIssues(`${prefix}.payload.majorChangeReasons`, payload.majorChangeReasons)
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return issues;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
export function refactorScanInvariantIssues(input: {
|
|
807
|
+
snapshot: ModuleStatisticsSnapshotV1;
|
|
808
|
+
assessment: RefactorAssessmentV1;
|
|
809
|
+
proposal?: RefactorProposalV1;
|
|
810
|
+
}): string[] {
|
|
811
|
+
const issues = [
|
|
812
|
+
...moduleStatisticsSnapshotInvariantIssues(input.snapshot),
|
|
813
|
+
...refactorAssessmentInvariantIssues(input.assessment),
|
|
814
|
+
...(input.proposal ? refactorProposalInvariantIssues(input.proposal) : [])
|
|
815
|
+
];
|
|
816
|
+
if (input.assessment.statisticsSnapshotDigest !== input.snapshot.snapshotDigest) {
|
|
817
|
+
issues.push("assessment.statisticsSnapshotDigest must reference the measured snapshot");
|
|
818
|
+
}
|
|
819
|
+
if (input.assessment.modelDigest !== input.snapshot.modelDigest) {
|
|
820
|
+
issues.push("assessment.modelDigest must match the snapshot modelDigest");
|
|
821
|
+
}
|
|
822
|
+
const proposalDigest = input.proposal ? input.proposal.proposalDigest : null;
|
|
823
|
+
if (input.assessment.proposalDigest !== proposalDigest) {
|
|
824
|
+
issues.push("assessment.proposalDigest must reference the submitted proposal");
|
|
825
|
+
}
|
|
826
|
+
const unresolved = input.proposal?.targetDelta?.unresolvedTargets ?? [];
|
|
827
|
+
if (unresolved.length > 0 && input.assessment.scale !== "insufficient_evidence") {
|
|
828
|
+
issues.push("assessment.scale must be insufficient_evidence while targetDelta.unresolvedTargets is non-empty");
|
|
829
|
+
}
|
|
830
|
+
if (unresolved.length > 0 && !input.assessment.scaleReasonCodes.includes("target-unresolved")) {
|
|
831
|
+
issues.push("assessment.scaleReasonCodes must include target-unresolved while targets stay unresolved");
|
|
832
|
+
}
|
|
833
|
+
return issues;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
export function refactorVerifyInvariantIssues(
|
|
837
|
+
afterSnapshot: ModuleStatisticsSnapshotV1,
|
|
838
|
+
evidence: RefactorResolutionEvidenceV1
|
|
839
|
+
): string[] {
|
|
840
|
+
const issues = [
|
|
841
|
+
...moduleStatisticsSnapshotInvariantIssues(afterSnapshot, "afterSnapshot"),
|
|
842
|
+
...refactorResolutionEvidenceInvariantIssues(evidence)
|
|
843
|
+
];
|
|
844
|
+
if (evidence.afterSnapshotDigest !== afterSnapshot.snapshotDigest) {
|
|
845
|
+
issues.push("resolutionEvidence.afterSnapshotDigest must reference the re-measured snapshot");
|
|
846
|
+
}
|
|
847
|
+
if (evidence.verifiedHeadSha !== afterSnapshot.worktree.headSha) {
|
|
848
|
+
issues.push("resolutionEvidence.verifiedHeadSha must match the re-measured worktree HEAD");
|
|
849
|
+
}
|
|
850
|
+
if (evidence.verifiedWorktreeDigest !== afterSnapshot.worktree.worktreeDigest) {
|
|
851
|
+
issues.push("resolutionEvidence.verifiedWorktreeDigest must match the re-measured worktree digest");
|
|
852
|
+
}
|
|
853
|
+
if (afterSnapshot.codeFacts.coverage !== "complete" && evidence.disposition === "resolved") {
|
|
854
|
+
issues.push("resolutionEvidence.disposition must not be resolved while after-snapshot coverage is incomplete");
|
|
855
|
+
}
|
|
856
|
+
const indexCoversWorktree =
|
|
857
|
+
afterSnapshot.codeFacts.indexedWorktreeDigest !== null
|
|
858
|
+
&& afterSnapshot.codeFacts.indexedWorktreeDigest === afterSnapshot.worktree.worktreeDigest;
|
|
859
|
+
if (!indexCoversWorktree && evidence.disposition === "resolved") {
|
|
860
|
+
issues.push("resolutionEvidence.disposition must not be resolved while the after-snapshot index does not cover the verified worktree");
|
|
861
|
+
}
|
|
862
|
+
return issues;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function authoredTargetDelta(delta: ArchitectureTargetDeltaV1): Omit<ArchitectureTargetDeltaV1, "interventionId" | "unresolvedTargets" | "extensions"> {
|
|
866
|
+
const {
|
|
867
|
+
interventionId: _interventionId,
|
|
868
|
+
unresolvedTargets: _unresolvedTargets,
|
|
869
|
+
extensions: _extensions,
|
|
870
|
+
...authored
|
|
871
|
+
} = delta;
|
|
872
|
+
return authored;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function isRefactorProposalAuthorSource(source: string): source is RefactorProposalAuthorSource {
|
|
876
|
+
return (REFACTOR_PROPOSAL_AUTHOR_SOURCES as readonly string[]).includes(source);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function isRefactorProposalAuthorKind(kind: string): kind is RefactorProposalAuthorKind {
|
|
880
|
+
return (REFACTOR_PROPOSAL_AUTHOR_KINDS as readonly string[]).includes(kind);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function refactorProposalAuthorIssues(author: RecommendationAuthorV1, prefix: string): string[] {
|
|
884
|
+
const issues: string[] = [];
|
|
885
|
+
if (author.id.trim() === "") issues.push(`${prefix}.authoredBy.id must not be empty`);
|
|
886
|
+
if (!isRefactorProposalAuthorSource(author.source)) {
|
|
887
|
+
issues.push(`${prefix}.authoredBy.source must not be ${author.source}; refactor proposals are agent or human authored`);
|
|
888
|
+
}
|
|
889
|
+
if (!isRefactorProposalAuthorKind(author.kind)) {
|
|
890
|
+
issues.push(`${prefix}.authoredBy.kind must not be ${author.kind}; refactor proposals are agent or human authored`);
|
|
891
|
+
return issues;
|
|
892
|
+
}
|
|
893
|
+
if (isRefactorProposalAuthorSource(author.source) && !REFACTOR_PROPOSAL_AUTHOR_PAIRS[author.kind].includes(author.source)) {
|
|
894
|
+
issues.push(`${prefix}.authoredBy.kind ${author.kind} is not compatible with source ${author.source}`);
|
|
895
|
+
}
|
|
896
|
+
return issues;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function outcomeSatisfied(expected: RefactorTargetOutcomeV1, observedValue: number | null): boolean {
|
|
900
|
+
switch (expected.operator) {
|
|
901
|
+
case "absent":
|
|
902
|
+
return observedValue === null || observedValue === 0;
|
|
903
|
+
case "present":
|
|
904
|
+
return observedValue !== null && observedValue !== 0;
|
|
905
|
+
case "equals":
|
|
906
|
+
return observedValue !== null && expected.value !== null && observedValue === expected.value;
|
|
907
|
+
case "greater_than":
|
|
908
|
+
return observedValue !== null && expected.value !== null && observedValue > expected.value;
|
|
909
|
+
case "less_than":
|
|
910
|
+
return observedValue !== null && expected.value !== null && observedValue < expected.value;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function recommendationPayloadShapeIssues(
|
|
915
|
+
category: RecommendationCategory,
|
|
916
|
+
payload: RecommendationPayloadV1,
|
|
917
|
+
prefix: string
|
|
918
|
+
): string[] {
|
|
919
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
920
|
+
return [`${prefix}.payload must be an object for category ${category}`];
|
|
921
|
+
}
|
|
922
|
+
const record = payload as unknown as Record<string, unknown>;
|
|
923
|
+
const required = REQUIRED_PAYLOAD_FIELDS[category];
|
|
924
|
+
const missing = required.filter((field) => !(field in record));
|
|
925
|
+
return missing.length === 0 ? [] : [`${prefix}.payload does not match category ${category}; missing ${missing.sort().join(", ")}`];
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function nonNegativeIntegerIssues(label: string, value: number | null): string[] {
|
|
929
|
+
if (value === null) return [];
|
|
930
|
+
return Number.isInteger(value) && value >= 0 ? [] : [`${label} must be a non-negative integer`];
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function ratioIssues(label: string, value: number | null): string[] {
|
|
934
|
+
if (value === null) return [];
|
|
935
|
+
return Number.isFinite(value) && value >= 0 && value <= 1 ? [] : [`${label} must be a ratio between 0 and 1`];
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const REQUIRED_PAYLOAD_FIELDS: Readonly<Record<RecommendationCategory, readonly string[]>> = {
|
|
939
|
+
practice: ["baselineDigest", "practiceId"],
|
|
940
|
+
refactor_proposal: [
|
|
941
|
+
"affectedNodeIds",
|
|
942
|
+
"assessmentDigest",
|
|
943
|
+
"baselineSnapshotDigest",
|
|
944
|
+
"killList",
|
|
945
|
+
"majorChangeReasons",
|
|
946
|
+
"proposalDigest",
|
|
947
|
+
"scale",
|
|
948
|
+
"targetOutcomes"
|
|
949
|
+
],
|
|
950
|
+
structural_observation: ["affectedNodeIds", "assessmentDigest", "baselineSnapshotDigest", "derivedOutcomes", "kind"]
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
function fingerprintPayloadSubset(category: RecommendationCategory, payload: RecommendationPayloadV1): Record<string, Json> {
|
|
954
|
+
if (category === "practice") {
|
|
955
|
+
return { baselineDigest: (payload as PracticeRecommendationPayloadV1).baselineDigest };
|
|
956
|
+
}
|
|
957
|
+
if (category === "structural_observation") {
|
|
958
|
+
const observation = payload as StructuralObservationPayloadV1;
|
|
959
|
+
return { kind: observation.kind, affectedNodeIds: [...observation.affectedNodeIds].sort() };
|
|
960
|
+
}
|
|
961
|
+
const proposal = payload as RefactorProposalPayloadV1;
|
|
962
|
+
return {
|
|
963
|
+
proposalDigest: proposal.proposalDigest,
|
|
964
|
+
scale: proposal.scale,
|
|
965
|
+
affectedNodeIds: [...proposal.affectedNodeIds].sort(),
|
|
966
|
+
majorChangeReasons: [...proposal.majorChangeReasons].sort()
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function outcomeIssues(label: string, outcomes: readonly RefactorTargetOutcomeV1[]): string[] {
|
|
971
|
+
const issues = sortedUniqueIssues(`${label}.outcomeId`, outcomes.map((outcome) => outcome.outcomeId));
|
|
972
|
+
for (const outcome of outcomes) {
|
|
973
|
+
const valueless = outcome.operator === "absent" || outcome.operator === "present";
|
|
974
|
+
if (valueless && outcome.value !== null) issues.push(`${label} operator ${outcome.operator} must not carry a value`);
|
|
975
|
+
if (!valueless && outcome.value === null) issues.push(`${label} operator ${outcome.operator} requires a value`);
|
|
976
|
+
if (outcome.metric.trim() === "") issues.push(`${label} metric must not be empty`);
|
|
977
|
+
}
|
|
978
|
+
return issues;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function digestIssues(label: string, value: string): string[] {
|
|
982
|
+
return SHA256_DIGEST_PATTERN.test(value) ? [] : [`${label} must be a sha256:<64-hex> digest`];
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function isPresent<T>(value: T | undefined): value is T {
|
|
986
|
+
return value !== undefined;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function sortedUniqueIssues(label: string, values: readonly string[]): string[] {
|
|
990
|
+
const expected = [...new Set(values)].sort();
|
|
991
|
+
return expected.length === values.length && expected.every((value, index) => value === values[index])
|
|
992
|
+
? []
|
|
993
|
+
: [`${label} must be sorted and unique`];
|
|
994
|
+
}
|
package/src/schema.ts
CHANGED
|
@@ -28,7 +28,11 @@ export type ArchContextErrorCode =
|
|
|
28
28
|
| "AC_USER_CONFIRMATION_REQUIRED"
|
|
29
29
|
| "AC_ENTITLEMENT_REQUIRED"
|
|
30
30
|
| "AC_ATTESTATION_REJECTED"
|
|
31
|
-
| "AC_TUNNEL_SCOPE_DENIED"
|
|
31
|
+
| "AC_TUNNEL_SCOPE_DENIED"
|
|
32
|
+
| "AC_MODEL_ADOPTION_REQUIRED"
|
|
33
|
+
| "AC_REFACTOR_STALE"
|
|
34
|
+
| "AC_REFACTOR_EVIDENCE_REQUIRED"
|
|
35
|
+
| "AC_REFACTOR_PROPOSAL_UNAUTHORED";
|
|
32
36
|
|
|
33
37
|
export interface ArchContextError {
|
|
34
38
|
code: ArchContextErrorCode;
|
|
@@ -65,7 +69,11 @@ export const ERROR_CATALOG: Record<ArchContextErrorCode, Omit<ArchContextError,
|
|
|
65
69
|
AC_USER_CONFIRMATION_REQUIRED: { code: "AC_USER_CONFIRMATION_REQUIRED", severity: "warning", retryable: true, action: "show-human-decision" },
|
|
66
70
|
AC_ENTITLEMENT_REQUIRED: { code: "AC_ENTITLEMENT_REQUIRED", severity: "error", retryable: false, action: "login-or-subscribe" },
|
|
67
71
|
AC_ATTESTATION_REJECTED: { code: "AC_ATTESTATION_REJECTED", severity: "error", retryable: false, action: "review-again" },
|
|
68
|
-
AC_TUNNEL_SCOPE_DENIED: { code: "AC_TUNNEL_SCOPE_DENIED", severity: "error", retryable: false, action: "reduce-scope" }
|
|
72
|
+
AC_TUNNEL_SCOPE_DENIED: { code: "AC_TUNNEL_SCOPE_DENIED", severity: "error", retryable: false, action: "reduce-scope" },
|
|
73
|
+
AC_MODEL_ADOPTION_REQUIRED: { code: "AC_MODEL_ADOPTION_REQUIRED", severity: "error", retryable: false, action: "adopt-architecture-model" },
|
|
74
|
+
AC_REFACTOR_STALE: { code: "AC_REFACTOR_STALE", severity: "warning", retryable: true, action: "rerun-refactor-scan" },
|
|
75
|
+
AC_REFACTOR_EVIDENCE_REQUIRED: { code: "AC_REFACTOR_EVIDENCE_REQUIRED", severity: "error", retryable: false, action: "run-refactor-verify" },
|
|
76
|
+
AC_REFACTOR_PROPOSAL_UNAUTHORED: { code: "AC_REFACTOR_PROPOSAL_UNAUTHORED", severity: "error", retryable: false, action: "attach-authoring-actor" }
|
|
69
77
|
};
|
|
70
78
|
|
|
71
79
|
export function okEnvelope<T extends Json>(requestId: string, data: T): JsonEnvelope<T> {
|