@diff-review-system/drs 4.0.1 → 5.0.0-rc.1

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 (218) hide show
  1. package/.pi/agents/review/unified-reviewer.md +2 -6
  2. package/.pi/agents/task/okf-wiki-maintainer.md +72 -0
  3. package/.pi/agents/task/review-assistant.md +22 -0
  4. package/.pi/workflows/github-pr-fix-review-issues-stacked.yaml +12 -4
  5. package/.pi/workflows/github-pr-review-post.yaml +29 -20
  6. package/.pi/workflows/github-pr-review.yaml +37 -8
  7. package/.pi/workflows/github-pr-update-agents-md-stacked.yaml +2 -0
  8. package/.pi/workflows/github-pr-visual-explain.yaml +46 -1
  9. package/.pi/workflows/gitlab-mr-fix-review-issues-stacked.yaml +12 -4
  10. package/.pi/workflows/gitlab-mr-review.yaml +20 -8
  11. package/.pi/workflows/gitlab-mr-update-agents-md-stacked.yaml +2 -0
  12. package/.pi/workflows/gitlab-mr-visual-explain.yaml +46 -1
  13. package/.pi/workflows/local-fix-review-issues.yaml +13 -6
  14. package/.pi/workflows/local-review.yaml +7 -1
  15. package/.pi/workflows/local-visual-explain.yaml +46 -1
  16. package/.pi/workflows/release-changelog-finalize.yaml +10 -0
  17. package/.pi/workflows/repository-wiki-check.yaml +40 -0
  18. package/.pi/workflows/repository-wiki-sync.yaml +187 -0
  19. package/.pi/workflows/temporal-control-smoke.yaml +63 -0
  20. package/.wiki-site/.vitepress/config.mts +452 -0
  21. package/.wiki-site/.vitepress/theme/PageLead.vue +122 -0
  22. package/.wiki-site/.vitepress/theme/index.ts +12 -0
  23. package/.wiki-site/.vitepress/theme/style.css +270 -0
  24. package/README.md +77 -4
  25. package/dist/cli/index.js +87 -4
  26. package/dist/cli/index.js.map +1 -1
  27. package/dist/cli/init.d.ts +6 -1
  28. package/dist/cli/init.d.ts.map +1 -1
  29. package/dist/cli/init.js +26 -10
  30. package/dist/cli/init.js.map +1 -1
  31. package/dist/cli/run-agent.d.ts +5 -7
  32. package/dist/cli/run-agent.d.ts.map +1 -1
  33. package/dist/cli/run-agent.js +56 -5
  34. package/dist/cli/run-agent.js.map +1 -1
  35. package/dist/cli/wiki.d.ts +3 -0
  36. package/dist/cli/wiki.d.ts.map +1 -0
  37. package/dist/cli/wiki.js +174 -0
  38. package/dist/cli/wiki.js.map +1 -0
  39. package/dist/cli/workflow-executor-selection.d.ts +3 -0
  40. package/dist/cli/workflow-executor-selection.d.ts.map +1 -0
  41. package/dist/cli/workflow-executor-selection.js +15 -0
  42. package/dist/cli/workflow-executor-selection.js.map +1 -0
  43. package/dist/cli/workflow.d.ts +40 -42
  44. package/dist/cli/workflow.d.ts.map +1 -1
  45. package/dist/cli/workflow.js +533 -868
  46. package/dist/cli/workflow.js.map +1 -1
  47. package/dist/github/client.d.ts.map +1 -1
  48. package/dist/github/client.js +2 -2
  49. package/dist/github/client.js.map +1 -1
  50. package/dist/github/platform-adapter.d.ts.map +1 -1
  51. package/dist/github/platform-adapter.js +15 -1
  52. package/dist/github/platform-adapter.js.map +1 -1
  53. package/dist/gitlab/client.d.ts +4 -0
  54. package/dist/gitlab/client.d.ts.map +1 -1
  55. package/dist/gitlab/client.js +17 -1
  56. package/dist/gitlab/client.js.map +1 -1
  57. package/dist/gitlab/platform-adapter.d.ts.map +1 -1
  58. package/dist/gitlab/platform-adapter.js +17 -1
  59. package/dist/gitlab/platform-adapter.js.map +1 -1
  60. package/dist/lib/agent-permissions.d.ts +44 -0
  61. package/dist/lib/agent-permissions.d.ts.map +1 -0
  62. package/dist/lib/agent-permissions.js +358 -0
  63. package/dist/lib/agent-permissions.js.map +1 -0
  64. package/dist/lib/agent-result.d.ts +10 -0
  65. package/dist/lib/agent-result.d.ts.map +1 -0
  66. package/dist/lib/agent-result.js +2 -0
  67. package/dist/lib/agent-result.js.map +1 -0
  68. package/dist/lib/comment-poster.d.ts +1 -1
  69. package/dist/lib/comment-poster.d.ts.map +1 -1
  70. package/dist/lib/comment-poster.js +38 -16
  71. package/dist/lib/comment-poster.js.map +1 -1
  72. package/dist/lib/config.d.ts +27 -1
  73. package/dist/lib/config.d.ts.map +1 -1
  74. package/dist/lib/config.js +17 -0
  75. package/dist/lib/config.js.map +1 -1
  76. package/dist/lib/error-comment-poster.d.ts +1 -1
  77. package/dist/lib/error-comment-poster.d.ts.map +1 -1
  78. package/dist/lib/error-comment-poster.js +15 -7
  79. package/dist/lib/error-comment-poster.js.map +1 -1
  80. package/dist/lib/okf-wiki.d.ts +65 -0
  81. package/dist/lib/okf-wiki.d.ts.map +1 -0
  82. package/dist/lib/okf-wiki.js +647 -0
  83. package/dist/lib/okf-wiki.js.map +1 -0
  84. package/dist/lib/output-paths.d.ts +0 -1
  85. package/dist/lib/output-paths.d.ts.map +1 -1
  86. package/dist/lib/output-paths.js +0 -1
  87. package/dist/lib/output-paths.js.map +1 -1
  88. package/dist/lib/platform-client.d.ts +2 -0
  89. package/dist/lib/platform-client.d.ts.map +1 -1
  90. package/dist/lib/project-setup.d.ts +7 -0
  91. package/dist/lib/project-setup.d.ts.map +1 -0
  92. package/dist/lib/project-setup.js +11 -0
  93. package/dist/lib/project-setup.js.map +1 -0
  94. package/dist/lib/review-artifact-store.d.ts +16 -0
  95. package/dist/lib/review-artifact-store.d.ts.map +1 -0
  96. package/dist/lib/review-artifact-store.js +84 -0
  97. package/dist/lib/review-artifact-store.js.map +1 -0
  98. package/dist/lib/review-artifact.d.ts +11 -1
  99. package/dist/lib/review-artifact.d.ts.map +1 -1
  100. package/dist/lib/review-artifact.js +203 -0
  101. package/dist/lib/review-artifact.js.map +1 -1
  102. package/dist/lib/review-core.d.ts +1 -0
  103. package/dist/lib/review-core.d.ts.map +1 -1
  104. package/dist/lib/review-core.js +11 -14
  105. package/dist/lib/review-core.js.map +1 -1
  106. package/dist/lib/review-orchestrator.d.ts +6 -1
  107. package/dist/lib/review-orchestrator.d.ts.map +1 -1
  108. package/dist/lib/review-orchestrator.js +3 -1
  109. package/dist/lib/review-orchestrator.js.map +1 -1
  110. package/dist/lib/review-parser.d.ts.map +1 -1
  111. package/dist/lib/review-parser.js +15 -12
  112. package/dist/lib/review-parser.js.map +1 -1
  113. package/dist/lib/wiki-delta.d.ts +71 -0
  114. package/dist/lib/wiki-delta.d.ts.map +1 -0
  115. package/dist/lib/wiki-delta.js +524 -0
  116. package/dist/lib/wiki-delta.js.map +1 -0
  117. package/dist/lib/wiki-run-summary.d.ts +45 -0
  118. package/dist/lib/wiki-run-summary.d.ts.map +1 -0
  119. package/dist/lib/wiki-run-summary.js +168 -0
  120. package/dist/lib/wiki-run-summary.js.map +1 -0
  121. package/dist/lib/wiki-search.d.ts +24 -0
  122. package/dist/lib/wiki-search.d.ts.map +1 -0
  123. package/dist/lib/wiki-search.js +200 -0
  124. package/dist/lib/wiki-search.js.map +1 -0
  125. package/dist/lib/wiki-site-graph.d.ts +33 -0
  126. package/dist/lib/wiki-site-graph.d.ts.map +1 -0
  127. package/dist/lib/wiki-site-graph.js +446 -0
  128. package/dist/lib/wiki-site-graph.js.map +1 -0
  129. package/dist/lib/wiki-site-safety.d.ts +6 -0
  130. package/dist/lib/wiki-site-safety.d.ts.map +1 -0
  131. package/dist/lib/wiki-site-safety.js +83 -0
  132. package/dist/lib/wiki-site-safety.js.map +1 -0
  133. package/dist/lib/wiki-site-smoke.d.ts +16 -0
  134. package/dist/lib/wiki-site-smoke.d.ts.map +1 -0
  135. package/dist/lib/wiki-site-smoke.js +270 -0
  136. package/dist/lib/wiki-site-smoke.js.map +1 -0
  137. package/dist/lib/wiki-site.d.ts +26 -0
  138. package/dist/lib/wiki-site.d.ts.map +1 -0
  139. package/dist/lib/wiki-site.js +221 -0
  140. package/dist/lib/wiki-site.js.map +1 -0
  141. package/dist/lib/workflow/artifact-store.d.ts +71 -0
  142. package/dist/lib/workflow/artifact-store.d.ts.map +1 -0
  143. package/dist/lib/workflow/artifact-store.js +121 -0
  144. package/dist/lib/workflow/artifact-store.js.map +1 -0
  145. package/dist/lib/workflow/compiled-plan.d.ts +87 -0
  146. package/dist/lib/workflow/compiled-plan.d.ts.map +1 -0
  147. package/dist/lib/workflow/compiled-plan.js +62 -0
  148. package/dist/lib/workflow/compiled-plan.js.map +1 -0
  149. package/dist/lib/workflow/executor.d.ts +18 -0
  150. package/dist/lib/workflow/executor.d.ts.map +1 -0
  151. package/dist/lib/workflow/executor.js +2 -0
  152. package/dist/lib/workflow/executor.js.map +1 -0
  153. package/dist/lib/workflow/graph.d.ts +30 -0
  154. package/dist/lib/workflow/graph.d.ts.map +1 -0
  155. package/dist/lib/workflow/graph.js +139 -0
  156. package/dist/lib/workflow/graph.js.map +1 -0
  157. package/dist/lib/workflow/input.d.ts +4 -0
  158. package/dist/lib/workflow/input.d.ts.map +1 -0
  159. package/dist/lib/workflow/input.js +4 -0
  160. package/dist/lib/workflow/input.js.map +1 -0
  161. package/dist/lib/workflow/node-executor.d.ts +19 -0
  162. package/dist/lib/workflow/node-executor.d.ts.map +1 -0
  163. package/dist/lib/workflow/node-executor.js +2 -0
  164. package/dist/lib/workflow/node-executor.js.map +1 -0
  165. package/dist/lib/workflow/planning.d.ts +52 -0
  166. package/dist/lib/workflow/planning.d.ts.map +1 -0
  167. package/dist/lib/workflow/planning.js +865 -0
  168. package/dist/lib/workflow/planning.js.map +1 -0
  169. package/dist/lib/workflow/types.d.ts +71 -0
  170. package/dist/lib/workflow/types.d.ts.map +1 -0
  171. package/dist/lib/workflow/types.js +2 -0
  172. package/dist/lib/workflow/types.js.map +1 -0
  173. package/dist/lib/workflow-artifacts.d.ts.map +1 -1
  174. package/dist/lib/workflow-artifacts.js +23 -1
  175. package/dist/lib/workflow-artifacts.js.map +1 -1
  176. package/dist/lib/write-json-output.d.ts.map +1 -1
  177. package/dist/lib/write-json-output.js +3 -8
  178. package/dist/lib/write-json-output.js.map +1 -1
  179. package/dist/pi/sdk.d.ts.map +1 -1
  180. package/dist/pi/sdk.js +276 -19
  181. package/dist/pi/sdk.js.map +1 -1
  182. package/dist/runtime/client.d.ts +3 -0
  183. package/dist/runtime/client.d.ts.map +1 -1
  184. package/dist/runtime/client.js +4 -0
  185. package/dist/runtime/client.js.map +1 -1
  186. package/dist/temporal/activities.d.ts +26 -0
  187. package/dist/temporal/activities.d.ts.map +1 -0
  188. package/dist/temporal/activities.js +219 -0
  189. package/dist/temporal/activities.js.map +1 -0
  190. package/dist/temporal/config.d.ts +4 -0
  191. package/dist/temporal/config.d.ts.map +1 -0
  192. package/dist/temporal/config.js +36 -0
  193. package/dist/temporal/config.js.map +1 -0
  194. package/dist/temporal/executor.d.ts +7 -0
  195. package/dist/temporal/executor.d.ts.map +1 -0
  196. package/dist/temporal/executor.js +240 -0
  197. package/dist/temporal/executor.js.map +1 -0
  198. package/dist/temporal/retry-policy.d.ts +28 -0
  199. package/dist/temporal/retry-policy.d.ts.map +1 -0
  200. package/dist/temporal/retry-policy.js +61 -0
  201. package/dist/temporal/retry-policy.js.map +1 -0
  202. package/dist/temporal/types.d.ts +86 -0
  203. package/dist/temporal/types.d.ts.map +1 -0
  204. package/dist/temporal/types.js +2 -0
  205. package/dist/temporal/types.js.map +1 -0
  206. package/dist/temporal/worker.d.ts +3 -0
  207. package/dist/temporal/worker.d.ts.map +1 -0
  208. package/dist/temporal/worker.js +18 -0
  209. package/dist/temporal/worker.js.map +1 -0
  210. package/dist/temporal/workflow-id.d.ts +27 -0
  211. package/dist/temporal/workflow-id.d.ts.map +1 -0
  212. package/dist/temporal/workflow-id.js +53 -0
  213. package/dist/temporal/workflow-id.js.map +1 -0
  214. package/dist/temporal/workflows.d.ts +6 -0
  215. package/dist/temporal/workflows.d.ts.map +1 -0
  216. package/dist/temporal/workflows.js +355 -0
  217. package/dist/temporal/workflows.js.map +1 -0
  218. package/package.json +24 -9
