phasegate 0.229.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 (106) 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/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/requirement-test-matrix.schema.json +15 -0
  12. package/docs/contracts/world-baseline.schema.json +35 -0
  13. package/docs/contracts/world-constraints.schema.json +56 -0
  14. package/docs/contracts/world-debts.schema.json +32 -0
  15. package/docs/contracts/world-obligation-report.schema.json +259 -0
  16. package/docs/contracts/world-waivers.schema.json +32 -0
  17. package/docs/guide/cli-reference.md +34 -0
  18. package/package.json +1 -1
  19. package/scripts/harness/attestation/application/ports/sha256-capability.ts +25 -0
  20. package/scripts/harness/attestation/composition-root.ts +14 -4
  21. package/scripts/harness/attestation/index.ts +6 -1
  22. package/scripts/harness/attestation/infrastructure/adapters/node-crypto-content-hasher-adapter.ts +6 -6
  23. package/scripts/harness/attestation/infrastructure/adapters/node-crypto-sha256-capability.ts +19 -0
  24. package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +4 -1
  25. package/scripts/harness/main.ts +107 -1
  26. package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +11 -0
  27. package/scripts/harness/nyquist-validation/application/usecases/check-ac-coverage-gate-usecase.ts +6 -0
  28. package/scripts/harness/nyquist-validation/application/usecases/generate-requirement-test-matrix-usecase.ts +8 -4
  29. package/scripts/harness/nyquist-validation/domain/entities/story-mapping.ts +29 -2
  30. package/scripts/harness/nyquist-validation/domain/services/ac-coverage-gate-policy.ts +28 -0
  31. package/scripts/harness/nyquist-validation/infrastructure/adapters/markdown-requirement-source-adapter.ts +71 -4
  32. package/scripts/harness/nyquist-validation/infrastructure/schema/matrix-schema-loader.ts +4 -0
  33. package/scripts/harness/traceability-model/application/dto/traceability-world-read-dto.ts +67 -0
  34. package/scripts/harness/traceability-model/application/facades/traceability-world-read-facade.ts +372 -0
  35. package/scripts/harness/traceability-model/application/ports/traceability-world-read-source-port.ts +61 -0
  36. package/scripts/harness/traceability-model/composition-root.ts +21 -5
  37. package/scripts/harness/traceability-model/index.ts +17 -6
  38. package/scripts/harness/traceability-model/infrastructure/adapters/file-system-traceability-world-read-adapter.ts +223 -0
  39. package/scripts/harness/traceability-model/infrastructure/parsers/story-catalog-parser.ts +86 -8
  40. package/scripts/harness/world-model/application/dto/world-inspection-dto.ts +43 -0
  41. package/scripts/harness/world-model/application/dto/world-obligation-report-dto.ts +54 -0
  42. package/scripts/harness/world-model/application/dto/world-resolved-config-input.ts +62 -0
  43. package/scripts/harness/world-model/application/ports/obligation-report-writer-port.ts +7 -0
  44. package/scripts/harness/world-model/application/ports/world-control-declaration-repository-port.ts +57 -0
  45. package/scripts/harness/world-model/application/ports/world-fact-source-port.ts +17 -0
  46. package/scripts/harness/world-model/application/usecases/build-snapshot-use-case.ts +111 -0
  47. package/scripts/harness/world-model/application/usecases/derive-obligations-use-case.ts +155 -0
  48. package/scripts/harness/world-model/application/usecases/derive-world-obligations-use-case.ts +144 -0
  49. package/scripts/harness/world-model/application/usecases/inspect-world-use-case.ts +74 -0
  50. package/scripts/harness/world-model/application/usecases/pin-constraint-endpoint-use-case.ts +139 -0
  51. package/scripts/harness/world-model/composition-root.ts +218 -0
  52. package/scripts/harness/world-model/domain/entities/constraint-record.ts +148 -0
  53. package/scripts/harness/world-model/domain/entities/control-declarations.ts +274 -0
  54. package/scripts/harness/world-model/domain/entities/edge.ts +41 -0
  55. package/scripts/harness/world-model/domain/entities/extraction-diagnostic.ts +60 -0
  56. package/scripts/harness/world-model/domain/entities/snapshot.ts +75 -0
  57. package/scripts/harness/world-model/domain/entities/world-node.ts +210 -0
  58. package/scripts/harness/world-model/domain/ports/world-hashing-port.ts +8 -0
  59. package/scripts/harness/world-model/domain/services/canonical-json-serializer.ts +113 -0
  60. package/scripts/harness/world-model/domain/services/constraint-evaluator.ts +486 -0
  61. package/scripts/harness/world-model/domain/services/obligation-derivation-service.ts +199 -0
  62. package/scripts/harness/world-model/domain/services/policy-inputs-digest-deriver.ts +66 -0
  63. package/scripts/harness/world-model/domain/services/snapshot-root-deriver.ts +218 -0
  64. package/scripts/harness/world-model/domain/services/text-content-normalizer.ts +30 -0
  65. package/scripts/harness/world-model/domain/services/violation-fingerprint-deriver.ts +183 -0
  66. package/scripts/harness/world-model/domain/value-objects/artifact-kind.ts +58 -0
  67. package/scripts/harness/world-model/domain/value-objects/change-provenance.ts +137 -0
  68. package/scripts/harness/world-model/domain/value-objects/corpus-role.ts +57 -0
  69. package/scripts/harness/world-model/domain/value-objects/declared-key.ts +35 -0
  70. package/scripts/harness/world-model/domain/value-objects/evaluation-id.ts +41 -0
  71. package/scripts/harness/world-model/domain/value-objects/explicit-constraint-relation.ts +53 -0
  72. package/scripts/harness/world-model/domain/value-objects/explicit-node-alias.ts +30 -0
  73. package/scripts/harness/world-model/domain/value-objects/node-pin.ts +34 -0
  74. package/scripts/harness/world-model/domain/value-objects/path-key.ts +103 -0
  75. package/scripts/harness/world-model/domain/value-objects/sha256-digest.ts +39 -0
  76. package/scripts/harness/world-model/domain/value-objects/violation-fingerprint.ts +36 -0
  77. package/scripts/harness/world-model/domain/value-objects/wcr-rule-id.ts +59 -0
  78. package/scripts/harness/world-model/domain/value-objects/world-node-id.ts +222 -0
  79. package/scripts/harness/world-model/index.ts +22 -0
  80. package/scripts/harness/world-model/infrastructure/adapters/adr-fact-extractor.ts +27 -0
  81. package/scripts/harness/world-model/infrastructure/adapters/assembled-world-fact-source.ts +17 -0
  82. package/scripts/harness/world-model/infrastructure/adapters/attestation-fact-extractor.ts +288 -0
  83. package/scripts/harness/world-model/infrastructure/adapters/attestation-sha256-world-hashing-adapter.ts +15 -0
  84. package/scripts/harness/world-model/infrastructure/adapters/composite-design-fact-source.ts +23 -0
  85. package/scripts/harness/world-model/infrastructure/adapters/design-corpus-fact-extractor.ts +285 -0
  86. package/scripts/harness/world-model/infrastructure/adapters/design-fact-extraction.ts +63 -0
  87. package/scripts/harness/world-model/infrastructure/adapters/file-system-obligation-report-writer-adapter.ts +37 -0
  88. package/scripts/harness/world-model/infrastructure/adapters/file-system-world-control-repository-adapters.ts +330 -0
  89. package/scripts/harness/world-model/infrastructure/adapters/integrity-manifest-fact-extractor.ts +101 -0
  90. package/scripts/harness/world-model/infrastructure/adapters/json-fact-extractor-support.ts +151 -0
  91. package/scripts/harness/world-model/infrastructure/adapters/markdown-design-fact-extractor.ts +493 -0
  92. package/scripts/harness/world-model/infrastructure/adapters/matrix-fact-extractor.ts +283 -0
  93. package/scripts/harness/world-model/infrastructure/adapters/product-fact-extractor.ts +29 -0
  94. package/scripts/harness/world-model/infrastructure/adapters/proposal-fact-extractor.ts +29 -0
  95. package/scripts/harness/world-model/infrastructure/adapters/runtime-fact-extraction.ts +19 -0
  96. package/scripts/harness/world-model/infrastructure/adapters/source-metadata-fact-extractor.ts +22 -0
  97. package/scripts/harness/world-model/infrastructure/adapters/test-reference-source-fact-extractor.ts +22 -0
  98. package/scripts/harness/world-model/infrastructure/adapters/traceability-design-fact-adapter.ts +112 -0
  99. package/scripts/harness/world-model/infrastructure/adapters/traceability-world-read-facade-merger.ts +38 -0
  100. package/scripts/harness/world-model/infrastructure/adapters/type-script-source-fact-extractor.ts +236 -0
  101. package/scripts/harness/world-model/infrastructure/adapters/unit-fact-extractor.ts +71 -0
  102. package/scripts/harness/world-model/infrastructure/adapters/world-control-declaration-mapper.ts +383 -0
  103. package/scripts/harness/world-model/presentation/cli/world-command-support.ts +60 -0
  104. package/scripts/harness/world-model/presentation/cli/world-derive-command-handler.ts +105 -0
  105. package/scripts/harness/world-model/presentation/cli/world-inspect-command-handler.ts +194 -0
  106. package/scripts/harness/world-model/presentation/cli/world-pin-command-handler.ts +100 -0
