@magnusekdahl/parallix 1.1.0 → 1.2.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.
- package/config/integration-pipelines.json +9 -0
- package/config/workflow.config.schema.json +12 -0
- package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +10 -8
- package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +14 -12
- package/docs/adr/0041-integration-pipeline-gates.md +1 -1
- package/docs/adr/0047-per-mission-change-size-budget.md +161 -0
- package/docs/adr/index.md +1 -0
- package/docs/use-cases.md +44 -0
- package/lib/agents/agents.js +149 -100
- package/lib/agents/claude-telemetry.js +14 -10
- package/lib/agents/claude.js +32 -11
- package/lib/agents/codex-telemetry.js +32 -19
- package/lib/agents/codex.js +36 -11
- package/lib/agents/limit-hit.js +42 -25
- package/lib/agents/mistral-telemetry.js +1 -1
- package/lib/agents/mistral.js +13 -3
- package/lib/agents/opencode-export.js +11 -5
- package/lib/agents/opencode-telemetry.js +75 -90
- package/lib/agents/opencode.js +86 -29
- package/lib/agents/stage-telemetry.js +3 -7
- package/lib/commands/active.js +139 -94
- package/lib/commands/checkpoint.js +3 -1
- package/lib/commands/config.js +5 -3
- package/lib/commands/coverage-gate.js +17 -10
- package/lib/commands/diff.js +11 -5
- package/lib/commands/draft.js +95 -55
- package/lib/commands/handoff.js +253 -65
- package/lib/commands/integrate.js +288 -188
- package/lib/commands/mission-start.js +39 -34
- package/lib/commands/rebase.js +32 -21
- package/lib/commands/repair-handoff.js +21 -18
- package/lib/commands/resolve-conflict.js +5 -2
- package/lib/commands/review.js +1 -0
- package/lib/commands/setup-review.js +1 -0
- package/lib/commands/stats-backfill.js +50 -41
- package/lib/commands/stats.js +653 -204
- package/lib/commands/status.js +33 -28
- package/lib/core/fmt.js +58 -15
- package/lib/core/git.js +7 -5
- package/lib/core/gitignore.js +6 -4
- package/lib/core/mission-utils.js +161 -88
- package/lib/core/nels.js +199 -0
- package/lib/core/persistent-data-migration.js +55 -23
- package/lib/core/product-config.js +44 -23
- package/lib/core/runtime-matrix.js +15 -2
- package/lib/core/spawn-tee.js +40 -13
- package/lib/core/state-map.js +32 -13
- package/lib/core/storage.js +19 -5
- package/lib/core/subagent-limit.js +28 -0
- package/lib/core/verification.js +29 -12
- package/lib/review/rebase.js +11 -3
- package/lib/review/review-adapter.js +93 -6
- package/lib/review/review-artifacts.js +89 -23
- package/lib/review/review-commands.js +139 -45
- package/lib/review/review-events.js +103 -52
- package/lib/review/review-loop.js +117 -87
- package/lib/review/review-polling.js +25 -5
- package/lib/review/review-prompts.js +36 -5
- package/lib/review/review-state.js +39 -15
- package/lib/tools/backlog.js +162 -71
- package/lib/tools/forgejo.js +290 -129
- package/lib/tools/gatekeeper.js +15 -0
- package/lib/tools/redgreen.js +220 -0
- package/lib/tools/sessions.js +12 -5
- package/lib/tools/setup-review.js +170 -70
- package/package.json +11 -3
- package/prompts/draft.md +6 -0
- package/templates/mission-scaffold.md +1 -1
|
@@ -93,7 +93,7 @@ const VALID_VERDICTS = Object.freeze([
|
|
|
93
93
|
*/
|
|
94
94
|
function reviewEventsDir(slug, rootDir = process.cwd()) {
|
|
95
95
|
const missionDir = findMissionDir(slug, rootDir);
|
|
96
|
-
if (!missionDir) return null;
|
|
96
|
+
if (!missionDir) {return null;}
|
|
97
97
|
const dir = path.join(missionDir, 'review-events');
|
|
98
98
|
if (!fs.existsSync(dir)) {
|
|
99
99
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -118,9 +118,9 @@ function reviewEventsDir(slug, rootDir = process.cwd()) {
|
|
|
118
118
|
* @param {string} [rootDir] - Root directory to resolve from
|
|
119
119
|
* @returns {string|null} - Absolute file path, or null if mission dir not found
|
|
120
120
|
*/
|
|
121
|
-
function eventFilePath(slug, eventType, round, actor, timestamp = null, rootDir = process.cwd()) {
|
|
121
|
+
function eventFilePath(slug, eventType, round, actor, /** @type {string|null} */ timestamp = null, rootDir = process.cwd()) {
|
|
122
122
|
const eventsDir = reviewEventsDir(slug, rootDir);
|
|
123
|
-
if (!eventsDir) return null;
|
|
123
|
+
if (!eventsDir) {return null;}
|
|
124
124
|
|
|
125
125
|
const ts = timestamp || generateEventTimestamp();
|
|
126
126
|
const sanitizedActor = sanitizeFilename(actor);
|
|
@@ -161,7 +161,7 @@ function generateEventTimestamp() {
|
|
|
161
161
|
* @returns {string} - Filesystem-safe string
|
|
162
162
|
*/
|
|
163
163
|
function sanitizeFilename(str) {
|
|
164
|
-
if (!str) return 'unknown';
|
|
164
|
+
if (!str) {return 'unknown';}
|
|
165
165
|
return String(str)
|
|
166
166
|
.toLowerCase()
|
|
167
167
|
// Replace @ and . with empty string first (common in emails/agent names)
|
|
@@ -179,8 +179,8 @@ function sanitizeFilename(str) {
|
|
|
179
179
|
* @param {string} eventType - The type to validate
|
|
180
180
|
* @returns {boolean} - True if valid
|
|
181
181
|
*/
|
|
182
|
-
function isValidEventType(eventType) {
|
|
183
|
-
return ALL_EVENT_TYPES.includes(eventType);
|
|
182
|
+
function isValidEventType(/** @type {string} */ eventType) {
|
|
183
|
+
return ALL_EVENT_TYPES.includes(/** @type {typeof ALL_EVENT_TYPES[number]} */ (eventType));
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
/**
|
|
@@ -206,8 +206,8 @@ function isValidVerdict(verdict) {
|
|
|
206
206
|
* @param {string} eventType - The event type
|
|
207
207
|
* @returns {boolean} - True if mirrored
|
|
208
208
|
*/
|
|
209
|
-
function shouldMirrorToProvider(eventType) {
|
|
210
|
-
return MIRRORED_EVENT_TYPES.has(eventType);
|
|
209
|
+
function shouldMirrorToProvider(/** @type {string} */ eventType) {
|
|
210
|
+
return MIRRORED_EVENT_TYPES.has(/** @type {typeof MIRRORED_EVENT_TYPES extends Set<infer T> ? T : string} */ (eventType));
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
// -------- Event Structure --------
|
|
@@ -221,8 +221,15 @@ function shouldMirrorToProvider(eventType) {
|
|
|
221
221
|
* @param {Object} [metadata] - Additional metadata (round, phase, actor, etc.)
|
|
222
222
|
* @returns {Object} - Normalized event structure
|
|
223
223
|
*/
|
|
224
|
+
/**
|
|
225
|
+
* @param {string} content
|
|
226
|
+
* @param {string} eventType
|
|
227
|
+
* @param {{[key: string]: any}} [metadata]
|
|
228
|
+
* @returns {{eventType: string, timestamp: string, content: string}}
|
|
229
|
+
*/
|
|
224
230
|
function normalizeEventContent(content, eventType, metadata = {}) {
|
|
225
|
-
|
|
231
|
+
/** @type {{eventType: string, timestamp: string, content: string, [key: string]: any}} */
|
|
232
|
+
const payload = { eventType, timestamp: '', content, ...metadata };
|
|
226
233
|
|
|
227
234
|
// Ensure we have the base metadata
|
|
228
235
|
payload.eventType = eventType;
|
|
@@ -241,6 +248,10 @@ function normalizeEventContent(content, eventType, metadata = {}) {
|
|
|
241
248
|
* @param {Object} event - Normalized event structure
|
|
242
249
|
* @returns {string} - Frontmatter block
|
|
243
250
|
*/
|
|
251
|
+
/**
|
|
252
|
+
* @param {{eventType: string, timestamp: string, round?: number, phase?: string, actor?: string, slug?: string, disposition?: string, verdict?: string, fixedItems?: string[], pushedBackItems?: string[], parkedItems?: string[], blockedReason?: string, followUpReference?: string}} event
|
|
253
|
+
* @returns {string}
|
|
254
|
+
*/
|
|
244
255
|
function buildEventFrontmatter(event) {
|
|
245
256
|
const lines = [
|
|
246
257
|
'---',
|
|
@@ -319,7 +330,7 @@ function hasWorkflowFooter(body) {
|
|
|
319
330
|
* @param {Object} comment - Comment object with body field
|
|
320
331
|
* @returns {string|null} - 'human_note' if human, null if workflow-generated
|
|
321
332
|
*/
|
|
322
|
-
function classifyComment(comment) {
|
|
333
|
+
function classifyComment(/** @type {{[key: string]: any}} */ comment) {
|
|
323
334
|
const body = comment.body || '';
|
|
324
335
|
// Workflow-generated comments have the metadata footer
|
|
325
336
|
if (hasWorkflowFooter(body)) {
|
|
@@ -349,6 +360,12 @@ function classifyComment(comment) {
|
|
|
349
360
|
* @param {Function} [options.error] - Error function
|
|
350
361
|
* @returns {Object} - { ok: boolean, created: Array, skipped: Array }
|
|
351
362
|
*/
|
|
363
|
+
/**
|
|
364
|
+
* @param {string} slug
|
|
365
|
+
* @param {string} actor
|
|
366
|
+
* @param {{getCommentsFn?: Function, createEventFn?: Function, readTokenFn?: Function, reviewIdentity?: string|null, forgejoUser?: string|null, worktree?: string, log?: Function, error?: Function}} [options]
|
|
367
|
+
* @returns {Promise<{ok: boolean, created: Array<any>, skipped: Array<any>, error?: string}>}
|
|
368
|
+
*/
|
|
352
369
|
async function consumeHumanNotes(slug, actor, options = {}) {
|
|
353
370
|
const {
|
|
354
371
|
getCommentsFn,
|
|
@@ -440,9 +457,13 @@ async function consumeHumanNotes(slug, actor, options = {}) {
|
|
|
440
457
|
* @param {Object} event - Normalized event structure
|
|
441
458
|
* @returns {string} - Complete file content
|
|
442
459
|
*/
|
|
460
|
+
/**
|
|
461
|
+
* @param {{content?: string, round?: number, phase?: string, slug?: string, eventType?: string, timestamp?: string}} event
|
|
462
|
+
* @returns {string}
|
|
463
|
+
*/
|
|
443
464
|
function renderEventFile(event) {
|
|
444
|
-
const frontmatter = buildEventFrontmatter(event);
|
|
445
|
-
const content = event.content || '';
|
|
465
|
+
const frontmatter = buildEventFrontmatter(/** @type {{eventType: string, timestamp: string, content?: string, round?: number, phase?: string, slug?: string}} */ (event));
|
|
466
|
+
const content = /** @type {string} */ (event.content || '');
|
|
446
467
|
|
|
447
468
|
// If the content already has a metadata footer, don't add another
|
|
448
469
|
const hasFooter = /\n\n---\n`\[workflow-round:\d+, workflow-phase:[^\]]+\]`/.test(content);
|
|
@@ -453,7 +474,7 @@ function renderEventFile(event) {
|
|
|
453
474
|
|
|
454
475
|
// Add footer if we have round and phase
|
|
455
476
|
const footer = event.round !== undefined && event.phase !== undefined
|
|
456
|
-
? buildEventFooter(event.slug, event.round, event.phase)
|
|
477
|
+
? buildEventFooter(/** @type {string} */ (event.slug), event.round, event.phase)
|
|
457
478
|
: '';
|
|
458
479
|
|
|
459
480
|
return `${frontmatter}\n\n${content}${footer}`;
|
|
@@ -473,9 +494,9 @@ function renderEventFile(event) {
|
|
|
473
494
|
* @param {string} [params.actor] - Agent family or 'human'
|
|
474
495
|
* @param {string} [params.disposition] - For implementer disposition events
|
|
475
496
|
* @param {string} [params.verdict] - For reviewer outcome events
|
|
476
|
-
* @param {Array} [params.fixedItems] - For implementer round summary
|
|
477
|
-
* @param {Array} [params.pushedBackItems] - For implementer round summary
|
|
478
|
-
* @param {Array} [params.parkedItems] - For implementer round summary
|
|
497
|
+
* @param {Array<any>} [params.fixedItems] - For implementer round summary
|
|
498
|
+
* @param {Array<any>} [params.pushedBackItems] - For implementer round summary
|
|
499
|
+
* @param {Array<any>} [params.parkedItems] - For implementer round summary
|
|
479
500
|
* @param {string} [params.blockedReason] - For BLOCKED events
|
|
480
501
|
* @param {string} [params.followUpReference] - For PARKED_FOLLOWUP events
|
|
481
502
|
* @param {string} [params.timestamp] - Custom timestamp
|
|
@@ -485,6 +506,13 @@ function renderEventFile(event) {
|
|
|
485
506
|
* @param {Function} [options.gitFn] - Git function override for testing
|
|
486
507
|
* @returns {Object} - { ok: boolean, path: string|null, error: string|null }
|
|
487
508
|
*/
|
|
509
|
+
/**
|
|
510
|
+
* @param {string} slug
|
|
511
|
+
* @param {string} eventType
|
|
512
|
+
* @param {{content: string, round?: number, phase?: string, actor?: string, disposition?: string, verdict?: string, fixedItems?: Array<any>, pushedBackItems?: Array<any>, parkedItems?: Array<any>, blockedReason?: string, followUpReference?: string, timestamp?: string}} params
|
|
513
|
+
* @param {{skipGit?: boolean, gitFn?: Function, log?: Function, error?: Function, worktree?: string, allowMissingRequiredFields?: boolean}} [options]
|
|
514
|
+
* @returns {{ok: boolean, path: string|null, error?: string|null, event?: Object}}
|
|
515
|
+
*/
|
|
488
516
|
function createEvent(slug, eventType, params, options = {}) {
|
|
489
517
|
const {
|
|
490
518
|
skipGit = false,
|
|
@@ -605,6 +633,14 @@ function createEvent(slug, eventType, params, options = {}) {
|
|
|
605
633
|
* @param {Object} [options] - Options (same as createEvent)
|
|
606
634
|
* @returns {Object} - { ok: boolean, path: string|null, importedFrom: string|null, error: string|null }
|
|
607
635
|
*/
|
|
636
|
+
/**
|
|
637
|
+
* @param {string} slug
|
|
638
|
+
* @param {string} artifactName
|
|
639
|
+
* @param {string} eventType
|
|
640
|
+
* @param {{round?: number, phase?: string, actor?: string}} [params]
|
|
641
|
+
* @param {{tmpDir?: string, log?: Function, error?: Function, worktree?: string}} [options]
|
|
642
|
+
* @returns {{ok: boolean, path: string|null, importedFrom: string|null, event?: Object, skipped?: boolean, legacyPath?: string, error?: string}}
|
|
643
|
+
*/
|
|
608
644
|
function importLegacyArtifact(slug, artifactName, eventType, params = {}, options = {}) {
|
|
609
645
|
const {
|
|
610
646
|
tmpDir = os.tmpdir(),
|
|
@@ -612,8 +648,7 @@ function importLegacyArtifact(slug, artifactName, eventType, params = {}, option
|
|
|
612
648
|
error = fmt.log.plainError,
|
|
613
649
|
worktree
|
|
614
650
|
} = options;
|
|
615
|
-
|
|
616
|
-
const rootDir = worktree || resolveWorktree(slug) || process.cwd();
|
|
651
|
+
|
|
617
652
|
const legacyPath = legacyArtifactPath(slug, artifactName, tmpDir);
|
|
618
653
|
|
|
619
654
|
// Check if legacy file exists
|
|
@@ -626,9 +661,9 @@ function importLegacyArtifact(slug, artifactName, eventType, params = {}, option
|
|
|
626
661
|
let content;
|
|
627
662
|
try {
|
|
628
663
|
content = fs.readFileSync(legacyPath, 'utf8');
|
|
629
|
-
} catch (err) {
|
|
630
|
-
error(fmt.status('FAIL', `Failed to read legacy artifact: ${err.message}`));
|
|
631
|
-
return { ok: false, path: null, importedFrom: legacyPath, error: err.message };
|
|
664
|
+
} catch (/** @type {unknown} */ err) {
|
|
665
|
+
error(fmt.status('FAIL', `Failed to read legacy artifact: ${/** @type {Error} */(err).message}`));
|
|
666
|
+
return { ok: false, path: null, importedFrom: legacyPath, error: /** @type {Error} */ (err).message };
|
|
632
667
|
}
|
|
633
668
|
|
|
634
669
|
// Create the normalized event
|
|
@@ -637,14 +672,14 @@ function importLegacyArtifact(slug, artifactName, eventType, params = {}, option
|
|
|
637
672
|
content,
|
|
638
673
|
...params,
|
|
639
674
|
// Default actor from artifact name
|
|
640
|
-
actor: params.actor || inferActorFromArtifact(
|
|
641
|
-
}, { ...options, worktree:
|
|
675
|
+
actor: params.actor || (/** @type {Function} */ (inferActorFromArtifact))()
|
|
676
|
+
}, { ...options, worktree: worktree || undefined, allowMissingRequiredFields: true });
|
|
642
677
|
|
|
643
678
|
if (!result.ok) {
|
|
644
|
-
return { ok: false, path: null, importedFrom: legacyPath, error: result.error };
|
|
679
|
+
return { ok: false, path: null, importedFrom: legacyPath, error: /** @type {string | undefined} */ (result.error) };
|
|
645
680
|
}
|
|
646
681
|
|
|
647
|
-
log(fmt.status('PASS', `Imported legacy artifact ${artifactName} to ${path.basename(result.path)}`));
|
|
682
|
+
log(fmt.status('PASS', `Imported legacy artifact ${artifactName} to ${path.basename(/** @type {string} */ (result.path))}`));
|
|
648
683
|
|
|
649
684
|
return {
|
|
650
685
|
ok: true,
|
|
@@ -659,7 +694,7 @@ function importLegacyArtifact(slug, artifactName, eventType, params = {}, option
|
|
|
659
694
|
* Infer actor from legacy artifact name (best guess).
|
|
660
695
|
* In practice, the actor should be passed explicitly from the workflow context.
|
|
661
696
|
*/
|
|
662
|
-
function inferActorFromArtifact(
|
|
697
|
+
function inferActorFromArtifact() {
|
|
663
698
|
// Can't reliably infer from artifact name alone
|
|
664
699
|
return 'unknown';
|
|
665
700
|
}
|
|
@@ -687,10 +722,14 @@ const LEGACY_ARTIFACT_TO_EVENT_TYPE = Object.freeze({
|
|
|
687
722
|
* @param {Object} [options] - Options
|
|
688
723
|
* @returns {Object} - { ok: boolean, imported: Array, errors: Array }
|
|
689
724
|
*/
|
|
725
|
+
/**
|
|
726
|
+
* @param {string} slug
|
|
727
|
+
* @param {{tmpDir?: string, error?: Function, worktree?: string, log?: Function}} [options]
|
|
728
|
+
* @returns {{ok: boolean, imported: Array<any>, errors: Array<any>}}
|
|
729
|
+
*/
|
|
690
730
|
function importAllLegacyArtifacts(slug, options = {}) {
|
|
691
731
|
const {
|
|
692
732
|
tmpDir = os.tmpdir(),
|
|
693
|
-
log = fmt.log.plain,
|
|
694
733
|
error = fmt.log.plainError,
|
|
695
734
|
worktree
|
|
696
735
|
} = options;
|
|
@@ -705,6 +744,7 @@ function importAllLegacyArtifacts(slug, options = {}) {
|
|
|
705
744
|
const phase = state ? state.phase : 'reviewing';
|
|
706
745
|
|
|
707
746
|
// Track which legacy files exist
|
|
747
|
+
/** @type {{[key: string]: string|null}} */
|
|
708
748
|
const legacyFiles = {};
|
|
709
749
|
for (const artifactName of Object.keys(LEGACY_ARTIFACT_TO_EVENT_TYPE)) {
|
|
710
750
|
const legacyPath = legacyArtifactPath(slug, artifactName, tmpDir);
|
|
@@ -724,14 +764,15 @@ function importAllLegacyArtifacts(slug, options = {}) {
|
|
|
724
764
|
if (fs.existsSync(verdictPath)) {
|
|
725
765
|
verdictContent = fs.readFileSync(verdictPath, 'utf8').trim();
|
|
726
766
|
}
|
|
727
|
-
} catch (err) {
|
|
728
|
-
error(fmt.status('WARN', `Failed to read verdict file: ${err.message}`));
|
|
767
|
+
} catch (/** @type {unknown} */ err) {
|
|
768
|
+
error(fmt.status('WARN', `Failed to read verdict file: ${/** @type {Error} */(err).message}`));
|
|
729
769
|
}
|
|
730
770
|
}
|
|
731
771
|
|
|
732
772
|
// Import review-outcome.md with verdict as metadata (only include verdict if valid)
|
|
773
|
+
/** @type {{round: number, phase: string, actor: string, verdict?: string}} */
|
|
733
774
|
const outcomeParams = { round, phase, actor: 'legacy' };
|
|
734
|
-
if (verdictContent && isValidVerdict(verdictContent)) {
|
|
775
|
+
if (verdictContent && isValidVerdict(/** @type {string} */ (verdictContent))) {
|
|
735
776
|
outcomeParams.verdict = verdictContent;
|
|
736
777
|
}
|
|
737
778
|
|
|
@@ -768,12 +809,13 @@ function importAllLegacyArtifacts(slug, options = {}) {
|
|
|
768
809
|
let verdictContent = null;
|
|
769
810
|
try {
|
|
770
811
|
verdictContent = fs.readFileSync(verdictPath, 'utf8').trim();
|
|
771
|
-
} catch (err) {
|
|
772
|
-
error(fmt.status('WARN', `Failed to read verdict file: ${err.message}`));
|
|
812
|
+
} catch (/** @type {unknown} */ err) {
|
|
813
|
+
error(fmt.status('WARN', `Failed to read verdict file: ${/** @type {Error} */(err).message}`));
|
|
773
814
|
}
|
|
774
815
|
|
|
816
|
+
/** @type {{round: number, phase: string, actor: string, verdict?: string}} */
|
|
775
817
|
const outcomeParams = { round, phase, actor: 'legacy' };
|
|
776
|
-
if (verdictContent && isValidVerdict(verdictContent)) {
|
|
818
|
+
if (verdictContent && isValidVerdict(/** @type {string} */ (verdictContent))) {
|
|
777
819
|
outcomeParams.verdict = verdictContent;
|
|
778
820
|
}
|
|
779
821
|
|
|
@@ -797,16 +839,16 @@ function importAllLegacyArtifacts(slug, options = {}) {
|
|
|
797
839
|
continue;
|
|
798
840
|
}
|
|
799
841
|
|
|
800
|
-
const result = importLegacyArtifact(slug, artifactName, LEGACY_ARTIFACT_TO_EVENT_TYPE[artifactName],
|
|
842
|
+
const result = importLegacyArtifact(slug, artifactName, /** @type {string} */ (LEGACY_ARTIFACT_TO_EVENT_TYPE[/** @type {keyof typeof LEGACY_ARTIFACT_TO_EVENT_TYPE} */ (artifactName)]),
|
|
801
843
|
{ round, phase, actor: 'legacy' },
|
|
802
844
|
{ ...options, worktree: rootDir });
|
|
803
845
|
|
|
804
846
|
if (result.ok) {
|
|
805
847
|
if (result.path) {
|
|
806
|
-
imported.push({ artifactName, eventType: LEGACY_ARTIFACT_TO_EVENT_TYPE[artifactName], path: result.path, legacyPath: result.legacyPath });
|
|
848
|
+
imported.push({ artifactName, eventType: LEGACY_ARTIFACT_TO_EVENT_TYPE[/** @type {keyof typeof LEGACY_ARTIFACT_TO_EVENT_TYPE} */ (artifactName)], path: result.path, legacyPath: result.legacyPath });
|
|
807
849
|
}
|
|
808
850
|
} else {
|
|
809
|
-
errors.push({ artifactName, eventType: LEGACY_ARTIFACT_TO_EVENT_TYPE[artifactName], error: result.error });
|
|
851
|
+
errors.push({ artifactName, eventType: LEGACY_ARTIFACT_TO_EVENT_TYPE[/** @type {keyof typeof LEGACY_ARTIFACT_TO_EVENT_TYPE} */ (artifactName)], error: result.error });
|
|
810
852
|
}
|
|
811
853
|
}
|
|
812
854
|
|
|
@@ -823,14 +865,18 @@ function importAllLegacyArtifacts(slug, options = {}) {
|
|
|
823
865
|
* @param {string} [options.rootDir] - Root directory
|
|
824
866
|
* @param {Function} [options.readdirSync] - readdirSync override for testing
|
|
825
867
|
* @param {Function} [options.readFileSync] - readFileSync override for testing
|
|
826
|
-
* @returns {Array} - Array of parsed event objects
|
|
868
|
+
* @returns {Array<any>} - Array of parsed event objects
|
|
869
|
+
*/
|
|
870
|
+
/**
|
|
871
|
+
* @param {string} slug
|
|
872
|
+
* @param {{rootDir?: string, readdirSync?: Function, readFileSync?: Function, error?: Function}} [options]
|
|
873
|
+
* @returns {Array<any>}
|
|
827
874
|
*/
|
|
828
875
|
function readAllEvents(slug, options = {}) {
|
|
829
876
|
const {
|
|
830
877
|
rootDir = process.cwd(),
|
|
831
878
|
readdirSync = fs.readdirSync,
|
|
832
879
|
readFileSync = fs.readFileSync,
|
|
833
|
-
log = fmt.log.plain,
|
|
834
880
|
error = fmt.log.plainError
|
|
835
881
|
} = options;
|
|
836
882
|
|
|
@@ -844,21 +890,21 @@ function readAllEvents(slug, options = {}) {
|
|
|
844
890
|
|
|
845
891
|
try {
|
|
846
892
|
files = readdirSync(eventsDir);
|
|
847
|
-
} catch (err) {
|
|
848
|
-
error(fmt.status('WARN', `Failed to read review-events directory: ${err.message}`));
|
|
893
|
+
} catch (/** @type {unknown} */ err) {
|
|
894
|
+
error(fmt.status('WARN', `Failed to read review-events directory: ${/** @type {Error} */(err).message}`));
|
|
849
895
|
return [];
|
|
850
896
|
}
|
|
851
897
|
|
|
852
898
|
for (const filename of files) {
|
|
853
|
-
if (!filename.endsWith('.md')) continue;
|
|
899
|
+
if (!filename.endsWith('.md')) {continue;}
|
|
854
900
|
|
|
855
901
|
const filePath = path.join(eventsDir, filename);
|
|
856
902
|
let content;
|
|
857
903
|
|
|
858
904
|
try {
|
|
859
905
|
content = readFileSync(filePath, 'utf8');
|
|
860
|
-
} catch (err) {
|
|
861
|
-
error(fmt.status('WARN', `Failed to read event file ${filename}: ${err.message}`));
|
|
906
|
+
} catch (/** @type {unknown} */ err) {
|
|
907
|
+
error(fmt.status('WARN', `Failed to read event file ${filename}: ${/** @type {Error} */(err).message}`));
|
|
862
908
|
continue;
|
|
863
909
|
}
|
|
864
910
|
|
|
@@ -886,6 +932,11 @@ function readAllEvents(slug, options = {}) {
|
|
|
886
932
|
* @param {string} filePath - File path (for fallback metadata)
|
|
887
933
|
* @returns {Object|null} - Parsed event or null
|
|
888
934
|
*/
|
|
935
|
+
/**
|
|
936
|
+
* @param {string} content
|
|
937
|
+
* @param {string} filePath
|
|
938
|
+
* @returns {{[key: string]: any}|null}
|
|
939
|
+
*/
|
|
889
940
|
function parseEventFile(content, filePath) {
|
|
890
941
|
// Try to parse frontmatter
|
|
891
942
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
@@ -900,9 +951,9 @@ function parseEventFile(content, filePath) {
|
|
|
900
951
|
// Parse YAML-like frontmatter
|
|
901
952
|
const lines = frontmatterText.split('\n');
|
|
902
953
|
for (const line of lines) {
|
|
903
|
-
if (!line.trim()) continue;
|
|
954
|
+
if (!line.trim()) {continue;}
|
|
904
955
|
const colonIdx = line.indexOf(':');
|
|
905
|
-
if (colonIdx === -1) continue;
|
|
956
|
+
if (colonIdx === -1) {continue;}
|
|
906
957
|
|
|
907
958
|
const key = line.slice(0, colonIdx).trim().toLowerCase();
|
|
908
959
|
const value = line.slice(colonIdx + 1).trim();
|
|
@@ -910,21 +961,21 @@ function parseEventFile(content, filePath) {
|
|
|
910
961
|
// Handle quoted strings
|
|
911
962
|
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
912
963
|
(value.startsWith("'") && value.endsWith("'"))) {
|
|
913
|
-
metadata[key] = value.slice(1, -1);
|
|
964
|
+
/** @type {any} */ (metadata)[key] = value.slice(1, -1);
|
|
914
965
|
}
|
|
915
966
|
// Handle JSON arrays
|
|
916
967
|
else if (value.startsWith('[') && value.endsWith(']')) {
|
|
917
968
|
try {
|
|
918
|
-
metadata[key] = JSON.parse(value);
|
|
969
|
+
/** @type {any} */ (metadata)[key] = JSON.parse(value);
|
|
919
970
|
} catch (_) {
|
|
920
|
-
metadata[key] = value;
|
|
971
|
+
/** @type {any} */ (metadata)[key] = value;
|
|
921
972
|
}
|
|
922
973
|
}
|
|
923
974
|
// Handle booleans and numbers
|
|
924
|
-
else if (value === 'true') metadata[key] = true;
|
|
925
|
-
else if (value === 'false') metadata[key] = false;
|
|
926
|
-
else if (!isNaN(value)) metadata[key] = Number(value);
|
|
927
|
-
else metadata[key] = value;
|
|
975
|
+
else if (value === 'true') {/** @type {any} */ (metadata)[key] = true;}
|
|
976
|
+
else if (value === 'false') {/** @type {any} */ (metadata)[key] = false;}
|
|
977
|
+
else if (value !== '' && !isNaN(Number(value))) {/** @type {any} */ (metadata)[key] = Number(value);}
|
|
978
|
+
else {/** @type {any} */ (metadata)[key] = value;}
|
|
928
979
|
}
|
|
929
980
|
}
|
|
930
981
|
|