phasegate 0.222.0 → 0.254.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 (108) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.ja.md +28 -1
  3. package/README.md +27 -0
  4. package/docs/ADR/001-l1-biome-editor-time-enforcement.md +2 -0
  5. package/docs/ADR/015-architecture-preset.md +2 -0
  6. package/docs/ADR/031-world-model-ownership-and-corpus-lifecycle.md +155 -0
  7. package/docs/ADR/032-world-node-identity.md +198 -0
  8. package/docs/ADR/033-world-snapshot-canonicalization.md +246 -0
  9. package/docs/ADR/034-world-constraint-semantics.md +218 -0
  10. package/docs/ADR/035-world-adoption-baseline-and-waiver.md +341 -0
  11. package/docs/ADR/036-world-model-and-doc-freshness.md +169 -0
  12. package/docs/ADR/037-world-cli-and-output-contract.md +398 -0
  13. package/docs/contracts/requirement-test-matrix.schema.json +15 -0
  14. package/docs/contracts/world-baseline.schema.json +35 -0
  15. package/docs/contracts/world-constraints.schema.json +56 -0
  16. package/docs/contracts/world-debts.schema.json +32 -0
  17. package/docs/contracts/world-obligation-report.schema.json +259 -0
  18. package/docs/contracts/world-waivers.schema.json +32 -0
  19. package/docs/guide/cli-reference.md +34 -0
  20. package/package.json +1 -1
  21. package/scripts/harness/attestation/application/ports/sha256-capability.ts +25 -0
  22. package/scripts/harness/attestation/composition-root.ts +14 -4
  23. package/scripts/harness/attestation/index.ts +6 -1
  24. package/scripts/harness/attestation/infrastructure/adapters/node-crypto-content-hasher-adapter.ts +6 -6
  25. package/scripts/harness/attestation/infrastructure/adapters/node-crypto-sha256-capability.ts +19 -0
  26. package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +4 -1
  27. package/scripts/harness/main.ts +107 -1
  28. package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +11 -0
  29. package/scripts/harness/nyquist-validation/application/usecases/check-ac-coverage-gate-usecase.ts +6 -0
  30. package/scripts/harness/nyquist-validation/application/usecases/generate-requirement-test-matrix-usecase.ts +8 -4
  31. package/scripts/harness/nyquist-validation/domain/entities/story-mapping.ts +29 -2
  32. package/scripts/harness/nyquist-validation/domain/services/ac-coverage-gate-policy.ts +28 -0
  33. package/scripts/harness/nyquist-validation/infrastructure/adapters/markdown-requirement-source-adapter.ts +71 -4
  34. package/scripts/harness/nyquist-validation/infrastructure/schema/matrix-schema-loader.ts +4 -0
  35. package/scripts/harness/traceability-model/application/dto/traceability-world-read-dto.ts +67 -0
  36. package/scripts/harness/traceability-model/application/facades/traceability-world-read-facade.ts +372 -0
  37. package/scripts/harness/traceability-model/application/ports/traceability-world-read-source-port.ts +61 -0
  38. package/scripts/harness/traceability-model/composition-root.ts +21 -5
  39. package/scripts/harness/traceability-model/index.ts +17 -6
  40. package/scripts/harness/traceability-model/infrastructure/adapters/file-system-traceability-world-read-adapter.ts +223 -0
  41. package/scripts/harness/traceability-model/infrastructure/parsers/story-catalog-parser.ts +86 -8
  42. package/scripts/harness/world-model/application/dto/world-inspection-dto.ts +43 -0
  43. package/scripts/harness/world-model/application/dto/world-obligation-report-dto.ts +54 -0
  44. package/scripts/harness/world-model/application/dto/world-resolved-config-input.ts +62 -0
  45. package/scripts/harness/world-model/application/ports/obligation-report-writer-port.ts +7 -0
  46. package/scripts/harness/world-model/application/ports/world-control-declaration-repository-port.ts +57 -0
  47. package/scripts/harness/world-model/application/ports/world-fact-source-port.ts +17 -0
  48. package/scripts/harness/world-model/application/usecases/build-snapshot-use-case.ts +111 -0
  49. package/scripts/harness/world-model/application/usecases/derive-obligations-use-case.ts +155 -0
  50. package/scripts/harness/world-model/application/usecases/derive-world-obligations-use-case.ts +144 -0
  51. package/scripts/harness/world-model/application/usecases/inspect-world-use-case.ts +74 -0
  52. package/scripts/harness/world-model/application/usecases/pin-constraint-endpoint-use-case.ts +139 -0
  53. package/scripts/harness/world-model/composition-root.ts +218 -0
  54. package/scripts/harness/world-model/domain/entities/constraint-record.ts +148 -0
  55. package/scripts/harness/world-model/domain/entities/control-declarations.ts +274 -0
  56. package/scripts/harness/world-model/domain/entities/edge.ts +41 -0
  57. package/scripts/harness/world-model/domain/entities/extraction-diagnostic.ts +60 -0
  58. package/scripts/harness/world-model/domain/entities/snapshot.ts +75 -0
  59. package/scripts/harness/world-model/domain/entities/world-node.ts +210 -0
  60. package/scripts/harness/world-model/domain/ports/world-hashing-port.ts +8 -0
  61. package/scripts/harness/world-model/domain/services/canonical-json-serializer.ts +113 -0
  62. package/scripts/harness/world-model/domain/services/constraint-evaluator.ts +486 -0
  63. package/scripts/harness/world-model/domain/services/obligation-derivation-service.ts +199 -0
  64. package/scripts/harness/world-model/domain/services/policy-inputs-digest-deriver.ts +66 -0
  65. package/scripts/harness/world-model/domain/services/snapshot-root-deriver.ts +218 -0
  66. package/scripts/harness/world-model/domain/services/text-content-normalizer.ts +30 -0
  67. package/scripts/harness/world-model/domain/services/violation-fingerprint-deriver.ts +183 -0
  68. package/scripts/harness/world-model/domain/value-objects/artifact-kind.ts +58 -0
  69. package/scripts/harness/world-model/domain/value-objects/change-provenance.ts +137 -0
  70. package/scripts/harness/world-model/domain/value-objects/corpus-role.ts +57 -0
  71. package/scripts/harness/world-model/domain/value-objects/declared-key.ts +35 -0
  72. package/scripts/harness/world-model/domain/value-objects/evaluation-id.ts +41 -0
  73. package/scripts/harness/world-model/domain/value-objects/explicit-constraint-relation.ts +53 -0
  74. package/scripts/harness/world-model/domain/value-objects/explicit-node-alias.ts +30 -0
  75. package/scripts/harness/world-model/domain/value-objects/node-pin.ts +34 -0
  76. package/scripts/harness/world-model/domain/value-objects/path-key.ts +103 -0
  77. package/scripts/harness/world-model/domain/value-objects/sha256-digest.ts +39 -0
  78. package/scripts/harness/world-model/domain/value-objects/violation-fingerprint.ts +36 -0
  79. package/scripts/harness/world-model/domain/value-objects/wcr-rule-id.ts +59 -0
  80. package/scripts/harness/world-model/domain/value-objects/world-node-id.ts +222 -0
  81. package/scripts/harness/world-model/index.ts +22 -0
  82. package/scripts/harness/world-model/infrastructure/adapters/adr-fact-extractor.ts +27 -0
  83. package/scripts/harness/world-model/infrastructure/adapters/assembled-world-fact-source.ts +17 -0
  84. package/scripts/harness/world-model/infrastructure/adapters/attestation-fact-extractor.ts +288 -0
  85. package/scripts/harness/world-model/infrastructure/adapters/attestation-sha256-world-hashing-adapter.ts +15 -0
  86. package/scripts/harness/world-model/infrastructure/adapters/composite-design-fact-source.ts +23 -0
  87. package/scripts/harness/world-model/infrastructure/adapters/design-corpus-fact-extractor.ts +285 -0
  88. package/scripts/harness/world-model/infrastructure/adapters/design-fact-extraction.ts +63 -0
  89. package/scripts/harness/world-model/infrastructure/adapters/file-system-obligation-report-writer-adapter.ts +37 -0
  90. package/scripts/harness/world-model/infrastructure/adapters/file-system-world-control-repository-adapters.ts +330 -0
  91. package/scripts/harness/world-model/infrastructure/adapters/integrity-manifest-fact-extractor.ts +101 -0
  92. package/scripts/harness/world-model/infrastructure/adapters/json-fact-extractor-support.ts +151 -0
  93. package/scripts/harness/world-model/infrastructure/adapters/markdown-design-fact-extractor.ts +493 -0
  94. package/scripts/harness/world-model/infrastructure/adapters/matrix-fact-extractor.ts +283 -0
  95. package/scripts/harness/world-model/infrastructure/adapters/product-fact-extractor.ts +29 -0
  96. package/scripts/harness/world-model/infrastructure/adapters/proposal-fact-extractor.ts +29 -0
  97. package/scripts/harness/world-model/infrastructure/adapters/runtime-fact-extraction.ts +19 -0
  98. package/scripts/harness/world-model/infrastructure/adapters/source-metadata-fact-extractor.ts +22 -0
  99. package/scripts/harness/world-model/infrastructure/adapters/test-reference-source-fact-extractor.ts +22 -0
  100. package/scripts/harness/world-model/infrastructure/adapters/traceability-design-fact-adapter.ts +112 -0
  101. package/scripts/harness/world-model/infrastructure/adapters/traceability-world-read-facade-merger.ts +38 -0
  102. package/scripts/harness/world-model/infrastructure/adapters/type-script-source-fact-extractor.ts +236 -0
  103. package/scripts/harness/world-model/infrastructure/adapters/unit-fact-extractor.ts +71 -0
  104. package/scripts/harness/world-model/infrastructure/adapters/world-control-declaration-mapper.ts +383 -0
  105. package/scripts/harness/world-model/presentation/cli/world-command-support.ts +60 -0
  106. package/scripts/harness/world-model/presentation/cli/world-derive-command-handler.ts +105 -0
  107. package/scripts/harness/world-model/presentation/cli/world-inspect-command-handler.ts +194 -0
  108. package/scripts/harness/world-model/presentation/cli/world-pin-command-handler.ts +100 -0
