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.
Files changed (154) hide show
  1. package/CHANGELOG.md +73 -1
  2. package/README.ja.md +31 -1
  3. package/README.md +30 -0
  4. package/docs/ADR/031-world-model-ownership-and-corpus-lifecycle.md +155 -0
  5. package/docs/ADR/032-world-node-identity.md +198 -0
  6. package/docs/ADR/033-world-snapshot-canonicalization.md +246 -0
  7. package/docs/ADR/034-world-constraint-semantics.md +218 -0
  8. package/docs/ADR/035-world-adoption-baseline-and-waiver.md +341 -0
  9. package/docs/ADR/036-world-model-and-doc-freshness.md +169 -0
  10. package/docs/ADR/037-world-cli-and-output-contract.md +398 -0
  11. package/docs/contracts/attestation-v2.schema.json +110 -0
  12. package/docs/contracts/requirement-test-matrix.schema.json +15 -0
  13. package/docs/contracts/world-baseline.schema.json +35 -0
  14. package/docs/contracts/world-constraints.schema.json +56 -0
  15. package/docs/contracts/world-debts.schema.json +32 -0
  16. package/docs/contracts/world-obligation-report.schema.json +259 -0
  17. package/docs/contracts/world-waivers.schema.json +32 -0
  18. package/docs/guide/cli-reference.md +45 -0
  19. package/docs/guide/configuration.md +56 -0
  20. package/docs/templates/ci/aidlc-gate.yml +42 -2
  21. package/package.json +1 -1
  22. package/scripts/harness/agent-integration/application/dto/open-world-obligations-context-dto.ts +30 -0
  23. package/scripts/harness/agent-integration/application/ports/world-obligations-query-port.ts +30 -0
  24. package/scripts/harness/agent-integration/application/usecases/get-open-world-obligations-context-usecase.ts +66 -0
  25. package/scripts/harness/agent-integration/infrastructure/adapters/world-model-open-obligations-query-adapter.ts +58 -0
  26. package/scripts/harness/agent-integration/presentation/session-start-hook.ts +37 -1
  27. package/scripts/harness/agent-integration/presentation/world-obligations-session-context.ts +60 -0
  28. package/scripts/harness/attestation/application/dto/attestation-document.ts +17 -4
  29. package/scripts/harness/attestation/application/mappers/attestation-record-mapper.ts +55 -4
  30. package/scripts/harness/attestation/application/ports/sha256-capability.ts +25 -0
  31. package/scripts/harness/attestation/application/ports/world-snapshot-root-provider.ts +10 -0
  32. package/scripts/harness/attestation/application/usecases/produce-attestation-usecase.ts +33 -7
  33. package/scripts/harness/attestation/composition-root.ts +19 -4
  34. package/scripts/harness/attestation/domain/entities/attestation-record.ts +37 -2
  35. package/scripts/harness/attestation/index.ts +13 -2
  36. package/scripts/harness/attestation/infrastructure/adapters/node-crypto-content-hasher-adapter.ts +6 -6
  37. package/scripts/harness/attestation/infrastructure/adapters/node-crypto-sha256-capability.ts +19 -0
  38. package/scripts/harness/attestation/presentation/handlers/attest-handler.ts +5 -2
  39. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +35 -8
  40. package/scripts/harness/config-foundation/application/mappers/world-model-config-mapper.ts +15 -0
  41. package/scripts/harness/config-foundation/domain/harness-config.ts +67 -87
  42. package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +77 -88
  43. package/scripts/harness/config-foundation/domain/value-objects/world-config.ts +198 -0
  44. package/scripts/harness/config-foundation/index.ts +14 -15
  45. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +24 -0
  46. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +24 -0
  47. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +24 -0
  48. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +83 -0
  49. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +83 -0
  50. package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +18 -4
  51. package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +4 -1
  52. package/scripts/harness/integrations/pre-commit.ts +169 -27
  53. package/scripts/harness/main.ts +276 -126
  54. package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +11 -0
  55. package/scripts/harness/nyquist-validation/application/usecases/check-ac-coverage-gate-usecase.ts +6 -0
  56. package/scripts/harness/nyquist-validation/application/usecases/generate-requirement-test-matrix-usecase.ts +8 -4
  57. package/scripts/harness/nyquist-validation/domain/entities/story-mapping.ts +29 -2
  58. package/scripts/harness/nyquist-validation/domain/services/ac-coverage-gate-policy.ts +28 -0
  59. package/scripts/harness/nyquist-validation/infrastructure/adapters/markdown-requirement-source-adapter.ts +71 -4
  60. package/scripts/harness/nyquist-validation/infrastructure/schema/matrix-schema-loader.ts +4 -0
  61. package/scripts/harness/traceability-model/application/dto/changed-design-fragment-dto.ts +31 -0
  62. package/scripts/harness/traceability-model/application/dto/traceability-world-read-dto.ts +67 -0
  63. package/scripts/harness/traceability-model/application/facades/design-change-read-facade.ts +14 -0
  64. package/scripts/harness/traceability-model/application/facades/traceability-world-read-facade.ts +372 -0
  65. package/scripts/harness/traceability-model/application/ports/staged-design-change-source-port.ts +9 -0
  66. package/scripts/harness/traceability-model/application/ports/traceability-world-read-source-port.ts +61 -0
  67. package/scripts/harness/traceability-model/composition-root.ts +26 -5
  68. package/scripts/harness/traceability-model/index.ts +26 -6
  69. package/scripts/harness/traceability-model/infrastructure/adapters/file-system-traceability-world-read-adapter.ts +223 -0
  70. package/scripts/harness/traceability-model/infrastructure/adapters/git-staged-design-change-adapter.ts +155 -0
  71. package/scripts/harness/traceability-model/infrastructure/parsers/story-catalog-parser.ts +86 -8
  72. package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +31 -0
  73. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +34 -0
  74. package/scripts/harness/validator-system/composition-root.ts +21 -6
  75. package/scripts/harness/validator-system/domain/ports/world-constraint-admission-policy-port.ts +28 -0
  76. package/scripts/harness/validator-system/domain/ports/world-constraint-rederivation-policy-port.ts +11 -0
  77. package/scripts/harness/validator-system/domain/services/design-change-declaration-policy.ts +75 -0
  78. package/scripts/harness/validator-system/domain/services/world-constraint-admission-service.ts +78 -0
  79. package/scripts/harness/validator-system/domain/services/world-constraint-rederivation-service.ts +76 -0
  80. package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +4 -0
  81. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +38 -16
  82. package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-admission-adapter.ts +56 -0
  83. package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-rederivation-adapter.ts +56 -0
  84. package/scripts/harness/world-model/application/dto/pinned-design-endpoint-dto.ts +19 -0
  85. package/scripts/harness/world-model/application/dto/world-inspection-dto.ts +43 -0
  86. package/scripts/harness/world-model/application/dto/world-obligation-report-dto.ts +54 -0
  87. package/scripts/harness/world-model/application/dto/world-resolved-config-input.ts +127 -0
  88. package/scripts/harness/world-model/application/dto/world-snapshot-root-dto.ts +8 -0
  89. package/scripts/harness/world-model/application/facades/pinned-design-endpoint-facade.ts +63 -0
  90. package/scripts/harness/world-model/application/facades/world-snapshot-root-facade.ts +19 -0
  91. package/scripts/harness/world-model/application/ports/obligation-report-writer-port.ts +7 -0
  92. package/scripts/harness/world-model/application/ports/world-control-declaration-repository-port.ts +57 -0
  93. package/scripts/harness/world-model/application/ports/world-fact-source-port.ts +17 -0
  94. package/scripts/harness/world-model/application/usecases/build-snapshot-use-case.ts +111 -0
  95. package/scripts/harness/world-model/application/usecases/derive-obligations-use-case.ts +155 -0
  96. package/scripts/harness/world-model/application/usecases/derive-world-obligations-use-case.ts +146 -0
  97. package/scripts/harness/world-model/application/usecases/inspect-world-use-case.ts +74 -0
  98. package/scripts/harness/world-model/application/usecases/pin-constraint-endpoint-use-case.ts +139 -0
  99. package/scripts/harness/world-model/composition-root.ts +247 -0
  100. package/scripts/harness/world-model/domain/entities/constraint-record.ts +148 -0
  101. package/scripts/harness/world-model/domain/entities/control-declarations.ts +274 -0
  102. package/scripts/harness/world-model/domain/entities/edge.ts +41 -0
  103. package/scripts/harness/world-model/domain/entities/extraction-diagnostic.ts +60 -0
  104. package/scripts/harness/world-model/domain/entities/snapshot.ts +75 -0
  105. package/scripts/harness/world-model/domain/entities/world-node.ts +210 -0
  106. package/scripts/harness/world-model/domain/ports/world-hashing-port.ts +8 -0
  107. package/scripts/harness/world-model/domain/services/canonical-json-serializer.ts +113 -0
  108. package/scripts/harness/world-model/domain/services/constraint-evaluator.ts +486 -0
  109. package/scripts/harness/world-model/domain/services/obligation-derivation-service.ts +199 -0
  110. package/scripts/harness/world-model/domain/services/policy-inputs-digest-deriver.ts +66 -0
  111. package/scripts/harness/world-model/domain/services/snapshot-root-deriver.ts +218 -0
  112. package/scripts/harness/world-model/domain/services/text-content-normalizer.ts +30 -0
  113. package/scripts/harness/world-model/domain/services/violation-fingerprint-deriver.ts +183 -0
  114. package/scripts/harness/world-model/domain/value-objects/artifact-kind.ts +58 -0
  115. package/scripts/harness/world-model/domain/value-objects/change-provenance.ts +137 -0
  116. package/scripts/harness/world-model/domain/value-objects/corpus-role.ts +57 -0
  117. package/scripts/harness/world-model/domain/value-objects/declared-key.ts +35 -0
  118. package/scripts/harness/world-model/domain/value-objects/evaluation-id.ts +41 -0
  119. package/scripts/harness/world-model/domain/value-objects/explicit-constraint-relation.ts +53 -0
  120. package/scripts/harness/world-model/domain/value-objects/explicit-node-alias.ts +30 -0
  121. package/scripts/harness/world-model/domain/value-objects/node-pin.ts +34 -0
  122. package/scripts/harness/world-model/domain/value-objects/path-key.ts +103 -0
  123. package/scripts/harness/world-model/domain/value-objects/sha256-digest.ts +39 -0
  124. package/scripts/harness/world-model/domain/value-objects/violation-fingerprint.ts +36 -0
  125. package/scripts/harness/world-model/domain/value-objects/wcr-rule-id.ts +59 -0
  126. package/scripts/harness/world-model/domain/value-objects/world-node-id.ts +222 -0
  127. package/scripts/harness/world-model/index.ts +33 -0
  128. package/scripts/harness/world-model/infrastructure/adapters/adr-fact-extractor.ts +27 -0
  129. package/scripts/harness/world-model/infrastructure/adapters/assembled-world-fact-source.ts +17 -0
  130. package/scripts/harness/world-model/infrastructure/adapters/attestation-fact-extractor.ts +323 -0
  131. package/scripts/harness/world-model/infrastructure/adapters/attestation-sha256-world-hashing-adapter.ts +15 -0
  132. package/scripts/harness/world-model/infrastructure/adapters/composite-design-fact-source.ts +23 -0
  133. package/scripts/harness/world-model/infrastructure/adapters/design-corpus-fact-extractor.ts +285 -0
  134. package/scripts/harness/world-model/infrastructure/adapters/design-fact-extraction.ts +63 -0
  135. package/scripts/harness/world-model/infrastructure/adapters/file-system-obligation-report-writer-adapter.ts +37 -0
  136. package/scripts/harness/world-model/infrastructure/adapters/file-system-world-control-repository-adapters.ts +332 -0
  137. package/scripts/harness/world-model/infrastructure/adapters/integrity-manifest-fact-extractor.ts +101 -0
  138. package/scripts/harness/world-model/infrastructure/adapters/json-fact-extractor-support.ts +151 -0
  139. package/scripts/harness/world-model/infrastructure/adapters/markdown-design-fact-extractor.ts +493 -0
  140. package/scripts/harness/world-model/infrastructure/adapters/matrix-fact-extractor.ts +283 -0
  141. package/scripts/harness/world-model/infrastructure/adapters/product-fact-extractor.ts +29 -0
  142. package/scripts/harness/world-model/infrastructure/adapters/proposal-fact-extractor.ts +29 -0
  143. package/scripts/harness/world-model/infrastructure/adapters/runtime-fact-extraction.ts +19 -0
  144. package/scripts/harness/world-model/infrastructure/adapters/source-metadata-fact-extractor.ts +22 -0
  145. package/scripts/harness/world-model/infrastructure/adapters/test-reference-source-fact-extractor.ts +22 -0
  146. package/scripts/harness/world-model/infrastructure/adapters/traceability-design-fact-adapter.ts +112 -0
  147. package/scripts/harness/world-model/infrastructure/adapters/traceability-world-read-facade-merger.ts +38 -0
  148. package/scripts/harness/world-model/infrastructure/adapters/type-script-source-fact-extractor.ts +236 -0
  149. package/scripts/harness/world-model/infrastructure/adapters/unit-fact-extractor.ts +71 -0
  150. package/scripts/harness/world-model/infrastructure/adapters/world-control-declaration-mapper.ts +383 -0
  151. package/scripts/harness/world-model/presentation/cli/world-command-support.ts +60 -0
  152. package/scripts/harness/world-model/presentation/cli/world-derive-command-handler.ts +109 -0
  153. package/scripts/harness/world-model/presentation/cli/world-inspect-command-handler.ts +194 -0
  154. package/scripts/harness/world-model/presentation/cli/world-pin-command-handler.ts +100 -0
