phasegate 0.229.0 → 0.264.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -1
- package/README.ja.md +31 -1
- package/README.md +30 -0
- package/docs/ADR/031-world-model-ownership-and-corpus-lifecycle.md +155 -0
- package/docs/ADR/032-world-node-identity.md +198 -0
- package/docs/ADR/033-world-snapshot-canonicalization.md +246 -0
- package/docs/ADR/034-world-constraint-semantics.md +218 -0
- package/docs/ADR/035-world-adoption-baseline-and-waiver.md +341 -0
- package/docs/ADR/036-world-model-and-doc-freshness.md +169 -0
- package/docs/ADR/037-world-cli-and-output-contract.md +398 -0
- package/docs/contracts/attestation-v2.schema.json +110 -0
- package/docs/contracts/requirement-test-matrix.schema.json +15 -0
- package/docs/contracts/world-baseline.schema.json +35 -0
- package/docs/contracts/world-constraints.schema.json +56 -0
- package/docs/contracts/world-debts.schema.json +32 -0
- package/docs/contracts/world-obligation-report.schema.json +259 -0
- package/docs/contracts/world-waivers.schema.json +32 -0
- package/docs/guide/cli-reference.md +45 -0
- package/docs/guide/configuration.md +56 -0
- package/docs/templates/ci/aidlc-gate.yml +42 -2
- package/package.json +1 -1
- package/scripts/harness/agent-integration/application/dto/open-world-obligations-context-dto.ts +30 -0
- package/scripts/harness/agent-integration/application/ports/world-obligations-query-port.ts +30 -0
- package/scripts/harness/agent-integration/application/usecases/get-open-world-obligations-context-usecase.ts +66 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/world-model-open-obligations-query-adapter.ts +58 -0
- package/scripts/harness/agent-integration/presentation/session-start-hook.ts +37 -1
- package/scripts/harness/agent-integration/presentation/world-obligations-session-context.ts +60 -0
- package/scripts/harness/attestation/application/dto/attestation-document.ts +17 -4
- package/scripts/harness/attestation/application/mappers/attestation-record-mapper.ts +55 -4
- package/scripts/harness/attestation/application/ports/sha256-capability.ts +25 -0
- package/scripts/harness/attestation/application/ports/world-snapshot-root-provider.ts +10 -0
- package/scripts/harness/attestation/application/usecases/produce-attestation-usecase.ts +33 -7
- package/scripts/harness/attestation/composition-root.ts +19 -4
- package/scripts/harness/attestation/domain/entities/attestation-record.ts +37 -2
- package/scripts/harness/attestation/index.ts +13 -2
- package/scripts/harness/attestation/infrastructure/adapters/node-crypto-content-hasher-adapter.ts +6 -6
- package/scripts/harness/attestation/infrastructure/adapters/node-crypto-sha256-capability.ts +19 -0
- package/scripts/harness/attestation/presentation/handlers/attest-handler.ts +5 -2
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +35 -8
- package/scripts/harness/config-foundation/application/mappers/world-model-config-mapper.ts +15 -0
- package/scripts/harness/config-foundation/domain/harness-config.ts +67 -87
- package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +77 -88
- package/scripts/harness/config-foundation/domain/value-objects/world-config.ts +198 -0
- package/scripts/harness/config-foundation/index.ts +14 -15
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +24 -0
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +24 -0
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +24 -0
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +83 -0
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +83 -0
- package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +18 -4
- package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +4 -1
- package/scripts/harness/integrations/pre-commit.ts +169 -27
- package/scripts/harness/main.ts +276 -126
- package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +11 -0
- package/scripts/harness/nyquist-validation/application/usecases/check-ac-coverage-gate-usecase.ts +6 -0
- package/scripts/harness/nyquist-validation/application/usecases/generate-requirement-test-matrix-usecase.ts +8 -4
- package/scripts/harness/nyquist-validation/domain/entities/story-mapping.ts +29 -2
- package/scripts/harness/nyquist-validation/domain/services/ac-coverage-gate-policy.ts +28 -0
- package/scripts/harness/nyquist-validation/infrastructure/adapters/markdown-requirement-source-adapter.ts +71 -4
- package/scripts/harness/nyquist-validation/infrastructure/schema/matrix-schema-loader.ts +4 -0
- package/scripts/harness/traceability-model/application/dto/changed-design-fragment-dto.ts +31 -0
- package/scripts/harness/traceability-model/application/dto/traceability-world-read-dto.ts +67 -0
- package/scripts/harness/traceability-model/application/facades/design-change-read-facade.ts +14 -0
- package/scripts/harness/traceability-model/application/facades/traceability-world-read-facade.ts +372 -0
- package/scripts/harness/traceability-model/application/ports/staged-design-change-source-port.ts +9 -0
- package/scripts/harness/traceability-model/application/ports/traceability-world-read-source-port.ts +61 -0
- package/scripts/harness/traceability-model/composition-root.ts +26 -5
- package/scripts/harness/traceability-model/index.ts +26 -6
- package/scripts/harness/traceability-model/infrastructure/adapters/file-system-traceability-world-read-adapter.ts +223 -0
- package/scripts/harness/traceability-model/infrastructure/adapters/git-staged-design-change-adapter.ts +155 -0
- package/scripts/harness/traceability-model/infrastructure/parsers/story-catalog-parser.ts +86 -8
- package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +31 -0
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +34 -0
- package/scripts/harness/validator-system/composition-root.ts +21 -6
- package/scripts/harness/validator-system/domain/ports/world-constraint-admission-policy-port.ts +28 -0
- package/scripts/harness/validator-system/domain/ports/world-constraint-rederivation-policy-port.ts +11 -0
- package/scripts/harness/validator-system/domain/services/design-change-declaration-policy.ts +75 -0
- package/scripts/harness/validator-system/domain/services/world-constraint-admission-service.ts +78 -0
- package/scripts/harness/validator-system/domain/services/world-constraint-rederivation-service.ts +76 -0
- package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +4 -0
- package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +38 -16
- package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-admission-adapter.ts +56 -0
- package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-rederivation-adapter.ts +56 -0
- package/scripts/harness/world-model/application/dto/pinned-design-endpoint-dto.ts +19 -0
- package/scripts/harness/world-model/application/dto/world-inspection-dto.ts +43 -0
- package/scripts/harness/world-model/application/dto/world-obligation-report-dto.ts +54 -0
- package/scripts/harness/world-model/application/dto/world-resolved-config-input.ts +127 -0
- package/scripts/harness/world-model/application/dto/world-snapshot-root-dto.ts +8 -0
- package/scripts/harness/world-model/application/facades/pinned-design-endpoint-facade.ts +63 -0
- package/scripts/harness/world-model/application/facades/world-snapshot-root-facade.ts +19 -0
- package/scripts/harness/world-model/application/ports/obligation-report-writer-port.ts +7 -0
- package/scripts/harness/world-model/application/ports/world-control-declaration-repository-port.ts +57 -0
- package/scripts/harness/world-model/application/ports/world-fact-source-port.ts +17 -0
- package/scripts/harness/world-model/application/usecases/build-snapshot-use-case.ts +111 -0
- package/scripts/harness/world-model/application/usecases/derive-obligations-use-case.ts +155 -0
- package/scripts/harness/world-model/application/usecases/derive-world-obligations-use-case.ts +146 -0
- package/scripts/harness/world-model/application/usecases/inspect-world-use-case.ts +74 -0
- package/scripts/harness/world-model/application/usecases/pin-constraint-endpoint-use-case.ts +139 -0
- package/scripts/harness/world-model/composition-root.ts +247 -0
- package/scripts/harness/world-model/domain/entities/constraint-record.ts +148 -0
- package/scripts/harness/world-model/domain/entities/control-declarations.ts +274 -0
- package/scripts/harness/world-model/domain/entities/edge.ts +41 -0
- package/scripts/harness/world-model/domain/entities/extraction-diagnostic.ts +60 -0
- package/scripts/harness/world-model/domain/entities/snapshot.ts +75 -0
- package/scripts/harness/world-model/domain/entities/world-node.ts +210 -0
- package/scripts/harness/world-model/domain/ports/world-hashing-port.ts +8 -0
- package/scripts/harness/world-model/domain/services/canonical-json-serializer.ts +113 -0
- package/scripts/harness/world-model/domain/services/constraint-evaluator.ts +486 -0
- package/scripts/harness/world-model/domain/services/obligation-derivation-service.ts +199 -0
- package/scripts/harness/world-model/domain/services/policy-inputs-digest-deriver.ts +66 -0
- package/scripts/harness/world-model/domain/services/snapshot-root-deriver.ts +218 -0
- package/scripts/harness/world-model/domain/services/text-content-normalizer.ts +30 -0
- package/scripts/harness/world-model/domain/services/violation-fingerprint-deriver.ts +183 -0
- package/scripts/harness/world-model/domain/value-objects/artifact-kind.ts +58 -0
- package/scripts/harness/world-model/domain/value-objects/change-provenance.ts +137 -0
- package/scripts/harness/world-model/domain/value-objects/corpus-role.ts +57 -0
- package/scripts/harness/world-model/domain/value-objects/declared-key.ts +35 -0
- package/scripts/harness/world-model/domain/value-objects/evaluation-id.ts +41 -0
- package/scripts/harness/world-model/domain/value-objects/explicit-constraint-relation.ts +53 -0
- package/scripts/harness/world-model/domain/value-objects/explicit-node-alias.ts +30 -0
- package/scripts/harness/world-model/domain/value-objects/node-pin.ts +34 -0
- package/scripts/harness/world-model/domain/value-objects/path-key.ts +103 -0
- package/scripts/harness/world-model/domain/value-objects/sha256-digest.ts +39 -0
- package/scripts/harness/world-model/domain/value-objects/violation-fingerprint.ts +36 -0
- package/scripts/harness/world-model/domain/value-objects/wcr-rule-id.ts +59 -0
- package/scripts/harness/world-model/domain/value-objects/world-node-id.ts +222 -0
- package/scripts/harness/world-model/index.ts +33 -0
- package/scripts/harness/world-model/infrastructure/adapters/adr-fact-extractor.ts +27 -0
- package/scripts/harness/world-model/infrastructure/adapters/assembled-world-fact-source.ts +17 -0
- package/scripts/harness/world-model/infrastructure/adapters/attestation-fact-extractor.ts +323 -0
- package/scripts/harness/world-model/infrastructure/adapters/attestation-sha256-world-hashing-adapter.ts +15 -0
- package/scripts/harness/world-model/infrastructure/adapters/composite-design-fact-source.ts +23 -0
- package/scripts/harness/world-model/infrastructure/adapters/design-corpus-fact-extractor.ts +285 -0
- package/scripts/harness/world-model/infrastructure/adapters/design-fact-extraction.ts +63 -0
- package/scripts/harness/world-model/infrastructure/adapters/file-system-obligation-report-writer-adapter.ts +37 -0
- package/scripts/harness/world-model/infrastructure/adapters/file-system-world-control-repository-adapters.ts +332 -0
- package/scripts/harness/world-model/infrastructure/adapters/integrity-manifest-fact-extractor.ts +101 -0
- package/scripts/harness/world-model/infrastructure/adapters/json-fact-extractor-support.ts +151 -0
- package/scripts/harness/world-model/infrastructure/adapters/markdown-design-fact-extractor.ts +493 -0
- package/scripts/harness/world-model/infrastructure/adapters/matrix-fact-extractor.ts +283 -0
- package/scripts/harness/world-model/infrastructure/adapters/product-fact-extractor.ts +29 -0
- package/scripts/harness/world-model/infrastructure/adapters/proposal-fact-extractor.ts +29 -0
- package/scripts/harness/world-model/infrastructure/adapters/runtime-fact-extraction.ts +19 -0
- package/scripts/harness/world-model/infrastructure/adapters/source-metadata-fact-extractor.ts +22 -0
- package/scripts/harness/world-model/infrastructure/adapters/test-reference-source-fact-extractor.ts +22 -0
- package/scripts/harness/world-model/infrastructure/adapters/traceability-design-fact-adapter.ts +112 -0
- package/scripts/harness/world-model/infrastructure/adapters/traceability-world-read-facade-merger.ts +38 -0
- package/scripts/harness/world-model/infrastructure/adapters/type-script-source-fact-extractor.ts +236 -0
- package/scripts/harness/world-model/infrastructure/adapters/unit-fact-extractor.ts +71 -0
- package/scripts/harness/world-model/infrastructure/adapters/world-control-declaration-mapper.ts +383 -0
- package/scripts/harness/world-model/presentation/cli/world-command-support.ts +60 -0
- package/scripts/harness/world-model/presentation/cli/world-derive-command-handler.ts +109 -0
- package/scripts/harness/world-model/presentation/cli/world-inspect-command-handler.ts +194 -0
- package/scripts/harness/world-model/presentation/cli/world-pin-command-handler.ts +100 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
// @unit world-model
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-293
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
ConstraintFactType,
|
|
7
|
+
ConstraintRecord,
|
|
8
|
+
MalformedConstraintDeclaration,
|
|
9
|
+
} from "../entities/constraint-record.js";
|
|
10
|
+
import type { Snapshot } from "../entities/snapshot.js";
|
|
11
|
+
import type { WorldNode } from "../entities/world-node.js";
|
|
12
|
+
import type { ChangeProvenance } from "../value-objects/change-provenance.js";
|
|
13
|
+
import type { ExplicitConstraintRelation } from "../value-objects/explicit-constraint-relation.js";
|
|
14
|
+
import type { ExplicitNodeAlias } from "../value-objects/explicit-node-alias.js";
|
|
15
|
+
import type { NodePin } from "../value-objects/node-pin.js";
|
|
16
|
+
import type { WorldNodeId } from "../value-objects/world-node-id.js";
|
|
17
|
+
import type { CanonicalJsonObject } from "./canonical-json-serializer.js";
|
|
18
|
+
|
|
19
|
+
export type EndpointRole = "claimant" | "premise";
|
|
20
|
+
export type EndpointResolutionStatus =
|
|
21
|
+
| "resolved"
|
|
22
|
+
| "resolved-via-alias"
|
|
23
|
+
| "duplicate"
|
|
24
|
+
| "invalid-alias"
|
|
25
|
+
| "deleted"
|
|
26
|
+
| "missing";
|
|
27
|
+
|
|
28
|
+
export interface EndpointResolutionDto {
|
|
29
|
+
readonly role: EndpointRole;
|
|
30
|
+
readonly declaredNodeId: string;
|
|
31
|
+
readonly pinnedDigest: string;
|
|
32
|
+
readonly status: EndpointResolutionStatus;
|
|
33
|
+
readonly resolvedNodeId: string | null;
|
|
34
|
+
readonly currentDigest: string | null;
|
|
35
|
+
readonly candidateCount: number;
|
|
36
|
+
readonly candidateContentDigests: readonly string[];
|
|
37
|
+
readonly locators: readonly string[];
|
|
38
|
+
readonly sourceDiagnosticCodes: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ConstraintFindingDto {
|
|
42
|
+
readonly ruleId: string;
|
|
43
|
+
readonly constraintId: string | null;
|
|
44
|
+
readonly factType: ConstraintFactType | null;
|
|
45
|
+
readonly endpoint: EndpointRole | null;
|
|
46
|
+
readonly claimant: EndpointResolutionDto | null;
|
|
47
|
+
readonly premise: EndpointResolutionDto | null;
|
|
48
|
+
readonly declarationArtifactId: string;
|
|
49
|
+
readonly declarationLocator: string;
|
|
50
|
+
readonly evidence: CanonicalJsonObject;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ConstraintEvaluationDto {
|
|
54
|
+
readonly constraintId: string;
|
|
55
|
+
readonly factType: ConstraintFactType;
|
|
56
|
+
readonly claimant: EndpointResolutionDto;
|
|
57
|
+
readonly premise: EndpointResolutionDto;
|
|
58
|
+
readonly findings: readonly ConstraintFindingDto[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ConstraintEvaluationInput {
|
|
62
|
+
readonly currentSnapshot: Snapshot;
|
|
63
|
+
readonly baselineSnapshot?: Snapshot;
|
|
64
|
+
readonly records: readonly ConstraintRecord[];
|
|
65
|
+
readonly malformedDeclarations?: readonly MalformedConstraintDeclaration[];
|
|
66
|
+
readonly aliases?: readonly ExplicitNodeAlias[];
|
|
67
|
+
readonly relations?: readonly ExplicitConstraintRelation[];
|
|
68
|
+
readonly changeProvenance: ChangeProvenance;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
|
|
72
|
+
const endpointOrder = (endpoint: EndpointRole | null): number =>
|
|
73
|
+
endpoint === "claimant" ? 0 : endpoint === "premise" ? 1 : 2;
|
|
74
|
+
|
|
75
|
+
const compareFindings = (left: ConstraintFindingDto, right: ConstraintFindingDto): number =>
|
|
76
|
+
compareStrings(left.constraintId ?? "", right.constraintId ?? "") ||
|
|
77
|
+
compareStrings(left.ruleId, right.ruleId) ||
|
|
78
|
+
endpointOrder(left.endpoint) - endpointOrder(right.endpoint) ||
|
|
79
|
+
compareStrings(JSON.stringify(left.evidence), JSON.stringify(right.evidence));
|
|
80
|
+
|
|
81
|
+
const locatorFor = (node: WorldNode): string => {
|
|
82
|
+
if (node.projection.type === "artifact" || node.projection.type === "source-file") {
|
|
83
|
+
return node.projection.pathKey;
|
|
84
|
+
}
|
|
85
|
+
if (node.projection.type === "fragment") {
|
|
86
|
+
return node.projection.artifactId;
|
|
87
|
+
}
|
|
88
|
+
return node.id.toString();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const candidatesFor = (snapshot: Snapshot | undefined, nodeId: WorldNodeId): readonly WorldNode[] =>
|
|
92
|
+
Object.freeze((snapshot?.nodes ?? []).filter((node) => node.id.equals(nodeId)));
|
|
93
|
+
|
|
94
|
+
const roleFor = (nodeId: WorldNodeId): string | null => {
|
|
95
|
+
const value = nodeId.toString();
|
|
96
|
+
let match = /^pgw:v1:artifact:[^:]+:([^:]+):/.exec(value);
|
|
97
|
+
if (match) return match[1];
|
|
98
|
+
match = /^pgw:v1:fragment:legacy:[^:]+:([^:]+):/.exec(value);
|
|
99
|
+
if (match) return match[1];
|
|
100
|
+
match = /^pgw:v1:fragment:([^:]+):/.exec(value);
|
|
101
|
+
return match?.[1] ?? null;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const compatibleAliasRole = (from: WorldNodeId, to: WorldNodeId): boolean => {
|
|
105
|
+
if (from.nodeType !== to.nodeType) return false;
|
|
106
|
+
const fromRole = roleFor(from);
|
|
107
|
+
const toRole = roleFor(to);
|
|
108
|
+
return fromRole === null || toRole === null || fromRole === toRole;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const freezeEndpoint = (endpoint: EndpointResolutionDto): EndpointResolutionDto =>
|
|
112
|
+
Object.freeze({
|
|
113
|
+
...endpoint,
|
|
114
|
+
candidateContentDigests: Object.freeze([...endpoint.candidateContentDigests]),
|
|
115
|
+
locators: Object.freeze([...endpoint.locators]),
|
|
116
|
+
sourceDiagnosticCodes: Object.freeze([...endpoint.sourceDiagnosticCodes]),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
interface EndpointResolution {
|
|
120
|
+
readonly dto: EndpointResolutionDto;
|
|
121
|
+
readonly currentNode: WorldNode | null;
|
|
122
|
+
readonly resolutionRuleId: "WCR-002" | "WCR-003" | "WCR-004" | "WCR-005" | null;
|
|
123
|
+
readonly evidence: CanonicalJsonObject;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const endpointDto = (
|
|
127
|
+
role: EndpointRole,
|
|
128
|
+
pin: NodePin,
|
|
129
|
+
status: EndpointResolutionStatus,
|
|
130
|
+
candidates: readonly WorldNode[],
|
|
131
|
+
resolvedNode: WorldNode | null,
|
|
132
|
+
resolvedNodeId: WorldNodeId | null = resolvedNode?.id ?? null,
|
|
133
|
+
sourceDiagnosticCodes: readonly string[] = [],
|
|
134
|
+
): EndpointResolutionDto =>
|
|
135
|
+
freezeEndpoint({
|
|
136
|
+
role,
|
|
137
|
+
declaredNodeId: pin.nodeId.toString(),
|
|
138
|
+
pinnedDigest: pin.contentDigest.toString(),
|
|
139
|
+
status,
|
|
140
|
+
resolvedNodeId: resolvedNodeId?.toString() ?? null,
|
|
141
|
+
currentDigest: resolvedNode?.contentDigest.toString() ?? null,
|
|
142
|
+
candidateCount: candidates.length,
|
|
143
|
+
candidateContentDigests: candidates.map((candidate) => candidate.contentDigest.toString()).sort(compareStrings),
|
|
144
|
+
locators: [...new Set(candidates.map(locatorFor))].sort(compareStrings),
|
|
145
|
+
sourceDiagnosticCodes: [...sourceDiagnosticCodes].sort(compareStrings),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const toEndpointCanonical = (endpoint: EndpointResolutionDto): CanonicalJsonObject => ({
|
|
149
|
+
candidateCount: endpoint.candidateCount,
|
|
150
|
+
candidateContentDigests: endpoint.candidateContentDigests,
|
|
151
|
+
currentDigest: endpoint.currentDigest,
|
|
152
|
+
declaredNodeId: endpoint.declaredNodeId,
|
|
153
|
+
locators: endpoint.locators,
|
|
154
|
+
pinnedDigest: endpoint.pinnedDigest,
|
|
155
|
+
resolvedNodeId: endpoint.resolvedNodeId,
|
|
156
|
+
role: endpoint.role,
|
|
157
|
+
sourceDiagnosticCodes: endpoint.sourceDiagnosticCodes,
|
|
158
|
+
status: endpoint.status,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const toFindingCanonical = (finding: ConstraintFindingDto): CanonicalJsonObject => ({
|
|
162
|
+
claimant: finding.claimant === null ? null : toEndpointCanonical(finding.claimant),
|
|
163
|
+
constraintId: finding.constraintId,
|
|
164
|
+
declarationArtifactId: finding.declarationArtifactId,
|
|
165
|
+
declarationLocator: finding.declarationLocator,
|
|
166
|
+
endpoint: finding.endpoint,
|
|
167
|
+
evidence: finding.evidence,
|
|
168
|
+
factType: finding.factType,
|
|
169
|
+
premise: finding.premise === null ? null : toEndpointCanonical(finding.premise),
|
|
170
|
+
ruleId: finding.ruleId,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const toEvaluationCanonical = (evaluation: ConstraintEvaluationDto): CanonicalJsonObject => ({
|
|
174
|
+
claimant: toEndpointCanonical(evaluation.claimant),
|
|
175
|
+
constraintId: evaluation.constraintId,
|
|
176
|
+
factType: evaluation.factType,
|
|
177
|
+
findings: evaluation.findings.map(toFindingCanonical),
|
|
178
|
+
premise: toEndpointCanonical(evaluation.premise),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
export class ConstraintEvaluationResult {
|
|
182
|
+
readonly evaluationId: string | null;
|
|
183
|
+
readonly evaluations: readonly ConstraintEvaluationDto[];
|
|
184
|
+
readonly findings: readonly ConstraintFindingDto[];
|
|
185
|
+
readonly changeProvenance: ChangeProvenance;
|
|
186
|
+
readonly scheduledConstraintIds: readonly string[];
|
|
187
|
+
|
|
188
|
+
constructor(props: {
|
|
189
|
+
readonly evaluationId: string | null;
|
|
190
|
+
readonly evaluations: readonly ConstraintEvaluationDto[];
|
|
191
|
+
readonly declarationFindings: readonly ConstraintFindingDto[];
|
|
192
|
+
readonly changeProvenance: ChangeProvenance;
|
|
193
|
+
readonly scheduledConstraintIds: readonly string[];
|
|
194
|
+
}) {
|
|
195
|
+
this.evaluationId = props.evaluationId;
|
|
196
|
+
this.evaluations = Object.freeze(
|
|
197
|
+
[...props.evaluations].sort((left, right) => compareStrings(left.constraintId, right.constraintId)),
|
|
198
|
+
);
|
|
199
|
+
this.findings = Object.freeze(
|
|
200
|
+
[...props.declarationFindings, ...this.evaluations.flatMap((evaluation) => evaluation.findings)].sort(
|
|
201
|
+
compareFindings,
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
this.changeProvenance = props.changeProvenance;
|
|
205
|
+
this.scheduledConstraintIds = Object.freeze([...props.scheduledConstraintIds].sort(compareStrings));
|
|
206
|
+
Object.freeze(this);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
toCanonicalValue(): CanonicalJsonObject {
|
|
210
|
+
return {
|
|
211
|
+
changeProvenance: this.changeProvenance.toCanonicalValue(),
|
|
212
|
+
evaluationId: this.evaluationId,
|
|
213
|
+
evaluations: this.evaluations.map(toEvaluationCanonical),
|
|
214
|
+
findings: this.findings.map(toFindingCanonical),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export class ConstraintEvaluator {
|
|
220
|
+
evaluateFull(input: ConstraintEvaluationInput): ConstraintEvaluationResult {
|
|
221
|
+
const records = [...input.records].sort((left, right) =>
|
|
222
|
+
compareStrings(left.constraintId.toString(), right.constraintId.toString()),
|
|
223
|
+
);
|
|
224
|
+
return this.evaluateRecords(
|
|
225
|
+
input,
|
|
226
|
+
records,
|
|
227
|
+
records.map((record) => record.constraintId.toString()),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
evaluateIncrementally(
|
|
232
|
+
input: ConstraintEvaluationInput,
|
|
233
|
+
changedNodeIds: readonly WorldNodeId[],
|
|
234
|
+
previous: ConstraintEvaluationResult,
|
|
235
|
+
): ConstraintEvaluationResult {
|
|
236
|
+
const changed = new Set(changedNodeIds.map((nodeId) => nodeId.toString()));
|
|
237
|
+
const previousById = new Map(previous.evaluations.map((evaluation) => [evaluation.constraintId, evaluation]));
|
|
238
|
+
const aliases = input.aliases ?? [];
|
|
239
|
+
const affected = input.records.filter((record) => {
|
|
240
|
+
if (!previousById.has(record.constraintId.toString())) return true;
|
|
241
|
+
if (changed.has(record.claimant.nodeId.toString()) || changed.has(record.premise.nodeId.toString())) return true;
|
|
242
|
+
return aliases.some(
|
|
243
|
+
(alias) =>
|
|
244
|
+
(alias.from.equals(record.claimant.nodeId) || alias.from.equals(record.premise.nodeId)) &&
|
|
245
|
+
changed.has(alias.to.toString()),
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
const affectedIds = new Set(affected.map((record) => record.constraintId.toString()));
|
|
249
|
+
const fresh = this.evaluateRecords(input, affected, [...affectedIds]);
|
|
250
|
+
const currentIds = new Set(input.records.map((record) => record.constraintId.toString()));
|
|
251
|
+
const retained = previous.evaluations.filter(
|
|
252
|
+
(evaluation) => currentIds.has(evaluation.constraintId) && !affectedIds.has(evaluation.constraintId),
|
|
253
|
+
);
|
|
254
|
+
return new ConstraintEvaluationResult({
|
|
255
|
+
evaluationId: input.currentSnapshot.evaluationId?.toString() ?? null,
|
|
256
|
+
evaluations: [...retained, ...fresh.evaluations],
|
|
257
|
+
declarationFindings: fresh.findings.filter((finding) => finding.constraintId === null),
|
|
258
|
+
changeProvenance: input.changeProvenance,
|
|
259
|
+
scheduledConstraintIds: [...affectedIds],
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private evaluateRecords(
|
|
264
|
+
input: ConstraintEvaluationInput,
|
|
265
|
+
records: readonly ConstraintRecord[],
|
|
266
|
+
scheduledConstraintIds: readonly string[],
|
|
267
|
+
): ConstraintEvaluationResult {
|
|
268
|
+
const declarationFindings = (input.malformedDeclarations ?? []).map((malformed) =>
|
|
269
|
+
Object.freeze({
|
|
270
|
+
ruleId: "WCR-001",
|
|
271
|
+
constraintId: null,
|
|
272
|
+
factType: null,
|
|
273
|
+
endpoint: null,
|
|
274
|
+
claimant: null,
|
|
275
|
+
premise: null,
|
|
276
|
+
declarationArtifactId: malformed.declarationArtifactId.toString(),
|
|
277
|
+
declarationLocator: malformed.declarationLocator,
|
|
278
|
+
evidence: malformed.toCanonicalValue(),
|
|
279
|
+
} satisfies ConstraintFindingDto),
|
|
280
|
+
);
|
|
281
|
+
const evaluations = records.map((record) => this.evaluateRecord(input, record));
|
|
282
|
+
return new ConstraintEvaluationResult({
|
|
283
|
+
evaluationId: input.currentSnapshot.evaluationId?.toString() ?? null,
|
|
284
|
+
evaluations,
|
|
285
|
+
declarationFindings,
|
|
286
|
+
changeProvenance: input.changeProvenance,
|
|
287
|
+
scheduledConstraintIds,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private evaluateRecord(input: ConstraintEvaluationInput, record: ConstraintRecord): ConstraintEvaluationDto {
|
|
292
|
+
const claimant = this.resolveEndpoint(input, record.claimant, "claimant");
|
|
293
|
+
const premise = this.resolveEndpoint(input, record.premise, "premise");
|
|
294
|
+
const findings: ConstraintFindingDto[] = [];
|
|
295
|
+
|
|
296
|
+
for (const resolution of [claimant, premise]) {
|
|
297
|
+
if (resolution.resolutionRuleId !== null && record.applies(resolution.resolutionRuleId)) {
|
|
298
|
+
findings.push(
|
|
299
|
+
this.finding(record, resolution.resolutionRuleId, resolution.dto.role, claimant.dto, premise.dto, {
|
|
300
|
+
...resolution.evidence,
|
|
301
|
+
}),
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (claimant.currentNode !== null && premise.currentNode !== null) {
|
|
307
|
+
const relationRule =
|
|
308
|
+
record.factType === "depends-on"
|
|
309
|
+
? "WCR-007"
|
|
310
|
+
: record.factType === "references" || record.factType === "refines"
|
|
311
|
+
? "WCR-006"
|
|
312
|
+
: null;
|
|
313
|
+
if (relationRule !== null && record.applies(relationRule) && !this.hasExplicitRelation(input, record)) {
|
|
314
|
+
findings.push(
|
|
315
|
+
this.finding(record, relationRule, null, claimant.dto, premise.dto, {
|
|
316
|
+
expectedRelation: `${record.claimant.nodeId.toString()} --${record.factType}--> ${record.premise.nodeId.toString()}`,
|
|
317
|
+
relationSource: "constraint-declaration",
|
|
318
|
+
}),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (record.applies("WCR-008")) {
|
|
323
|
+
if (!claimant.currentNode.contentDigest.equals(record.claimant.contentDigest)) {
|
|
324
|
+
findings.push(
|
|
325
|
+
this.finding(record, "WCR-008", "claimant", claimant.dto, premise.dto, {
|
|
326
|
+
expectedDigest: record.claimant.contentDigest.toString(),
|
|
327
|
+
observedDigest: claimant.currentNode.contentDigest.toString(),
|
|
328
|
+
}),
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if (!premise.currentNode.contentDigest.equals(record.premise.contentDigest)) {
|
|
332
|
+
findings.push(
|
|
333
|
+
this.finding(record, "WCR-008", "premise", claimant.dto, premise.dto, {
|
|
334
|
+
expectedDigest: record.premise.contentDigest.toString(),
|
|
335
|
+
observedDigest: premise.currentNode.contentDigest.toString(),
|
|
336
|
+
}),
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
if (
|
|
340
|
+
record.factType === "content-equals" &&
|
|
341
|
+
!claimant.currentNode.contentDigest.equals(premise.currentNode.contentDigest)
|
|
342
|
+
) {
|
|
343
|
+
findings.push(
|
|
344
|
+
this.finding(record, "WCR-008", null, claimant.dto, premise.dto, {
|
|
345
|
+
claimantDigest: claimant.currentNode.contentDigest.toString(),
|
|
346
|
+
expectedRelation: "current endpoint digests are equal",
|
|
347
|
+
premiseDigest: premise.currentNode.contentDigest.toString(),
|
|
348
|
+
}),
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return Object.freeze({
|
|
355
|
+
constraintId: record.constraintId.toString(),
|
|
356
|
+
factType: record.factType,
|
|
357
|
+
claimant: claimant.dto,
|
|
358
|
+
premise: premise.dto,
|
|
359
|
+
findings: Object.freeze(findings.sort(compareFindings)),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private resolveEndpoint(input: ConstraintEvaluationInput, pin: NodePin, role: EndpointRole): EndpointResolution {
|
|
364
|
+
const exact = candidatesFor(input.currentSnapshot, pin.nodeId);
|
|
365
|
+
const exactDiagnosticCodes = this.sourceDiagnosticCodes(input.currentSnapshot, pin.nodeId);
|
|
366
|
+
if (exact.length > 1) {
|
|
367
|
+
return {
|
|
368
|
+
dto: endpointDto(role, pin, "duplicate", exact, null, null, exactDiagnosticCodes),
|
|
369
|
+
currentNode: null,
|
|
370
|
+
resolutionRuleId: "WCR-005",
|
|
371
|
+
evidence: { candidateCount: exact.length, resolution: "exact" },
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (exact.length === 1) {
|
|
375
|
+
return {
|
|
376
|
+
dto: endpointDto(role, pin, "resolved", exact, exact[0]),
|
|
377
|
+
currentNode: exact[0],
|
|
378
|
+
resolutionRuleId: null,
|
|
379
|
+
evidence: {},
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const aliases = (input.aliases ?? []).filter((alias) => alias.from.equals(pin.nodeId));
|
|
384
|
+
if (aliases.length > 0) {
|
|
385
|
+
if (
|
|
386
|
+
aliases.length !== 1 ||
|
|
387
|
+
aliases[0].from.equals(aliases[0].to) ||
|
|
388
|
+
(input.aliases ?? []).some((candidate) => candidate.from.equals(aliases[0].to)) ||
|
|
389
|
+
!compatibleAliasRole(aliases[0].from, aliases[0].to)
|
|
390
|
+
) {
|
|
391
|
+
return {
|
|
392
|
+
dto: endpointDto(role, pin, "invalid-alias", [], null, aliases[0]?.to ?? null),
|
|
393
|
+
currentNode: null,
|
|
394
|
+
resolutionRuleId: "WCR-004",
|
|
395
|
+
evidence: {
|
|
396
|
+
aliasCount: aliases.length,
|
|
397
|
+
aliasTargets: aliases.map((alias) => alias.to.toString()).sort(compareStrings),
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const targetCandidates = candidatesFor(input.currentSnapshot, aliases[0].to);
|
|
402
|
+
const targetDiagnosticCodes = this.sourceDiagnosticCodes(input.currentSnapshot, aliases[0].to);
|
|
403
|
+
if (targetCandidates.length > 1) {
|
|
404
|
+
return {
|
|
405
|
+
dto: endpointDto(role, pin, "duplicate", targetCandidates, null, aliases[0].to, targetDiagnosticCodes),
|
|
406
|
+
currentNode: null,
|
|
407
|
+
resolutionRuleId: "WCR-005",
|
|
408
|
+
evidence: { candidateCount: targetCandidates.length, resolution: "alias-target" },
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
if (targetCandidates.length === 1) {
|
|
412
|
+
return {
|
|
413
|
+
dto: endpointDto(role, pin, "resolved-via-alias", targetCandidates, targetCandidates[0], aliases[0].to),
|
|
414
|
+
currentNode: targetCandidates[0],
|
|
415
|
+
resolutionRuleId: null,
|
|
416
|
+
evidence: {},
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
dto: endpointDto(role, pin, "invalid-alias", [], null, aliases[0].to),
|
|
421
|
+
currentNode: null,
|
|
422
|
+
resolutionRuleId: "WCR-004",
|
|
423
|
+
evidence: { aliasTarget: aliases[0].to.toString(), targetCandidateCount: 0 },
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const baseline = candidatesFor(input.baselineSnapshot, pin.nodeId);
|
|
428
|
+
if (baseline.length > 0) {
|
|
429
|
+
return {
|
|
430
|
+
dto: endpointDto(role, pin, "deleted", [], null),
|
|
431
|
+
currentNode: null,
|
|
432
|
+
resolutionRuleId: "WCR-003",
|
|
433
|
+
evidence: { baselineCandidateCount: baseline.length },
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
dto: endpointDto(role, pin, "missing", [], null),
|
|
438
|
+
currentNode: null,
|
|
439
|
+
resolutionRuleId: "WCR-002",
|
|
440
|
+
evidence: { baselineCandidateCount: 0 },
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private hasExplicitRelation(input: ConstraintEvaluationInput, record: ConstraintRecord): boolean {
|
|
445
|
+
return (input.relations ?? []).some(
|
|
446
|
+
(relation) =>
|
|
447
|
+
relation.constraintId.equals(record.constraintId) &&
|
|
448
|
+
relation.factType === record.factType &&
|
|
449
|
+
relation.claimantId.equals(record.claimant.nodeId) &&
|
|
450
|
+
relation.premiseId.equals(record.premise.nodeId),
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private sourceDiagnosticCodes(snapshot: Snapshot, nodeId: WorldNodeId): readonly string[] {
|
|
455
|
+
return Object.freeze(
|
|
456
|
+
[
|
|
457
|
+
...new Set(
|
|
458
|
+
snapshot.extractionDiagnostics
|
|
459
|
+
.filter((diagnostic) => diagnostic.nodeId?.equals(nodeId))
|
|
460
|
+
.map((diagnostic) => diagnostic.code),
|
|
461
|
+
),
|
|
462
|
+
].sort(compareStrings),
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private finding(
|
|
467
|
+
record: ConstraintRecord,
|
|
468
|
+
ruleId: string,
|
|
469
|
+
endpoint: EndpointRole | null,
|
|
470
|
+
claimant: EndpointResolutionDto,
|
|
471
|
+
premise: EndpointResolutionDto,
|
|
472
|
+
evidence: CanonicalJsonObject,
|
|
473
|
+
): ConstraintFindingDto {
|
|
474
|
+
return Object.freeze({
|
|
475
|
+
ruleId,
|
|
476
|
+
constraintId: record.constraintId.toString(),
|
|
477
|
+
factType: record.factType,
|
|
478
|
+
endpoint,
|
|
479
|
+
claimant,
|
|
480
|
+
premise,
|
|
481
|
+
declarationArtifactId: record.declarationArtifactId.toString(),
|
|
482
|
+
declarationLocator: record.declarationLocator,
|
|
483
|
+
evidence: Object.freeze({ ...evidence }),
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// @unit world-model
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-295
|
|
4
|
+
|
|
5
|
+
import type { AdoptionBaseline, SemanticDebtDeclaration, WorldWaiver } from "../entities/control-declarations.js";
|
|
6
|
+
import type { EvaluationId } from "../value-objects/evaluation-id.js";
|
|
7
|
+
import type { Sha256Digest } from "../value-objects/sha256-digest.js";
|
|
8
|
+
import type { CanonicalJsonObject } from "./canonical-json-serializer.js";
|
|
9
|
+
import type { ConstraintFindingDto } from "./constraint-evaluator.js";
|
|
10
|
+
import type { ViolationFingerprintDeriver } from "./violation-fingerprint-deriver.js";
|
|
11
|
+
|
|
12
|
+
export type ObligationClassification = "adopted-legacy" | "new-structural" | "invalid-declaration" | "waived";
|
|
13
|
+
|
|
14
|
+
export interface DerivedStructuralObligation {
|
|
15
|
+
readonly violationFingerprint: string;
|
|
16
|
+
readonly ruleId: string;
|
|
17
|
+
readonly constraintId: string | null;
|
|
18
|
+
readonly factType: string | null;
|
|
19
|
+
readonly subject: CanonicalJsonObject;
|
|
20
|
+
readonly claimantPin: CanonicalJsonObject | null;
|
|
21
|
+
readonly premisePin: CanonicalJsonObject | null;
|
|
22
|
+
readonly expected: CanonicalJsonObject;
|
|
23
|
+
readonly observed: CanonicalJsonObject;
|
|
24
|
+
readonly classification: ObligationClassification;
|
|
25
|
+
readonly waiver: CanonicalJsonObject | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DerivedRepaidBaselineEntry {
|
|
29
|
+
readonly violationFingerprint: string;
|
|
30
|
+
readonly ruleId: string;
|
|
31
|
+
readonly constraintId: string | null;
|
|
32
|
+
readonly classification: "repaid";
|
|
33
|
+
readonly disposition: "cleanup-required";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DerivedPolicyDiagnostic {
|
|
37
|
+
readonly code: string;
|
|
38
|
+
readonly subjectId: string | null;
|
|
39
|
+
readonly details: CanonicalJsonObject;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DerivedObligationSummary {
|
|
43
|
+
readonly structuralObligations: number;
|
|
44
|
+
readonly repaidBaselineEntries: number;
|
|
45
|
+
readonly declaredSemanticDebts: number;
|
|
46
|
+
readonly policyDiagnostics: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DerivedObligationReport {
|
|
50
|
+
readonly schemaVersion: "phasegate-world-obligation-report/v1";
|
|
51
|
+
readonly evaluationId: string;
|
|
52
|
+
readonly rulesetVersion: string;
|
|
53
|
+
readonly policyInputsDigest: string;
|
|
54
|
+
readonly structuralObligations: readonly DerivedStructuralObligation[];
|
|
55
|
+
readonly repaidBaselineEntries: readonly DerivedRepaidBaselineEntry[];
|
|
56
|
+
readonly declaredSemanticDebts: readonly CanonicalJsonObject[];
|
|
57
|
+
readonly policyDiagnostics: readonly DerivedPolicyDiagnostic[];
|
|
58
|
+
readonly summary: DerivedObligationSummary;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ObligationDerivationInput {
|
|
62
|
+
readonly evaluationId: EvaluationId;
|
|
63
|
+
readonly rulesetVersion: string;
|
|
64
|
+
readonly policyInputsDigest: Sha256Digest;
|
|
65
|
+
readonly findings: readonly ConstraintFindingDto[];
|
|
66
|
+
readonly baseline: AdoptionBaseline | null;
|
|
67
|
+
readonly waivers: readonly WorldWaiver[];
|
|
68
|
+
readonly semanticDebts: readonly SemanticDebtDeclaration[];
|
|
69
|
+
readonly policyAsOfDate: string | null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
|
|
73
|
+
|
|
74
|
+
const diagnostic = (
|
|
75
|
+
code: string,
|
|
76
|
+
subjectId: string | null,
|
|
77
|
+
details: CanonicalJsonObject = {},
|
|
78
|
+
): DerivedPolicyDiagnostic => Object.freeze({ code, subjectId, details: Object.freeze(details) });
|
|
79
|
+
|
|
80
|
+
export class ObligationDerivationService {
|
|
81
|
+
constructor(private readonly fingerprintDeriver: ViolationFingerprintDeriver) {}
|
|
82
|
+
|
|
83
|
+
derive(input: ObligationDerivationInput): DerivedObligationReport {
|
|
84
|
+
const baselineMatchesRuleset = input.baseline === null || input.baseline.rulesetVersion === input.rulesetVersion;
|
|
85
|
+
const policyDiagnostics: DerivedPolicyDiagnostic[] = [];
|
|
86
|
+
if (!baselineMatchesRuleset && input.baseline !== null) {
|
|
87
|
+
policyDiagnostics.push(
|
|
88
|
+
diagnostic("baseline-ruleset-mismatch", input.baseline.sourceEvaluationId.toString(), {
|
|
89
|
+
baselineRulesetVersion: input.baseline.rulesetVersion,
|
|
90
|
+
currentRulesetVersion: input.rulesetVersion,
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const baselineEntries = baselineMatchesRuleset ? (input.baseline?.entries ?? []) : [];
|
|
96
|
+
const baselineFingerprints = new Set(baselineEntries.map((entry) => entry.violationFingerprint.toString()));
|
|
97
|
+
const waiverByFingerprint = new Map<string, WorldWaiver>();
|
|
98
|
+
for (const waiver of [...input.waivers].sort((left, right) => compareStrings(left.waiverId, right.waiverId))) {
|
|
99
|
+
const active = input.policyAsOfDate !== null && input.policyAsOfDate < waiver.expiresOn;
|
|
100
|
+
if (active) {
|
|
101
|
+
waiverByFingerprint.set(waiver.violationFingerprint.toString(), waiver);
|
|
102
|
+
} else {
|
|
103
|
+
policyDiagnostics.push(
|
|
104
|
+
diagnostic("expired-waiver", waiver.waiverId, {
|
|
105
|
+
expiresOn: waiver.expiresOn,
|
|
106
|
+
policyAsOfDate: input.policyAsOfDate,
|
|
107
|
+
violationFingerprint: waiver.violationFingerprint.toString(),
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const byFingerprint = new Map<
|
|
114
|
+
string,
|
|
115
|
+
{ readonly finding: ConstraintFindingDto; readonly preimage: CanonicalJsonObject }
|
|
116
|
+
>();
|
|
117
|
+
for (const finding of input.findings) {
|
|
118
|
+
const derived = this.fingerprintDeriver.derive(finding, input.rulesetVersion);
|
|
119
|
+
const key = derived.fingerprint.toString();
|
|
120
|
+
if (!byFingerprint.has(key)) {
|
|
121
|
+
byFingerprint.set(key, { finding, preimage: derived.preimage });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const structuralObligations = [...byFingerprint.entries()]
|
|
126
|
+
.map(([violationFingerprint, value]): DerivedStructuralObligation => {
|
|
127
|
+
const invalidDeclaration = value.finding.ruleId === "WCR-001";
|
|
128
|
+
const waiver = invalidDeclaration ? null : (waiverByFingerprint.get(violationFingerprint) ?? null);
|
|
129
|
+
const classification: ObligationClassification = invalidDeclaration
|
|
130
|
+
? "invalid-declaration"
|
|
131
|
+
: waiver !== null
|
|
132
|
+
? "waived"
|
|
133
|
+
: baselineFingerprints.has(violationFingerprint)
|
|
134
|
+
? "adopted-legacy"
|
|
135
|
+
: "new-structural";
|
|
136
|
+
return Object.freeze({
|
|
137
|
+
violationFingerprint,
|
|
138
|
+
ruleId: value.finding.ruleId,
|
|
139
|
+
constraintId: value.finding.constraintId,
|
|
140
|
+
factType: value.finding.factType,
|
|
141
|
+
subject: value.preimage.subject as CanonicalJsonObject,
|
|
142
|
+
claimantPin: value.preimage.claimantPin as CanonicalJsonObject | null,
|
|
143
|
+
premisePin: value.preimage.premisePin as CanonicalJsonObject | null,
|
|
144
|
+
expected: value.preimage.expected as CanonicalJsonObject,
|
|
145
|
+
observed: value.preimage.observed as CanonicalJsonObject,
|
|
146
|
+
classification,
|
|
147
|
+
waiver:
|
|
148
|
+
waiver === null
|
|
149
|
+
? null
|
|
150
|
+
: Object.freeze({
|
|
151
|
+
waiverId: waiver.waiverId,
|
|
152
|
+
reason: waiver.reason,
|
|
153
|
+
expiresOn: waiver.expiresOn,
|
|
154
|
+
workItemId: waiver.workItemId,
|
|
155
|
+
}),
|
|
156
|
+
});
|
|
157
|
+
})
|
|
158
|
+
.sort((left, right) => compareStrings(left.violationFingerprint, right.violationFingerprint));
|
|
159
|
+
|
|
160
|
+
const currentFingerprints = new Set(byFingerprint.keys());
|
|
161
|
+
const repaidBaselineEntries = baselineEntries
|
|
162
|
+
.filter((entry) => !currentFingerprints.has(entry.violationFingerprint.toString()))
|
|
163
|
+
.map(
|
|
164
|
+
(entry): DerivedRepaidBaselineEntry =>
|
|
165
|
+
Object.freeze({
|
|
166
|
+
violationFingerprint: entry.violationFingerprint.toString(),
|
|
167
|
+
ruleId: entry.ruleId.toString(),
|
|
168
|
+
constraintId: entry.constraintId?.toString() ?? null,
|
|
169
|
+
classification: "repaid",
|
|
170
|
+
disposition: "cleanup-required",
|
|
171
|
+
}),
|
|
172
|
+
)
|
|
173
|
+
.sort((left, right) => compareStrings(left.violationFingerprint, right.violationFingerprint));
|
|
174
|
+
const declaredSemanticDebts = [...input.semanticDebts]
|
|
175
|
+
.sort((left, right) => compareStrings(left.debtId, right.debtId))
|
|
176
|
+
.map((debt) => debt.toCanonicalValue());
|
|
177
|
+
policyDiagnostics.sort(
|
|
178
|
+
(left, right) =>
|
|
179
|
+
compareStrings(left.code, right.code) || compareStrings(left.subjectId ?? "", right.subjectId ?? ""),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return Object.freeze({
|
|
183
|
+
schemaVersion: "phasegate-world-obligation-report/v1",
|
|
184
|
+
evaluationId: input.evaluationId.toString(),
|
|
185
|
+
rulesetVersion: input.rulesetVersion,
|
|
186
|
+
policyInputsDigest: input.policyInputsDigest.toString(),
|
|
187
|
+
structuralObligations: Object.freeze(structuralObligations),
|
|
188
|
+
repaidBaselineEntries: Object.freeze(repaidBaselineEntries),
|
|
189
|
+
declaredSemanticDebts: Object.freeze(declaredSemanticDebts),
|
|
190
|
+
policyDiagnostics: Object.freeze(policyDiagnostics),
|
|
191
|
+
summary: Object.freeze({
|
|
192
|
+
declaredSemanticDebts: declaredSemanticDebts.length,
|
|
193
|
+
policyDiagnostics: policyDiagnostics.length,
|
|
194
|
+
repaidBaselineEntries: repaidBaselineEntries.length,
|
|
195
|
+
structuralObligations: structuralObligations.length,
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|