phasegate 0.229.0 → 0.264.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/CHANGELOG.md +73 -1
  2. package/README.ja.md +31 -1
  3. package/README.md +30 -0
  4. package/docs/ADR/031-world-model-ownership-and-corpus-lifecycle.md +155 -0
  5. package/docs/ADR/032-world-node-identity.md +198 -0
  6. package/docs/ADR/033-world-snapshot-canonicalization.md +246 -0
  7. package/docs/ADR/034-world-constraint-semantics.md +218 -0
  8. package/docs/ADR/035-world-adoption-baseline-and-waiver.md +341 -0
  9. package/docs/ADR/036-world-model-and-doc-freshness.md +169 -0
  10. package/docs/ADR/037-world-cli-and-output-contract.md +398 -0
  11. package/docs/contracts/attestation-v2.schema.json +110 -0
  12. package/docs/contracts/requirement-test-matrix.schema.json +15 -0
  13. package/docs/contracts/world-baseline.schema.json +35 -0
  14. package/docs/contracts/world-constraints.schema.json +56 -0
  15. package/docs/contracts/world-debts.schema.json +32 -0
  16. package/docs/contracts/world-obligation-report.schema.json +259 -0
  17. package/docs/contracts/world-waivers.schema.json +32 -0
  18. package/docs/guide/cli-reference.md +45 -0
  19. package/docs/guide/configuration.md +56 -0
  20. package/docs/templates/ci/aidlc-gate.yml +42 -2
  21. package/package.json +1 -1
  22. package/scripts/harness/agent-integration/application/dto/open-world-obligations-context-dto.ts +30 -0
  23. package/scripts/harness/agent-integration/application/ports/world-obligations-query-port.ts +30 -0
  24. package/scripts/harness/agent-integration/application/usecases/get-open-world-obligations-context-usecase.ts +66 -0
  25. package/scripts/harness/agent-integration/infrastructure/adapters/world-model-open-obligations-query-adapter.ts +58 -0
  26. package/scripts/harness/agent-integration/presentation/session-start-hook.ts +37 -1
  27. package/scripts/harness/agent-integration/presentation/world-obligations-session-context.ts +60 -0
  28. package/scripts/harness/attestation/application/dto/attestation-document.ts +17 -4
  29. package/scripts/harness/attestation/application/mappers/attestation-record-mapper.ts +55 -4
  30. package/scripts/harness/attestation/application/ports/sha256-capability.ts +25 -0
  31. package/scripts/harness/attestation/application/ports/world-snapshot-root-provider.ts +10 -0
  32. package/scripts/harness/attestation/application/usecases/produce-attestation-usecase.ts +33 -7
  33. package/scripts/harness/attestation/composition-root.ts +19 -4
  34. package/scripts/harness/attestation/domain/entities/attestation-record.ts +37 -2
  35. package/scripts/harness/attestation/index.ts +13 -2
  36. package/scripts/harness/attestation/infrastructure/adapters/node-crypto-content-hasher-adapter.ts +6 -6
  37. package/scripts/harness/attestation/infrastructure/adapters/node-crypto-sha256-capability.ts +19 -0
  38. package/scripts/harness/attestation/presentation/handlers/attest-handler.ts +5 -2
  39. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +35 -8
  40. package/scripts/harness/config-foundation/application/mappers/world-model-config-mapper.ts +15 -0
  41. package/scripts/harness/config-foundation/domain/harness-config.ts +67 -87
  42. package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +77 -88
  43. package/scripts/harness/config-foundation/domain/value-objects/world-config.ts +198 -0
  44. package/scripts/harness/config-foundation/index.ts +14 -15
  45. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +24 -0
  46. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +24 -0
  47. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +24 -0
  48. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +83 -0
  49. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +83 -0
  50. package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +18 -4
  51. package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +4 -1
  52. package/scripts/harness/integrations/pre-commit.ts +169 -27
  53. package/scripts/harness/main.ts +276 -126
  54. package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +11 -0
  55. package/scripts/harness/nyquist-validation/application/usecases/check-ac-coverage-gate-usecase.ts +6 -0
  56. package/scripts/harness/nyquist-validation/application/usecases/generate-requirement-test-matrix-usecase.ts +8 -4
  57. package/scripts/harness/nyquist-validation/domain/entities/story-mapping.ts +29 -2
  58. package/scripts/harness/nyquist-validation/domain/services/ac-coverage-gate-policy.ts +28 -0
  59. package/scripts/harness/nyquist-validation/infrastructure/adapters/markdown-requirement-source-adapter.ts +71 -4
  60. package/scripts/harness/nyquist-validation/infrastructure/schema/matrix-schema-loader.ts +4 -0
  61. package/scripts/harness/traceability-model/application/dto/changed-design-fragment-dto.ts +31 -0
  62. package/scripts/harness/traceability-model/application/dto/traceability-world-read-dto.ts +67 -0
  63. package/scripts/harness/traceability-model/application/facades/design-change-read-facade.ts +14 -0
  64. package/scripts/harness/traceability-model/application/facades/traceability-world-read-facade.ts +372 -0
  65. package/scripts/harness/traceability-model/application/ports/staged-design-change-source-port.ts +9 -0
  66. package/scripts/harness/traceability-model/application/ports/traceability-world-read-source-port.ts +61 -0
  67. package/scripts/harness/traceability-model/composition-root.ts +26 -5
  68. package/scripts/harness/traceability-model/index.ts +26 -6
  69. package/scripts/harness/traceability-model/infrastructure/adapters/file-system-traceability-world-read-adapter.ts +223 -0
  70. package/scripts/harness/traceability-model/infrastructure/adapters/git-staged-design-change-adapter.ts +155 -0
  71. package/scripts/harness/traceability-model/infrastructure/parsers/story-catalog-parser.ts +86 -8
  72. package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +31 -0
  73. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +34 -0
  74. package/scripts/harness/validator-system/composition-root.ts +21 -6
  75. package/scripts/harness/validator-system/domain/ports/world-constraint-admission-policy-port.ts +28 -0
  76. package/scripts/harness/validator-system/domain/ports/world-constraint-rederivation-policy-port.ts +11 -0
  77. package/scripts/harness/validator-system/domain/services/design-change-declaration-policy.ts +75 -0
  78. package/scripts/harness/validator-system/domain/services/world-constraint-admission-service.ts +78 -0
  79. package/scripts/harness/validator-system/domain/services/world-constraint-rederivation-service.ts +76 -0
  80. package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +4 -0
  81. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +38 -16
  82. package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-admission-adapter.ts +56 -0
  83. package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-rederivation-adapter.ts +56 -0
  84. package/scripts/harness/world-model/application/dto/pinned-design-endpoint-dto.ts +19 -0
  85. package/scripts/harness/world-model/application/dto/world-inspection-dto.ts +43 -0
  86. package/scripts/harness/world-model/application/dto/world-obligation-report-dto.ts +54 -0
  87. package/scripts/harness/world-model/application/dto/world-resolved-config-input.ts +127 -0
  88. package/scripts/harness/world-model/application/dto/world-snapshot-root-dto.ts +8 -0
  89. package/scripts/harness/world-model/application/facades/pinned-design-endpoint-facade.ts +63 -0
  90. package/scripts/harness/world-model/application/facades/world-snapshot-root-facade.ts +19 -0
  91. package/scripts/harness/world-model/application/ports/obligation-report-writer-port.ts +7 -0
  92. package/scripts/harness/world-model/application/ports/world-control-declaration-repository-port.ts +57 -0
  93. package/scripts/harness/world-model/application/ports/world-fact-source-port.ts +17 -0
  94. package/scripts/harness/world-model/application/usecases/build-snapshot-use-case.ts +111 -0
  95. package/scripts/harness/world-model/application/usecases/derive-obligations-use-case.ts +155 -0
  96. package/scripts/harness/world-model/application/usecases/derive-world-obligations-use-case.ts +146 -0
  97. package/scripts/harness/world-model/application/usecases/inspect-world-use-case.ts +74 -0
  98. package/scripts/harness/world-model/application/usecases/pin-constraint-endpoint-use-case.ts +139 -0
  99. package/scripts/harness/world-model/composition-root.ts +247 -0
  100. package/scripts/harness/world-model/domain/entities/constraint-record.ts +148 -0
  101. package/scripts/harness/world-model/domain/entities/control-declarations.ts +274 -0
  102. package/scripts/harness/world-model/domain/entities/edge.ts +41 -0
  103. package/scripts/harness/world-model/domain/entities/extraction-diagnostic.ts +60 -0
  104. package/scripts/harness/world-model/domain/entities/snapshot.ts +75 -0
  105. package/scripts/harness/world-model/domain/entities/world-node.ts +210 -0
  106. package/scripts/harness/world-model/domain/ports/world-hashing-port.ts +8 -0
  107. package/scripts/harness/world-model/domain/services/canonical-json-serializer.ts +113 -0
  108. package/scripts/harness/world-model/domain/services/constraint-evaluator.ts +486 -0
  109. package/scripts/harness/world-model/domain/services/obligation-derivation-service.ts +199 -0
  110. package/scripts/harness/world-model/domain/services/policy-inputs-digest-deriver.ts +66 -0
  111. package/scripts/harness/world-model/domain/services/snapshot-root-deriver.ts +218 -0
  112. package/scripts/harness/world-model/domain/services/text-content-normalizer.ts +30 -0
  113. package/scripts/harness/world-model/domain/services/violation-fingerprint-deriver.ts +183 -0
  114. package/scripts/harness/world-model/domain/value-objects/artifact-kind.ts +58 -0
  115. package/scripts/harness/world-model/domain/value-objects/change-provenance.ts +137 -0
  116. package/scripts/harness/world-model/domain/value-objects/corpus-role.ts +57 -0
  117. package/scripts/harness/world-model/domain/value-objects/declared-key.ts +35 -0
  118. package/scripts/harness/world-model/domain/value-objects/evaluation-id.ts +41 -0
  119. package/scripts/harness/world-model/domain/value-objects/explicit-constraint-relation.ts +53 -0
  120. package/scripts/harness/world-model/domain/value-objects/explicit-node-alias.ts +30 -0
  121. package/scripts/harness/world-model/domain/value-objects/node-pin.ts +34 -0
  122. package/scripts/harness/world-model/domain/value-objects/path-key.ts +103 -0
  123. package/scripts/harness/world-model/domain/value-objects/sha256-digest.ts +39 -0
  124. package/scripts/harness/world-model/domain/value-objects/violation-fingerprint.ts +36 -0
  125. package/scripts/harness/world-model/domain/value-objects/wcr-rule-id.ts +59 -0
  126. package/scripts/harness/world-model/domain/value-objects/world-node-id.ts +222 -0
  127. package/scripts/harness/world-model/index.ts +33 -0
  128. package/scripts/harness/world-model/infrastructure/adapters/adr-fact-extractor.ts +27 -0
  129. package/scripts/harness/world-model/infrastructure/adapters/assembled-world-fact-source.ts +17 -0
  130. package/scripts/harness/world-model/infrastructure/adapters/attestation-fact-extractor.ts +323 -0
  131. package/scripts/harness/world-model/infrastructure/adapters/attestation-sha256-world-hashing-adapter.ts +15 -0
  132. package/scripts/harness/world-model/infrastructure/adapters/composite-design-fact-source.ts +23 -0
  133. package/scripts/harness/world-model/infrastructure/adapters/design-corpus-fact-extractor.ts +285 -0
  134. package/scripts/harness/world-model/infrastructure/adapters/design-fact-extraction.ts +63 -0
  135. package/scripts/harness/world-model/infrastructure/adapters/file-system-obligation-report-writer-adapter.ts +37 -0
  136. package/scripts/harness/world-model/infrastructure/adapters/file-system-world-control-repository-adapters.ts +332 -0
  137. package/scripts/harness/world-model/infrastructure/adapters/integrity-manifest-fact-extractor.ts +101 -0
  138. package/scripts/harness/world-model/infrastructure/adapters/json-fact-extractor-support.ts +151 -0
  139. package/scripts/harness/world-model/infrastructure/adapters/markdown-design-fact-extractor.ts +493 -0
  140. package/scripts/harness/world-model/infrastructure/adapters/matrix-fact-extractor.ts +283 -0
  141. package/scripts/harness/world-model/infrastructure/adapters/product-fact-extractor.ts +29 -0
  142. package/scripts/harness/world-model/infrastructure/adapters/proposal-fact-extractor.ts +29 -0
  143. package/scripts/harness/world-model/infrastructure/adapters/runtime-fact-extraction.ts +19 -0
  144. package/scripts/harness/world-model/infrastructure/adapters/source-metadata-fact-extractor.ts +22 -0
  145. package/scripts/harness/world-model/infrastructure/adapters/test-reference-source-fact-extractor.ts +22 -0
  146. package/scripts/harness/world-model/infrastructure/adapters/traceability-design-fact-adapter.ts +112 -0
  147. package/scripts/harness/world-model/infrastructure/adapters/traceability-world-read-facade-merger.ts +38 -0
  148. package/scripts/harness/world-model/infrastructure/adapters/type-script-source-fact-extractor.ts +236 -0
  149. package/scripts/harness/world-model/infrastructure/adapters/unit-fact-extractor.ts +71 -0
  150. package/scripts/harness/world-model/infrastructure/adapters/world-control-declaration-mapper.ts +383 -0
  151. package/scripts/harness/world-model/presentation/cli/world-command-support.ts +60 -0
  152. package/scripts/harness/world-model/presentation/cli/world-derive-command-handler.ts +109 -0
  153. package/scripts/harness/world-model/presentation/cli/world-inspect-command-handler.ts +194 -0
  154. package/scripts/harness/world-model/presentation/cli/world-pin-command-handler.ts +100 -0
