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,283 @@
1
+ // @unit world-model
2
+ // @layer infrastructure
3
+ // @work-item-id WI-290
4
+ // @work-item-id WI-292
5
+
6
+ import type { RequirementTestMatrixDto } from "../../../nyquist-validation/index.js";
7
+ import { ExtractionDiagnostic } from "../../domain/entities/extraction-diagnostic.js";
8
+ import { WorldNode } from "../../domain/entities/world-node.js";
9
+ import type { WorldHashingPort } from "../../domain/ports/world-hashing-port.js";
10
+ import { type CanonicalJsonObject, CanonicalJsonSerializer } from "../../domain/services/canonical-json-serializer.js";
11
+ import { ArtifactKind } from "../../domain/value-objects/artifact-kind.js";
12
+ import { CorpusRole } from "../../domain/value-objects/corpus-role.js";
13
+ import { PathKey } from "../../domain/value-objects/path-key.js";
14
+ import type { TestReferenceBinding, TestReferenceType } from "../../domain/value-objects/world-node-id.js";
15
+ import {
16
+ assertExactKeys,
17
+ OwnerProjectionError,
18
+ projectionDiagnostic,
19
+ readOptionalJson,
20
+ requireArray,
21
+ requireObject,
22
+ requireString,
23
+ } from "./json-fact-extractor-support.js";
24
+ import type { RuntimeFactExtraction } from "./runtime-fact-extraction.js";
25
+
26
+ const DEFAULT_MATRIX_PATH = ".harness/requirement-test-matrix.json";
27
+ const SUPPORTED_VERSIONS = new Set(["1.0", "1.1", "1.2"]);
28
+ type CoverageStatus = "planned" | "required";
29
+
30
+ const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
31
+ const compareAcIds = (left: string, right: string): number => {
32
+ const leftNumber = /^AC-(\d+)$/.exec(left);
33
+ const rightNumber = /^AC-(\d+)$/.exec(right);
34
+ if (leftNumber && rightNumber) {
35
+ const difference = Number(leftNumber[1]) - Number(rightNumber[1]);
36
+ if (difference !== 0) return difference;
37
+ }
38
+ return compareStrings(left, right);
39
+ };
40
+
41
+ interface MatrixReferenceProjection {
42
+ readonly storyId: string;
43
+ readonly acId: string;
44
+ readonly binding: TestReferenceBinding;
45
+ readonly testType: TestReferenceType;
46
+ readonly filePath: string;
47
+ readonly testName: string | null;
48
+ }
49
+
50
+ interface MatrixProjection {
51
+ readonly version: RequirementTestMatrixDto["version"];
52
+ readonly stories: readonly {
53
+ readonly storyId: string;
54
+ readonly coverageStatus: CoverageStatus;
55
+ readonly coverageLifecycle: readonly CoverageStatus[];
56
+ readonly storyMappings: readonly {
57
+ readonly acId: string;
58
+ readonly testReferences: readonly MatrixReferenceProjection[];
59
+ }[];
60
+ }[];
61
+ }
62
+
63
+ export interface MatrixFactExtractorDeps {
64
+ readonly rootDir: string;
65
+ readonly hashingPort: WorldHashingPort;
66
+ readonly matrixPath?: string;
67
+ readonly serializer?: CanonicalJsonSerializer;
68
+ }
69
+
70
+ export class MatrixFactExtractor {
71
+ private readonly deps: MatrixFactExtractorDeps;
72
+ private readonly serializer: CanonicalJsonSerializer;
73
+
74
+ constructor(deps: MatrixFactExtractorDeps) {
75
+ this.deps = deps;
76
+ this.serializer = deps.serializer ?? new CanonicalJsonSerializer();
77
+ }
78
+
79
+ async extract(): Promise<RuntimeFactExtraction> {
80
+ const relativePath = this.deps.matrixPath ?? DEFAULT_MATRIX_PATH;
81
+ const read = await readOptionalJson(this.deps.rootDir, relativePath, "matrix");
82
+ if (read.state !== "present") {
83
+ return { nodes: [], edges: [], diagnostics: [read.diagnostic] };
84
+ }
85
+ try {
86
+ const projection = this.project(read.value);
87
+ const artifact = WorldNode.artifact({
88
+ artifactKind: ArtifactKind.generatedArtifact(),
89
+ corpusRole: CorpusRole.generated(),
90
+ path: read.path,
91
+ digest: this.deps.hashingPort.sha256(this.serializer.serialize(projection)),
92
+ attributes: projection as unknown as CanonicalJsonObject,
93
+ });
94
+ const candidates = projection.stories.flatMap((story) =>
95
+ story.storyMappings.flatMap((mapping) =>
96
+ mapping.testReferences.map((reference) =>
97
+ WorldNode.testReference({
98
+ storyId: reference.storyId,
99
+ acId: reference.acId,
100
+ binding: reference.binding,
101
+ testType: reference.testType,
102
+ filePath: PathKey.create(reference.filePath),
103
+ testName: reference.testName ?? undefined,
104
+ digest: this.deps.hashingPort.sha256(this.serializer.serialize(reference)),
105
+ attributes: { matrixPath: relativePath },
106
+ }),
107
+ ),
108
+ ),
109
+ );
110
+ const diagnostics: ExtractionDiagnostic[] = [];
111
+ const references = this.admitUniqueReferences(candidates, read.path, diagnostics);
112
+ return {
113
+ nodes: [artifact, ...references].sort((left, right) => compareStrings(left.id.toString(), right.id.toString())),
114
+ edges: [],
115
+ diagnostics,
116
+ };
117
+ } catch (error) {
118
+ return {
119
+ nodes: [],
120
+ edges: [],
121
+ diagnostics: [projectionDiagnostic(read.path, "matrix", error)],
122
+ };
123
+ }
124
+ }
125
+
126
+ private project(value: unknown): MatrixProjection {
127
+ const root = requireObject(value, "matrix");
128
+ assertExactKeys(root, ["version", "generatedAt", "stories"], "matrix");
129
+ const version = requireString(root.version, "matrix.version");
130
+ if (!SUPPORTED_VERSIONS.has(version)) {
131
+ throw new OwnerProjectionError("unsupported-provider-schema", `unsupported matrix version: ${version}`, {
132
+ schemaVersion: version,
133
+ });
134
+ }
135
+ requireString(root.generatedAt, "matrix.generatedAt");
136
+ const stories = requireArray(root.stories, "matrix.stories").map((rawStory, storyIndex) => {
137
+ const story = requireObject(rawStory, `matrix.stories[${storyIndex}]`);
138
+ assertExactKeys(
139
+ story,
140
+ ["storyId", "coverageStatus", "coverageLifecycle", "storyMappings"],
141
+ `matrix.stories[${storyIndex}]`,
142
+ );
143
+ const storyId = requireString(story.storyId, `matrix.stories[${storyIndex}].storyId`);
144
+ const coverageStatus = this.projectCoverageStatus(
145
+ story.coverageStatus,
146
+ `matrix.stories[${storyIndex}].coverageStatus`,
147
+ );
148
+ const coverageLifecycle = this.projectCoverageLifecycle(
149
+ story.coverageLifecycle,
150
+ coverageStatus,
151
+ `matrix.stories[${storyIndex}].coverageLifecycle`,
152
+ );
153
+ const storyMappings = requireArray(story.storyMappings, `matrix.stories[${storyIndex}].storyMappings`).map(
154
+ (rawMapping, mappingIndex) => {
155
+ const field = `matrix.stories[${storyIndex}].storyMappings[${mappingIndex}]`;
156
+ const mapping = requireObject(rawMapping, field);
157
+ assertExactKeys(mapping, ["acId", "testReferences"], field);
158
+ const acId = requireString(mapping.acId, `${field}.acId`);
159
+ const testReferences = requireArray(mapping.testReferences, `${field}.testReferences`).map(
160
+ (rawReference, referenceIndex) =>
161
+ this.projectReference(rawReference, `${field}.testReferences[${referenceIndex}]`, storyId, acId),
162
+ );
163
+ return {
164
+ acId,
165
+ testReferences: testReferences.sort((left, right) =>
166
+ compareStrings(this.referenceKey(left), this.referenceKey(right)),
167
+ ),
168
+ };
169
+ },
170
+ );
171
+ return {
172
+ storyId,
173
+ coverageStatus,
174
+ coverageLifecycle,
175
+ storyMappings: storyMappings.sort((left, right) => compareAcIds(left.acId, right.acId)),
176
+ };
177
+ });
178
+ return {
179
+ version,
180
+ stories: stories.sort((left, right) => compareStrings(left.storyId, right.storyId)),
181
+ };
182
+ }
183
+
184
+ private projectCoverageStatus(value: unknown, field: string): CoverageStatus {
185
+ if (value === undefined) return "required";
186
+ const status = requireString(value, field);
187
+ if (status !== "planned" && status !== "required") {
188
+ throw new OwnerProjectionError("malformed-provider-document", `${field} is invalid`, { field });
189
+ }
190
+ return status;
191
+ }
192
+
193
+ private projectCoverageLifecycle(
194
+ value: unknown,
195
+ status: CoverageStatus,
196
+ field: string,
197
+ ): readonly CoverageStatus[] {
198
+ if (value === undefined) return Object.freeze([status]);
199
+ const lifecycle = requireArray(value, field).map((entry, index) => {
200
+ const item = requireString(entry, `${field}[${index}]`);
201
+ if (item !== "planned" && item !== "required") {
202
+ throw new OwnerProjectionError("malformed-provider-document", `${field}[${index}] is invalid`, { field });
203
+ }
204
+ return item;
205
+ });
206
+ const validSequence =
207
+ (lifecycle.length === 1 && (lifecycle[0] === "planned" || lifecycle[0] === "required"))
208
+ || (lifecycle.length === 2 && lifecycle[0] === "planned" && lifecycle[1] === "required");
209
+ if (!validSequence || lifecycle[lifecycle.length - 1] !== status) {
210
+ throw new OwnerProjectionError("malformed-provider-document", `${field} is not a forward lifecycle`, { field });
211
+ }
212
+ return Object.freeze(lifecycle);
213
+ }
214
+
215
+ private projectReference(value: unknown, field: string, storyId: string, acId: string): MatrixReferenceProjection {
216
+ const reference = requireObject(value, field);
217
+ assertExactKeys(reference, ["filePath", "testType", "testName", "binding"], field);
218
+ const filePath = requireString(reference.filePath, `${field}.filePath`);
219
+ PathKey.create(filePath);
220
+ const testType = requireString(reference.testType, `${field}.testType`);
221
+ if (testType !== "unit" && testType !== "it" && testType !== "scenario") {
222
+ throw new OwnerProjectionError("malformed-provider-document", `${field}.testType is invalid`, {
223
+ field: `${field}.testType`,
224
+ });
225
+ }
226
+ const binding = reference.binding === undefined ? "file" : requireString(reference.binding, `${field}.binding`);
227
+ if (binding !== "ac" && binding !== "file") {
228
+ throw new OwnerProjectionError("malformed-provider-document", `${field}.binding is invalid`, {
229
+ field: `${field}.binding`,
230
+ });
231
+ }
232
+ const testName = reference.testName === undefined ? null : requireString(reference.testName, `${field}.testName`);
233
+ return {
234
+ storyId,
235
+ acId,
236
+ binding,
237
+ testType,
238
+ filePath,
239
+ testName,
240
+ };
241
+ }
242
+
243
+ private admitUniqueReferences(
244
+ candidates: readonly WorldNode[],
245
+ pathKey: PathKey,
246
+ diagnostics: ExtractionDiagnostic[],
247
+ ): readonly WorldNode[] {
248
+ const groups = new Map<string, WorldNode[]>();
249
+ for (const candidate of candidates) {
250
+ const id = candidate.id.toString();
251
+ const current = groups.get(id) ?? [];
252
+ current.push(candidate);
253
+ groups.set(id, current);
254
+ }
255
+ const admitted: WorldNode[] = [];
256
+ for (const [id, entries] of groups) {
257
+ if (entries.length === 1) {
258
+ admitted.push(entries[0]);
259
+ continue;
260
+ }
261
+ diagnostics.push(
262
+ ExtractionDiagnostic.create({
263
+ code: "duplicate-node-id",
264
+ nodeId: entries[0].id,
265
+ path: pathKey,
266
+ payload: { candidates: entries.length, provider: "matrix", testReferenceId: id },
267
+ }),
268
+ );
269
+ }
270
+ return admitted;
271
+ }
272
+
273
+ private referenceKey(reference: MatrixReferenceProjection): string {
274
+ return [
275
+ reference.storyId,
276
+ reference.acId,
277
+ reference.binding,
278
+ reference.testType,
279
+ reference.filePath,
280
+ reference.testName ?? "",
281
+ ].join("\u0000");
282
+ }
283
+ }
@@ -0,0 +1,29 @@
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 ProductFactExtractorDeps {
10
+ readonly rootDir: string;
11
+ readonly markdownExtractor: MarkdownDesignFactExtractor;
12
+ readonly productRoot?: string;
13
+ }
14
+
15
+ export class ProductFactExtractor extends FileSystemDesignFactScope implements DesignFactSource {
16
+ constructor(deps: ProductFactExtractorDeps) {
17
+ super({
18
+ rootDir: deps.rootDir,
19
+ relativeRoot: deps.productRoot ?? "docs/product",
20
+ role: CorpusRole.product(),
21
+ markdownExtractor: deps.markdownExtractor,
22
+ include: (relativePath) => relativePath.endsWith(".md"),
23
+ skip: (relativePath) => /\/units\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*_unit\.md$/.test(relativePath),
24
+ metadata: (relativePath, ownerIndex) => ({
25
+ storyIds: ownerIndex.storyIdsBySourcePath.get(relativePath),
26
+ }),
27
+ });
28
+ }
29
+ }
@@ -0,0 +1,29 @@
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 ProposalFactExtractorDeps {
10
+ readonly rootDir: string;
11
+ readonly markdownExtractor: MarkdownDesignFactExtractor;
12
+ readonly inceptionRoot?: string;
13
+ }
14
+
15
+ export class ProposalFactExtractor extends FileSystemDesignFactScope implements DesignFactSource {
16
+ constructor(deps: ProposalFactExtractorDeps) {
17
+ super({
18
+ rootDir: deps.rootDir,
19
+ relativeRoot: deps.inceptionRoot ?? "docs/inception",
20
+ role: CorpusRole.inception(),
21
+ markdownExtractor: deps.markdownExtractor,
22
+ include: (relativePath) => relativePath.endsWith(".md"),
23
+ skip: () => false,
24
+ metadata: (relativePath, ownerIndex) => ({
25
+ ownerWorkItemId: ownerIndex.workItemIdByDescriptionPath.get(relativePath),
26
+ }),
27
+ });
28
+ }
29
+ }
@@ -0,0 +1,19 @@
1
+ // @unit world-model
2
+ // @layer infrastructure
3
+ // @work-item-id WI-290
4
+
5
+ import type { Edge } from "../../domain/entities/edge.js";
6
+ import type { ExtractionDiagnostic } from "../../domain/entities/extraction-diagnostic.js";
7
+ import type { WorldNode } from "../../domain/entities/world-node.js";
8
+
9
+ export interface RuntimeFactExtraction {
10
+ readonly nodes: readonly WorldNode[];
11
+ readonly edges: readonly Edge[];
12
+ readonly diagnostics: readonly ExtractionDiagnostic[];
13
+ }
14
+
15
+ export const EMPTY_RUNTIME_FACT_EXTRACTION: RuntimeFactExtraction = Object.freeze({
16
+ nodes: Object.freeze([]),
17
+ edges: Object.freeze([]),
18
+ diagnostics: Object.freeze([]),
19
+ });
@@ -0,0 +1,22 @@
1
+ // @unit world-model
2
+ // @layer infrastructure
3
+ // @work-item-id WI-290, WI-291
4
+
5
+ import type { WorldHashingPort } from "../../domain/ports/world-hashing-port.js";
6
+ import { TypeScriptSourceFactExtractor } from "./type-script-source-fact-extractor.js";
7
+
8
+ export interface SourceMetadataFactExtractorDeps {
9
+ readonly rootDir: string;
10
+ readonly hashingPort: WorldHashingPort;
11
+ readonly sourceRoot?: string;
12
+ }
13
+
14
+ export class SourceMetadataFactExtractor extends TypeScriptSourceFactExtractor {
15
+ constructor(deps: SourceMetadataFactExtractorDeps) {
16
+ super({
17
+ ...deps,
18
+ sourceKind: "implementation",
19
+ include: (relativePath) => !relativePath.includes("/__tests__/"),
20
+ });
21
+ }
22
+ }
@@ -0,0 +1,22 @@
1
+ // @unit world-model
2
+ // @layer infrastructure
3
+ // @work-item-id WI-290, WI-291
4
+
5
+ import type { WorldHashingPort } from "../../domain/ports/world-hashing-port.js";
6
+ import { TypeScriptSourceFactExtractor } from "./type-script-source-fact-extractor.js";
7
+
8
+ export interface TestReferenceSourceFactExtractorDeps {
9
+ readonly rootDir: string;
10
+ readonly hashingPort: WorldHashingPort;
11
+ readonly sourceRoot?: string;
12
+ }
13
+
14
+ export class TestReferenceSourceFactExtractor extends TypeScriptSourceFactExtractor {
15
+ constructor(deps: TestReferenceSourceFactExtractorDeps) {
16
+ super({
17
+ ...deps,
18
+ sourceKind: "test",
19
+ include: (relativePath) => relativePath.includes("/__tests__/"),
20
+ });
21
+ }
22
+ }
@@ -0,0 +1,112 @@
1
+ // @unit world-model
2
+ // @layer infrastructure
3
+ // @work-item-id WI-289
4
+
5
+ import type { TraceabilityWorldReadDto } from "../../../traceability-model/index.js";
6
+ import { ExtractionDiagnostic } from "../../domain/entities/extraction-diagnostic.js";
7
+ import { WorldNode } from "../../domain/entities/world-node.js";
8
+ import type { WorldHashingPort } from "../../domain/ports/world-hashing-port.js";
9
+ import { CanonicalJsonSerializer } from "../../domain/services/canonical-json-serializer.js";
10
+ import { PathKey } from "../../domain/value-objects/path-key.js";
11
+ import type { TraceabilityDesignFactIndex } from "./design-fact-extraction.js";
12
+
13
+ export interface TraceabilityWorldReadFacadeContract {
14
+ read(): Promise<TraceabilityWorldReadDto>;
15
+ }
16
+
17
+ export interface TraceabilityDesignFactAdapterDeps {
18
+ readonly facade: TraceabilityWorldReadFacadeContract;
19
+ readonly hashingPort: WorldHashingPort;
20
+ readonly serializer?: CanonicalJsonSerializer;
21
+ }
22
+
23
+ const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
24
+
25
+ const optionalPath = (sourcePaths: readonly string[]): PathKey | undefined => {
26
+ if (sourcePaths.length === 0) return undefined;
27
+ try {
28
+ return PathKey.create([...sourcePaths].sort(compareStrings)[0]);
29
+ } catch {
30
+ return undefined;
31
+ }
32
+ };
33
+
34
+ export class TraceabilityDesignFactAdapter {
35
+ private readonly facade: TraceabilityWorldReadFacadeContract;
36
+ private readonly hashingPort: WorldHashingPort;
37
+ private readonly serializer: CanonicalJsonSerializer;
38
+
39
+ constructor(deps: TraceabilityDesignFactAdapterDeps) {
40
+ this.facade = deps.facade;
41
+ this.hashingPort = deps.hashingPort;
42
+ this.serializer = deps.serializer ?? new CanonicalJsonSerializer();
43
+ }
44
+
45
+ async read(): Promise<TraceabilityDesignFactIndex> {
46
+ const dto = await this.facade.read();
47
+ if (dto.schemaVersion !== "phasegate-traceability-world-read/v1") {
48
+ return {
49
+ workItemNodes: [],
50
+ unitIdByDefinitionPath: new Map(),
51
+ storyIdsBySourcePath: new Map(),
52
+ workItemIdByDescriptionPath: new Map(),
53
+ diagnostics: [
54
+ ExtractionDiagnostic.create({
55
+ code: "unsupported-provider-schema",
56
+ payload: { provider: "traceability-model", schemaVersion: dto.schemaVersion },
57
+ }),
58
+ ],
59
+ };
60
+ }
61
+
62
+ const workItemNodes = dto.workItems.map((workItem) =>
63
+ WorldNode.workItem({
64
+ workItemId: workItem.workItemId,
65
+ digest: this.hashingPort.sha256(this.serializer.serialize(workItem)),
66
+ attributes: {
67
+ affects: [...workItem.affects].sort(compareStrings),
68
+ descriptionPath: workItem.descriptionPath,
69
+ legacyIds: [...workItem.legacyIds].sort(compareStrings),
70
+ severity: workItem.severity,
71
+ status: workItem.status,
72
+ type: workItem.type,
73
+ },
74
+ }),
75
+ );
76
+ const unitIdByDefinitionPath = new Map(
77
+ dto.units
78
+ .map((unit) => [unit.definitionPath, unit.unitId] as const)
79
+ .sort((left, right) => compareStrings(left[0], right[0])),
80
+ );
81
+ const storyIdsBySourcePath = new Map<string, readonly string[]>();
82
+ for (const story of dto.stories) {
83
+ const current = storyIdsBySourcePath.get(story.sourcePath) ?? [];
84
+ storyIdsBySourcePath.set(story.sourcePath, [...current, story.storyId].sort(compareStrings));
85
+ }
86
+ const workItemIdByDescriptionPath = new Map(
87
+ dto.workItems
88
+ .map((workItem) => [workItem.descriptionPath, workItem.workItemId] as const)
89
+ .sort((left, right) => compareStrings(left[0], right[0])),
90
+ );
91
+ const diagnostics = dto.diagnostics.map((entry) =>
92
+ ExtractionDiagnostic.create({
93
+ code: "provider-diagnostic",
94
+ path: optionalPath(entry.sourcePaths),
95
+ payload: {
96
+ message: entry.message,
97
+ provider: "traceability-model",
98
+ providerCode: entry.code,
99
+ sourcePaths: [...entry.sourcePaths].sort(compareStrings),
100
+ subjectId: entry.subjectId,
101
+ },
102
+ }),
103
+ );
104
+ return {
105
+ workItemNodes: workItemNodes.sort((left, right) => compareStrings(left.id.toString(), right.id.toString())),
106
+ unitIdByDefinitionPath,
107
+ storyIdsBySourcePath,
108
+ workItemIdByDescriptionPath,
109
+ diagnostics,
110
+ };
111
+ }
112
+ }
@@ -0,0 +1,38 @@
1
+ // @unit world-model
2
+ // @layer infrastructure
3
+ // @work-item-id WI-291
4
+
5
+ import type { TraceabilityWorldReadDto } from "../../../traceability-model/index.js";
6
+ import type { TraceabilityWorldReadFacadeContract } from "./traceability-design-fact-adapter.js";
7
+
8
+ const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
9
+
10
+ const mergePlainDtos = <T>(values: readonly (readonly T[])[]): readonly T[] => {
11
+ const byProjection = new Map<string, T>();
12
+ for (const value of values.flat()) {
13
+ const key = JSON.stringify(value);
14
+ if (!byProjection.has(key)) byProjection.set(key, value);
15
+ }
16
+ return [...byProjection.entries()].sort(([left], [right]) => compareStrings(left, right)).map(([, value]) => value);
17
+ };
18
+
19
+ export class TraceabilityWorldReadFacadeMerger implements TraceabilityWorldReadFacadeContract {
20
+ constructor(private readonly facades: readonly TraceabilityWorldReadFacadeContract[]) {
21
+ if (facades.length === 0) throw new Error("at least one traceability facade is required");
22
+ }
23
+
24
+ async read(): Promise<TraceabilityWorldReadDto> {
25
+ const results = await Promise.all(this.facades.map((facade) => facade.read()));
26
+ const unsupported = results.find((result) => result.schemaVersion !== "phasegate-traceability-world-read/v1");
27
+ if (unsupported) return unsupported;
28
+ return {
29
+ schemaVersion: "phasegate-traceability-world-read/v1",
30
+ units: mergePlainDtos(results.map((result) => result.units)),
31
+ stories: mergePlainDtos(results.map((result) => result.stories)),
32
+ acceptanceCriteria: mergePlainDtos(results.map((result) => result.acceptanceCriteria)),
33
+ workItems: mergePlainDtos(results.map((result) => result.workItems)),
34
+ testReferences: mergePlainDtos(results.map((result) => result.testReferences)),
35
+ diagnostics: mergePlainDtos(results.map((result) => result.diagnostics)),
36
+ };
37
+ }
38
+ }