archgraph-argo 0.1.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 (54) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +115 -0
  3. package/argo/package.json +8 -0
  4. package/argo/rules/intent-architecture-global-rule.md +45 -0
  5. package/argo/schema/ImplementationToCodingHandoff.schema.json +252 -0
  6. package/argo/schema/ImplementationToIntentTraceProposal.schema.json +180 -0
  7. package/argo/schema/IntentToImplementationHandoff.schema.json +75 -0
  8. package/argo/schema/SystemArchitecture.schema.json +378 -0
  9. package/argo/schema/archimate3.2.pdf +0 -0
  10. package/argo/scripts/ARCHITECTURE.md +57 -0
  11. package/argo/scripts/archimate32-rules.js +12301 -0
  12. package/argo/scripts/argo-mcp-server.js +629 -0
  13. package/argo/scripts/argo-paths.js +77 -0
  14. package/argo/scripts/ensureArgoHarnessEnvironment.js +340 -0
  15. package/argo/scripts/generateArchitectureDiffPlantuml.js +466 -0
  16. package/argo/scripts/graph-rag/ARCHITECTURE.md +192 -0
  17. package/argo/scripts/graph-rag/canonicalProjectionAuthority.js +45 -0
  18. package/argo/scripts/graph-rag/defaultSemanticRetrieval.js +969 -0
  19. package/argo/scripts/graph-rag/embeddingQualificationGate.js +59 -0
  20. package/argo/scripts/graph-rag/externalProductionConfig.js +74 -0
  21. package/argo/scripts/graph-rag/liveEmbeddingIndexGate.js +129 -0
  22. package/argo/scripts/graph-rag/liveEmbeddingNeo4jBoundary.js +137 -0
  23. package/argo/scripts/graph-rag/liveEmbeddingProviderClient.js +49 -0
  24. package/argo/scripts/graph-rag/liveEmbeddingProviderConfig.js +481 -0
  25. package/argo/scripts/graph-rag/mutationEmbeddingVectorLifecycle.js +1261 -0
  26. package/argo/scripts/graph-rag/neo4jNativeRetrieval.js +37 -0
  27. package/argo/scripts/graph-rag/productionGraphRagRuntime.js +1624 -0
  28. package/argo/scripts/graph-rag/semantic-persistence/ARCHITECTURE.md +51 -0
  29. package/argo/scripts/graph-rag/semantic-persistence/productionSemanticBackfill.js +241 -0
  30. package/argo/scripts/graph-rag/semantic-persistence/productionSemanticCheckpointStore.js +99 -0
  31. package/argo/scripts/graph-rag/semantic-persistence/productionSemanticNeo4jAdapter.js +149 -0
  32. package/argo/scripts/graph-rag/semantic-persistence/productionSemanticProjectionStore.js +171 -0
  33. package/argo/scripts/graph-rag/semanticOperatorError.js +38 -0
  34. package/argo/scripts/graph-rag/semanticOperatorJourney.js +459 -0
  35. package/argo/scripts/graph-rag/semanticReadinessAttestationStore.js +398 -0
  36. package/argo/scripts/graph-rag/systemMetadataCommandAdapter.js +269 -0
  37. package/argo/scripts/graph-semantics.js +220 -0
  38. package/argo/scripts/neo4j-system-architecture-store.js +777 -0
  39. package/argo/scripts/repositoryArgoEnvironment.js +101 -0
  40. package/argo/scripts/runArchitectureTests.js +583 -0
  41. package/argo/scripts/semanticOperatorJourneyCli.js +91 -0
  42. package/argo/scripts/syncSystemArchitectureToNeo4j.js +67 -0
  43. package/argo/scripts/systemarchitecture-mcp-server.js +2965 -0
  44. package/argo/scripts/test-executors/_template.js +58 -0
  45. package/argo/scripts/test-executors/default.js +199 -0
  46. package/argo/scripts/validateStageHandoff.js +459 -0
  47. package/argo/scripts/validateSystemArchitecture.js +254 -0
  48. package/argo/scripts/validateTraceProposal.js +181 -0
  49. package/argo/scripts/validator-mcp-server.js +377 -0
  50. package/argo/skills/argo-init/SKILL.md +110 -0
  51. package/bin/argo-deploy.js +12 -0
  52. package/install-argo.ps1 +112 -0
  53. package/package.json +28 -0
  54. package/vendor/neo4j-driver-6.2.0.tgz +0 -0