@@ -0,0 +1,647 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { lstat, readFile, readdir, realpath, rename, rm, writeFile } from 'fs/promises';
3
+ import { isAbsolute, relative, resolve, sep } from 'path';
4
+ import * as path from 'path';
5
+ import * as yaml from 'yaml';
6
+ import { resolveWithinWorkingDir } from './path-utils.js';
7
+ import { analyzeWikiConceptGraph, extractWikiSiteConceptLinks, } from './wiki-site-graph.js';
8
+ export const SUPPORTED_OKF_VERSION = '0.1';
9
+ /** Generate stable progressive-disclosure indexes for every non-empty bundle directory. */
10
+ export async function synchronizeOkfIndexes(workingDir, root = 'wiki', version = SUPPORTED_OKF_VERSION) {
11
+ requireSupportedVersion(version);
12
+ const bundle = await resolveBundleRoot(workingDir, root);
13
+ const directories = await collectBundleDirectories(bundle.absolutePath);
14
+ const indexedDirectories = new Set();
15
+ let updated = 0;
16
+ for (const directory of directories) {
17
+ const conceptLinks = [];
18
+ for (const filename of directory.files) {
19
+ if (!isConceptFilename(filename))
20
+ continue;
21
+ const filePath = resolve(directory.absolutePath, filename);
22
+ const metadata = readDisplayMetadata(await readFile(filePath, 'utf-8'));
23
+ conceptLinks.push({
24
+ href: encodeURIComponent(filename),
25
+ label: metadata.title ?? titleFromSlug(filename.slice(0, -3)),
26
+ ...(metadata.description ? { description: metadata.description } : {}),
27
+ });
28
+ }
29
+ const directoryLinks = directory.directories
30
+ .filter((name) => indexedDirectories.has(resolve(directory.absolutePath, name)))
31
+ .map((name) => ({
32
+ href: `${encodeURIComponent(name)}/`,
33
+ label: titleFromSlug(name),
34
+ }));
35
+ if (conceptLinks.length === 0 && directoryLinks.length === 0) {
36
+ const indexPath = resolve(directory.absolutePath, 'index.md');
37
+ try {
38
+ await lstat(indexPath);
39
+ await rm(indexPath, { force: true });
40
+ updated += 1;
41
+ }
42
+ catch (error) {
43
+ if (error.code !== 'ENOENT')
44
+ throw error;
45
+ }
46
+ continue;
47
+ }
48
+ const isRoot = directory.relativePath === '';
49
+ const content = renderIndex(conceptLinks, directoryLinks, isRoot ? version : undefined);
50
+ const indexPath = resolve(directory.absolutePath, 'index.md');
51
+ const existing = directory.files.includes('index.md')
52
+ ? await readFile(indexPath, 'utf-8')
53
+ : undefined;
54
+ if (existing !== content) {
55
+ await writeFileAtomically(indexPath, content);
56
+ updated += 1;
57
+ }
58
+ indexedDirectories.add(directory.absolutePath);
59
+ }
60
+ return {
61
+ version: SUPPORTED_OKF_VERSION,
62
+ root: bundle.relativePath,
63
+ indexes: indexedDirectories.size,
64
+ updated,
65
+ };
66
+ }
67
+ async function writeFileAtomically(filePath, content) {
68
+ const temporaryPath = `${filePath}.tmp-${process.pid}-${randomUUID()}`;
69
+ try {
70
+ await writeFile(temporaryPath, content, { encoding: 'utf-8', flag: 'wx' });
71
+ await rename(temporaryPath, filePath);
72
+ }
73
+ finally {
74
+ await rm(temporaryPath, { force: true });
75
+ }
76
+ }
77
+ /** Validate an OKF bundle without modifying it. */
78
+ export async function validateOkfBundle(workingDir, root = 'wiki', version = SUPPORTED_OKF_VERSION) {
79
+ requireSupportedVersion(version);
80
+ const errors = [];
81
+ const warnings = [];
82
+ let bundle;
83
+ try {
84
+ bundle = await resolveBundleRoot(workingDir, root);
85
+ }
86
+ catch (error) {
87
+ return {
88
+ valid: false,
89
+ version: SUPPORTED_OKF_VERSION,
90
+ root: root.trim() || root,
91
+ concepts: 0,
92
+ indexes: 0,
93
+ logs: 0,
94
+ errors: [issue('invalid_bundle_root', errorMessage(error))],
95
+ graph: emptyGraphMetrics(),
96
+ warnings,
97
+ };
98
+ }
99
+ const entries = await collectBundleEntries(bundle.absolutePath, errors);
100
+ const files = new Set(entries.map((entry) => entry.relativePath));
101
+ const conceptIds = new Set(entries
102
+ .filter((entry) => isConceptFilename(path.posix.basename(entry.relativePath)))
103
+ .map((entry) => entry.relativePath.replace(/\.md$/u, '')));
104
+ const graphConcepts = [];
105
+ const citedSources = [];
106
+ let concepts = 0;
107
+ let indexes = 0;
108
+ let logs = 0;
109
+ for (const entry of entries) {
110
+ if (!entry.relativePath.endsWith('.md'))
111
+ continue;
112
+ const filename = path.posix.basename(entry.relativePath);
113
+ const content = await readFile(entry.absolutePath, 'utf-8');
114
+ if (filename === 'index.md') {
115
+ indexes += 1;
116
+ validateIndex(content, entry.relativePath, version, errors);
117
+ }
118
+ else if (filename === 'log.md') {
119
+ logs += 1;
120
+ validateLog(content, entry.relativePath, errors);
121
+ }
122
+ else {
123
+ concepts += 1;
124
+ const sources = validateConcept(content, entry.relativePath, errors, warnings);
125
+ for (const source of sources) {
126
+ citedSources.push({ concept: entry.relativePath, source: source.path });
127
+ }
128
+ const id = entry.relativePath.replace(/\.md$/u, '');
129
+ graphConcepts.push({
130
+ id,
131
+ links: extractWikiSiteConceptLinks(content, id, conceptIds),
132
+ });
133
+ }
134
+ validateInternalLinks(content, entry.relativePath, files, warnings);
135
+ }
136
+ await warnOnMissingCitedSources(workingDir, citedSources, warnings);
137
+ if (concepts === 0) {
138
+ errors.push(issue('empty_bundle', 'The bundle must contain at least one concept document.'));
139
+ }
140
+ const graph = analyzeWikiConceptGraph(graphConcepts);
141
+ for (const id of graph.orphanIds) {
142
+ warnings.push(issue('orphan_concept', 'Concept has no incoming or outgoing semantic links to another concept.', `${id}.md`));
143
+ }
144
+ return {
145
+ valid: errors.length === 0,
146
+ version: SUPPORTED_OKF_VERSION,
147
+ root: bundle.relativePath,
148
+ concepts,
149
+ indexes,
150
+ logs,
151
+ errors,
152
+ graph: graph.metrics,
153
+ warnings,
154
+ };
155
+ }
156
+ export function formatOkfValidationErrors(result) {
157
+ return result.errors
158
+ .map(({ code, message, path: issuePath }) => issuePath ? `- ${issuePath}: [${code}] ${message}` : `- [${code}] ${message}`)
159
+ .join('\n');
160
+ }
161
+ /** Validate one proposed OKF Markdown document before a mutation tool writes it. */
162
+ export function validateOkfDocument(content, relativePath) {
163
+ const normalizedPath = relativePath.replaceAll('\\', '/');
164
+ const filename = path.posix.basename(normalizedPath);
165
+ const errors = [];
166
+ const warnings = [];
167
+ if (filename === 'index.md') {
168
+ errors.push(issue('generated_index', 'index.md is generated deterministically and cannot be written by an agent.', normalizedPath));
169
+ }
170
+ else if (filename === 'log.md') {
171
+ validateLog(content, normalizedPath, errors);
172
+ }
173
+ else if (!isConceptFilename(filename)) {
174
+ errors.push(issue('invalid_concept_path', 'OKF documents must use a Markdown (.md) path.', normalizedPath));
175
+ }
176
+ else {
177
+ validateConcept(content, normalizedPath, errors, warnings);
178
+ }
179
+ return {
180
+ valid: errors.length === 0,
181
+ path: normalizedPath,
182
+ errors,
183
+ warnings,
184
+ };
185
+ }
186
+ /**
187
+ * Parse the producer-defined `drs_sources` provenance extension. Returns the parsed sources, or a
188
+ * validation message when the field is malformed. Cited paths must be repository-relative and stay
189
+ * inside the repository.
190
+ */
191
+ export function parseOkfConceptSources(fields) {
192
+ if (!('drs_sources' in fields))
193
+ return [];
194
+ const value = fields.drs_sources;
195
+ if (!Array.isArray(value)) {
196
+ return 'Optional field `drs_sources` should be a list of source references.';
197
+ }
198
+ const wholeFileCitations = new Set();
199
+ const symbolCitations = new Map();
200
+ for (const entry of value) {
201
+ if (!isRecord(entry) || typeof entry.path !== 'string') {
202
+ return '`drs_sources` entries should define a non-empty string `path`.';
203
+ }
204
+ const sourcePath = normalizeSourcePath(entry.path);
205
+ if (!sourcePath) {
206
+ return `\`drs_sources\` paths must be repository-relative and stay inside the repository: ${entry.path}`;
207
+ }
208
+ if ('symbols' in entry) {
209
+ if (!Array.isArray(entry.symbols) ||
210
+ entry.symbols.some((symbol) => typeof symbol !== 'string' || !symbol.trim())) {
211
+ return `\`drs_sources\` entry \`symbols\` should be a list of non-empty strings: ${sourcePath}`;
212
+ }
213
+ if (wholeFileCitations.has(sourcePath))
214
+ continue;
215
+ const symbols = symbolCitations.get(sourcePath) ?? new Set();
216
+ for (const symbol of entry.symbols)
217
+ symbols.add(symbol.trim());
218
+ symbolCitations.set(sourcePath, symbols);
219
+ }
220
+ else {
221
+ // A whole-file citation covers every symbol and wins regardless of entry order.
222
+ wholeFileCitations.add(sourcePath);
223
+ symbolCitations.delete(sourcePath);
224
+ }
225
+ }
226
+ return [...wholeFileCitations, ...symbolCitations.keys()]
227
+ .sort(compareStrings)
228
+ .map((sourcePath) => {
229
+ const symbols = symbolCitations.get(sourcePath);
230
+ return symbols?.size
231
+ ? { path: sourcePath, symbols: [...symbols].sort(compareStrings) }
232
+ : { path: sourcePath };
233
+ });
234
+ }
235
+ function normalizeSourcePath(value) {
236
+ const trimmed = value.trim();
237
+ if (!trimmed || isAbsolute(trimmed) || trimmed.startsWith('~'))
238
+ return undefined;
239
+ const normalized = path.posix.normalize(trimmed.replaceAll('\\', '/'));
240
+ if (normalized === '.' ||
241
+ normalized === '..' ||
242
+ normalized.startsWith('../') ||
243
+ path.posix.isAbsolute(normalized)) {
244
+ return undefined;
245
+ }
246
+ return normalized;
247
+ }
248
+ /** Load the `drs_sources` reverse map (repository source path -> bundle concept paths). */
249
+ export async function loadOkfProvenanceMap(workingDir, root = 'wiki') {
250
+ const bundle = await resolveBundleRoot(workingDir, root);
251
+ const traversalErrors = [];
252
+ const entries = await collectBundleEntries(bundle.absolutePath, traversalErrors);
253
+ if (traversalErrors.length > 0) {
254
+ throw new Error(`Cannot load OKF provenance:\n${traversalErrors
255
+ .map(({ code, message, path: issuePath }) => issuePath ? `- ${issuePath}: [${code}] ${message}` : `- [${code}] ${message}`)
256
+ .join('\n')}`);
257
+ }
258
+ const provenance = new Map();
259
+ for (const entry of entries) {
260
+ if (!isConceptFilename(path.posix.basename(entry.relativePath)))
261
+ continue;
262
+ const parsed = parseFrontmatter(await readFile(entry.absolutePath, 'utf-8'));
263
+ if (typeof parsed === 'string')
264
+ continue;
265
+ const sources = parseOkfConceptSources(parsed.fields);
266
+ if (typeof sources === 'string')
267
+ continue;
268
+ for (const source of sources) {
269
+ const concepts = provenance.get(source.path) ?? new Set();
270
+ concepts.add(entry.relativePath);
271
+ provenance.set(source.path, concepts);
272
+ }
273
+ }
274
+ const reverseMap = Object.create(null);
275
+ for (const sourcePath of [...provenance.keys()].sort(compareStrings)) {
276
+ reverseMap[sourcePath] = [...(provenance.get(sourcePath) ?? [])].sort(compareStrings);
277
+ }
278
+ return reverseMap;
279
+ }
280
+ /** Load validated concept documents from an OKF bundle in stable path order. */
281
+ export async function loadOkfConcepts(workingDir, root = 'wiki', version = SUPPORTED_OKF_VERSION) {
282
+ const validation = await validateOkfBundle(workingDir, root, version);
283
+ if (!validation.valid) {
284
+ throw new Error(`Cannot load invalid OKF bundle:\n${formatOkfValidationErrors(validation)}`);
285
+ }
286
+ const bundle = await resolveBundleRoot(workingDir, root);
287
+ const traversalErrors = [];
288
+ const entries = await collectBundleEntries(bundle.absolutePath, traversalErrors);
289
+ if (traversalErrors.length > 0) {
290
+ throw new Error(`Cannot load invalid OKF bundle:\n${traversalErrors
291
+ .map(({ code, message, path: issuePath }) => issuePath ? `- ${issuePath}: [${code}] ${message}` : `- [${code}] ${message}`)
292
+ .join('\n')}`);
293
+ }
294
+ const concepts = [];
295
+ for (const entry of entries) {
296
+ const filename = path.posix.basename(entry.relativePath);
297
+ if (!isConceptFilename(filename))
298
+ continue;
299
+ const parsed = parseFrontmatter(await readFile(entry.absolutePath, 'utf-8'));
300
+ if (typeof parsed === 'string') {
301
+ throw new Error(`Cannot load invalid OKF concept ${entry.relativePath}: ${parsed}`);
302
+ }
303
+ const type = normalizeDisplayText(parsed.fields.type);
304
+ if (!type) {
305
+ throw new Error(`Cannot load invalid OKF concept ${entry.relativePath}: missing type`);
306
+ }
307
+ const title = normalizeDisplayText(parsed.fields.title);
308
+ const description = normalizeDisplayText(parsed.fields.description);
309
+ const tags = Array.isArray(parsed.fields.tags)
310
+ ? parsed.fields.tags.flatMap((tag) => {
311
+ const normalized = normalizeDisplayText(tag);
312
+ return normalized ? [normalized] : [];
313
+ })
314
+ : [];
315
+ const sources = parseOkfConceptSources(parsed.fields);
316
+ if (typeof sources === 'string') {
317
+ throw new Error(`Cannot load invalid OKF concept ${entry.relativePath}: ${sources}`);
318
+ }
319
+ concepts.push({
320
+ body: parsed.body,
321
+ path: entry.relativePath,
322
+ ...(sources.length ? { sources } : {}),
323
+ tags,
324
+ type,
325
+ ...(title ? { title } : {}),
326
+ ...(description ? { description } : {}),
327
+ });
328
+ }
329
+ return { concepts, root: bundle.relativePath };
330
+ }
331
+ async function resolveBundleRoot(workingDir, root) {
332
+ const requestedRoot = root.trim();
333
+ if (!requestedRoot)
334
+ throw new Error('OKF bundle root cannot be empty.');
335
+ const workingRoot = resolve(workingDir);
336
+ const absolutePath = resolveWithinWorkingDir(workingDir, requestedRoot, 'access');
337
+ if (absolutePath === workingRoot) {
338
+ throw new Error('OKF bundle root must be a subdirectory of the working directory.');
339
+ }
340
+ let rootStat;
341
+ try {
342
+ rootStat = await lstat(absolutePath);
343
+ }
344
+ catch {
345
+ throw new Error(`OKF bundle root does not exist: ${requestedRoot}`);
346
+ }
347
+ if (rootStat.isSymbolicLink())
348
+ throw new Error('OKF bundle root cannot be a symbolic link.');
349
+ if (!rootStat.isDirectory())
350
+ throw new Error(`OKF bundle root is not a directory: ${requestedRoot}`);
351
+ const [realWorkingRoot, realBundleRoot] = await Promise.all([
352
+ realpath(workingRoot),
353
+ realpath(absolutePath),
354
+ ]);
355
+ const realRelative = relative(realWorkingRoot, realBundleRoot);
356
+ if (realRelative.startsWith('..') || isAbsolute(realRelative)) {
357
+ throw new Error(`OKF bundle root resolves outside the working directory: ${requestedRoot}`);
358
+ }
359
+ return {
360
+ absolutePath,
361
+ relativePath: toPosixPath(relative(workingRoot, absolutePath)),
362
+ };
363
+ }
364
+ async function collectBundleDirectories(absoluteRoot, absoluteDirectory = absoluteRoot) {
365
+ const entries = await readdir(absoluteDirectory, { withFileTypes: true });
366
+ const directories = entries
367
+ .filter((entry) => entry.isDirectory())
368
+ .map((entry) => entry.name)
369
+ .sort();
370
+ const files = entries
371
+ .filter((entry) => entry.isFile())
372
+ .map((entry) => entry.name)
373
+ .sort();
374
+ const descendants = await Promise.all(directories.map((name) => collectBundleDirectories(absoluteRoot, resolve(absoluteDirectory, name))));
375
+ return [
376
+ ...descendants.flat(),
377
+ {
378
+ absolutePath: absoluteDirectory,
379
+ relativePath: toPosixPath(relative(absoluteRoot, absoluteDirectory)),
380
+ directories,
381
+ files,
382
+ },
383
+ ];
384
+ }
385
+ async function collectBundleEntries(absoluteRoot, errors, absoluteDirectory = absoluteRoot) {
386
+ const entries = await readdir(absoluteDirectory, { withFileTypes: true });
387
+ const collected = [];
388
+ for (const entry of entries.sort((left, right) => compareStrings(left.name, right.name))) {
389
+ const absolutePath = resolve(absoluteDirectory, entry.name);
390
+ const relativePath = toPosixPath(relative(absoluteRoot, absolutePath));
391
+ if (entry.isSymbolicLink()) {
392
+ errors.push(issue('symbolic_link', 'Symbolic links are not supported inside an OKF bundle.', relativePath));
393
+ }
394
+ else if (entry.isDirectory()) {
395
+ collected.push(...(await collectBundleEntries(absoluteRoot, errors, absolutePath)));
396
+ }
397
+ else if (entry.isFile()) {
398
+ collected.push({ absolutePath, relativePath });
399
+ }
400
+ }
401
+ return collected;
402
+ }
403
+ function validateConcept(content, relativePath, errors, warnings) {
404
+ const parsed = parseFrontmatter(content);
405
+ if (typeof parsed === 'string') {
406
+ errors.push(issue('invalid_frontmatter', parsed, relativePath));
407
+ return [];
408
+ }
409
+ const { fields } = parsed;
410
+ if (typeof fields.type !== 'string' || !fields.type.trim()) {
411
+ errors.push(issue('invalid_type', 'Concept frontmatter requires a non-empty string `type`.', relativePath));
412
+ }
413
+ for (const field of ['title', 'description', 'resource']) {
414
+ if (field in fields && (typeof fields[field] !== 'string' || !fields[field].trim())) {
415
+ warnings.push(issue(`invalid_${field}`, `Optional field \`${field}\` should be a non-empty string when present.`, relativePath));
416
+ }
417
+ }
418
+ if ('tags' in fields &&
419
+ (!Array.isArray(fields.tags) ||
420
+ fields.tags.some((tag) => typeof tag !== 'string' || !tag.trim()))) {
421
+ warnings.push(issue('invalid_tags', 'Optional field `tags` should be a list of non-empty strings.', relativePath));
422
+ }
423
+ if ('timestamp' in fields &&
424
+ (typeof fields.timestamp !== 'string' || Number.isNaN(Date.parse(fields.timestamp)))) {
425
+ warnings.push(issue('invalid_timestamp', 'Optional field `timestamp` should be an ISO 8601 datetime.', relativePath));
426
+ }
427
+ return validateConceptProvenance(fields, relativePath, errors, warnings);
428
+ }
429
+ function validateConceptProvenance(fields, relativePath, errors, warnings) {
430
+ const sources = parseOkfConceptSources(fields);
431
+ if (typeof sources === 'string') {
432
+ errors.push(issue('invalid_drs_sources', sources, relativePath));
433
+ return [];
434
+ }
435
+ if (sources.length === 0) {
436
+ warnings.push(issue('missing_provenance', 'Concept declares no `drs_sources` provenance for its repository evidence.', relativePath));
437
+ }
438
+ return sources;
439
+ }
440
+ async function warnOnMissingCitedSources(workingDir, citations, warnings) {
441
+ const existence = new Map();
442
+ await Promise.all([...new Set(citations.map((citation) => citation.source))].map(async (source) => {
443
+ let exists = false;
444
+ try {
445
+ const stats = await lstat(resolveWithinWorkingDir(workingDir, source, 'read'));
446
+ exists = stats.isFile() || stats.isDirectory();
447
+ }
448
+ catch {
449
+ exists = false;
450
+ }
451
+ existence.set(source, exists);
452
+ }));
453
+ for (const { concept, source } of citations) {
454
+ if (existence.get(source))
455
+ continue;
456
+ warnings.push(issue('missing_source', `Cited source path does not exist: ${source}`, concept));
457
+ }
458
+ }
459
+ function validateIndex(content, relativePath, version, errors) {
460
+ let body = stripBom(content);
461
+ if (body.startsWith('---\n') || body.startsWith('---\r\n')) {
462
+ if (relativePath !== 'index.md') {
463
+ errors.push(issue('index_frontmatter', 'Only the bundle-root index.md may contain frontmatter.', relativePath));
464
+ return;
465
+ }
466
+ const parsed = parseFrontmatter(body);
467
+ if (typeof parsed === 'string') {
468
+ errors.push(issue('invalid_index_frontmatter', parsed, relativePath));
469
+ return;
470
+ }
471
+ if (typeof parsed.fields.okf_version !== 'string' || parsed.fields.okf_version !== version) {
472
+ errors.push(issue('invalid_okf_version', `Root index.md declares an unsupported OKF version; expected ${version}.`, relativePath));
473
+ }
474
+ body = parsed.body;
475
+ }
476
+ if (!/^#\s+\S+/mu.test(body)) {
477
+ errors.push(issue('invalid_index', 'index.md must contain a section heading.', relativePath));
478
+ }
479
+ if (!/^\s*[-*+]\s+\[[^\]]+\]\([^)]+\)/mu.test(body)) {
480
+ errors.push(issue('invalid_index', 'index.md must contain at least one Markdown link entry.', relativePath));
481
+ }
482
+ }
483
+ function validateLog(content, relativePath, errors) {
484
+ const body = stripBom(content);
485
+ if (body.startsWith('---\n') || body.startsWith('---\r\n')) {
486
+ errors.push(issue('log_frontmatter', 'log.md must not contain frontmatter.', relativePath));
487
+ return;
488
+ }
489
+ if (!/^#\s+\S+/mu.test(body)) {
490
+ errors.push(issue('invalid_log', 'log.md must contain a title heading.', relativePath));
491
+ }
492
+ const dateHeadings = [...body.matchAll(/^##\s+(.+?)\s*$/gmu)];
493
+ for (const match of dateHeadings) {
494
+ const date = match[1];
495
+ if (!isIsoDate(date)) {
496
+ errors.push(issue('invalid_log_date', `Log date heading must use YYYY-MM-DD: ${date}`, relativePath));
497
+ }
498
+ }
499
+ }
500
+ function validateInternalLinks(content, sourcePath, files, warnings) {
501
+ for (const target of extractMarkdownLinks(content)) {
502
+ if (isExternalOrAnchorLink(target))
503
+ continue;
504
+ const linkPath = target.split(/[?#]/u, 1)[0].replace(/^<|>$/gu, '');
505
+ if (!linkPath)
506
+ continue;
507
+ let decodedPath;
508
+ try {
509
+ decodedPath = decodeURIComponent(linkPath);
510
+ }
511
+ catch {
512
+ warnings.push(issue('invalid_link', `Link is not valid URI encoding: ${target}`, sourcePath));
513
+ continue;
514
+ }
515
+ const resolved = decodedPath.startsWith('/')
516
+ ? path.posix.normalize(decodedPath.slice(1))
517
+ : path.posix.normalize(path.posix.join(path.posix.dirname(sourcePath), decodedPath));
518
+ if (resolved === '..' || resolved.startsWith('../')) {
519
+ warnings.push(issue('link_outside_bundle', `Link resolves outside the OKF bundle: ${target}`, sourcePath));
520
+ continue;
521
+ }
522
+ const candidate = resolved.endsWith('/') ? `${resolved}index.md` : resolved;
523
+ if (!files.has(candidate)) {
524
+ warnings.push(issue('broken_link', `Link target does not exist: ${target}`, sourcePath));
525
+ }
526
+ }
527
+ }
528
+ function parseFrontmatter(content) {
529
+ const normalized = stripBom(content);
530
+ const lines = normalized.split(/\r?\n/u);
531
+ if (lines[0] !== '---')
532
+ return 'File must begin with a YAML frontmatter delimiter (`---`).';
533
+ const closingLine = lines.indexOf('---', 1);
534
+ if (closingLine === -1)
535
+ return 'Opening frontmatter has no closing `---` delimiter.';
536
+ let fields;
537
+ try {
538
+ fields = yaml.parse(lines.slice(1, closingLine).join('\n'), {
539
+ maxAliasCount: 100,
540
+ schema: 'core',
541
+ uniqueKeys: true,
542
+ });
543
+ }
544
+ catch (error) {
545
+ return `Frontmatter is not valid YAML: ${errorMessage(error)}`;
546
+ }
547
+ if (!isRecord(fields))
548
+ return 'Frontmatter must be a YAML mapping.';
549
+ return {
550
+ fields,
551
+ body: lines.slice(closingLine + 1).join('\n'),
552
+ };
553
+ }
554
+ function readDisplayMetadata(content) {
555
+ const parsed = parseFrontmatter(content);
556
+ if (typeof parsed === 'string')
557
+ return {};
558
+ const title = normalizeDisplayText(parsed.fields.title);
559
+ const description = normalizeDisplayText(parsed.fields.description);
560
+ return {
561
+ ...(title ? { title } : {}),
562
+ ...(description ? { description } : {}),
563
+ };
564
+ }
565
+ function renderIndex(concepts, directories, version) {
566
+ const sections = [
567
+ renderIndexSection('Concepts', concepts),
568
+ renderIndexSection('Directories', directories),
569
+ ]
570
+ .filter(Boolean)
571
+ .join('\n\n');
572
+ const frontmatter = version ? `---\nokf_version: ${JSON.stringify(version)}\n---\n\n` : '';
573
+ return `${frontmatter}${sections}\n`;
574
+ }
575
+ function renderIndexSection(heading, links) {
576
+ if (links.length === 0)
577
+ return '';
578
+ const items = [...links]
579
+ .sort((left, right) => compareStrings(left.href, right.href))
580
+ .map(({ description, href, label }) => {
581
+ const link = `* [${escapeMarkdownLabel(label)}](${href})`;
582
+ return description ? `${link} - ${description}` : link;
583
+ });
584
+ return `# ${heading}\n\n${items.join('\n')}`;
585
+ }
586
+ function extractMarkdownLinks(content) {
587
+ return [...content.matchAll(/(?<!!)\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/gu)].map((match) => match[1]);
588
+ }
589
+ function isExternalOrAnchorLink(target) {
590
+ return target.startsWith('#') || target.startsWith('//') || /^[a-z][a-z0-9+.-]*:/iu.test(target);
591
+ }
592
+ function isConceptFilename(filename) {
593
+ return filename.endsWith('.md') && filename !== 'index.md' && filename !== 'log.md';
594
+ }
595
+ function isIsoDate(value) {
596
+ if (!/^\d{4}-\d{2}-\d{2}$/u.test(value))
597
+ return false;
598
+ const parsed = new Date(`${value}T00:00:00.000Z`);
599
+ return !Number.isNaN(parsed.valueOf()) && parsed.toISOString().slice(0, 10) === value;
600
+ }
601
+ function requireSupportedVersion(version) {
602
+ if (version !== SUPPORTED_OKF_VERSION) {
603
+ throw new Error(`Unsupported OKF version "${version}". DRS currently supports ${SUPPORTED_OKF_VERSION}.`);
604
+ }
605
+ }
606
+ function normalizeDisplayText(value) {
607
+ if (typeof value !== 'string' || !value.trim())
608
+ return undefined;
609
+ return value.replace(/\s+/gu, ' ').trim();
610
+ }
611
+ function titleFromSlug(slug) {
612
+ return slug
613
+ .split(/[-_\s]+/u)
614
+ .filter(Boolean)
615
+ .map((word) => `${word[0].toUpperCase()}${word.slice(1)}`)
616
+ .join(' ');
617
+ }
618
+ function escapeMarkdownLabel(value) {
619
+ return value.replaceAll('\\', '\\\\').replaceAll('[', '\\[').replaceAll(']', '\\]');
620
+ }
621
+ function stripBom(value) {
622
+ return value.replace(/^\uFEFF/u, '');
623
+ }
624
+ function toPosixPath(value) {
625
+ return sep === '/' ? value : value.split(sep).join('/');
626
+ }
627
+ function compareStrings(left, right) {
628
+ return left < right ? -1 : left > right ? 1 : 0;
629
+ }
630
+ function emptyGraphMetrics() {
631
+ return {
632
+ directedEdgeCount: 0,
633
+ nodeCount: 0,
634
+ orphanConceptCount: 0,
635
+ weaklyConnectedConceptCount: 0,
636
+ };
637
+ }
638
+ function issue(code, message, issuePath) {
639
+ return { code, message, ...(issuePath ? { path: issuePath } : {}) };
640
+ }
641
+ function isRecord(value) {
642
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
643
+ }
644
+ function errorMessage(error) {
645
+ return error instanceof Error ? error.message : String(error);
646
+ }
647
+ //# sourceMappingURL=okf-wiki.js.map