@@ -0,0 +1,222 @@
1
+ // @unit world-model
2
+ // @layer domain
3
+ // @work-item-id WI-287
4
+ import { ArtifactKind } from "./artifact-kind.js";
5
+ import { CorpusRole } from "./corpus-role.js";
6
+ import { DeclaredKey } from "./declared-key.js";
7
+ import { decodeWorldIdComponent, encodeWorldIdComponent, PathKey } from "./path-key.js";
8
+ import { Sha256Digest } from "./sha256-digest.js";
9
+
10
+ export type WorldNodeType =
11
+ | "artifact"
12
+ | "source-file"
13
+ | "fragment"
14
+ | "work-item"
15
+ | "test-reference"
16
+ | "explicit-claim"
17
+ | "constraint"
18
+ | "snapshot";
19
+
20
+ export type TestReferenceBinding = "ac" | "file";
21
+ export type TestReferenceType = "unit" | "it" | "scenario";
22
+
23
+ export interface TestReferenceIdInput {
24
+ readonly storyId: string;
25
+ readonly acId: string;
26
+ readonly binding?: TestReferenceBinding;
27
+ readonly testType: TestReferenceType;
28
+ readonly filePath: PathKey;
29
+ readonly testName?: string;
30
+ }
31
+
32
+ export class InvalidWorldNodeIdError extends Error {
33
+ constructor(value: string) {
34
+ super(`Invalid World node ID: "${value}"`);
35
+ this.name = "InvalidWorldNodeIdError";
36
+ }
37
+ }
38
+
39
+ const assertArtifactRole = (kind: ArtifactKind, role: CorpusRole): void => {
40
+ const valid =
41
+ (kind.value === "design-document" &&
42
+ (role.value === "product" || role.value === "inception" || role.value === "adr")) ||
43
+ (kind.value === "generated-artifact" && role.value === "generated") ||
44
+ (kind.value === "external-declaration" && role.value === "external");
45
+ if (!valid) {
46
+ throw new InvalidWorldNodeIdError(`${kind.toString()}:${role.toString()}`);
47
+ }
48
+ };
49
+
50
+ const reconstruct = (candidate: WorldNodeId, source: string): WorldNodeId => {
51
+ if (candidate.toString() !== source) {
52
+ throw new InvalidWorldNodeIdError(source);
53
+ }
54
+ return candidate;
55
+ };
56
+
57
+ export class WorldNodeId {
58
+ readonly value: string;
59
+ readonly nodeType: WorldNodeType;
60
+
61
+ private constructor(value: string, nodeType: WorldNodeType) {
62
+ this.value = value;
63
+ this.nodeType = nodeType;
64
+ Object.freeze(this);
65
+ }
66
+
67
+ static artifact(kind: ArtifactKind, role: CorpusRole, path: PathKey): WorldNodeId {
68
+ assertArtifactRole(kind, role);
69
+ return new WorldNodeId(
70
+ `pgw:v1:artifact:${kind.toString()}:${role.toString()}:${path.toEncodedString()}`,
71
+ "artifact",
72
+ );
73
+ }
74
+
75
+ static sourceFile(path: PathKey): WorldNodeId {
76
+ return new WorldNodeId(`pgw:v1:source-file:${path.toEncodedString()}`, "source-file");
77
+ }
78
+
79
+ static fragment(role: CorpusRole, key: DeclaredKey): WorldNodeId {
80
+ return new WorldNodeId(`pgw:v1:fragment:${role.toString()}:${key.toString()}`, "fragment");
81
+ }
82
+
83
+ static legacyFragment(kind: ArtifactKind, role: CorpusRole, path: PathKey): WorldNodeId {
84
+ assertArtifactRole(kind, role);
85
+ return new WorldNodeId(
86
+ `pgw:v1:fragment:legacy:${kind.toString()}:${role.toString()}:${path.toEncodedString()}`,
87
+ "fragment",
88
+ );
89
+ }
90
+
91
+ static workItem(workItemId: string): WorldNodeId {
92
+ if (!/^WI-\d+$/.test(workItemId)) {
93
+ throw new InvalidWorldNodeIdError(workItemId);
94
+ }
95
+ return new WorldNodeId(`pgw:v1:work-item:${workItemId}`, "work-item");
96
+ }
97
+
98
+ static testReference(input: TestReferenceIdInput): WorldNodeId {
99
+ const binding = input.binding ?? "file";
100
+ if (binding !== "ac" && binding !== "file") {
101
+ throw new InvalidWorldNodeIdError(binding);
102
+ }
103
+ if (input.testType !== "unit" && input.testType !== "it" && input.testType !== "scenario") {
104
+ throw new InvalidWorldNodeIdError(input.testType);
105
+ }
106
+ const storyId = encodeWorldIdComponent(input.storyId);
107
+ const acId = encodeWorldIdComponent(input.acId);
108
+ const nameKey = input.testName === undefined ? "none" : `value:${encodeWorldIdComponent(input.testName)}`;
109
+ return new WorldNodeId(
110
+ [
111
+ "pgw:v1:test-reference",
112
+ storyId,
113
+ acId,
114
+ binding,
115
+ input.testType,
116
+ input.filePath.toEncodedString(),
117
+ "name",
118
+ nameKey,
119
+ ].join(":"),
120
+ "test-reference",
121
+ );
122
+ }
123
+
124
+ static explicitClaim(key: DeclaredKey): WorldNodeId {
125
+ return new WorldNodeId(`pgw:v1:explicit-claim:${key.toString()}`, "explicit-claim");
126
+ }
127
+
128
+ static constraint(key: DeclaredKey): WorldNodeId {
129
+ return new WorldNodeId(`pgw:v1:constraint:${key.toString()}`, "constraint");
130
+ }
131
+
132
+ static snapshot(corpusRoot: Sha256Digest): WorldNodeId {
133
+ return new WorldNodeId(`pgw:v1:snapshot:${corpusRoot.toString()}`, "snapshot");
134
+ }
135
+
136
+ static parse(value: string): WorldNodeId {
137
+ try {
138
+ let match = /^pgw:v1:artifact:([^:]+):([^:]+):(.+)$/.exec(value);
139
+ if (match) {
140
+ return reconstruct(
141
+ WorldNodeId.artifact(
142
+ ArtifactKind.parse(match[1]),
143
+ CorpusRole.parse(match[2]),
144
+ PathKey.fromEncodedString(match[3]),
145
+ ),
146
+ value,
147
+ );
148
+ }
149
+
150
+ match = /^pgw:v1:source-file:(.+)$/.exec(value);
151
+ if (match) {
152
+ return reconstruct(WorldNodeId.sourceFile(PathKey.fromEncodedString(match[1])), value);
153
+ }
154
+
155
+ match = /^pgw:v1:fragment:legacy:([^:]+):([^:]+):(.+)$/.exec(value);
156
+ if (match) {
157
+ return reconstruct(
158
+ WorldNodeId.legacyFragment(
159
+ ArtifactKind.parse(match[1]),
160
+ CorpusRole.parse(match[2]),
161
+ PathKey.fromEncodedString(match[3]),
162
+ ),
163
+ value,
164
+ );
165
+ }
166
+
167
+ match = /^pgw:v1:fragment:([^:]+):(.+)$/.exec(value);
168
+ if (match) {
169
+ return reconstruct(WorldNodeId.fragment(CorpusRole.parse(match[1]), DeclaredKey.create(match[2])), value);
170
+ }
171
+
172
+ match = /^pgw:v1:work-item:(WI-\d+)$/.exec(value);
173
+ if (match) {
174
+ return reconstruct(WorldNodeId.workItem(match[1]), value);
175
+ }
176
+
177
+ match = /^pgw:v1:test-reference:([^:]+):([^:]+):(ac|file):(unit|it|scenario):(.+):name:(none|value:(.*))$/.exec(
178
+ value,
179
+ );
180
+ if (match) {
181
+ const testName = match[6] === "none" ? undefined : decodeWorldIdComponent(match[7]);
182
+ return reconstruct(
183
+ WorldNodeId.testReference({
184
+ storyId: decodeWorldIdComponent(match[1]),
185
+ acId: decodeWorldIdComponent(match[2]),
186
+ binding: match[3] as TestReferenceBinding,
187
+ testType: match[4] as TestReferenceType,
188
+ filePath: PathKey.fromEncodedString(match[5]),
189
+ testName,
190
+ }),
191
+ value,
192
+ );
193
+ }
194
+
195
+ match = /^pgw:v1:explicit-claim:(.+)$/.exec(value);
196
+ if (match) {
197
+ return reconstruct(WorldNodeId.explicitClaim(DeclaredKey.create(match[1])), value);
198
+ }
199
+
200
+ match = /^pgw:v1:constraint:(.+)$/.exec(value);
201
+ if (match) {
202
+ return reconstruct(WorldNodeId.constraint(DeclaredKey.create(match[1])), value);
203
+ }
204
+
205
+ match = /^pgw:v1:snapshot:(sha256:[0-9a-f]{64})$/.exec(value);
206
+ if (match) {
207
+ return reconstruct(WorldNodeId.snapshot(Sha256Digest.create(match[1])), value);
208
+ }
209
+ } catch {
210
+ throw new InvalidWorldNodeIdError(value);
211
+ }
212
+ throw new InvalidWorldNodeIdError(value);
213
+ }
214
+
215
+ equals(other: WorldNodeId): boolean {
216
+ return this.value === other.value;
217
+ }
218
+
219
+ toString(): string {
220
+ return this.value;
221
+ }
222
+ }
@@ -0,0 +1,22 @@
1
+ // @unit world-model
2
+ // @layer public-api
3
+ // @work-item-id WI-291, WI-296
4
+
5
+ export type {
6
+ WorldExtractionDiagnosticDto,
7
+ WorldInspectionDto,
8
+ WorldInventoryCountDto,
9
+ WorldJsonObject,
10
+ WorldJsonValue,
11
+ } from "./application/dto/world-inspection-dto.js";
12
+ export type {
13
+ WorldCorpusConfig,
14
+ WorldResolvedConfigInput,
15
+ } from "./application/dto/world-resolved-config-input.js";
16
+ export type { WorldModelModuleOptions } from "./composition-root.js";
17
+ export { createWorldModelModule } from "./composition-root.js";
18
+ export type { WorldCommandResult } from "./presentation/cli/world-command-support.js";
19
+ export { WorldDeriveCommandHandler } from "./presentation/cli/world-derive-command-handler.js";
20
+ export type { WorldInspectCommandResult } from "./presentation/cli/world-inspect-command-handler.js";
21
+ export { WorldInspectCommandHandler } from "./presentation/cli/world-inspect-command-handler.js";
22
+ export { WorldPinCommandHandler } from "./presentation/cli/world-pin-command-handler.js";
@@ -0,0 +1,27 @@
1
+ // @unit world-model
2
+ // @layer infrastructure
3
+ // @work-item-id WI-289, WI-291
4
+
5
+ import { CorpusRole } from "../../domain/value-objects/corpus-role.js";
6
+ import type { DesignFactSource } from "./design-fact-extraction.js";
7
+ import { FileSystemDesignFactScope, type MarkdownDesignFactExtractor } from "./markdown-design-fact-extractor.js";
8
+
9
+ export interface AdrFactExtractorDeps {
10
+ readonly rootDir: string;
11
+ readonly markdownExtractor: MarkdownDesignFactExtractor;
12
+ readonly adrRoot?: string;
13
+ }
14
+
15
+ export class AdrFactExtractor extends FileSystemDesignFactScope implements DesignFactSource {
16
+ constructor(deps: AdrFactExtractorDeps) {
17
+ super({
18
+ rootDir: deps.rootDir,
19
+ relativeRoot: deps.adrRoot ?? "docs/ADR",
20
+ role: CorpusRole.adr(),
21
+ markdownExtractor: deps.markdownExtractor,
22
+ include: (relativePath) => relativePath.endsWith(".md"),
23
+ skip: () => false,
24
+ metadata: () => ({}),
25
+ });
26
+ }
27
+ }
@@ -0,0 +1,17 @@
1
+ // @unit world-model
2
+ // @layer infrastructure
3
+ // @work-item-id WI-291
4
+
5
+ import type { WorldFactBatch, WorldFactSourcePort } from "../../application/ports/world-fact-source-port.js";
6
+
7
+ export interface WorldFactExtractorContract {
8
+ extract(): Promise<WorldFactBatch>;
9
+ }
10
+
11
+ export class AssembledWorldFactSource implements WorldFactSourcePort {
12
+ constructor(private readonly extractors: readonly WorldFactExtractorContract[]) {}
13
+
14
+ async extract(): Promise<readonly WorldFactBatch[]> {
15
+ return Promise.all(this.extractors.map((extractor) => extractor.extract()));
16
+ }
17
+ }
@@ -0,0 +1,288 @@
1
+ // @unit world-model
2
+ // @layer infrastructure
3
+ // @work-item-id WI-290
4
+
5
+ import type {
6
+ AttestationDocument,
7
+ VerifyAttestationHandler,
8
+ VerifyAttestationOutput,
9
+ } from "../../../attestation/index.js";
10
+ import { ExtractionDiagnostic } from "../../domain/entities/extraction-diagnostic.js";
11
+ import { WorldNode } from "../../domain/entities/world-node.js";
12
+ import type { WorldHashingPort } from "../../domain/ports/world-hashing-port.js";
13
+ import { type CanonicalJsonObject, CanonicalJsonSerializer } from "../../domain/services/canonical-json-serializer.js";
14
+ import { ArtifactKind } from "../../domain/value-objects/artifact-kind.js";
15
+ import { CorpusRole } from "../../domain/value-objects/corpus-role.js";
16
+ import {
17
+ assertExactKeys,
18
+ OwnerProjectionError,
19
+ projectionDiagnostic,
20
+ readOptionalJson,
21
+ requireArray,
22
+ requireBoolean,
23
+ requireObject,
24
+ requireString,
25
+ } from "./json-fact-extractor-support.js";
26
+ import type { RuntimeFactExtraction } from "./runtime-fact-extraction.js";
27
+
28
+ const DEFAULT_ATTESTATION_PATH = ".harness/attestation.json";
29
+ const SHA256 = /^sha256:[0-9a-f]{64}$/;
30
+ const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
31
+
32
+ export interface AttestationVerificationFacade {
33
+ verify(filePath: string): Promise<VerifyAttestationOutput>;
34
+ }
35
+
36
+ export class AttestationVerificationHandlerAdapter implements AttestationVerificationFacade {
37
+ constructor(private readonly handler: Pick<VerifyAttestationHandler, "handle">) {}
38
+
39
+ async verify(filePath: string): Promise<VerifyAttestationOutput> {
40
+ const result = await this.handler.handle({ file: filePath, emitJson: true });
41
+ const parsed = JSON.parse(result.output) as VerifyAttestationOutput;
42
+ if (
43
+ typeof parsed !== "object" ||
44
+ parsed === null ||
45
+ typeof parsed.ok !== "boolean" ||
46
+ typeof parsed.checks !== "object" ||
47
+ parsed.checks === null ||
48
+ !Array.isArray(parsed.mismatches)
49
+ ) {
50
+ throw new Error("public attestation verify handler returned an invalid DTO");
51
+ }
52
+ return parsed;
53
+ }
54
+ }
55
+
56
+ export interface AttestationFactExtractorDeps {
57
+ readonly rootDir: string;
58
+ readonly hashingPort: WorldHashingPort;
59
+ readonly verificationFacade: AttestationVerificationFacade;
60
+ readonly attestationPath?: string;
61
+ readonly serializer?: CanonicalJsonSerializer;
62
+ }
63
+
64
+ export class AttestationFactExtractor {
65
+ private readonly deps: AttestationFactExtractorDeps;
66
+ private readonly serializer: CanonicalJsonSerializer;
67
+
68
+ constructor(deps: AttestationFactExtractorDeps) {
69
+ this.deps = deps;
70
+ this.serializer = deps.serializer ?? new CanonicalJsonSerializer();
71
+ }
72
+
73
+ async extract(): Promise<RuntimeFactExtraction> {
74
+ const relativePath = this.deps.attestationPath ?? DEFAULT_ATTESTATION_PATH;
75
+ const read = await readOptionalJson(this.deps.rootDir, relativePath, "attestation");
76
+ if (read.state !== "present") {
77
+ return { nodes: [], edges: [], diagnostics: [read.diagnostic] };
78
+ }
79
+ try {
80
+ const document = this.parseDocument(read.value);
81
+ let verification: VerifyAttestationOutput;
82
+ try {
83
+ verification = await this.deps.verificationFacade.verify(relativePath);
84
+ } catch (error) {
85
+ return {
86
+ nodes: [],
87
+ edges: [],
88
+ diagnostics: [
89
+ ExtractionDiagnostic.create({
90
+ code: "attestation-verification-failure",
91
+ path: read.path,
92
+ payload: { message: error instanceof Error ? error.message : String(error) },
93
+ }),
94
+ ],
95
+ };
96
+ }
97
+ const projection = this.project(document, verification);
98
+ const artifact = WorldNode.artifact({
99
+ artifactKind: ArtifactKind.generatedArtifact(),
100
+ corpusRole: CorpusRole.generated(),
101
+ path: read.path,
102
+ digest: this.deps.hashingPort.sha256(this.serializer.serialize(projection)),
103
+ attributes: projection as unknown as CanonicalJsonObject,
104
+ });
105
+ return { nodes: [artifact], edges: [], diagnostics: [] };
106
+ } catch (error) {
107
+ return {
108
+ nodes: [],
109
+ edges: [],
110
+ diagnostics: [projectionDiagnostic(read.path, "attestation", error)],
111
+ };
112
+ }
113
+ }
114
+
115
+ private parseDocument(value: unknown): AttestationDocument {
116
+ const root = requireObject(value, "attestation");
117
+ assertExactKeys(
118
+ root,
119
+ ["schemaVersion", "predicateType", "subject", "inputs", "granularity", "acBoundScope", "metadata", "signature"],
120
+ "attestation",
121
+ );
122
+ const schemaVersion = requireString(root.schemaVersion, "attestation.schemaVersion");
123
+ if (schemaVersion !== "phasegate-attestation/v1") {
124
+ throw new OwnerProjectionError(
125
+ "unsupported-provider-schema",
126
+ `unsupported attestation schema: ${schemaVersion}`,
127
+ { schemaVersion },
128
+ );
129
+ }
130
+ const predicateType = requireString(root.predicateType, "attestation.predicateType");
131
+
132
+ const subject = requireObject(root.subject, "attestation.subject");
133
+ assertExactKeys(subject, ["command", "gateResult", "validatorSet"], "attestation.subject");
134
+ const command = requireString(subject.command, "attestation.subject.command");
135
+ const gateResult = requireString(subject.gateResult, "attestation.subject.gateResult");
136
+ if (gateResult !== "pass" && gateResult !== "fail") {
137
+ throw new OwnerProjectionError("malformed-provider-document", "attestation gateResult is invalid");
138
+ }
139
+ const validatorSet = requireArray(subject.validatorSet, "attestation.subject.validatorSet").map((raw, index) => {
140
+ const field = `attestation.subject.validatorSet[${index}]`;
141
+ const outcome = requireObject(raw, field);
142
+ assertExactKeys(outcome, ["validatorId", "passed", "skipped"], field);
143
+ return {
144
+ validatorId: requireString(outcome.validatorId, `${field}.validatorId`),
145
+ passed: requireBoolean(outcome.passed, `${field}.passed`),
146
+ skipped: requireBoolean(outcome.skipped, `${field}.skipped`),
147
+ };
148
+ });
149
+
150
+ const inputs = requireObject(root.inputs, "attestation.inputs");
151
+ assertExactKeys(inputs, ["digestAlgorithm", "sources", "inputDigest"], "attestation.inputs");
152
+ if (inputs.digestAlgorithm !== "sha256") {
153
+ throw new OwnerProjectionError("malformed-provider-document", "attestation digestAlgorithm is invalid");
154
+ }
155
+ const sources = requireArray(inputs.sources, "attestation.inputs.sources").map((raw, index) => {
156
+ const field = `attestation.inputs.sources[${index}]`;
157
+ const source = requireObject(raw, field);
158
+ assertExactKeys(source, ["path", "digest"], field);
159
+ const digest = requireString(source.digest, `${field}.digest`);
160
+ this.assertDigest(digest, `${field}.digest`);
161
+ return {
162
+ path: requireString(source.path, `${field}.path`),
163
+ digest,
164
+ };
165
+ });
166
+ const inputDigest = requireString(inputs.inputDigest, "attestation.inputs.inputDigest");
167
+ this.assertDigest(inputDigest, "attestation.inputs.inputDigest");
168
+
169
+ const granularity = requireObject(root.granularity, "attestation.granularity");
170
+ assertExactKeys(granularity, ["traceability"], "attestation.granularity");
171
+ const traceability = requireObject(granularity.traceability, "attestation.granularity.traceability");
172
+ assertExactKeys(
173
+ traceability,
174
+ ["validator", "level", "claim", "knownLimitations"],
175
+ "attestation.granularity.traceability",
176
+ );
177
+ const level = requireString(traceability.level, "attestation.granularity.traceability.level");
178
+ if (level !== "file" && level !== "ac") {
179
+ throw new OwnerProjectionError("malformed-provider-document", "attestation granularity level is invalid");
180
+ }
181
+ const knownLimitations = requireArray(
182
+ traceability.knownLimitations,
183
+ "attestation.granularity.traceability.knownLimitations",
184
+ ).map((item, index) => requireString(item, `attestation.granularity.traceability.knownLimitations[${index}]`));
185
+ const acBoundScope = requireArray(root.acBoundScope, "attestation.acBoundScope").map((item, index) =>
186
+ requireString(item, `attestation.acBoundScope[${index}]`),
187
+ );
188
+
189
+ const metadata = requireObject(root.metadata, "attestation.metadata");
190
+ assertExactKeys(metadata, ["producedAt", "producer", "gitCommit"], "attestation.metadata");
191
+ const gitCommit =
192
+ metadata.gitCommit === null ? null : requireString(metadata.gitCommit, "attestation.metadata.gitCommit");
193
+
194
+ const signature = requireObject(root.signature, "attestation.signature");
195
+ assertExactKeys(signature, ["mode", "attestationDigest", "algorithm", "keyId", "value"], "attestation.signature");
196
+ const mode = requireString(signature.mode, "attestation.signature.mode");
197
+ if (mode !== "unsigned-poc" && mode !== "signed") {
198
+ throw new OwnerProjectionError("malformed-provider-document", "attestation signature mode is invalid");
199
+ }
200
+ const attestationDigest = requireString(signature.attestationDigest, "attestation.signature.attestationDigest");
201
+ this.assertDigest(attestationDigest, "attestation.signature.attestationDigest");
202
+ const nullableString = (candidate: unknown, field: string): string | null =>
203
+ candidate === null ? null : requireString(candidate, field);
204
+
205
+ return {
206
+ schemaVersion,
207
+ predicateType,
208
+ subject: { command, gateResult, validatorSet },
209
+ inputs: {
210
+ digestAlgorithm: "sha256",
211
+ sources,
212
+ inputDigest,
213
+ },
214
+ granularity: {
215
+ traceability: {
216
+ validator: requireString(traceability.validator, "attestation.granularity.traceability.validator"),
217
+ level,
218
+ claim: requireString(traceability.claim, "attestation.granularity.traceability.claim"),
219
+ knownLimitations,
220
+ },
221
+ },
222
+ acBoundScope,
223
+ metadata: {
224
+ producedAt: requireString(metadata.producedAt, "attestation.metadata.producedAt"),
225
+ producer: requireString(metadata.producer, "attestation.metadata.producer"),
226
+ gitCommit,
227
+ },
228
+ signature: {
229
+ mode,
230
+ attestationDigest,
231
+ algorithm: nullableString(signature.algorithm, "attestation.signature.algorithm"),
232
+ keyId: nullableString(signature.keyId, "attestation.signature.keyId"),
233
+ value: nullableString(signature.value, "attestation.signature.value"),
234
+ },
235
+ };
236
+ }
237
+
238
+ private project(document: AttestationDocument, verification: VerifyAttestationOutput): CanonicalJsonObject {
239
+ return {
240
+ schemaVersion: document.schemaVersion,
241
+ predicateType: document.predicateType,
242
+ subject: {
243
+ command: document.subject.command,
244
+ gateResult: document.subject.gateResult,
245
+ validatorSet: [...document.subject.validatorSet]
246
+ .sort((left, right) => compareStrings(left.validatorId, right.validatorId))
247
+ .map((outcome) => ({
248
+ validatorId: outcome.validatorId,
249
+ passed: outcome.passed,
250
+ skipped: outcome.skipped,
251
+ })),
252
+ },
253
+ inputs: {
254
+ digestAlgorithm: document.inputs.digestAlgorithm,
255
+ sources: document.inputs.sources
256
+ .filter((source) => source.path !== "git:HEAD")
257
+ .map((source) => ({ path: source.path, digest: source.digest }))
258
+ .sort((left, right) => compareStrings(left.path, right.path)),
259
+ },
260
+ granularity: {
261
+ traceability: {
262
+ validator: document.granularity.traceability.validator,
263
+ level: document.granularity.traceability.level,
264
+ claim: document.granularity.traceability.claim,
265
+ knownLimitations: [...document.granularity.traceability.knownLimitations].sort(compareStrings),
266
+ },
267
+ },
268
+ acBoundScope: [...document.acBoundScope].sort(compareStrings),
269
+ verificationStatus: {
270
+ ok: verification.ok,
271
+ checks: {
272
+ schema: verification.checks.schema,
273
+ mode: verification.checks.mode,
274
+ attestationDigest: verification.checks.attestationDigest,
275
+ inputHashes: verification.checks.inputHashes,
276
+ granularity: verification.checks.granularity,
277
+ acBoundScope: verification.checks.acBoundScope,
278
+ },
279
+ },
280
+ };
281
+ }
282
+
283
+ private assertDigest(value: string, field: string): void {
284
+ if (!SHA256.test(value)) {
285
+ throw new OwnerProjectionError("malformed-provider-document", `${field} is not SHA-256`, { field });
286
+ }
287
+ }
288
+ }
@@ -0,0 +1,15 @@
1
+ // @unit world-model
2
+ // @layer infrastructure
3
+ // @work-item-id WI-291
4
+
5
+ import type { Sha256Capability } from "../../../attestation/index.js";
6
+ import type { WorldHashingPort } from "../../domain/ports/world-hashing-port.js";
7
+ import { Sha256Digest } from "../../domain/value-objects/sha256-digest.js";
8
+
9
+ export class AttestationSha256WorldHashingAdapter implements WorldHashingPort {
10
+ constructor(private readonly capability: Sha256Capability) {}
11
+
12
+ sha256(bytes: Uint8Array): Sha256Digest {
13
+ return Sha256Digest.create(this.capability.hashBytes(bytes));
14
+ }
15
+ }
@@ -0,0 +1,23 @@
1
+ // @unit world-model
2
+ // @layer infrastructure
3
+ // @work-item-id WI-291
4
+
5
+ import type {
6
+ DesignFactCandidateExtraction,
7
+ DesignFactSource,
8
+ TraceabilityDesignFactIndex,
9
+ } from "./design-fact-extraction.js";
10
+
11
+ export class CompositeDesignFactSource implements DesignFactSource {
12
+ constructor(private readonly sources: readonly DesignFactSource[]) {}
13
+
14
+ async extract(ownerIndex: TraceabilityDesignFactIndex): Promise<DesignFactCandidateExtraction> {
15
+ const results = await Promise.all(this.sources.map((source) => source.extract(ownerIndex)));
16
+ return {
17
+ nodeCandidates: results.flatMap((result) => result.nodeCandidates),
18
+ workItemReferences: results.flatMap((result) => result.workItemReferences),
19
+ reflectionReferences: results.flatMap((result) => result.reflectionReferences),
20
+ diagnostics: results.flatMap((result) => result.diagnostics),
21
+ };
22
+ }
23
+ }