@@ -0,0 +1,66 @@
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 { WorldHashingPort } from "../ports/world-hashing-port.js";
7
+ import type { Sha256Digest } from "../value-objects/sha256-digest.js";
8
+ import type { CanonicalJsonObject, CanonicalJsonSerializer } from "./canonical-json-serializer.js";
9
+
10
+ export interface PolicyInputsDigestInput {
11
+ readonly baseline: AdoptionBaseline | null;
12
+ readonly waivers: readonly WorldWaiver[];
13
+ readonly semanticDebts: readonly SemanticDebtDeclaration[];
14
+ readonly policyAsOfDate: string | null;
15
+ }
16
+
17
+ export interface PolicyInputsDigestDerivation {
18
+ readonly digest: Sha256Digest;
19
+ readonly preimage: CanonicalJsonObject;
20
+ readonly canonicalBytes: Uint8Array;
21
+ readonly resolvedPolicyAsOfDate: string | null;
22
+ }
23
+
24
+ const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
25
+
26
+ const requireCalendarDate = (value: string | null): string => {
27
+ if (value === null) throw new Error("policyAsOfDate is required when waivers exist");
28
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
29
+ if (!match) throw new Error("policyAsOfDate must be YYYY-MM-DD");
30
+ const date = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
31
+ if (
32
+ date.getUTCFullYear() !== Number(match[1]) ||
33
+ date.getUTCMonth() !== Number(match[2]) - 1 ||
34
+ date.getUTCDate() !== Number(match[3])
35
+ ) {
36
+ throw new Error("policyAsOfDate must be a valid calendar date");
37
+ }
38
+ return value;
39
+ };
40
+
41
+ export class PolicyInputsDigestDeriver {
42
+ constructor(
43
+ private readonly serializer: CanonicalJsonSerializer,
44
+ private readonly hashingPort: WorldHashingPort,
45
+ ) {}
46
+
47
+ derive(input: PolicyInputsDigestInput): PolicyInputsDigestDerivation {
48
+ const waivers = [...input.waivers].sort((left, right) => compareStrings(left.waiverId, right.waiverId));
49
+ const semanticDebts = [...input.semanticDebts].sort((left, right) => compareStrings(left.debtId, right.debtId));
50
+ const resolvedPolicyAsOfDate = waivers.length === 0 ? null : requireCalendarDate(input.policyAsOfDate);
51
+ const preimage: CanonicalJsonObject = {
52
+ schemaVersion: "phasegate-world-policy-inputs/v1",
53
+ adoptionBaseline: input.baseline?.toCanonicalValue() ?? null,
54
+ waivers: waivers.map((waiver) => waiver.toCanonicalValue()),
55
+ semanticDebts: semanticDebts.map((debt) => debt.toCanonicalValue()),
56
+ policyAsOfDate: resolvedPolicyAsOfDate,
57
+ };
58
+ const canonicalBytes = this.serializer.serialize(preimage);
59
+ return Object.freeze({
60
+ digest: this.hashingPort.sha256(canonicalBytes),
61
+ preimage: Object.freeze(preimage),
62
+ canonicalBytes,
63
+ resolvedPolicyAsOfDate,
64
+ });
65
+ }
66
+ }
@@ -0,0 +1,218 @@
1
+ // @unit world-model
2
+ // @layer domain
3
+ // @work-item-id WI-287
4
+ import type { Edge } from "../entities/edge.js";
5
+ import type { ExtractionDiagnostic } from "../entities/extraction-diagnostic.js";
6
+ import { Snapshot } from "../entities/snapshot.js";
7
+ import type { WorldNode } from "../entities/world-node.js";
8
+ import type { WorldHashingPort } from "../ports/world-hashing-port.js";
9
+ import { EvaluationId } from "../value-objects/evaluation-id.js";
10
+ import type { Sha256Digest } from "../value-objects/sha256-digest.js";
11
+ import type { WorldNodeId } from "../value-objects/world-node-id.js";
12
+ import type { CanonicalJsonObject, CanonicalJsonSerializer } from "./canonical-json-serializer.js";
13
+
14
+ export interface CorpusRootInput {
15
+ readonly schemaVersion: string;
16
+ readonly extractorVersion: string;
17
+ readonly corpusConfigDigest: Sha256Digest;
18
+ readonly nodes: readonly WorldNode[];
19
+ readonly edges: readonly Edge[];
20
+ readonly extractionDiagnostics: readonly ExtractionDiagnostic[];
21
+ }
22
+
23
+ export interface CanonicalDeclaration {
24
+ readonly id: WorldNodeId;
25
+ readonly value: CanonicalJsonObject;
26
+ }
27
+
28
+ export interface ConstraintRootInput {
29
+ readonly schemaVersion: string;
30
+ readonly rulesetVersion: string;
31
+ readonly constraintConfigDigest: Sha256Digest;
32
+ readonly constraints: readonly CanonicalDeclaration[];
33
+ readonly explicitClaims: readonly CanonicalDeclaration[];
34
+ readonly aliases: readonly CanonicalDeclaration[];
35
+ readonly declarationDiagnostics: readonly ExtractionDiagnostic[];
36
+ }
37
+
38
+ export interface EvaluationIdInput {
39
+ readonly schemaVersion: string;
40
+ readonly rulesetVersion: string;
41
+ readonly corpusRoot: Sha256Digest;
42
+ readonly constraintRoot: Sha256Digest;
43
+ readonly evaluationConfigDigest: Sha256Digest;
44
+ readonly policyInputsDigest: Sha256Digest;
45
+ }
46
+
47
+ export interface RootDerivation {
48
+ readonly root: Sha256Digest;
49
+ readonly canonicalBytes: Uint8Array;
50
+ }
51
+
52
+ export interface EvaluationIdDerivation {
53
+ readonly evaluationId: EvaluationId;
54
+ readonly canonicalBytes: Uint8Array;
55
+ }
56
+
57
+ const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
58
+
59
+ const compareTuples = (left: readonly string[], right: readonly string[]): number => {
60
+ const length = Math.max(left.length, right.length);
61
+ for (let index = 0; index < length; index += 1) {
62
+ const compared = compareStrings(left[index] ?? "", right[index] ?? "");
63
+ if (compared !== 0) {
64
+ return compared;
65
+ }
66
+ }
67
+ return 0;
68
+ };
69
+
70
+ const assertVersion = (name: string, value: string): void => {
71
+ if (value.trim().length === 0) {
72
+ throw new Error(`${name} must not be empty`);
73
+ }
74
+ };
75
+
76
+ export class SnapshotRootDeriver {
77
+ constructor(
78
+ private readonly serializer: CanonicalJsonSerializer,
79
+ private readonly hashingPort: WorldHashingPort,
80
+ ) {}
81
+
82
+ private sortedNodes(input: readonly WorldNode[]): readonly WorldNode[] {
83
+ return [...input].sort((left, right) => compareStrings(left.id.toString(), right.id.toString()));
84
+ }
85
+
86
+ private sortedEdges(input: readonly Edge[]): readonly Edge[] {
87
+ return [...input].sort((left, right) =>
88
+ compareTuples(
89
+ [left.edgeType.toString(), left.from.toString(), left.to.toString(), this.serializer.stringify(left.qualifier)],
90
+ [
91
+ right.edgeType.toString(),
92
+ right.from.toString(),
93
+ right.to.toString(),
94
+ this.serializer.stringify(right.qualifier),
95
+ ],
96
+ ),
97
+ );
98
+ }
99
+
100
+ private sortedDiagnostics(input: readonly ExtractionDiagnostic[]): readonly ExtractionDiagnostic[] {
101
+ return [...input].sort((left, right) =>
102
+ compareTuples(
103
+ [
104
+ left.code,
105
+ left.nodeId?.toString() ?? "",
106
+ left.path?.toString() ?? "",
107
+ String(left.line ?? 0).padStart(10, "0"),
108
+ this.serializer.stringify(left.payload),
109
+ ],
110
+ [
111
+ right.code,
112
+ right.nodeId?.toString() ?? "",
113
+ right.path?.toString() ?? "",
114
+ String(right.line ?? 0).padStart(10, "0"),
115
+ this.serializer.stringify(right.payload),
116
+ ],
117
+ ),
118
+ );
119
+ }
120
+
121
+ private corpusProjection(input: CorpusRootInput): {
122
+ readonly nodes: readonly WorldNode[];
123
+ readonly edges: readonly Edge[];
124
+ readonly diagnostics: readonly ExtractionDiagnostic[];
125
+ readonly value: CanonicalJsonObject;
126
+ } {
127
+ assertVersion("schemaVersion", input.schemaVersion);
128
+ assertVersion("extractorVersion", input.extractorVersion);
129
+ const nodes = this.sortedNodes(input.nodes);
130
+ const edges = this.sortedEdges(input.edges);
131
+ const diagnostics = this.sortedDiagnostics(input.extractionDiagnostics);
132
+ return {
133
+ nodes,
134
+ edges,
135
+ diagnostics,
136
+ value: {
137
+ corpusConfigDigest: input.corpusConfigDigest.toString(),
138
+ edges: edges.map((edge) => edge.toCanonicalValue()),
139
+ extractionDiagnostics: diagnostics.map((diagnostic) => diagnostic.toCanonicalValue()),
140
+ extractorVersion: input.extractorVersion,
141
+ nodes: nodes.map((node) => node.toCanonicalValue()),
142
+ schemaVersion: input.schemaVersion,
143
+ },
144
+ };
145
+ }
146
+
147
+ deriveCorpusRoot(input: CorpusRootInput): RootDerivation {
148
+ const projection = this.corpusProjection(input);
149
+ const canonicalBytes = this.serializer.serialize(projection.value);
150
+ return Object.freeze({
151
+ root: this.hashingPort.sha256(canonicalBytes),
152
+ canonicalBytes,
153
+ });
154
+ }
155
+
156
+ buildSnapshot(input: CorpusRootInput): Snapshot {
157
+ const projection = this.corpusProjection(input);
158
+ const canonicalBytes = this.serializer.serialize(projection.value);
159
+ const corpusRoot = this.hashingPort.sha256(canonicalBytes);
160
+ return Snapshot.create({
161
+ schemaVersion: input.schemaVersion,
162
+ extractorVersion: input.extractorVersion,
163
+ corpusConfigDigest: input.corpusConfigDigest,
164
+ nodes: projection.nodes,
165
+ edges: projection.edges,
166
+ extractionDiagnostics: projection.diagnostics,
167
+ corpusRoot,
168
+ canonicalBytes,
169
+ });
170
+ }
171
+
172
+ private sortedDeclarations(declarations: readonly CanonicalDeclaration[]): readonly CanonicalDeclaration[] {
173
+ return [...declarations].sort((left, right) => compareStrings(left.id.toString(), right.id.toString()));
174
+ }
175
+
176
+ deriveConstraintRoot(input: ConstraintRootInput): RootDerivation {
177
+ assertVersion("schemaVersion", input.schemaVersion);
178
+ assertVersion("rulesetVersion", input.rulesetVersion);
179
+ const toValue = (declaration: CanonicalDeclaration): CanonicalJsonObject => ({
180
+ id: declaration.id.toString(),
181
+ value: declaration.value,
182
+ });
183
+ const projection: CanonicalJsonObject = {
184
+ aliases: this.sortedDeclarations(input.aliases).map(toValue),
185
+ constraintConfigDigest: input.constraintConfigDigest.toString(),
186
+ constraints: this.sortedDeclarations(input.constraints).map(toValue),
187
+ declarationDiagnostics: this.sortedDiagnostics(input.declarationDiagnostics).map((diagnostic) =>
188
+ diagnostic.toCanonicalValue(),
189
+ ),
190
+ explicitClaims: this.sortedDeclarations(input.explicitClaims).map(toValue),
191
+ rulesetVersion: input.rulesetVersion,
192
+ schemaVersion: input.schemaVersion,
193
+ };
194
+ const canonicalBytes = this.serializer.serialize(projection);
195
+ return Object.freeze({
196
+ root: this.hashingPort.sha256(canonicalBytes),
197
+ canonicalBytes,
198
+ });
199
+ }
200
+
201
+ deriveEvaluationId(input: EvaluationIdInput): EvaluationIdDerivation {
202
+ assertVersion("schemaVersion", input.schemaVersion);
203
+ assertVersion("rulesetVersion", input.rulesetVersion);
204
+ const projection: CanonicalJsonObject = {
205
+ constraintRoot: input.constraintRoot.toString(),
206
+ corpusRoot: input.corpusRoot.toString(),
207
+ evaluationConfigDigest: input.evaluationConfigDigest.toString(),
208
+ policyInputsDigest: input.policyInputsDigest.toString(),
209
+ rulesetVersion: input.rulesetVersion,
210
+ schemaVersion: input.schemaVersion,
211
+ };
212
+ const canonicalBytes = this.serializer.serialize(projection);
213
+ return Object.freeze({
214
+ evaluationId: EvaluationId.fromDigest(this.hashingPort.sha256(canonicalBytes)),
215
+ canonicalBytes,
216
+ });
217
+ }
218
+ }
@@ -0,0 +1,30 @@
1
+ // @unit world-model
2
+ // @layer domain
3
+ // @work-item-id WI-287
4
+ import { ExtractionDiagnostic } from "../entities/extraction-diagnostic.js";
5
+
6
+ export type TextNormalizationResult =
7
+ | { readonly ok: true; readonly text: string; readonly bytes: Uint8Array }
8
+ | { readonly ok: false; readonly diagnostic: ExtractionDiagnostic };
9
+
10
+ export class TextContentNormalizer {
11
+ normalize(bytes: Uint8Array): TextNormalizationResult {
12
+ try {
13
+ const decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
14
+ const text = decoded.replace(/\r\n?/g, "\n");
15
+ return Object.freeze({
16
+ ok: true,
17
+ text,
18
+ bytes: new TextEncoder().encode(text),
19
+ });
20
+ } catch {
21
+ return Object.freeze({
22
+ ok: false,
23
+ diagnostic: ExtractionDiagnostic.create({
24
+ code: "invalid-utf8",
25
+ payload: { byteLength: bytes.byteLength },
26
+ }),
27
+ });
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,183 @@
1
+ // @unit world-model
2
+ // @layer domain
3
+ // @work-item-id WI-295, WI-296
4
+
5
+ import type { WorldHashingPort } from "../ports/world-hashing-port.js";
6
+ import { ViolationFingerprint } from "../value-objects/violation-fingerprint.js";
7
+ import type { CanonicalJsonObject, CanonicalJsonSerializer } from "./canonical-json-serializer.js";
8
+ import type { ConstraintFindingDto, EndpointResolutionDto } from "./constraint-evaluator.js";
9
+
10
+ export interface ViolationFingerprintDerivation {
11
+ readonly fingerprint: ViolationFingerprint;
12
+ readonly preimage: CanonicalJsonObject;
13
+ readonly canonicalBytes: Uint8Array;
14
+ }
15
+
16
+ const pinOf = (endpoint: EndpointResolutionDto | null): CanonicalJsonObject | null =>
17
+ endpoint === null ? null : { nodeId: endpoint.declaredNodeId, contentDigest: endpoint.pinnedDigest };
18
+
19
+ const subjectRole = (finding: ConstraintFindingDto): "claimant" | "premise" | "both" | "declaration" | "global" => {
20
+ if (finding.endpoint !== null) return finding.endpoint;
21
+ if (finding.ruleId === "WCR-001") return "declaration";
22
+ if (
23
+ finding.ruleId === "WCR-006" ||
24
+ finding.ruleId === "WCR-007" ||
25
+ (finding.ruleId === "WCR-008" && finding.factType === "content-equals")
26
+ ) {
27
+ return "both";
28
+ }
29
+ return "global";
30
+ };
31
+
32
+ const subjectOf = (finding: ConstraintFindingDto): CanonicalJsonObject => {
33
+ const endpointRole = subjectRole(finding);
34
+ const globalNodeId =
35
+ endpointRole === "global" && typeof finding.evidence.nodeId === "string" ? finding.evidence.nodeId : undefined;
36
+ const ids =
37
+ endpointRole === "claimant"
38
+ ? [finding.claimant?.declaredNodeId]
39
+ : endpointRole === "premise"
40
+ ? [finding.premise?.declaredNodeId]
41
+ : endpointRole === "both" || endpointRole === "global"
42
+ ? [globalNodeId, finding.claimant?.declaredNodeId, finding.premise?.declaredNodeId]
43
+ : [];
44
+ return {
45
+ endpointRole,
46
+ nodeIds: [...new Set(ids.filter((value): value is string => value !== undefined))].sort(),
47
+ };
48
+ };
49
+
50
+ const endpointFor = (finding: ConstraintFindingDto): EndpointResolutionDto | null =>
51
+ finding.endpoint === "claimant" ? finding.claimant : finding.endpoint === "premise" ? finding.premise : null;
52
+
53
+ const expectedObserved = (
54
+ finding: ConstraintFindingDto,
55
+ ): { readonly expected: CanonicalJsonObject; readonly observed: CanonicalJsonObject } => {
56
+ const endpoint = endpointFor(finding);
57
+ switch (finding.ruleId) {
58
+ case "WCR-001":
59
+ return {
60
+ expected: { declarationAdmission: "well-formed" },
61
+ observed: {
62
+ declaredConstraintId:
63
+ typeof finding.evidence.declaredConstraintId === "string" ? finding.evidence.declaredConstraintId : null,
64
+ },
65
+ };
66
+ case "WCR-002":
67
+ case "WCR-003":
68
+ return {
69
+ expected: {
70
+ contentDigest: endpoint?.pinnedDigest ?? null,
71
+ nodeId: endpoint?.declaredNodeId ?? null,
72
+ presence: "resolved",
73
+ },
74
+ observed: {
75
+ baselineCandidateCount:
76
+ typeof finding.evidence.baselineCandidateCount === "number"
77
+ ? finding.evidence.baselineCandidateCount
78
+ : null,
79
+ candidateCount: endpoint?.candidateCount ?? 0,
80
+ currentDigest: endpoint?.currentDigest ?? null,
81
+ status: endpoint?.status ?? "missing",
82
+ },
83
+ };
84
+ case "WCR-004":
85
+ return {
86
+ expected: { aliasResolution: "single-hop-resolved" },
87
+ observed: {
88
+ aliasCount: typeof finding.evidence.aliasCount === "number" ? finding.evidence.aliasCount : null,
89
+ aliasTarget: typeof finding.evidence.aliasTarget === "string" ? finding.evidence.aliasTarget : null,
90
+ aliasTargets: Array.isArray(finding.evidence.aliasTargets) ? [...finding.evidence.aliasTargets].sort() : [],
91
+ resolutionStatus: endpoint?.status ?? "invalid-alias",
92
+ targetCandidateCount:
93
+ typeof finding.evidence.targetCandidateCount === "number" ? finding.evidence.targetCandidateCount : null,
94
+ },
95
+ };
96
+ case "WCR-005":
97
+ return {
98
+ expected: {
99
+ candidateCount: 1,
100
+ nodeId:
101
+ endpoint?.declaredNodeId ?? (typeof finding.evidence.nodeId === "string" ? finding.evidence.nodeId : null),
102
+ },
103
+ observed: {
104
+ candidateContentDigests:
105
+ endpoint !== null
106
+ ? [...endpoint.candidateContentDigests].sort()
107
+ : Array.isArray(finding.evidence.candidateContentDigests)
108
+ ? finding.evidence.candidateContentDigests
109
+ .filter((value): value is string => typeof value === "string")
110
+ .sort()
111
+ : [],
112
+ candidateCount:
113
+ endpoint?.candidateCount ??
114
+ (typeof finding.evidence.candidateCount === "number" ? finding.evidence.candidateCount : 0),
115
+ resolution: typeof finding.evidence.resolution === "string" ? finding.evidence.resolution : null,
116
+ },
117
+ };
118
+ case "WCR-006":
119
+ case "WCR-007":
120
+ return {
121
+ expected: {
122
+ claimantNodeId: finding.claimant?.declaredNodeId ?? null,
123
+ factType: finding.factType,
124
+ premiseNodeId: finding.premise?.declaredNodeId ?? null,
125
+ relationSource: "constraint-declaration",
126
+ },
127
+ observed: { relationPresent: false },
128
+ };
129
+ case "WCR-008":
130
+ if (finding.endpoint !== null) {
131
+ return {
132
+ expected: {
133
+ contentDigest: endpoint?.pinnedDigest ?? null,
134
+ nodeId: endpoint?.declaredNodeId ?? null,
135
+ },
136
+ observed: {
137
+ contentDigest: endpoint?.currentDigest ?? null,
138
+ resolvedNodeId: endpoint?.resolvedNodeId ?? null,
139
+ },
140
+ };
141
+ }
142
+ return {
143
+ expected: { relation: "current endpoint digests are equal" },
144
+ observed: {
145
+ claimantDigest: finding.claimant?.currentDigest ?? null,
146
+ premiseDigest: finding.premise?.currentDigest ?? null,
147
+ },
148
+ };
149
+ default:
150
+ return { expected: {}, observed: {} };
151
+ }
152
+ };
153
+
154
+ export class ViolationFingerprintDeriver {
155
+ constructor(
156
+ private readonly serializer: CanonicalJsonSerializer,
157
+ private readonly hashingPort: WorldHashingPort,
158
+ ) {}
159
+
160
+ derive(finding: ConstraintFindingDto, rulesetVersion: string): ViolationFingerprintDerivation {
161
+ if (rulesetVersion.trim().length === 0) throw new Error("rulesetVersion must be non-empty");
162
+ const evidence = expectedObserved(finding);
163
+ const preimage: CanonicalJsonObject = {
164
+ schemaVersion: "phasegate-world-violation-fingerprint/v1",
165
+ rulesetVersion,
166
+ ruleId: finding.ruleId,
167
+ constraintId: finding.constraintId,
168
+ factType: finding.factType,
169
+ subject: subjectOf(finding),
170
+ claimantPin: pinOf(finding.claimant),
171
+ premisePin: pinOf(finding.premise),
172
+ expected: evidence.expected,
173
+ observed: evidence.observed,
174
+ };
175
+ const canonicalBytes = this.serializer.serialize(preimage);
176
+ const digest = this.hashingPort.sha256(canonicalBytes);
177
+ return Object.freeze({
178
+ fingerprint: ViolationFingerprint.create(`pgw:v1:violation-fingerprint:${digest.toString()}`),
179
+ preimage: Object.freeze(preimage),
180
+ canonicalBytes,
181
+ });
182
+ }
183
+ }
@@ -0,0 +1,58 @@
1
+ // @unit world-model
2
+ // @layer domain
3
+ // @work-item-id WI-287
4
+ export type ArtifactKindValue = "design-document" | "source" | "generated-artifact" | "external-declaration";
5
+
6
+ const VALUES: readonly ArtifactKindValue[] = [
7
+ "design-document",
8
+ "source",
9
+ "generated-artifact",
10
+ "external-declaration",
11
+ ];
12
+
13
+ export class InvalidArtifactKindError extends Error {
14
+ constructor(value: string) {
15
+ super(`Invalid World artifact kind: "${value}"`);
16
+ this.name = "InvalidArtifactKindError";
17
+ }
18
+ }
19
+
20
+ export class ArtifactKind {
21
+ readonly value: ArtifactKindValue;
22
+
23
+ private constructor(value: ArtifactKindValue) {
24
+ this.value = value;
25
+ Object.freeze(this);
26
+ }
27
+
28
+ static parse(value: string): ArtifactKind {
29
+ if (!VALUES.includes(value as ArtifactKindValue)) {
30
+ throw new InvalidArtifactKindError(value);
31
+ }
32
+ return new ArtifactKind(value as ArtifactKindValue);
33
+ }
34
+
35
+ static designDocument(): ArtifactKind {
36
+ return new ArtifactKind("design-document");
37
+ }
38
+
39
+ static source(): ArtifactKind {
40
+ return new ArtifactKind("source");
41
+ }
42
+
43
+ static generatedArtifact(): ArtifactKind {
44
+ return new ArtifactKind("generated-artifact");
45
+ }
46
+
47
+ static externalDeclaration(): ArtifactKind {
48
+ return new ArtifactKind("external-declaration");
49
+ }
50
+
51
+ equals(other: ArtifactKind): boolean {
52
+ return this.value === other.value;
53
+ }
54
+
55
+ toString(): string {
56
+ return this.value;
57
+ }
58
+ }
@@ -0,0 +1,137 @@
1
+ // @unit world-model
2
+ // @layer domain
3
+ // @work-item-id WI-293
4
+
5
+ import type { Snapshot } from "../entities/snapshot.js";
6
+ import type { WorldNode } from "../entities/world-node.js";
7
+ import type { CanonicalJsonObject } from "../services/canonical-json-serializer.js";
8
+ import type { Sha256Digest } from "./sha256-digest.js";
9
+ import type { WorldNodeId } from "./world-node-id.js";
10
+
11
+ export type ChangedCandidateKind = "added" | "removed" | "modified" | "candidate-cardinality-changed";
12
+
13
+ export interface ChangedCandidate {
14
+ readonly nodeId: WorldNodeId;
15
+ readonly changeKind: ChangedCandidateKind;
16
+ readonly baselineDigest: Sha256Digest | null;
17
+ readonly currentDigest: Sha256Digest | null;
18
+ readonly baselineLocators: readonly string[];
19
+ readonly currentLocators: readonly string[];
20
+ }
21
+
22
+ const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
23
+
24
+ const locatorFor = (node: WorldNode): string => {
25
+ if (node.projection.type === "artifact" || node.projection.type === "source-file") {
26
+ return node.projection.pathKey;
27
+ }
28
+ if (node.projection.type === "fragment") {
29
+ return node.projection.artifactId;
30
+ }
31
+ return node.id.toString();
32
+ };
33
+
34
+ const groupNodes = (nodes: readonly WorldNode[]): Map<string, readonly WorldNode[]> => {
35
+ const mutable = new Map<string, WorldNode[]>();
36
+ for (const node of nodes) {
37
+ const key = node.id.toString();
38
+ const current = mutable.get(key) ?? [];
39
+ current.push(node);
40
+ mutable.set(key, current);
41
+ }
42
+ return new Map(
43
+ [...mutable].map(([key, values]) => [
44
+ key,
45
+ Object.freeze(
46
+ [...values].sort((left, right) =>
47
+ compareStrings(
48
+ `${left.contentDigest.toString()}\0${locatorFor(left)}`,
49
+ `${right.contentDigest.toString()}\0${locatorFor(right)}`,
50
+ ),
51
+ ),
52
+ ),
53
+ ]),
54
+ );
55
+ };
56
+
57
+ const signature = (nodes: readonly WorldNode[]): string =>
58
+ nodes.map((node) => `${node.contentDigest.toString()}\0${locatorFor(node)}`).join("\u0001");
59
+
60
+ const candidateFor = (
61
+ nodeId: WorldNodeId,
62
+ baselineNodes: readonly WorldNode[],
63
+ currentNodes: readonly WorldNode[],
64
+ ): ChangedCandidate | null => {
65
+ let changeKind: ChangedCandidateKind;
66
+ if (baselineNodes.length === 0) {
67
+ changeKind = "added";
68
+ } else if (currentNodes.length === 0) {
69
+ changeKind = "removed";
70
+ } else if (baselineNodes.length !== currentNodes.length) {
71
+ changeKind = "candidate-cardinality-changed";
72
+ } else if (signature(baselineNodes) !== signature(currentNodes)) {
73
+ changeKind = "modified";
74
+ } else {
75
+ return null;
76
+ }
77
+ return Object.freeze({
78
+ nodeId,
79
+ changeKind,
80
+ baselineDigest: baselineNodes.length === 1 ? baselineNodes[0].contentDigest : null,
81
+ currentDigest: currentNodes.length === 1 ? currentNodes[0].contentDigest : null,
82
+ baselineLocators: Object.freeze([...new Set(baselineNodes.map(locatorFor))].sort(compareStrings)),
83
+ currentLocators: Object.freeze([...new Set(currentNodes.map(locatorFor))].sort(compareStrings)),
84
+ });
85
+ };
86
+
87
+ export class ChangeProvenance {
88
+ readonly baselineSnapshotId: WorldNodeId | null;
89
+ readonly baselineCorpusRoot: Sha256Digest | null;
90
+ readonly currentSnapshotId: WorldNodeId;
91
+ readonly currentCorpusRoot: Sha256Digest;
92
+ readonly changedCandidates: readonly ChangedCandidate[];
93
+
94
+ private constructor(
95
+ baselineSnapshot: Snapshot | null,
96
+ currentSnapshot: Snapshot,
97
+ changedCandidates: readonly ChangedCandidate[],
98
+ ) {
99
+ this.baselineSnapshotId = baselineSnapshot?.id ?? null;
100
+ this.baselineCorpusRoot = baselineSnapshot?.corpusRoot ?? null;
101
+ this.currentSnapshotId = currentSnapshot.id;
102
+ this.currentCorpusRoot = currentSnapshot.corpusRoot;
103
+ this.changedCandidates = changedCandidates;
104
+ Object.freeze(this);
105
+ }
106
+
107
+ static between(baselineSnapshot: Snapshot | null, currentSnapshot: Snapshot): ChangeProvenance {
108
+ const baselineGroups = groupNodes(baselineSnapshot?.nodes ?? []);
109
+ const currentGroups = groupNodes(currentSnapshot.nodes);
110
+ const ids = [...new Set([...baselineGroups.keys(), ...currentGroups.keys()])].sort(compareStrings);
111
+ const changedCandidates = ids.flatMap((id) => {
112
+ const baselineNodes = baselineGroups.get(id) ?? [];
113
+ const currentNodes = currentGroups.get(id) ?? [];
114
+ const sample = currentNodes[0] ?? baselineNodes[0];
115
+ const candidate = candidateFor(sample.id, baselineNodes, currentNodes);
116
+ return candidate === null ? [] : [candidate];
117
+ });
118
+ return new ChangeProvenance(baselineSnapshot, currentSnapshot, Object.freeze(changedCandidates));
119
+ }
120
+
121
+ toCanonicalValue(): CanonicalJsonObject {
122
+ return {
123
+ baselineCorpusRoot: this.baselineCorpusRoot?.toString() ?? null,
124
+ baselineSnapshotId: this.baselineSnapshotId?.toString() ?? null,
125
+ changedCandidates: this.changedCandidates.map((candidate) => ({
126
+ baselineDigest: candidate.baselineDigest?.toString() ?? null,
127
+ baselineLocators: candidate.baselineLocators,
128
+ changeKind: candidate.changeKind,
129
+ currentDigest: candidate.currentDigest?.toString() ?? null,
130
+ currentLocators: candidate.currentLocators,
131
+ nodeId: candidate.nodeId.toString(),
132
+ })),
133
+ currentCorpusRoot: this.currentCorpusRoot.toString(),
134
+ currentSnapshotId: this.currentSnapshotId.toString(),
135
+ };
136
+ }
137
+ }