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,285 @@
1
+ // @unit world-model
2
+ // @layer infrastructure
3
+ // @work-item-id WI-289
4
+
5
+ import { Edge } from "../../domain/entities/edge.js";
6
+ import { ExtractionDiagnostic } from "../../domain/entities/extraction-diagnostic.js";
7
+ import type { WorldNode } from "../../domain/entities/world-node.js";
8
+ import { CanonicalJsonSerializer } from "../../domain/services/canonical-json-serializer.js";
9
+ import { CorpusRole } from "../../domain/value-objects/corpus-role.js";
10
+ import { DeclaredKey } from "../../domain/value-objects/declared-key.js";
11
+ import { PathKey } from "../../domain/value-objects/path-key.js";
12
+ import { WorldNodeId } from "../../domain/value-objects/world-node-id.js";
13
+ import type {
14
+ DesignFactCandidateExtraction,
15
+ DesignFactExtraction,
16
+ DesignFactNodeCandidate,
17
+ DesignFactSource,
18
+ ReflectionReferenceCandidate,
19
+ WorkItemReferenceCandidate,
20
+ } from "./design-fact-extraction.js";
21
+ import type { TraceabilityDesignFactAdapter } from "./traceability-design-fact-adapter.js";
22
+
23
+ const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
24
+
25
+ const groupBy = <T>(values: readonly T[], keyOf: (value: T) => string): ReadonlyMap<string, readonly T[]> => {
26
+ const result = new Map<string, T[]>();
27
+ for (const value of values) {
28
+ const key = keyOf(value);
29
+ const current = result.get(key) ?? [];
30
+ current.push(value);
31
+ result.set(key, current);
32
+ }
33
+ return result;
34
+ };
35
+
36
+ const mergeCandidateExtractions = (
37
+ results: readonly DesignFactCandidateExtraction[],
38
+ ): DesignFactCandidateExtraction => ({
39
+ nodeCandidates: results.flatMap((result) => result.nodeCandidates),
40
+ workItemReferences: results.flatMap((result) => result.workItemReferences),
41
+ reflectionReferences: results.flatMap((result) => result.reflectionReferences),
42
+ diagnostics: results.flatMap((result) => result.diagnostics),
43
+ });
44
+
45
+ export interface DesignCorpusFactExtractorDeps {
46
+ readonly traceabilityAdapter: TraceabilityDesignFactAdapter;
47
+ readonly productExtractor: DesignFactSource;
48
+ readonly proposalExtractor: DesignFactSource;
49
+ readonly adrExtractor: DesignFactSource;
50
+ readonly unitExtractor: DesignFactSource;
51
+ }
52
+
53
+ export class DesignCorpusFactExtractor {
54
+ private readonly deps: DesignCorpusFactExtractorDeps;
55
+ private readonly serializer = new CanonicalJsonSerializer();
56
+
57
+ constructor(deps: DesignCorpusFactExtractorDeps) {
58
+ this.deps = deps;
59
+ }
60
+
61
+ async extract(): Promise<DesignFactExtraction> {
62
+ const ownerIndex = await this.deps.traceabilityAdapter.read();
63
+ const extracted = mergeCandidateExtractions(
64
+ await Promise.all([
65
+ this.deps.productExtractor.extract(ownerIndex),
66
+ this.deps.proposalExtractor.extract(ownerIndex),
67
+ this.deps.adrExtractor.extract(ownerIndex),
68
+ this.deps.unitExtractor.extract(ownerIndex),
69
+ ]),
70
+ );
71
+ const diagnostics = [...ownerIndex.diagnostics, ...extracted.diagnostics];
72
+ const workItemCandidates: DesignFactNodeCandidate[] = ownerIndex.workItemNodes.map((node) => ({
73
+ node,
74
+ path: PathKey.create(String(node.attributes.descriptionPath)),
75
+ }));
76
+ const candidates = [...extracted.nodeCandidates, ...workItemCandidates];
77
+ const collisionPaths = this.findCaseFoldCollisions(candidates, diagnostics);
78
+ const pathAdmitted = candidates.filter((candidate) => !collisionPaths.has(candidate.path.toString()));
79
+ const { admitted, duplicateNodeIds } = this.admitUniqueNodes(pathAdmitted, diagnostics);
80
+ const nodeById = new Map(admitted.map((candidate) => [candidate.node.id.toString(), candidate.node]));
81
+ const edges = [
82
+ ...this.resolveWorkItemReferences(extracted.workItemReferences, nodeById, diagnostics),
83
+ ...this.resolveReflections(extracted.reflectionReferences, nodeById, duplicateNodeIds, diagnostics),
84
+ ];
85
+ const uniqueEdges = [...groupBy(edges, (edge) => this.serializer.stringify(edge.toCanonicalValue())).values()].map(
86
+ (entries) => entries[0],
87
+ );
88
+
89
+ return {
90
+ nodes: admitted
91
+ .map((candidate) => candidate.node)
92
+ .sort((left, right) => compareStrings(left.id.toString(), right.id.toString())),
93
+ edges: uniqueEdges.sort((left, right) =>
94
+ compareStrings(
95
+ this.serializer.stringify(left.toCanonicalValue()),
96
+ this.serializer.stringify(right.toCanonicalValue()),
97
+ ),
98
+ ),
99
+ diagnostics: diagnostics.sort((left, right) =>
100
+ compareStrings(
101
+ this.serializer.stringify(left.toCanonicalValue()),
102
+ this.serializer.stringify(right.toCanonicalValue()),
103
+ ),
104
+ ),
105
+ };
106
+ }
107
+
108
+ private findCaseFoldCollisions(
109
+ candidates: readonly DesignFactNodeCandidate[],
110
+ diagnostics: ExtractionDiagnostic[],
111
+ ): ReadonlySet<string> {
112
+ const pathsByFolded = new Map<string, Set<string>>();
113
+ for (const candidate of candidates) {
114
+ if (candidate.node.id.nodeType === "work-item") continue;
115
+ const path = candidate.path.toString();
116
+ const folded = path.toLowerCase();
117
+ const paths = pathsByFolded.get(folded) ?? new Set<string>();
118
+ paths.add(path);
119
+ pathsByFolded.set(folded, paths);
120
+ }
121
+ const collisions = new Set<string>();
122
+ for (const paths of pathsByFolded.values()) {
123
+ if (paths.size < 2) continue;
124
+ const candidatePaths = [...paths].sort(compareStrings);
125
+ for (const path of candidatePaths) collisions.add(path);
126
+ diagnostics.push(
127
+ ExtractionDiagnostic.create({
128
+ code: "case-fold-path-collision",
129
+ path: PathKey.create(candidatePaths[0]),
130
+ payload: { candidatePaths },
131
+ }),
132
+ );
133
+ }
134
+ return collisions;
135
+ }
136
+
137
+ private admitUniqueNodes(
138
+ candidates: readonly DesignFactNodeCandidate[],
139
+ diagnostics: ExtractionDiagnostic[],
140
+ ): {
141
+ readonly admitted: readonly DesignFactNodeCandidate[];
142
+ readonly duplicateNodeIds: ReadonlySet<string>;
143
+ } {
144
+ const admitted: DesignFactNodeCandidate[] = [];
145
+ const duplicateNodeIds = new Set<string>();
146
+ for (const [nodeId, entries] of groupBy(candidates, (candidate) => candidate.node.id.toString())) {
147
+ if (entries.length === 1) {
148
+ admitted.push(entries[0]);
149
+ continue;
150
+ }
151
+ duplicateNodeIds.add(nodeId);
152
+ const candidatePaths = [...new Set(entries.map((entry) => entry.path.toString()))].sort(compareStrings);
153
+ diagnostics.push(
154
+ ExtractionDiagnostic.create({
155
+ code: "duplicate-node-id",
156
+ nodeId: entries[0].node.id,
157
+ path: PathKey.create(candidatePaths[0]),
158
+ payload: { candidatePaths, candidates: entries.length },
159
+ }),
160
+ );
161
+ }
162
+ return { admitted, duplicateNodeIds };
163
+ }
164
+
165
+ private resolveWorkItemReferences(
166
+ references: readonly WorkItemReferenceCandidate[],
167
+ nodeById: ReadonlyMap<string, WorldNode>,
168
+ diagnostics: ExtractionDiagnostic[],
169
+ ): readonly Edge[] {
170
+ const result: Edge[] = [];
171
+ for (const reference of references) {
172
+ const source = nodeById.get(reference.sourceNodeId.toString());
173
+ if (!source) continue;
174
+ const workItemId = WorldNodeId.workItem(reference.workItemId);
175
+ if (!nodeById.has(workItemId.toString())) {
176
+ diagnostics.push(
177
+ ExtractionDiagnostic.create({
178
+ code: "unknown-work-item-reference",
179
+ nodeId: reference.sourceNodeId,
180
+ path: reference.path,
181
+ line: reference.line,
182
+ payload: { workItemId: reference.workItemId },
183
+ }),
184
+ );
185
+ continue;
186
+ }
187
+ const role = reference.role.toString();
188
+ const edge =
189
+ role === "product"
190
+ ? Edge.create({
191
+ edgeType: DeclaredKey.create("reflected-in"),
192
+ from: workItemId,
193
+ to: reference.sourceNodeId,
194
+ qualifier: { line: reference.line, path: reference.path.toString() },
195
+ })
196
+ : Edge.create({
197
+ edgeType: DeclaredKey.create(role === "inception" ? "proposed-by" : "traces-to"),
198
+ from: reference.sourceNodeId,
199
+ to: workItemId,
200
+ qualifier: { line: reference.line, path: reference.path.toString() },
201
+ });
202
+ result.push(edge);
203
+ }
204
+ return result;
205
+ }
206
+
207
+ private resolveReflections(
208
+ references: readonly ReflectionReferenceCandidate[],
209
+ nodeById: ReadonlyMap<string, WorldNode>,
210
+ duplicateNodeIds: ReadonlySet<string>,
211
+ diagnostics: ExtractionDiagnostic[],
212
+ ): readonly Edge[] {
213
+ const result: Edge[] = [];
214
+ const groups = groupBy(
215
+ references,
216
+ (reference) => `${reference.sourceFragmentId.toString()}\u0000${reference.target}`,
217
+ );
218
+ for (const entries of groups.values()) {
219
+ const reference = entries[0];
220
+ if (entries.length > 1) {
221
+ diagnostics.push(
222
+ ExtractionDiagnostic.create({
223
+ code: "duplicate-reflection-target",
224
+ nodeId: reference.sourceFragmentId,
225
+ path: reference.path,
226
+ line: reference.line,
227
+ payload: { candidates: entries.length, target: reference.target },
228
+ }),
229
+ );
230
+ continue;
231
+ }
232
+ if (!nodeById.has(reference.sourceFragmentId.toString())) continue;
233
+ const match = /^inception:(.+)$/.exec(reference.target);
234
+ let targetId: WorldNodeId;
235
+ try {
236
+ if (!match) throw new Error("invalid role");
237
+ targetId = WorldNodeId.fragment(CorpusRole.inception(), DeclaredKey.create(match[1]));
238
+ } catch {
239
+ diagnostics.push(
240
+ ExtractionDiagnostic.create({
241
+ code: "invalid-reflection-target",
242
+ nodeId: reference.sourceFragmentId,
243
+ path: reference.path,
244
+ line: reference.line,
245
+ payload: { target: reference.target },
246
+ }),
247
+ );
248
+ continue;
249
+ }
250
+ if (duplicateNodeIds.has(targetId.toString())) {
251
+ diagnostics.push(
252
+ ExtractionDiagnostic.create({
253
+ code: "ambiguous-reflection-target",
254
+ nodeId: reference.sourceFragmentId,
255
+ path: reference.path,
256
+ line: reference.line,
257
+ payload: { target: reference.target },
258
+ }),
259
+ );
260
+ continue;
261
+ }
262
+ if (!nodeById.has(targetId.toString())) {
263
+ diagnostics.push(
264
+ ExtractionDiagnostic.create({
265
+ code: "missing-reflection-target",
266
+ nodeId: reference.sourceFragmentId,
267
+ path: reference.path,
268
+ line: reference.line,
269
+ payload: { target: reference.target },
270
+ }),
271
+ );
272
+ continue;
273
+ }
274
+ result.push(
275
+ Edge.create({
276
+ edgeType: DeclaredKey.create("reflected-as"),
277
+ from: targetId,
278
+ to: reference.sourceFragmentId,
279
+ qualifier: { line: reference.line, path: reference.path.toString() },
280
+ }),
281
+ );
282
+ }
283
+ return result;
284
+ }
285
+ }
@@ -0,0 +1,63 @@
1
+ // @unit world-model
2
+ // @layer infrastructure
3
+ // @work-item-id WI-289
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
+ import type { CorpusRole } from "../../domain/value-objects/corpus-role.js";
9
+ import type { PathKey } from "../../domain/value-objects/path-key.js";
10
+ import type { WorldNodeId } from "../../domain/value-objects/world-node-id.js";
11
+
12
+ export interface DesignFactNodeCandidate {
13
+ readonly node: WorldNode;
14
+ readonly path: PathKey;
15
+ readonly line?: number;
16
+ }
17
+
18
+ export interface WorkItemReferenceCandidate {
19
+ readonly sourceNodeId: WorldNodeId;
20
+ readonly workItemId: string;
21
+ readonly role: CorpusRole;
22
+ readonly path: PathKey;
23
+ readonly line: number;
24
+ }
25
+
26
+ export interface ReflectionReferenceCandidate {
27
+ readonly sourceFragmentId: WorldNodeId;
28
+ readonly target: string;
29
+ readonly path: PathKey;
30
+ readonly line: number;
31
+ }
32
+
33
+ export interface DesignFactCandidateExtraction {
34
+ readonly nodeCandidates: readonly DesignFactNodeCandidate[];
35
+ readonly workItemReferences: readonly WorkItemReferenceCandidate[];
36
+ readonly reflectionReferences: readonly ReflectionReferenceCandidate[];
37
+ readonly diagnostics: readonly ExtractionDiagnostic[];
38
+ }
39
+
40
+ export interface DesignFactExtraction {
41
+ readonly nodes: readonly WorldNode[];
42
+ readonly edges: readonly Edge[];
43
+ readonly diagnostics: readonly ExtractionDiagnostic[];
44
+ }
45
+
46
+ export interface TraceabilityDesignFactIndex {
47
+ readonly workItemNodes: readonly WorldNode[];
48
+ readonly unitIdByDefinitionPath: ReadonlyMap<string, string>;
49
+ readonly storyIdsBySourcePath: ReadonlyMap<string, readonly string[]>;
50
+ readonly workItemIdByDescriptionPath: ReadonlyMap<string, string>;
51
+ readonly diagnostics: readonly ExtractionDiagnostic[];
52
+ }
53
+
54
+ export interface DesignFactSource {
55
+ extract(ownerIndex: TraceabilityDesignFactIndex): Promise<DesignFactCandidateExtraction>;
56
+ }
57
+
58
+ export const EMPTY_DESIGN_FACT_CANDIDATES: DesignFactCandidateExtraction = Object.freeze({
59
+ nodeCandidates: Object.freeze([]),
60
+ workItemReferences: Object.freeze([]),
61
+ reflectionReferences: Object.freeze([]),
62
+ diagnostics: Object.freeze([]),
63
+ });
@@ -0,0 +1,37 @@
1
+ // @unit world-model
2
+ // @layer infrastructure
3
+ // @work-item-id WI-295, WI-296
4
+
5
+ import { mkdir, rename, rm, writeFile } from "node:fs/promises";
6
+ import path from "node:path";
7
+ import type { ObligationReportWriterPort } from "../../application/ports/obligation-report-writer-port.js";
8
+ import { PathKey } from "../../domain/value-objects/path-key.js";
9
+
10
+ let temporarySequence = 0;
11
+
12
+ export interface FileSystemObligationReportWriterOptions {
13
+ readonly rootDir: string;
14
+ readonly reportPath?: string;
15
+ }
16
+
17
+ export class FileSystemObligationReportWriterAdapter implements ObligationReportWriterPort {
18
+ private readonly reportPath: string;
19
+
20
+ constructor(private readonly options: FileSystemObligationReportWriterOptions) {
21
+ this.reportPath = options.reportPath ?? ".harness/world-obligations.json";
22
+ }
23
+
24
+ async write(bytes: Uint8Array, reportPath?: string): Promise<void> {
25
+ const resolvedPath = PathKey.create(reportPath ?? this.reportPath).toString();
26
+ const absolutePath = path.join(this.options.rootDir, resolvedPath);
27
+ await mkdir(path.dirname(absolutePath), { recursive: true });
28
+ const temporaryPath = `${absolutePath}.${process.pid}-${temporarySequence++}.tmp`;
29
+ try {
30
+ await writeFile(temporaryPath, bytes, { flag: "wx" });
31
+ await rename(temporaryPath, absolutePath);
32
+ } catch (error) {
33
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
34
+ throw error;
35
+ }
36
+ }
37
+ }