@@ -0,0 +1,459 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const {
5
+ getArgoRoot,
6
+ getWorkspaceRoot,
7
+ } = require('./argo-paths.js');
8
+
9
+ const repoRoot = getWorkspaceRoot();
10
+ const SYSTEM_ARCHITECTURE_PATH = 'design/KG/SystemArchitecture.json';
11
+ const SUPPORTED_ACCEPTANCE_ENTRY_EXTENSIONS = new Set(['.js', '.cjs', '.mjs', '.py', '.java']);
12
+ const DISALLOWED_ACCEPTANCE_CRITERIA_PATTERNS = [
13
+ /^\s*(?:node|npm|npx|pnpm|yarn|python|py|bun)\b/i,
14
+ /^\s*(?:\.\\|\.\/)?[^\s]+\s+[^:]+/i,
15
+ /[`'"|;&]/,
16
+ ];
17
+
18
+ const HANDOFFS = {
19
+ 'intent-to-implementation': {
20
+ filePath: '.argo/temp/IntentToImplementationHandoff.json',
21
+ schemaPath: '.argo/schema/IntentToImplementationHandoff.schema.json',
22
+ validate: validateIntentToImplementation,
23
+ },
24
+ 'implementation-to-coding': {
25
+ filePath: '.argo/temp/ImplementationToCodingHandoff.json',
26
+ schemaPath: '.argo/schema/ImplementationToCodingHandoff.schema.json',
27
+ validate: validateImplementationToCoding,
28
+ },
29
+ };
30
+
31
+ function main() {
32
+ const stage = process.argv[2];
33
+ const stages = stage ? [stage] : Object.keys(HANDOFFS);
34
+ const errors = [];
35
+
36
+ for (const currentStage of stages) {
37
+ const config = HANDOFFS[currentStage];
38
+ if (!config) {
39
+ errors.push(`Unknown stage '${currentStage}'. Expected one of: ${Object.keys(HANDOFFS).join(', ')}`);
40
+ continue;
41
+ }
42
+ validateStage(currentStage, config, errors);
43
+ }
44
+
45
+ if (errors.length > 0) {
46
+ console.error('Stage handoff validation failed:');
47
+ for (const error of errors) {
48
+ console.error(`- ${error}`);
49
+ }
50
+ process.exit(1);
51
+ }
52
+
53
+ console.log(`Stage handoff validation passed for: ${stages.join(', ')}`);
54
+ }
55
+
56
+ function validateStage(stage, config, errors) {
57
+ const handoffAbsolutePath = path.join(repoRoot, config.filePath);
58
+ const schemaAbsolutePath = resolveSchemaAbsolutePath(config.schemaPath);
59
+
60
+ if (!fs.existsSync(schemaAbsolutePath)) {
61
+ errors.push(`${stage}: schema file is missing at ${config.schemaPath}`);
62
+ return;
63
+ }
64
+
65
+ if (!fs.existsSync(handoffAbsolutePath)) {
66
+ errors.push(`${stage}: handoff file is missing at ${config.filePath}`);
67
+ return;
68
+ }
69
+
70
+ let document;
71
+ try {
72
+ document = JSON.parse(fs.readFileSync(handoffAbsolutePath, 'utf8'));
73
+ } catch (error) {
74
+ errors.push(`${stage}: failed to parse ${config.filePath}: ${String(error)}`);
75
+ return;
76
+ }
77
+
78
+ config.validate(document, errors, config.filePath);
79
+ }
80
+
81
+ function resolveSchemaAbsolutePath(schemaPath) {
82
+ const bundledCandidate = path.join(getArgoRoot(), 'schema', path.basename(schemaPath));
83
+ if (fs.existsSync(bundledCandidate)) {
84
+ return bundledCandidate;
85
+ }
86
+ return path.join(repoRoot, schemaPath);
87
+ }
88
+
89
+ function validateIntentToImplementation(document, errors, filePath) {
90
+ requireString(document, 'stage', errors, filePath);
91
+ requireString(document, 'generatedAt', errors, filePath);
92
+ requireString(document, 'sourceIntentGraphPath', errors, filePath);
93
+ const intentElementIds = requireStringArray(document, 'intentElementIds', true, errors, filePath);
94
+ ['explicitTestcases', 'frozenBaselines', 'requiredImplementationArtifacts'].forEach((legacyField) => {
95
+ if (Object.prototype.hasOwnProperty.call(document, legacyField)) {
96
+ errors.push(`${filePath}.${legacyField} must not be present. Intent-to-implementation handoff only carries architecture element ids; explicit testcase baselines belong in ${SYSTEM_ARCHITECTURE_PATH}`);
97
+ }
98
+ });
99
+
100
+ const graphPath = requireString(document, 'sourceIntentGraphPath', errors, filePath);
101
+ if (graphPath) {
102
+ ensureRepoPathExists(graphPath, `${filePath}.sourceIntentGraphPath`, errors);
103
+ }
104
+
105
+ const graphDocument = loadSystemArchitecture(errors, filePath);
106
+ if (graphDocument && Array.isArray(intentElementIds)) {
107
+ const elementIds = new Set((graphDocument.elements || []).map((element) => element && element.id).filter(Boolean));
108
+ intentElementIds.forEach((elementId, index) => {
109
+ if (!elementIds.has(elementId)) {
110
+ errors.push(`${filePath}.intentElementIds[${index}] references missing intent architecture element '${elementId}' in ${SYSTEM_ARCHITECTURE_PATH}`);
111
+ }
112
+ });
113
+ validateIntentElementTestcases(graphDocument, intentElementIds, errors, filePath);
114
+ }
115
+
116
+ const questions = document.openQuestions;
117
+ if (Array.isArray(questions)) {
118
+ questions.forEach((question, index) => {
119
+ requireString(question, 'question', errors, `${filePath}.openQuestions[${index}]`);
120
+ requireString(question, 'recommendedAnswer', errors, `${filePath}.openQuestions[${index}]`);
121
+ requireString(question, 'reason', errors, `${filePath}.openQuestions[${index}]`);
122
+ });
123
+ }
124
+ }
125
+
126
+ function validateIntentElementTestcases(graphDocument, intentElementIds, errors, filePath) {
127
+ const elementsById = buildElementsById(graphDocument);
128
+
129
+ intentElementIds.forEach((elementId, index) => {
130
+ if (!elementsById.has(elementId)) {
131
+ return;
132
+ }
133
+
134
+ const element = elementsById.get(elementId);
135
+ if (!hasMountedTestcase(element)) {
136
+ errors.push(buildMissingMountedTestcaseError(filePath, index, element, elementId));
137
+ }
138
+ });
139
+ }
140
+
141
+ function buildElementsById(graphDocument) {
142
+ const elementsById = new Map();
143
+
144
+ (graphDocument.elements || []).forEach((element) => {
145
+ if (element && typeof element.id === 'string' && element.id.trim() !== '') {
146
+ elementsById.set(element.id, element);
147
+ }
148
+ });
149
+
150
+ return elementsById;
151
+ }
152
+
153
+ function hasMountedTestcase(element) {
154
+ return Array.isArray(element && element.testcases) && element.testcases.length > 0;
155
+ }
156
+
157
+ function buildMissingMountedTestcaseError(filePath, handoffElementIndex, element, fallbackElementId) {
158
+ const elementId = element && element.id ? element.id : fallbackElementId;
159
+ const elementName = element && element.name ? element.name : '<unnamed>';
160
+ const functionalPointHint = describeFunctionalPointHint(element);
161
+ return `${filePath}.intentElementIds[${handoffElementIndex}] intent element '${elementId}' ('${elementName}') has no mounted testcases. ` +
162
+ `Mount Acceptance Test testcases that cover this element${functionalPointHint} under the exact element before validating intent-to-implementation.`;
163
+ }
164
+
165
+ function describeFunctionalPointHint(element) {
166
+ const functionalPoints = extractFunctionalPointDescriptions(element);
167
+ if (functionalPoints.length === 0) {
168
+ return "'s functional points";
169
+ }
170
+
171
+ return `'s functional points (${functionalPoints.join('; ')})`;
172
+ }
173
+
174
+ function extractFunctionalPointDescriptions(element) {
175
+ if (!Array.isArray(element && element.attributes)) {
176
+ return [];
177
+ }
178
+
179
+ return element.attributes
180
+ .filter((attribute) => attribute && typeof attribute.name === 'string' && /functional/i.test(attribute.name))
181
+ .map((attribute) => typeof attribute.description === 'string' ? attribute.description.trim() : '')
182
+ .filter(Boolean);
183
+ }
184
+
185
+ function validateImplementationToCoding(document, errors, filePath) {
186
+ const graphDocument = loadSystemArchitecture(errors, filePath);
187
+ const acceptanceCriteriaByTestcase = buildAcceptanceCriteriaByTestcase(graphDocument, errors, filePath);
188
+
189
+ requireString(document, 'stage', errors, filePath);
190
+ requireString(document, 'generatedAt', errors, filePath);
191
+ const graphPath = requireString(document, 'sourceIntentGraphPath', errors, filePath);
192
+ if (graphPath) {
193
+ ensureRepoPathExists(graphPath, `${filePath}.sourceIntentGraphPath`, errors);
194
+ }
195
+
196
+ const implementationContracts = requireStringArray(document, 'implementationContracts', true, errors, filePath);
197
+ if (Array.isArray(implementationContracts)) {
198
+ implementationContracts.forEach((contractPath, index) => {
199
+ ensureRepoPathExists(contractPath, `${filePath}.implementationContracts[${index}]`, errors);
200
+ });
201
+ }
202
+
203
+ const explicitEntrypoints = requireArray(document, 'explicitEntrypoints', true, errors, filePath);
204
+ if (Array.isArray(explicitEntrypoints)) {
205
+ explicitEntrypoints.forEach((entry, index) => {
206
+ const testcaseName = requireString(entry, 'testcaseName', errors, `${filePath}.explicitEntrypoints[${index}]`);
207
+ const entryPath = requireString(entry, 'entryPath', errors, `${filePath}.explicitEntrypoints[${index}]`);
208
+ requireString(entry, 'controlPoint', errors, `${filePath}.explicitEntrypoints[${index}]`);
209
+ requireString(entry, 'observationPoint', errors, `${filePath}.explicitEntrypoints[${index}]`);
210
+ const status = requireString(entry, 'initialExecutionStatus', errors, `${filePath}.explicitEntrypoints[${index}]`);
211
+ requireString(entry, 'initialExecutionCommand', errors, `${filePath}.explicitEntrypoints[${index}]`);
212
+ if (entryPath) {
213
+ validateAcceptanceEntryReference(entryPath, `${filePath}.explicitEntrypoints[${index}].entryPath`, errors);
214
+ }
215
+ if (status && !['passed', 'failed'].includes(status)) {
216
+ errors.push(`${filePath}.explicitEntrypoints[${index}].initialExecutionStatus must be 'passed' or 'failed'`);
217
+ }
218
+ if (status === 'failed') {
219
+ requireString(entry, 'failureReason', errors, `${filePath}.explicitEntrypoints[${index}]`);
220
+ }
221
+ if (testcaseName && entryPath) {
222
+ const acceptanceCriteria = acceptanceCriteriaByTestcase.get(testcaseName);
223
+ if (!acceptanceCriteria) {
224
+ errors.push(`${filePath}.explicitEntrypoints[${index}] testcase '${testcaseName}' is missing from ${SYSTEM_ARCHITECTURE_PATH} or has an empty acceptanceCriteria`);
225
+ } else if (normalizeEntrypointReference(acceptanceCriteria) !== normalizeEntrypointReference(entryPath)) {
226
+ errors.push(`${filePath}.explicitEntrypoints[${index}] entryPath '${entryPath}' must match ${SYSTEM_ARCHITECTURE_PATH} acceptanceCriteria '${acceptanceCriteria}' for testcase '${testcaseName}'`);
227
+ }
228
+ }
229
+ });
230
+ }
231
+
232
+ const criticalTests = requireArray(document, 'criticalNonExplicitTests', false, errors, filePath) || [];
233
+ criticalTests.forEach((test, index) => validateNonExplicitTest(test, `${filePath}.criticalNonExplicitTests[${index}]`, errors));
234
+
235
+ const supportingTests = requireArray(document, 'supportingNonExplicitTests', false, errors, filePath) || [];
236
+ supportingTests.forEach((test, index) => validateNonExplicitTest(test, `${filePath}.supportingNonExplicitTests[${index}]`, errors));
237
+
238
+ const failureRecordsPath = requireString(document, 'expectedFailureRecordsPath', errors, filePath);
239
+ if (failureRecordsPath) {
240
+ ensureRepoPathExists(failureRecordsPath, `${filePath}.expectedFailureRecordsPath`, errors);
241
+ }
242
+
243
+ const codingTargets = requireArray(document, 'codingTargets', true, errors, filePath);
244
+ if (Array.isArray(codingTargets)) {
245
+ codingTargets.forEach((target, index) => {
246
+ requireString(target, 'failureSignal', errors, `${filePath}.codingTargets[${index}]`);
247
+ requireString(target, 'nextAction', errors, `${filePath}.codingTargets[${index}]`);
248
+ });
249
+ }
250
+
251
+ const taskExecutionPlan = document.taskExecutionPlan;
252
+ if (!taskExecutionPlan || typeof taskExecutionPlan !== 'object' || Array.isArray(taskExecutionPlan)) {
253
+ errors.push(`${filePath}.taskExecutionPlan must be an object`);
254
+ } else {
255
+ requireString(taskExecutionPlan, 'executionStrategy', errors, `${filePath}.taskExecutionPlan`);
256
+ const tasks = requireArray(taskExecutionPlan, 'tasks', true, errors, `${filePath}.taskExecutionPlan`);
257
+ if (Array.isArray(tasks)) {
258
+ const taskIds = new Set();
259
+ tasks.forEach((task, index) => {
260
+ const prefix = `${filePath}.taskExecutionPlan.tasks[${index}]`;
261
+ const taskId = requireString(task, 'taskId', errors, prefix);
262
+ requireString(task, 'title', errors, prefix);
263
+ requireString(task, 'objective', errors, prefix);
264
+ requireString(task, 'completionSignal', errors, prefix);
265
+
266
+ const steps = requireArray(task, 'steps', true, errors, prefix);
267
+ if (Array.isArray(steps)) {
268
+ steps.forEach((step, stepIndex) => {
269
+ if (typeof step !== 'string' || step.trim() === '') {
270
+ errors.push(`${prefix}.steps[${stepIndex}] must be a non-empty string`);
271
+ }
272
+ });
273
+ }
274
+
275
+ if (taskId) {
276
+ if (taskIds.has(taskId)) {
277
+ errors.push(`${prefix}.taskId '${taskId}' must be unique within taskExecutionPlan.tasks`);
278
+ }
279
+ taskIds.add(taskId);
280
+ }
281
+ });
282
+
283
+ tasks.forEach((task, index) => {
284
+ const dependsOn = Array.isArray(task.dependsOn) ? task.dependsOn : [];
285
+ dependsOn.forEach((dependencyId, dependencyIndex) => {
286
+ if (typeof dependencyId !== 'string' || dependencyId.trim() === '') {
287
+ errors.push(`${filePath}.taskExecutionPlan.tasks[${index}].dependsOn[${dependencyIndex}] must be a non-empty string`);
288
+ return;
289
+ }
290
+ if (!taskIds.has(dependencyId)) {
291
+ errors.push(`${filePath}.taskExecutionPlan.tasks[${index}].dependsOn[${dependencyIndex}] references unknown taskId '${dependencyId}'`);
292
+ }
293
+ });
294
+
295
+ validateOptionalStringArray(task, 'relatedTestcases', `${filePath}.taskExecutionPlan.tasks[${index}]`, errors);
296
+ validateOptionalStringArray(task, 'targetPaths', `${filePath}.taskExecutionPlan.tasks[${index}]`, errors);
297
+ });
298
+ }
299
+ }
300
+
301
+ const frozenFiles = requireStringArray(document, 'frozenFiles', true, errors, filePath);
302
+ if (Array.isArray(frozenFiles)) {
303
+ frozenFiles.forEach((frozenFile, index) => {
304
+ ensureRepoPathExists(frozenFile, `${filePath}.frozenFiles[${index}]`, errors);
305
+ });
306
+ }
307
+ }
308
+
309
+ function validateNonExplicitTest(test, prefix, errors) {
310
+ const testPath = requireString(test, 'path', errors, prefix);
311
+ requireString(test, 'controlPoint', errors, prefix);
312
+ requireString(test, 'observationPoint', errors, prefix);
313
+ if (testPath) {
314
+ ensureRepoPathExists(testPath, `${prefix}.path`, errors);
315
+ }
316
+ }
317
+
318
+ function loadSystemArchitecture(errors, filePath) {
319
+ const absolutePath = path.join(repoRoot, SYSTEM_ARCHITECTURE_PATH);
320
+ if (!fs.existsSync(absolutePath)) {
321
+ errors.push(`${filePath}: required graph file is missing at ${SYSTEM_ARCHITECTURE_PATH}`);
322
+ return undefined;
323
+ }
324
+
325
+ try {
326
+ return JSON.parse(fs.readFileSync(absolutePath, 'utf8'));
327
+ } catch (error) {
328
+ errors.push(`${filePath}: failed to parse ${SYSTEM_ARCHITECTURE_PATH}: ${String(error)}`);
329
+ return undefined;
330
+ }
331
+ }
332
+
333
+ function buildAcceptanceCriteriaByTestcase(graphDocument, errors, filePath) {
334
+ const mapping = new Map();
335
+ if (!graphDocument || !Array.isArray(graphDocument.elements)) {
336
+ return mapping;
337
+ }
338
+
339
+ graphDocument.elements.forEach((element, elementIndex) => {
340
+ if (!Array.isArray(element.testcases)) {
341
+ return;
342
+ }
343
+
344
+ element.testcases.forEach((testcase, testcaseIndex) => {
345
+ if (!testcase || typeof testcase.name !== 'string' || testcase.name.trim() === '') {
346
+ return;
347
+ }
348
+
349
+ const testcaseName = testcase.name.trim();
350
+ const acceptanceCriteria = typeof testcase.acceptanceCriteria === 'string'
351
+ ? testcase.acceptanceCriteria.trim()
352
+ : '';
353
+
354
+ if (!acceptanceCriteria) {
355
+ errors.push(`${filePath}: ${SYSTEM_ARCHITECTURE_PATH}.elements[${elementIndex}].testcases[${testcaseIndex}].acceptanceCriteria must be a non-empty entrypoint string for testcase '${testcaseName}'`);
356
+ return;
357
+ }
358
+
359
+ validateAcceptanceEntryReference(
360
+ acceptanceCriteria,
361
+ `${SYSTEM_ARCHITECTURE_PATH}.testcase(${testcaseName}).acceptanceCriteria`,
362
+ errors,
363
+ );
364
+
365
+ mapping.set(testcaseName, acceptanceCriteria);
366
+ });
367
+ });
368
+
369
+ return mapping;
370
+ }
371
+
372
+ function validateAcceptanceEntryReference(value, label, errors) {
373
+ if (typeof value !== 'string' || value.trim() === '') {
374
+ errors.push(`${label} must be a non-empty string`);
375
+ return;
376
+ }
377
+
378
+ const trimmed = value.trim();
379
+ for (const pattern of DISALLOWED_ACCEPTANCE_CRITERIA_PATTERNS) {
380
+ if (pattern.test(trimmed)) {
381
+ errors.push(`${label} must be a single workspace-relative testcase entrypoint, not a descriptive sentence or wrapped command`);
382
+ return;
383
+ }
384
+ }
385
+
386
+ const scriptPath = normalizeEntrypointReference(trimmed);
387
+ const extension = path.extname(scriptPath).toLowerCase();
388
+ if (!SUPPORTED_ACCEPTANCE_ENTRY_EXTENSIONS.has(extension)) {
389
+ errors.push(`${label} must point to a single executable entry file (${Array.from(SUPPORTED_ACCEPTANCE_ENTRY_EXTENSIONS).join(', ')}) optionally followed by a pytest ::selector`);
390
+ return;
391
+ }
392
+
393
+ ensureRepoPathExists(scriptPath, label, errors);
394
+ }
395
+
396
+ function normalizeEntrypointReference(value) {
397
+ const [scriptPath] = String(value).split('::');
398
+ const [pathWithoutFragment] = scriptPath.split('#');
399
+ return pathWithoutFragment.replace(/\\/g, '/').replace(/^\.\//, '').trim();
400
+ }
401
+
402
+ function requireString(object, key, errors, prefix) {
403
+ if (!object || typeof object[key] !== 'string' || object[key].trim() === '') {
404
+ errors.push(`${prefix}.${key} must be a non-empty string`);
405
+ return undefined;
406
+ }
407
+ return object[key];
408
+ }
409
+
410
+ function requireArray(object, key, mustHaveItems, errors, prefix) {
411
+ if (!object || !Array.isArray(object[key])) {
412
+ errors.push(`${prefix}.${key} must be an array`);
413
+ return undefined;
414
+ }
415
+ if (mustHaveItems && object[key].length === 0) {
416
+ errors.push(`${prefix}.${key} must not be empty`);
417
+ }
418
+ return object[key];
419
+ }
420
+
421
+ function requireStringArray(object, key, mustHaveItems, errors, prefix) {
422
+ const value = requireArray(object, key, mustHaveItems, errors, prefix);
423
+ if (!Array.isArray(value)) {
424
+ return undefined;
425
+ }
426
+ value.forEach((entry, index) => {
427
+ if (typeof entry !== 'string' || entry.trim() === '') {
428
+ errors.push(`${prefix}.${key}[${index}] must be a non-empty string`);
429
+ }
430
+ });
431
+ return value;
432
+ }
433
+
434
+ function validateOptionalStringArray(object, key, prefix, errors) {
435
+ if (object[key] === undefined) {
436
+ return;
437
+ }
438
+
439
+ if (!Array.isArray(object[key])) {
440
+ errors.push(`${prefix}.${key} must be an array`);
441
+ return;
442
+ }
443
+
444
+ object[key].forEach((entry, index) => {
445
+ if (typeof entry !== 'string' || entry.trim() === '') {
446
+ errors.push(`${prefix}.${key}[${index}] must be a non-empty string`);
447
+ }
448
+ });
449
+ }
450
+
451
+ function ensureRepoPathExists(relativePath, label, errors) {
452
+ const normalized = relativePath.replace(/[\\/]+/g, path.sep);
453
+ const absolutePath = path.join(repoRoot, normalized);
454
+ if (!fs.existsSync(absolutePath)) {
455
+ errors.push(`${label} points to a missing path: ${relativePath}`);
456
+ }
457
+ }
458
+
459
+ main();