@oss-autopilot/core 1.15.1 → 1.15.2

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.
@@ -33,6 +33,15 @@ export interface DailyCheckResult {
33
33
  repoGroups: RepoGroup[];
34
34
  failures: PRCheckFailure[];
35
35
  }
36
+ /**
37
+ * Convert a full DailyCheckResult to the compact DailyOutput for JSON serialization (#287).
38
+ * Deduplicates PR objects: category arrays become PR URL references,
39
+ * full objects live only in digest.openPRs. Reduces JSON payload size ~60-70%.
40
+ *
41
+ * Exported for the `daily --json` contract test (#986), which pins this
42
+ * shape transformation without spinning up the full fetch pipeline.
43
+ */
44
+ export declare function toDailyOutput(result: DailyCheckResult): DailyOutput;
36
45
  /**
37
46
  * Core daily check logic, extracted for reuse by the startup command.
38
47
  * Fetches all open PRs, updates state, and returns structured output.
@@ -370,8 +370,11 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
370
370
  * Convert a full DailyCheckResult to the compact DailyOutput for JSON serialization (#287).
371
371
  * Deduplicates PR objects: category arrays become PR URL references,
372
372
  * full objects live only in digest.openPRs. Reduces JSON payload size ~60-70%.
373
+ *
374
+ * Exported for the `daily --json` contract test (#986), which pins this
375
+ * shape transformation without spinning up the full fetch pipeline.
373
376
  */