@@ -0,0 +1,60 @@
1
+ // @unit world-model
2
+ // @layer domain
3
+ // @work-item-id WI-287
4
+ import type { CanonicalJsonObject } from "../services/canonical-json-serializer.js";
5
+ import type { PathKey } from "../value-objects/path-key.js";
6
+ import type { WorldNodeId } from "../value-objects/world-node-id.js";
7
+
8
+ const DIAGNOSTIC_CODE_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
9
+
10
+ export interface ExtractionDiagnosticProps {
11
+ readonly code: string;
12
+ readonly nodeId?: WorldNodeId;
13
+ readonly path?: PathKey;
14
+ readonly line?: number;
15
+ readonly payload?: CanonicalJsonObject;
16
+ }
17
+
18
+ export class InvalidExtractionDiagnosticError extends Error {
19
+ constructor(message: string) {
20
+ super(`Invalid World extraction diagnostic: ${message}`);
21
+ this.name = "InvalidExtractionDiagnosticError";
22
+ }
23
+ }
24
+
25
+ export class ExtractionDiagnostic {
26
+ readonly code: string;
27
+ readonly nodeId?: WorldNodeId;
28
+ readonly path?: PathKey;
29
+ readonly line?: number;
30
+ readonly payload: CanonicalJsonObject;
31
+
32
+ private constructor(props: ExtractionDiagnosticProps) {
33
+ this.code = props.code;
34
+ this.nodeId = props.nodeId;
35
+ this.path = props.path;
36
+ this.line = props.line;
37
+ this.payload = Object.freeze({ ...(props.payload ?? {}) });
38
+ Object.freeze(this);
39
+ }
40
+
41
+ static create(props: ExtractionDiagnosticProps): ExtractionDiagnostic {
42
+ if (!DIAGNOSTIC_CODE_PATTERN.test(props.code)) {
43
+ throw new InvalidExtractionDiagnosticError(`invalid code "${props.code}"`);
44
+ }
45
+ if (props.line !== undefined && (!Number.isInteger(props.line) || props.line < 1)) {
46
+ throw new InvalidExtractionDiagnosticError(`invalid line "${props.line}"`);
47
+ }
48
+ return new ExtractionDiagnostic(props);
49
+ }
50
+
51
+ toCanonicalValue(): CanonicalJsonObject {
52
+ return {
53
+ code: this.code,
54
+ line: this.line ?? null,
55
+ nodeId: this.nodeId?.toString() ?? null,
56
+ pathKey: this.path?.toString() ?? null,
57
+ payload: this.payload,
58
+ };
59
+ }
60
+ }
@@ -0,0 +1,75 @@
1
+ // @unit world-model
2
+ // @layer domain
3
+ // @work-item-id WI-287
4
+
5
+ import type { EvaluationId } from "../value-objects/evaluation-id.js";
6
+ import type { Sha256Digest } from "../value-objects/sha256-digest.js";
7
+ import { WorldNodeId } from "../value-objects/world-node-id.js";
8
+ import type { Edge } from "./edge.js";
9
+ import type { ExtractionDiagnostic } from "./extraction-diagnostic.js";
10
+ import type { WorldNode } from "./world-node.js";
11
+
12
+ export interface SnapshotProps {
13
+ readonly schemaVersion: string;
14
+ readonly extractorVersion: string;
15
+ readonly corpusConfigDigest: Sha256Digest;
16
+ readonly nodes: readonly WorldNode[];
17
+ readonly edges: readonly Edge[];
18
+ readonly extractionDiagnostics: readonly ExtractionDiagnostic[];
19
+ readonly corpusRoot: Sha256Digest;
20
+ readonly canonicalBytes: Uint8Array;
21
+ readonly constraintRoot?: Sha256Digest;
22
+ readonly evaluationId?: EvaluationId;
23
+ }
24
+
25
+ export class Snapshot {
26
+ readonly id: WorldNodeId;
27
+ readonly schemaVersion: string;
28
+ readonly extractorVersion: string;
29
+ readonly corpusConfigDigest: Sha256Digest;
30
+ readonly nodes: readonly WorldNode[];
31
+ readonly edges: readonly Edge[];
32
+ readonly extractionDiagnostics: readonly ExtractionDiagnostic[];
33
+ readonly corpusRoot: Sha256Digest;
34
+ readonly constraintRoot?: Sha256Digest;
35
+ readonly evaluationId?: EvaluationId;
36
+ private readonly canonicalBytesValue: Uint8Array;
37
+
38
+ private constructor(props: SnapshotProps) {
39
+ this.id = WorldNodeId.snapshot(props.corpusRoot);
40
+ this.schemaVersion = props.schemaVersion;
41
+ this.extractorVersion = props.extractorVersion;
42
+ this.corpusConfigDigest = props.corpusConfigDigest;
43
+ this.nodes = Object.freeze([...props.nodes]);
44
+ this.edges = Object.freeze([...props.edges]);
45
+ this.extractionDiagnostics = Object.freeze([...props.extractionDiagnostics]);
46
+ this.corpusRoot = props.corpusRoot;
47
+ this.canonicalBytesValue = Uint8Array.from(props.canonicalBytes);
48
+ this.constraintRoot = props.constraintRoot;
49
+ this.evaluationId = props.evaluationId;
50
+ Object.freeze(this);
51
+ }
52
+
53
+ static create(props: SnapshotProps): Snapshot {
54
+ return new Snapshot(props);
55
+ }
56
+
57
+ get canonicalBytes(): Uint8Array {
58
+ return Uint8Array.from(this.canonicalBytesValue);
59
+ }
60
+
61
+ withEvaluationRoots(constraintRoot: Sha256Digest, evaluationId: EvaluationId): Snapshot {
62
+ return new Snapshot({
63
+ schemaVersion: this.schemaVersion,
64
+ extractorVersion: this.extractorVersion,
65
+ corpusConfigDigest: this.corpusConfigDigest,
66
+ nodes: this.nodes,
67
+ edges: this.edges,
68
+ extractionDiagnostics: this.extractionDiagnostics,
69
+ corpusRoot: this.corpusRoot,
70
+ canonicalBytes: this.canonicalBytes,
71
+ constraintRoot,
72
+ evaluationId,
73
+ });
74
+ }
75
+ }
@@ -0,0 +1,210 @@
1
+ // @unit world-model
2
+ // @layer domain
3
+ // @work-item-id WI-287, WI-290
4
+ import type { CanonicalJsonObject } from "../services/canonical-json-serializer.js";
5
+ import type { ArtifactKind } from "../value-objects/artifact-kind.js";
6
+ import type { CorpusRole } from "../value-objects/corpus-role.js";
7
+ import type { DeclaredKey } from "../value-objects/declared-key.js";
8
+ import type { PathKey } from "../value-objects/path-key.js";
9
+ import type { Sha256Digest } from "../value-objects/sha256-digest.js";
10
+ import { type TestReferenceBinding, type TestReferenceType, WorldNodeId } from "../value-objects/world-node-id.js";
11
+
12
+ export type WorldNodeProjection =
13
+ | {
14
+ readonly type: "artifact";
15
+ readonly artifactKind: string;
16
+ readonly corpusRole: string;
17
+ readonly pathKey: string;
18
+ }
19
+ | { readonly type: "source-file"; readonly pathKey: string }
20
+ | {
21
+ readonly type: "fragment";
22
+ readonly identityMode: "explicit" | "legacy-whole-file";
23
+ readonly corpusRole: string;
24
+ readonly artifactId: string;
25
+ }
26
+ | { readonly type: "work-item"; readonly providerId: string }
27
+ | {
28
+ readonly type: "test-reference";
29
+ readonly storyId: string;
30
+ readonly acId: string;
31
+ readonly binding: TestReferenceBinding;
32
+ readonly testType: TestReferenceType;
33
+ readonly filePath: string;
34
+ readonly testName: string | null;
35
+ };
36
+
37
+ interface CommonNodeProps {
38
+ readonly digest: Sha256Digest;
39
+ readonly attributes?: CanonicalJsonObject;
40
+ }
41
+
42
+ export interface ArtifactNodeProps extends CommonNodeProps {
43
+ readonly artifactKind: ArtifactKind;
44
+ readonly corpusRole: CorpusRole;
45
+ readonly path: PathKey;
46
+ }
47
+
48
+ export interface SourceFileNodeProps extends CommonNodeProps {
49
+ readonly path: PathKey;
50
+ }
51
+
52
+ export interface ExplicitFragmentNodeProps extends CommonNodeProps {
53
+ readonly corpusRole: CorpusRole;
54
+ readonly declaredKey: DeclaredKey;
55
+ readonly artifactId: WorldNodeId;
56
+ }
57
+
58
+ export interface LegacyFragmentNodeProps extends CommonNodeProps {
59
+ readonly artifactKind: ArtifactKind;
60
+ readonly corpusRole: CorpusRole;
61
+ readonly path: PathKey;
62
+ readonly artifactId: WorldNodeId;
63
+ }
64
+
65
+ export interface WorkItemNodeProps extends CommonNodeProps {
66
+ readonly workItemId: string;
67
+ }
68
+
69
+ export interface TestReferenceNodeProps extends CommonNodeProps {
70
+ readonly storyId: string;
71
+ readonly acId: string;
72
+ readonly binding?: TestReferenceBinding;
73
+ readonly testType: TestReferenceType;
74
+ readonly filePath: PathKey;
75
+ readonly testName?: string;
76
+ }
77
+
78
+ export class InvalidWorldNodeError extends Error {
79
+ constructor(message: string) {
80
+ super(`Invalid World node: ${message}`);
81
+ this.name = "InvalidWorldNodeError";
82
+ }
83
+ }
84
+
85
+ export class WorldNode {
86
+ readonly id: WorldNodeId;
87
+ readonly contentDigest: Sha256Digest;
88
+ readonly projection: WorldNodeProjection;
89
+ readonly attributes: CanonicalJsonObject;
90
+
91
+ private constructor(
92
+ id: WorldNodeId,
93
+ contentDigest: Sha256Digest,
94
+ projection: WorldNodeProjection,
95
+ attributes?: CanonicalJsonObject,
96
+ ) {
97
+ this.id = id;
98
+ this.contentDigest = contentDigest;
99
+ this.projection = Object.freeze({ ...projection });
100
+ this.attributes = Object.freeze({ ...(attributes ?? {}) });
101
+ Object.freeze(this);
102
+ }
103
+
104
+ static artifact(props: ArtifactNodeProps): WorldNode {
105
+ const id = WorldNodeId.artifact(props.artifactKind, props.corpusRole, props.path);
106
+ return new WorldNode(
107
+ id,
108
+ props.digest,
109
+ {
110
+ type: "artifact",
111
+ artifactKind: props.artifactKind.toString(),
112
+ corpusRole: props.corpusRole.toString(),
113
+ pathKey: props.path.toString(),
114
+ },
115
+ props.attributes,
116
+ );
117
+ }
118
+
119
+ static sourceFile(props: SourceFileNodeProps): WorldNode {
120
+ return new WorldNode(
121
+ WorldNodeId.sourceFile(props.path),
122
+ props.digest,
123
+ { type: "source-file", pathKey: props.path.toString() },
124
+ props.attributes,
125
+ );
126
+ }
127
+
128
+ static explicitFragment(props: ExplicitFragmentNodeProps): WorldNode {
129
+ if (props.artifactId.nodeType !== "artifact") {
130
+ throw new InvalidWorldNodeError("explicit Fragment parent must be an Artifact");
131
+ }
132
+ const parentRole = /^pgw:v1:artifact:[^:]+:([^:]+):/.exec(props.artifactId.toString())?.[1];
133
+ if (parentRole !== props.corpusRole.toString()) {
134
+ throw new InvalidWorldNodeError("explicit Fragment corpus role must match its parent Artifact");
135
+ }
136
+ return new WorldNode(
137
+ WorldNodeId.fragment(props.corpusRole, props.declaredKey),
138
+ props.digest,
139
+ {
140
+ type: "fragment",
141
+ identityMode: "explicit",
142
+ corpusRole: props.corpusRole.toString(),
143
+ artifactId: props.artifactId.toString(),
144
+ },
145
+ props.attributes,
146
+ );
147
+ }
148
+
149
+ static legacyFragment(props: LegacyFragmentNodeProps): WorldNode {
150
+ const expectedArtifactId = WorldNodeId.artifact(props.artifactKind, props.corpusRole, props.path);
151
+ if (!props.artifactId.equals(expectedArtifactId)) {
152
+ throw new InvalidWorldNodeError("legacy Fragment parent does not match its artifact tuple");
153
+ }
154
+ return new WorldNode(
155
+ WorldNodeId.legacyFragment(props.artifactKind, props.corpusRole, props.path),
156
+ props.digest,
157
+ {
158
+ type: "fragment",
159
+ identityMode: "legacy-whole-file",
160
+ corpusRole: props.corpusRole.toString(),
161
+ artifactId: props.artifactId.toString(),
162
+ },
163
+ props.attributes,
164
+ );
165
+ }
166
+
167
+ static workItem(props: WorkItemNodeProps): WorldNode {
168
+ return new WorldNode(
169
+ WorldNodeId.workItem(props.workItemId),
170
+ props.digest,
171
+ { type: "work-item", providerId: props.workItemId },
172
+ props.attributes,
173
+ );
174
+ }
175
+
176
+ static testReference(props: TestReferenceNodeProps): WorldNode {
177
+ const binding = props.binding ?? "file";
178
+ return new WorldNode(
179
+ WorldNodeId.testReference({
180
+ storyId: props.storyId,
181
+ acId: props.acId,
182
+ binding,
183
+ testType: props.testType,
184
+ filePath: props.filePath,
185
+ testName: props.testName,
186
+ }),
187
+ props.digest,
188
+ {
189
+ type: "test-reference",
190
+ storyId: props.storyId,
191
+ acId: props.acId,
192
+ binding,
193
+ testType: props.testType,
194
+ filePath: props.filePath.toString(),
195
+ testName: props.testName ?? null,
196
+ },
197
+ props.attributes,
198
+ );
199
+ }
200
+
201
+ toCanonicalValue(): CanonicalJsonObject {
202
+ return {
203
+ attributes: this.attributes,
204
+ contentDigest: this.contentDigest.toString(),
205
+ id: this.id.toString(),
206
+ nodeType: this.id.nodeType,
207
+ projection: this.projection as CanonicalJsonObject,
208
+ };
209
+ }
210
+ }
@@ -0,0 +1,8 @@
1
+ // @unit world-model
2
+ // @layer domain
3
+ // @work-item-id WI-287
4
+ import type { Sha256Digest } from "../value-objects/sha256-digest.js";
5
+
6
+ export interface WorldHashingPort {
7
+ sha256(bytes: Uint8Array): Sha256Digest;
8
+ }
@@ -0,0 +1,113 @@
1
+ // @unit world-model
2
+ // @layer domain
3
+ // @work-item-id WI-287
4
+ export type CanonicalJsonPrimitive = null | boolean | number | string;
5
+ export type CanonicalJsonArray = readonly CanonicalJsonValue[];
6
+ export interface CanonicalJsonObject {
7
+ readonly [key: string]: CanonicalJsonValue;
8
+ }
9
+ export type CanonicalJsonValue = CanonicalJsonPrimitive | CanonicalJsonArray | CanonicalJsonObject;
10
+
11
+ export class CanonicalizationError extends Error {
12
+ constructor(message: string) {
13
+ super(`World canonicalization failed: ${message}`);
14
+ this.name = "CanonicalizationError";
15
+ }
16
+ }
17
+
18
+ const isPlainObject = (value: object): boolean => {
19
+ const prototype = Object.getPrototypeOf(value);
20
+ return prototype === Object.prototype || prototype === null;
21
+ };
22
+
23
+ const stringifyValue = (value: unknown, ancestors: WeakSet<object>, path: string): string => {
24
+ if (value === null) {
25
+ return "null";
26
+ }
27
+
28
+ switch (typeof value) {
29
+ case "boolean":
30
+ case "string":
31
+ return JSON.stringify(value);
32
+ case "number":
33
+ if (!Number.isFinite(value)) {
34
+ throw new CanonicalizationError(`${path} contains a non-finite number`);
35
+ }
36
+ return JSON.stringify(value);
37
+ case "undefined":
38
+ case "bigint":
39
+ case "function":
40
+ case "symbol":
41
+ throw new CanonicalizationError(`${path} contains unsupported ${typeof value}`);
42
+ case "object":
43
+ break;
44
+ default:
45
+ throw new CanonicalizationError(`${path} contains an unsupported value`);
46
+ }
47
+
48
+ if (ancestors.has(value)) {
49
+ throw new CanonicalizationError(`${path} contains a cycle`);
50
+ }
51
+ ancestors.add(value);
52
+
53
+ try {
54
+ if (Array.isArray(value)) {
55
+ if (Object.getOwnPropertySymbols(value).length > 0) {
56
+ throw new CanonicalizationError(`${path} contains a symbol-keyed array property`);
57
+ }
58
+ const propertyNames = Object.getOwnPropertyNames(value);
59
+ const allowedNames = new Set(["length", ...value.map((_, index) => String(index))]);
60
+ if (propertyNames.some((name) => !allowedNames.has(name))) {
61
+ throw new CanonicalizationError(`${path} contains a non-index array property`);
62
+ }
63
+
64
+ const entries: string[] = [];
65
+ for (let index = 0; index < value.length; index += 1) {
66
+ if (!(index in value)) {
67
+ throw new CanonicalizationError(`${path}[${index}] is sparse`);
68
+ }
69
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
70
+ if (!descriptor || !("value" in descriptor)) {
71
+ throw new CanonicalizationError(`${path}[${index}] is an accessor property`);
72
+ }
73
+ entries.push(stringifyValue(value[index], ancestors, `${path}[${index}]`));
74
+ }
75
+ return `[${entries.join(",")}]`;
76
+ }
77
+
78
+ if (!isPlainObject(value)) {
79
+ throw new CanonicalizationError(`${path} is not a plain object`);
80
+ }
81
+ if (Object.getOwnPropertySymbols(value).length > 0) {
82
+ throw new CanonicalizationError(`${path} contains a symbol-keyed property`);
83
+ }
84
+
85
+ const propertyNames = Object.getOwnPropertyNames(value);
86
+ const enumerableKeys = Object.keys(value);
87
+ if (propertyNames.length !== enumerableKeys.length) {
88
+ throw new CanonicalizationError(`${path} contains a non-enumerable property`);
89
+ }
90
+
91
+ const record = value as Record<string, unknown>;
92
+ const entries = enumerableKeys.sort().map((key) => {
93
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
94
+ if (!descriptor || !("value" in descriptor)) {
95
+ throw new CanonicalizationError(`${path}.${key} is an accessor property`);
96
+ }
97
+ return `${JSON.stringify(key)}:${stringifyValue(record[key], ancestors, `${path}.${key}`)}`;
98
+ });
99
+ return `{${entries.join(",")}}`;
100
+ } finally {
101
+ ancestors.delete(value);
102
+ }
103
+ };
104
+
105
+ export class CanonicalJsonSerializer {
106
+ serialize(value: unknown): Uint8Array {
107
+ return new TextEncoder().encode(this.stringify(value));
108
+ }
109
+
110
+ stringify(value: unknown): string {
111
+ return stringifyValue(value, new WeakSet<object>(), "$");
112
+ }
113
+ }