@@ -0,0 +1,493 @@
1
+ // @unit world-model
2
+ // @layer infrastructure
3
+ // @work-item-id WI-289
4
+
5
+ import type { Dirent } from "node:fs";
6
+ import { readdir, readFile } from "node:fs/promises";
7
+ import path from "node:path";
8
+ import { ExtractionDiagnostic } from "../../domain/entities/extraction-diagnostic.js";
9
+ import { WorldNode } from "../../domain/entities/world-node.js";
10
+ import type { WorldHashingPort } from "../../domain/ports/world-hashing-port.js";
11
+ import { TextContentNormalizer } from "../../domain/services/text-content-normalizer.js";
12
+ import { ArtifactKind } from "../../domain/value-objects/artifact-kind.js";
13
+ import type { CorpusRole } from "../../domain/value-objects/corpus-role.js";
14
+ import { DeclaredKey } from "../../domain/value-objects/declared-key.js";
15
+ import { PathKey } from "../../domain/value-objects/path-key.js";
16
+ import type {
17
+ DesignFactCandidateExtraction,
18
+ DesignFactNodeCandidate,
19
+ ReflectionReferenceCandidate,
20
+ TraceabilityDesignFactIndex,
21
+ WorkItemReferenceCandidate,
22
+ } from "./design-fact-extraction.js";
23
+
24
+ const FRAGMENT_MARKER = /^\s*<!--\s*@world-fragment-id\s+(\S+)\s*-->\s*$/;
25
+ const REFLECTION_MARKER = /^\s*<!--\s*@world-reflects\s+(\S+)\s*-->\s*$/;
26
+ const WORK_ITEM_MARKER = /^\s*<!--\s*@work-item-id\s+(.+?)\s*-->\s*$/;
27
+ const BARE_WORK_ITEM_MARKER = /^\s*@work-item-id\s+(.+?)\s*$/;
28
+ const MIGRATION_MARKER = /^\s*<!--\s*@world-fragment-migration\s+complete\s*-->\s*$/;
29
+ const HEADING = /^(#{1,6})\s+(.+?)\s*#*\s*$/;
30
+ const FENCE = /^\s*(`{3,}|~{3,})/;
31
+
32
+ const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
33
+
34
+ interface ParsedLine {
35
+ readonly text: string;
36
+ readonly line: number;
37
+ readonly inFence: boolean;
38
+ }
39
+
40
+ interface FragmentPrelude {
41
+ readonly markerIndex: number;
42
+ readonly preludeStart: number;
43
+ readonly preludeEnd: number;
44
+ readonly headingIndex: number;
45
+ readonly key: string;
46
+ readonly headingLevel: number;
47
+ readonly headingText: string;
48
+ readonly workItemLines: readonly { readonly value: string; readonly line: number }[];
49
+ readonly reflectionLines: readonly { readonly value: string; readonly line: number }[];
50
+ }
51
+
52
+ export interface MarkdownDesignFactInput {
53
+ readonly path: PathKey;
54
+ readonly role: CorpusRole;
55
+ readonly bytes: Uint8Array;
56
+ readonly unitId?: string;
57
+ readonly storyIds?: readonly string[];
58
+ readonly ownerWorkItemId?: string;
59
+ }
60
+
61
+ export interface MarkdownDesignFactExtractorDeps {
62
+ readonly hashingPort: WorldHashingPort;
63
+ readonly normalizer?: TextContentNormalizer;
64
+ }
65
+
66
+ const toParsedLines = (text: string): readonly ParsedLine[] => {
67
+ const result: ParsedLine[] = [];
68
+ let activeFence: "`" | "~" | null = null;
69
+ const lines = text.split("\n");
70
+ for (let index = 0; index < lines.length; index += 1) {
71
+ const currentFence = activeFence;
72
+ const fence = FENCE.exec(lines[index]);
73
+ result.push({ text: lines[index], line: index + 1, inFence: currentFence !== null });
74
+ if (!fence) continue;
75
+ const character = fence[1][0] as "`" | "~";
76
+ if (activeFence === null) activeFence = character;
77
+ else if (activeFence === character) activeFence = null;
78
+ }
79
+ return result;
80
+ };
81
+
82
+ const isAllowedPreludeLine = (line: ParsedLine): boolean =>
83
+ !line.inFence &&
84
+ (FRAGMENT_MARKER.test(line.text) || REFLECTION_MARKER.test(line.text) || WORK_ITEM_MARKER.test(line.text));
85
+
86
+ const looksLikeMarkerDeclaration = (line: string, marker: string): boolean =>
87
+ new RegExp(`^\\s*(?:<!--\\s*)?${marker}`).test(line);
88
+
89
+ const diagnostic = (
90
+ code: string,
91
+ pathKey: PathKey,
92
+ line: number | undefined,
93
+ payload: Record<string, string | number | boolean | null | readonly string[]>,
94
+ ): ExtractionDiagnostic => ExtractionDiagnostic.create({ code, path: pathKey, line, payload });
95
+
96
+ const parseWorkItemIds = (value: string): readonly string[] => [...new Set(value.match(/\bWI-\d+\b/g) ?? [])];
97
+
98
+ const findFrontmatterEnd = (lines: readonly ParsedLine[]): number | null => {
99
+ if (lines[0]?.text !== "---") return null;
100
+ for (let index = 1; index < lines.length; index += 1) {
101
+ if (lines[index].text === "---") return index;
102
+ }
103
+ return null;
104
+ };
105
+
106
+ export class MarkdownDesignFactExtractor {
107
+ private readonly hashingPort: WorldHashingPort;
108
+ private readonly normalizer: TextContentNormalizer;
109
+
110
+ constructor(deps: MarkdownDesignFactExtractorDeps) {
111
+ this.hashingPort = deps.hashingPort;
112
+ this.normalizer = deps.normalizer ?? new TextContentNormalizer();
113
+ }
114
+
115
+ extractFile(input: MarkdownDesignFactInput): DesignFactCandidateExtraction {
116
+ const normalized = this.normalizer.normalize(input.bytes);
117
+ if (!normalized.ok) {
118
+ return {
119
+ nodeCandidates: [],
120
+ workItemReferences: [],
121
+ reflectionReferences: [],
122
+ diagnostics: [
123
+ ExtractionDiagnostic.create({
124
+ code: normalized.diagnostic.code,
125
+ path: input.path,
126
+ payload: normalized.diagnostic.payload,
127
+ }),
128
+ ],
129
+ };
130
+ }
131
+
132
+ const lines = toParsedLines(normalized.text);
133
+ const diagnostics: ExtractionDiagnostic[] = [];
134
+ const preludes = this.parseFragmentPreludes(lines, input.path, diagnostics);
135
+ const admittedPreludes = preludes
136
+ .map((prelude) => {
137
+ try {
138
+ return { prelude, key: DeclaredKey.create(prelude.key) };
139
+ } catch {
140
+ diagnostics.push(
141
+ diagnostic("malformed-fragment-marker", input.path, lines[prelude.markerIndex].line, {
142
+ raw: lines[prelude.markerIndex].text,
143
+ }),
144
+ );
145
+ return null;
146
+ }
147
+ })
148
+ .filter((entry): entry is NonNullable<typeof entry> => entry !== null);
149
+ const completion = this.parseMigrationCompletion(lines, input.path, admittedPreludes.length, diagnostics);
150
+ const migrationState = completion ? "explicit" : preludes.length > 0 ? "mixed" : "whole-file";
151
+ const artifactDigest = this.hashingPort.sha256(normalized.bytes);
152
+ const artifact = WorldNode.artifact({
153
+ artifactKind: ArtifactKind.designDocument(),
154
+ corpusRole: input.role,
155
+ path: input.path,
156
+ digest: artifactDigest,
157
+ attributes: {
158
+ migrationState,
159
+ ...(input.unitId === undefined ? {} : { unitId: input.unitId }),
160
+ ...(input.storyIds === undefined ? {} : { storyIds: [...input.storyIds].sort(compareStrings) }),
161
+ ...(input.ownerWorkItemId === undefined ? {} : { ownerWorkItemId: input.ownerWorkItemId }),
162
+ },
163
+ });
164
+ const nodeCandidates: DesignFactNodeCandidate[] = [{ node: artifact, path: input.path }];
165
+ const workItemReferences: WorkItemReferenceCandidate[] = [];
166
+ const reflectionReferences: ReflectionReferenceCandidate[] = [];
167
+ const boundWorkItemLines = new Set<number>();
168
+
169
+ for (let index = 0; index < admittedPreludes.length; index += 1) {
170
+ const { prelude, key } = admittedPreludes[index];
171
+ const nextHeadingIndex = admittedPreludes[index + 1]?.prelude.headingIndex ?? lines.length;
172
+ const fragmentBytes = new TextEncoder().encode(
173
+ lines
174
+ .slice(prelude.headingIndex, nextHeadingIndex)
175
+ .map((line) => line.text)
176
+ .join("\n"),
177
+ );
178
+ const fragment = WorldNode.explicitFragment({
179
+ corpusRole: input.role,
180
+ declaredKey: key,
181
+ artifactId: artifact.id,
182
+ digest: this.hashingPort.sha256(fragmentBytes),
183
+ attributes: {
184
+ locator: {
185
+ endLine: nextHeadingIndex,
186
+ headingLevel: prelude.headingLevel,
187
+ headingLine: prelude.headingIndex + 1,
188
+ headingText: prelude.headingText,
189
+ markerLine: prelude.markerIndex + 1,
190
+ startLine: prelude.headingIndex + 1,
191
+ },
192
+ migrationState,
193
+ },
194
+ });
195
+ nodeCandidates.push({ node: fragment, path: input.path, line: prelude.markerIndex + 1 });
196
+
197
+ for (const declaration of prelude.workItemLines) {
198
+ boundWorkItemLines.add(declaration.line);
199
+ this.appendWorkItemReferences(
200
+ declaration.value,
201
+ declaration.line,
202
+ fragment.id,
203
+ input,
204
+ workItemReferences,
205
+ diagnostics,
206
+ );
207
+ }
208
+ for (const declaration of prelude.reflectionLines) {
209
+ if (input.role.toString() !== "product") {
210
+ diagnostics.push(
211
+ diagnostic("invalid-reflection-source-role", input.path, declaration.line, {
212
+ role: input.role.toString(),
213
+ target: declaration.value,
214
+ }),
215
+ );
216
+ continue;
217
+ }
218
+ reflectionReferences.push({
219
+ sourceFragmentId: fragment.id,
220
+ target: declaration.value,
221
+ path: input.path,
222
+ line: declaration.line,
223
+ });
224
+ }
225
+ }
226
+
227
+ if (!completion) {
228
+ const legacy = WorldNode.legacyFragment({
229
+ artifactKind: ArtifactKind.designDocument(),
230
+ corpusRole: input.role,
231
+ path: input.path,
232
+ artifactId: artifact.id,
233
+ digest: artifactDigest,
234
+ attributes: {
235
+ locator: { endLine: lines.length, startLine: 1 },
236
+ migrationState,
237
+ },
238
+ });
239
+ nodeCandidates.push({ node: legacy, path: input.path, line: 1 });
240
+ }
241
+
242
+ for (const line of lines) {
243
+ if (line.inFence || boundWorkItemLines.has(line.line)) continue;
244
+ const match = WORK_ITEM_MARKER.exec(line.text) ?? BARE_WORK_ITEM_MARKER.exec(line.text);
245
+ if (match) {
246
+ this.appendWorkItemReferences(match[1], line.line, artifact.id, input, workItemReferences, diagnostics);
247
+ } else if (looksLikeMarkerDeclaration(line.text, "@work-item-id")) {
248
+ diagnostics.push(diagnostic("malformed-work-item-reference", input.path, line.line, { raw: line.text }));
249
+ }
250
+ if (looksLikeMarkerDeclaration(line.text, "@world-reflects")) {
251
+ if (!REFLECTION_MARKER.test(line.text)) {
252
+ diagnostics.push(diagnostic("malformed-reflection-marker", input.path, line.line, { raw: line.text }));
253
+ } else {
254
+ const bound = preludes.some(
255
+ (prelude) => line.line >= prelude.preludeStart + 1 && line.line <= prelude.preludeEnd + 1,
256
+ );
257
+ if (!bound)
258
+ diagnostics.push(diagnostic("orphan-reflection-marker", input.path, line.line, { raw: line.text }));
259
+ }
260
+ }
261
+ }
262
+
263
+ return {
264
+ nodeCandidates,
265
+ workItemReferences,
266
+ reflectionReferences,
267
+ diagnostics: diagnostics.sort((left, right) => {
268
+ const code = compareStrings(left.code, right.code);
269
+ if (code !== 0) return code;
270
+ return (left.line ?? 0) - (right.line ?? 0);
271
+ }),
272
+ };
273
+ }
274
+
275
+ private parseFragmentPreludes(
276
+ lines: readonly ParsedLine[],
277
+ pathKey: PathKey,
278
+ diagnostics: ExtractionDiagnostic[],
279
+ ): readonly FragmentPrelude[] {
280
+ const result: FragmentPrelude[] = [];
281
+ const processed = new Set<number>();
282
+ for (let index = 0; index < lines.length; index += 1) {
283
+ const line = lines[index];
284
+ if (line.inFence) continue;
285
+ if (looksLikeMarkerDeclaration(line.text, "@world-fragment-id") && !FRAGMENT_MARKER.test(line.text)) {
286
+ diagnostics.push(diagnostic("malformed-fragment-marker", pathKey, line.line, { raw: line.text }));
287
+ continue;
288
+ }
289
+ const marker = FRAGMENT_MARKER.exec(line.text);
290
+ if (!marker || processed.has(index)) continue;
291
+
292
+ let start = index;
293
+ while (start > 0 && isAllowedPreludeLine(lines[start - 1])) start -= 1;
294
+ let end = index;
295
+ while (end + 1 < lines.length && isAllowedPreludeLine(lines[end + 1])) end += 1;
296
+ for (let candidate = start; candidate <= end; candidate += 1) processed.add(candidate);
297
+ const fragmentMarkers = lines.slice(start, end + 1).filter((candidate) => FRAGMENT_MARKER.test(candidate.text));
298
+ const heading = lines[end + 1] ? HEADING.exec(lines[end + 1].text) : null;
299
+ if (fragmentMarkers.length !== 1) {
300
+ diagnostics.push(
301
+ diagnostic("multiple-fragment-markers", pathKey, line.line, { candidates: fragmentMarkers.length }),
302
+ );
303
+ continue;
304
+ }
305
+ if (!heading) {
306
+ diagnostics.push(diagnostic("orphan-fragment-marker", pathKey, line.line, { raw: line.text }));
307
+ continue;
308
+ }
309
+ const workItemLines = lines.slice(start, end + 1).flatMap((candidate) => {
310
+ const match = WORK_ITEM_MARKER.exec(candidate.text);
311
+ return match ? [{ value: match[1], line: candidate.line }] : [];
312
+ });
313
+ const reflectionLines = lines.slice(start, end + 1).flatMap((candidate) => {
314
+ const match = REFLECTION_MARKER.exec(candidate.text);
315
+ return match ? [{ value: match[1], line: candidate.line }] : [];
316
+ });
317
+ result.push({
318
+ markerIndex: index,
319
+ preludeStart: start,
320
+ preludeEnd: end,
321
+ headingIndex: end + 1,
322
+ key: marker[1],
323
+ headingLevel: heading[1].length,
324
+ headingText: heading[2],
325
+ workItemLines,
326
+ reflectionLines,
327
+ });
328
+ }
329
+ return result.sort((left, right) => left.headingIndex - right.headingIndex);
330
+ }
331
+
332
+ private parseMigrationCompletion(
333
+ lines: readonly ParsedLine[],
334
+ pathKey: PathKey,
335
+ fragmentCount: number,
336
+ diagnostics: ExtractionDiagnostic[],
337
+ ): boolean {
338
+ const exact: ParsedLine[] = [];
339
+ for (const line of lines) {
340
+ if (line.inFence) continue;
341
+ if (looksLikeMarkerDeclaration(line.text, "@world-fragment-migration") && !MIGRATION_MARKER.test(line.text)) {
342
+ diagnostics.push(diagnostic("malformed-migration-marker", pathKey, line.line, { raw: line.text }));
343
+ } else if (MIGRATION_MARKER.test(line.text)) exact.push(line);
344
+ }
345
+ if (exact.length === 0) return false;
346
+ if (exact.length > 1) {
347
+ diagnostics.push(
348
+ ExtractionDiagnostic.create({
349
+ code: "duplicate-migration-marker",
350
+ path: pathKey,
351
+ line: exact[0].line,
352
+ payload: { candidates: exact.length },
353
+ }),
354
+ );
355
+ return false;
356
+ }
357
+ const firstHeading = lines.findIndex((line) => !line.inFence && HEADING.test(line.text));
358
+ const frontmatterEnd = findFrontmatterEnd(lines);
359
+ const validPosition =
360
+ frontmatterEnd === null
361
+ ? firstHeading < 0 || exact[0].line - 1 < firstHeading
362
+ : exact[0].line - 1 === frontmatterEnd + 1;
363
+ if (!validPosition) {
364
+ diagnostics.push(
365
+ ExtractionDiagnostic.create({
366
+ code: "misplaced-migration-marker",
367
+ path: pathKey,
368
+ line: exact[0].line,
369
+ payload: {},
370
+ }),
371
+ );
372
+ return false;
373
+ }
374
+ if (fragmentCount === 0) {
375
+ diagnostics.push(
376
+ ExtractionDiagnostic.create({
377
+ code: "migration-complete-without-fragments",
378
+ path: pathKey,
379
+ line: exact[0].line,
380
+ payload: {},
381
+ }),
382
+ );
383
+ return false;
384
+ }
385
+ return true;
386
+ }
387
+
388
+ private appendWorkItemReferences(
389
+ value: string,
390
+ line: number,
391
+ sourceNodeId: WorkItemReferenceCandidate["sourceNodeId"],
392
+ input: MarkdownDesignFactInput,
393
+ references: WorkItemReferenceCandidate[],
394
+ diagnostics: ExtractionDiagnostic[],
395
+ ): void {
396
+ const workItemIds = parseWorkItemIds(value);
397
+ if (workItemIds.length === 0) {
398
+ diagnostics.push(diagnostic("malformed-work-item-reference", input.path, line, { raw: value }));
399
+ return;
400
+ }
401
+ for (const workItemId of workItemIds) {
402
+ references.push({ sourceNodeId, workItemId, role: input.role, path: input.path, line });
403
+ }
404
+ }
405
+ }
406
+
407
+ export interface FileSystemDesignFactScopeDeps {
408
+ readonly rootDir: string;
409
+ readonly relativeRoot: string;
410
+ readonly role: CorpusRole;
411
+ readonly markdownExtractor: MarkdownDesignFactExtractor;
412
+ readonly include: (relativePath: string) => boolean;
413
+ readonly skip: (relativePath: string) => boolean;
414
+ readonly metadata: (
415
+ relativePath: string,
416
+ ownerIndex: TraceabilityDesignFactIndex,
417
+ ) => Pick<MarkdownDesignFactInput, "unitId" | "storyIds" | "ownerWorkItemId">;
418
+ }
419
+
420
+ export class FileSystemDesignFactScope {
421
+ private readonly deps: FileSystemDesignFactScopeDeps;
422
+
423
+ constructor(deps: FileSystemDesignFactScopeDeps) {
424
+ this.deps = deps;
425
+ }
426
+
427
+ async extract(ownerIndex: TraceabilityDesignFactIndex): Promise<DesignFactCandidateExtraction> {
428
+ const result: {
429
+ nodeCandidates: DesignFactNodeCandidate[];
430
+ workItemReferences: WorkItemReferenceCandidate[];
431
+ reflectionReferences: ReflectionReferenceCandidate[];
432
+ diagnostics: ExtractionDiagnostic[];
433
+ } = { nodeCandidates: [], workItemReferences: [], reflectionReferences: [], diagnostics: [] };
434
+ const absoluteRoot = path.join(this.deps.rootDir, this.deps.relativeRoot);
435
+ const visit = async (directory: string): Promise<void> => {
436
+ let entries: Dirent[];
437
+ try {
438
+ entries = await readdir(directory, { withFileTypes: true });
439
+ } catch (error) {
440
+ const relativePath = path.relative(this.deps.rootDir, directory).split(path.sep).join("/");
441
+ result.diagnostics.push(
442
+ ExtractionDiagnostic.create({
443
+ code: "corpus-read-failure",
444
+ path: PathKey.create(relativePath),
445
+ payload: { message: error instanceof Error ? error.message : String(error) },
446
+ }),
447
+ );
448
+ return;
449
+ }
450
+ entries.sort((left, right) => compareStrings(left.name, right.name));
451
+ for (const entry of entries) {
452
+ const absolutePath = path.join(directory, entry.name);
453
+ const relativePath = path.relative(this.deps.rootDir, absolutePath).split(path.sep).join("/");
454
+ if (entry.isDirectory()) {
455
+ await visit(absolutePath);
456
+ continue;
457
+ }
458
+ if (this.deps.skip(relativePath)) continue;
459
+ const pathKey = PathKey.create(relativePath);
460
+ if (entry.isSymbolicLink()) {
461
+ result.diagnostics.push(diagnostic("unsupported-file-type", pathKey, undefined, { fileType: "symlink" }));
462
+ continue;
463
+ }
464
+ if (!entry.isFile() || !this.deps.include(relativePath)) {
465
+ result.diagnostics.push(
466
+ diagnostic("unsupported-corpus-file", pathKey, undefined, { extension: path.extname(relativePath) }),
467
+ );
468
+ continue;
469
+ }
470
+ try {
471
+ const extraction = this.deps.markdownExtractor.extractFile({
472
+ path: pathKey,
473
+ role: this.deps.role,
474
+ bytes: await readFile(absolutePath),
475
+ ...this.deps.metadata(relativePath, ownerIndex),
476
+ });
477
+ result.nodeCandidates.push(...extraction.nodeCandidates);
478
+ result.workItemReferences.push(...extraction.workItemReferences);
479
+ result.reflectionReferences.push(...extraction.reflectionReferences);
480
+ result.diagnostics.push(...extraction.diagnostics);
481
+ } catch (error) {
482
+ result.diagnostics.push(
483
+ diagnostic("corpus-read-failure", pathKey, undefined, {
484
+ message: error instanceof Error ? error.message : String(error),
485
+ }),
486
+ );
487
+ }
488
+ }
489
+ };
490
+ await visit(absoluteRoot);
491
+ return result;
492
+ }
493
+ }