374
- function toDailyOutput(result) {
377
+ export function toDailyOutput(result) {
375
378
  return {
376
379
  digest: deduplicateDigest(result.digest),
377
380
  capacity: result.capacity,
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { createScout } from '@oss-scout/core';
6
6
  import { getStateManager, requireGitHubToken } from '../core/index.js';
7
+ import { loadSkippedIssues } from './skip-file-parser.js';
7
8
  /**
8
9
  * Build a ScoutState from the current AgentState.
9
10
  * Maps oss-autopilot's config and state fields to oss-scout's state format.
@@ -52,7 +53,7 @@ export function buildScoutState() {
52
53
  openedAt: pr.createdAt,
53
54
  })),
54
55
  savedResults: [],
55
- skippedIssues: [],
56
+ skippedIssues: loadSkippedIssues(config.skippedIssuesPath),
56
57
  lastRunAt: state.lastRunAt,
57
58
  };
58
59
  }
@@ -0,0 +1,25 @@
1
+ export interface SkipAddOptions {
2
+ issueUrl: string;
3
+ /** Override the configured `skippedIssuesPath`. */
4
+ skipFilePath?: string;
5
+ /** Injected for deterministic tests. Defaults to `new Date()`. */
6
+ now?: Date;
7
+ }
8
+ export interface SkipAddOutput {
9
+ added: boolean;
10
+ alreadyPresent: boolean;
11
+ url: string;
12
+ path: string;
13
+ /** Populated only when an entry was appended. */
14
+ date?: string;
15
+ }
16
+ /**
17
+ * Append an issue URL to the skipped-issues file.
18
+ *
19
+ * Creates the file with the standard header if it doesn't exist. If the URL
20
+ * is already present (per the skip-file-parser's view of the file), this is
21
+ * a no-op and returns `alreadyPresent: true`.
22
+ *
23
+ * @throws when no path can be resolved or the URL isn't a GitHub issue/PR URL.
24
+ */
25
+ export declare function runSkipAdd(options: SkipAddOptions): SkipAddOutput;
@@ -0,0 +1,48 @@
1
+ import * as fs from 'fs';
2
+ import { loadSkippedIssues } from './skip-file-parser.js';
3
+ import { getStateManager } from '../core/index.js';
4
+ // Keep in sync with GITHUB_URL_RE in skip-file-parser.ts.
5
+ const GITHUB_URL_RE = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/(?:issues|pull)\/(\d+)(?:[/?#].*)?$/;
6
+ const FILE_HEADER = '# Skipped Issues — auto-culled after 90 days\n# Format: YYYY-MM-DD URL\n\n';
7
+ function formatUtcDate(d) {
8
+ return d.toISOString().slice(0, 10);
9
+ }
10
+ /**
11
+ * Append an issue URL to the skipped-issues file.
12
+ *
13
+ * Creates the file with the standard header if it doesn't exist. If the URL
14
+ * is already present (per the skip-file-parser's view of the file), this is
15
+ * a no-op and returns `alreadyPresent: true`.
16
+ *
17
+ * @throws when no path can be resolved or the URL isn't a GitHub issue/PR URL.
18
+ */
19
+ export function runSkipAdd(options) {
20
+ const skipFilePath = options.skipFilePath ?? getStateManager().getState().config.skippedIssuesPath;
21
+ if (!skipFilePath) {
22
+ throw new Error('No skipped-issues path configured. Set one via `oss-autopilot config --set skippedIssuesPath=<path>` or pass --path.');
23
+ }
24
+ if (!GITHUB_URL_RE.test(options.issueUrl)) {
25
+ throw new Error(`Invalid GitHub issue or PR URL: ${options.issueUrl}`);
26
+ }
27
+ const existing = loadSkippedIssues(skipFilePath);
28
+ if (existing.some((entry) => entry.url === options.issueUrl)) {
29
+ return {
30
+ added: false,
31
+ alreadyPresent: true,
32
+ url: options.issueUrl,
33
+ path: skipFilePath,
34
+ };
35
+ }
36
+ if (!fs.existsSync(skipFilePath)) {
37
+ fs.writeFileSync(skipFilePath, FILE_HEADER);
38
+ }
39
+ const date = formatUtcDate(options.now ?? new Date());
40
+ fs.appendFileSync(skipFilePath, `${date} ${options.issueUrl}\n`);
41
+ return {
42
+ added: true,
43
+ alreadyPresent: false,
44
+ url: options.issueUrl,
45
+ path: skipFilePath,
46
+ date,
47
+ };
48
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Parser for the skipped-issues markdown file (#989).
3
+ *
4
+ * The file format is one entry per line:
5
+ * 2026-04-15 https://github.com/owner/repo/issues/123
6
+ * Lines starting with `#` and blank lines are ignored.
7
+ *
8
+ * Produces SkippedIssue entries that plug directly into oss-scout's ScoutState
9
+ * so the search engine filters already-skipped URLs out of results.
10
+ */
11
+ import type { SkippedIssue } from '@oss-scout/core';
12
+ /**
13
+ * Parse the raw text of a skipped-issues file into SkippedIssue entries.
14
+ * Pure function — no I/O. Malformed lines are warned and skipped; the rest
15
+ * pass through unchanged.
16
+ */
17
+ export declare function parseSkippedIssuesContent(content: string): SkippedIssue[];
18
+ /**
19
+ * Read the skipped-issues file from disk and parse it.
20
+ * Returns `[]` when:
21
+ * - `path` is undefined or empty,
22
+ * - the file does not exist,
23
+ * - the file cannot be read (a warning is logged).
24
+ */
25
+ export declare function loadSkippedIssues(path: string | undefined): SkippedIssue[];
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Parser for the skipped-issues markdown file (#989).
3
+ *
4
+ * The file format is one entry per line:
5
+ * 2026-04-15 https://github.com/owner/repo/issues/123
6
+ * Lines starting with `#` and blank lines are ignored.
7
+ *
8
+ * Produces SkippedIssue entries that plug directly into oss-scout's ScoutState
9
+ * so the search engine filters already-skipped URLs out of results.
10
+ */
11
+ import * as fs from 'fs';
12
+ import { warn } from '../core/logger.js';
13
+ import { errorMessage } from '../core/errors.js';
14
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
15
+ const GITHUB_URL_RE = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/(?:issues|pull)\/(\d+)(?:[/?#].*)?$/;
16
+ /**
17
+ * Parse the raw text of a skipped-issues file into SkippedIssue entries.
18
+ * Pure function — no I/O. Malformed lines are warned and skipped; the rest
19
+ * pass through unchanged.
20
+ */
21
+ export function parseSkippedIssuesContent(content) {
22
+ const results = [];
23
+ const lines = content.split(/\r?\n/);
24
+ for (let i = 0; i < lines.length; i++) {
25
+ const lineNumber = i + 1;
26
+ const line = lines[i].trim();
27
+ if (line === '' || line.startsWith('#'))
28
+ continue;
29
+ // Split on first whitespace run: "YYYY-MM-DD <url>"
30
+ const match = line.match(/^(\S+)\s+(\S+)\s*$/);
31
+ if (!match) {
32
+ warn('skip-file-parser', `Line ${lineNumber}: malformed (expected "<date> <url>"): ${line}`);
33
+ continue;
34
+ }
35
+ const [, dateStr, url] = match;
36
+ if (!DATE_RE.test(dateStr)) {
37
+ warn('skip-file-parser', `Line ${lineNumber}: invalid date format (expected YYYY-MM-DD): ${line}`);
38
+ continue;
39
+ }
40
+ const dateMs = Date.parse(`${dateStr}T00:00:00.000Z`);
41
+ if (Number.isNaN(dateMs)) {
42
+ warn('skip-file-parser', `Line ${lineNumber}: unparseable date: ${line}`);
43
+ continue;
44
+ }
45
+ // Guard against JS Date normalization — Date.parse silently shifts
46
+ // invalid calendar dates (e.g. 2026-02-30 → 2026-03-02). Without this
47
+ // round-trip check the entry would be stored under the wrong date and
48
+ // scout's 90-day cull would run against a shifted value.
49
+ const roundTrip = new Date(dateMs).toISOString().slice(0, 10);
50
+ if (roundTrip !== dateStr) {
51
+ warn('skip-file-parser', `Line ${lineNumber}: invalid calendar date ${dateStr} (would be normalized to ${roundTrip}): ${line}`);
52
+ continue;
53
+ }
54
+ const urlMatch = url.match(GITHUB_URL_RE);
55
+ if (!urlMatch) {
56
+ warn('skip-file-parser', `Line ${lineNumber}: non-GitHub-issue URL: ${line}`);
57
+ continue;
58
+ }
59
+ const [, repo, numberStr] = urlMatch;
60
+ const number = Number.parseInt(numberStr, 10);
61
+ results.push({
62
+ url,
63
+ repo,
64
+ number,
65
+ title: '',
66
+ skippedAt: new Date(dateMs).toISOString(),
67
+ });
68
+ }
69
+ return results;
70
+ }
71
+ /**
72
+ * Read the skipped-issues file from disk and parse it.
73
+ * Returns `[]` when:
74
+ * - `path` is undefined or empty,
75
+ * - the file does not exist,
76
+ * - the file cannot be read (a warning is logged).
77
+ */
78
+ export function loadSkippedIssues(path) {
79
+ if (!path)
80
+ return [];
81
+ if (!fs.existsSync(path))
82
+ return [];
83
+ let content;
84
+ try {
85
+ content = fs.readFileSync(path, 'utf-8');
86
+ }
87
+ catch (err) {
88
+ warn('skip-file-parser', `Failed to read skipped-issues file at ${path}: ${errorMessage(err)}`);
89
+ return [];
90
+ }
91
+ return parseSkippedIssuesContent(content);
92
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "1.15.1",
3
+ "version": "1.15.2",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {