@delfini/action-core 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.
- package/README.md +37 -0
- package/dist/adapters/factory.d.ts +3 -0
- package/dist/adapters/factory.d.ts.map +1 -0
- package/dist/adapters/factory.js +2 -0
- package/dist/adapters/single-call/model.d.ts +4 -0
- package/dist/adapters/single-call/model.d.ts.map +1 -0
- package/dist/adapters/single-call/model.js +46 -0
- package/dist/adapters/single-call/orchestrator.d.ts +10 -0
- package/dist/adapters/single-call/orchestrator.d.ts.map +1 -0
- package/dist/adapters/single-call/orchestrator.js +97 -0
- package/dist/adapters/single-call/prompt.md +360 -0
- package/dist/analysis-input.d.ts +18 -0
- package/dist/analysis-input.d.ts.map +1 -0
- package/dist/analysis-input.js +60 -0
- package/dist/diff-builder.d.ts +16 -0
- package/dist/diff-builder.d.ts.map +1 -0
- package/dist/diff-builder.js +35 -0
- package/dist/doc-exclusion.d.ts +17 -0
- package/dist/doc-exclusion.d.ts.map +1 -0
- package/dist/doc-exclusion.js +85 -0
- package/dist/doc-reader.d.ts +34 -0
- package/dist/doc-reader.d.ts.map +1 -0
- package/dist/doc-reader.js +172 -0
- package/dist/github-client-shared.d.ts +27 -0
- package/dist/github-client-shared.d.ts.map +1 -0
- package/dist/github-client-shared.js +240 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/pipeline-inputs.d.ts +23 -0
- package/dist/pipeline-inputs.d.ts.map +1 -0
- package/dist/pipeline-inputs.js +42 -0
- package/dist/ports/orchestrator.d.ts +5 -0
- package/dist/ports/orchestrator.d.ts.map +1 -0
- package/dist/ports/orchestrator.js +1 -0
- package/dist/smart-skip.d.ts +10 -0
- package/dist/smart-skip.d.ts.map +1 -0
- package/dist/smart-skip.js +85 -0
- package/package.json +41 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import * as core from '@actions/core';
|
|
2
|
+
import * as github from '@actions/github';
|
|
3
|
+
import { readDocsAtHeadViaGitTrees } from './doc-reader.js';
|
|
4
|
+
export function getPrContext() {
|
|
5
|
+
const { payload, repo } = github.context;
|
|
6
|
+
if (payload.pull_request) {
|
|
7
|
+
return {
|
|
8
|
+
owner: repo.owner,
|
|
9
|
+
repo: repo.repo,
|
|
10
|
+
pullNumber: payload.pull_request.number,
|
|
11
|
+
headSha: payload.pull_request.head.sha,
|
|
12
|
+
baseSha: payload.pull_request.base.sha,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (payload.issue?.pull_request && payload.comment) {
|
|
16
|
+
const prUrl = payload.issue.pull_request.url;
|
|
17
|
+
return {
|
|
18
|
+
owner: repo.owner,
|
|
19
|
+
repo: repo.repo,
|
|
20
|
+
pullNumber: payload.issue.number,
|
|
21
|
+
headSha: '',
|
|
22
|
+
baseSha: '',
|
|
23
|
+
...extractShasFromIssueComment(prUrl),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Unsupported event: expected pull_request or issue_comment on a PR, got ${github.context.eventName}`);
|
|
27
|
+
}
|
|
28
|
+
export async function listChangedFiles(octokit, ctx) {
|
|
29
|
+
const files = [];
|
|
30
|
+
let page = 1;
|
|
31
|
+
while (true) {
|
|
32
|
+
const { data } = await octokit.rest.pulls.listFiles({
|
|
33
|
+
owner: ctx.owner,
|
|
34
|
+
repo: ctx.repo,
|
|
35
|
+
pull_number: ctx.pullNumber,
|
|
36
|
+
per_page: 100,
|
|
37
|
+
page,
|
|
38
|
+
});
|
|
39
|
+
if (data.length === 0) {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
for (const file of data) {
|
|
43
|
+
files.push({
|
|
44
|
+
filename: file.filename,
|
|
45
|
+
status: file.status,
|
|
46
|
+
patch: file.patch,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (data.length < 100) {
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
page++;
|
|
53
|
+
}
|
|
54
|
+
return files;
|
|
55
|
+
}
|
|
56
|
+
export async function getFileContent(octokit, ctx, path, ref) {
|
|
57
|
+
try {
|
|
58
|
+
const { data } = await octokit.rest.repos.getContent({
|
|
59
|
+
owner: ctx.owner,
|
|
60
|
+
repo: ctx.repo,
|
|
61
|
+
path,
|
|
62
|
+
ref,
|
|
63
|
+
});
|
|
64
|
+
if (Array.isArray(data) || data.type !== 'file' || !data.content) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return Buffer.from(data.content, 'base64').toString('utf-8');
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (typeof error === 'object' &&
|
|
71
|
+
error !== null &&
|
|
72
|
+
'status' in error &&
|
|
73
|
+
error.status === 404) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export async function readDocs(octokit, ctx, docScope, options = {}) {
|
|
80
|
+
// v6.1 — walk at the PR head, not the merge-base. After Approve and Commit
|
|
81
|
+
// splices doc updates onto the PR branch, the head ref carries those updates;
|
|
82
|
+
// walking at base would re-detect the same drift forever (and would force
|
|
83
|
+
// FR88f to keep creating new pending findings every re-run because section
|
|
84
|
+
// anchors are LLM-non-deterministic). Reading at head closes that loop:
|
|
85
|
+
// Approve and Commit -> head advances -> next walk sees the updated doc ->
|
|
86
|
+
// PASS verdict -> Stream 4a auto-supersede -> review resolves.
|
|
87
|
+
//
|
|
88
|
+
// Story 3.12 (ADR-2026-06-01) — Full mode now reads via ONE recursive
|
|
89
|
+
// git-trees call + the shared `isFileInDocScope` matcher (picomatch@4),
|
|
90
|
+
// exactly as Lite mode does (Story P2.6). `docScope` is the canonical
|
|
91
|
+
// `string[]` (multi-path/glob). The old per-directory `getContent` walk
|
|
92
|
+
// (`readDocsFromPath`) is retired in this story.
|
|
93
|
+
return readDocsAtHeadViaGitTrees(octokit, ctx.owner, ctx.repo, docScope, ctx.headSha, options);
|
|
94
|
+
}
|
|
95
|
+
// v6.0 thin top-level PR comment (Story 3.11). Uses issues.createComment /
|
|
96
|
+
// updateComment (NOT pulls.createReview) — replaces the pre-v6.0 Request
|
|
97
|
+
// Changes / Approve review primitive on success paths. Idempotent on `Bot`
|
|
98
|
+
// author + label-prefix marker — re-runs update the same comment in place.
|
|
99
|
+
//
|
|
100
|
+
// The hidden HTML marker (DELFINI_PR_COMMENT_MARKER) is appended to every
|
|
101
|
+
// v6.0 thin template body so the matcher can be specific without depending
|
|
102
|
+
// on visible body text. A foreign Bot whose comment happens to start with
|
|
103
|
+
// `**` (Dependabot, Renovate, etc.) is NOT matched because they don't carry
|
|
104
|
+
// the marker. The matcher uses `body.includes(marker)` not `startsWith` so
|
|
105
|
+
// the marker can ride at the end of the body without affecting layout.
|
|
106
|
+
//
|
|
107
|
+
// Pagination: caps at 5 pages (500 comments) before warn-and-create — same
|
|
108
|
+
// shape as postOrUpdateReview's 100-cap warn pattern. PRs with >500 comments
|
|
109
|
+
// are pathological; warn-and-create produces a benign duplicate at worst.
|
|
110
|
+
const PR_COMMENT_PAGINATION_PAGE_CAP = 5;
|
|
111
|
+
const PR_COMMENT_PAGINATION_PER_PAGE = 100;
|
|
112
|
+
// Hidden HTML marker — invisible in rendered markdown, byte-stable in source.
|
|
113
|
+
// Keep this constant in sync with the marker appended by emitStreamDecision.
|
|
114
|
+
export const DELFINI_PR_COMMENT_MARKER = '<!-- delfini-pr-comment -->';
|
|
115
|
+
export async function postOrUpdatePrComment(octokit, ctx, body, options) {
|
|
116
|
+
let existingId = null;
|
|
117
|
+
let extraMatches = 0;
|
|
118
|
+
let pagesScanned = 0;
|
|
119
|
+
for (let page = 1; page <= PR_COMMENT_PAGINATION_PAGE_CAP; page++) {
|
|
120
|
+
const { data } = await octokit.rest.issues.listComments({
|
|
121
|
+
owner: ctx.owner,
|
|
122
|
+
repo: ctx.repo,
|
|
123
|
+
issue_number: ctx.pullNumber,
|
|
124
|
+
per_page: PR_COMMENT_PAGINATION_PER_PAGE,
|
|
125
|
+
page,
|
|
126
|
+
});
|
|
127
|
+
pagesScanned += 1;
|
|
128
|
+
const matches = data.filter((c) => c.user?.type === 'Bot' && (c.body ?? '').includes(options.marker));
|
|
129
|
+
if (matches.length > 0) {
|
|
130
|
+
if (existingId === null) {
|
|
131
|
+
existingId = matches[0].id;
|
|
132
|
+
extraMatches = matches.length - 1;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
extraMatches += matches.length;
|
|
136
|
+
}
|
|
137
|
+
// Continue paginating to count duplicates across pages for the warn-on-many path.
|
|
138
|
+
// Stop early once we've scanned enough to be confident — first match is the
|
|
139
|
+
// one we'll update; extras just produce a warning.
|
|
140
|
+
if (data.length < PR_COMMENT_PAGINATION_PER_PAGE)
|
|
141
|
+
break;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (data.length < PR_COMMENT_PAGINATION_PER_PAGE)
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
if (extraMatches > 0) {
|
|
148
|
+
core.warning(`Delfini: ${extraMatches + 1} matching Delfini PR comments found — updating the first; consider manual cleanup of duplicates.`);
|
|
149
|
+
}
|
|
150
|
+
if (existingId === null &&
|
|
151
|
+
pagesScanned === PR_COMMENT_PAGINATION_PAGE_CAP) {
|
|
152
|
+
core.warning(`Delfini: PR comment scan capped at ${PR_COMMENT_PAGINATION_PAGE_CAP * PR_COMMENT_PAGINATION_PER_PAGE} comments — idempotency may misfire on this PR.`);
|
|
153
|
+
}
|
|
154
|
+
if (existingId !== null) {
|
|
155
|
+
try {
|
|
156
|
+
await octokit.rest.issues.updateComment({
|
|
157
|
+
owner: ctx.owner,
|
|
158
|
+
repo: ctx.repo,
|
|
159
|
+
comment_id: existingId,
|
|
160
|
+
body,
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
// 404 = comment was deleted between listComments and updateComment
|
|
166
|
+
// (manual delete by reviewer, race with another tool). Fall through to
|
|
167
|
+
// createComment so the PR doesn't end up with no Delfini comment at all.
|
|
168
|
+
if (isHttpStatus(error, 404)) {
|
|
169
|
+
core.warning(`Delfini: PR comment #${existingId} disappeared between scan and update — creating a fresh comment.`);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
await octokit.rest.issues.createComment({
|
|
177
|
+
owner: ctx.owner,
|
|
178
|
+
repo: ctx.repo,
|
|
179
|
+
issue_number: ctx.pullNumber,
|
|
180
|
+
body,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function isHttpStatus(error, status) {
|
|
184
|
+
return (typeof error === 'object' &&
|
|
185
|
+
error !== null &&
|
|
186
|
+
'status' in error &&
|
|
187
|
+
error.status === status);
|
|
188
|
+
}
|
|
189
|
+
// 403 (read-only token) and 404 (resource not visible) both signal the
|
|
190
|
+
// workflow lacks permission to write a check or PR comment — typically a fork
|
|
191
|
+
// PR. Callers tolerate these (warn + continue) rather than crashing the run.
|
|
192
|
+
// Story P2.2 (AC7) — moved here from pipeline.ts so the Full and Lite
|
|
193
|
+
// pipelines share one predicate.
|
|
194
|
+
export function isForbiddenError(error) {
|
|
195
|
+
return isHttpStatus(error, 403) || isHttpStatus(error, 404);
|
|
196
|
+
}
|
|
197
|
+
export async function createCheckStatus(octokit, ctx, conclusion, title, summary, detailsUrl) {
|
|
198
|
+
// Conditional spread keeps the request body free of an explicit
|
|
199
|
+
// `details_url: undefined` — Octokit treats absent and `undefined` fields
|
|
200
|
+
// differently for some endpoints, and we want GitHub's default Details
|
|
201
|
+
// affordance (the built-in check_run page) when no URL is provided.
|
|
202
|
+
//
|
|
203
|
+
// 'in_progress' is the sentinel for "attention needed, static yellow icon":
|
|
204
|
+
// we use the legacy Commit Statuses API with state='pending' because that
|
|
205
|
+
// renders a STATIC yellow dot (no spinner). The check_runs API has no
|
|
206
|
+
// conclusion that renders static yellow — status='in_progress' animates,
|
|
207
|
+
// conclusion='action_required' renders red, and conclusion='neutral' renders
|
|
208
|
+
// grey. Chromatic and other "needs review" integrations also use commit
|
|
209
|
+
// statuses for this exact reason. The 140-char description cap is enforced
|
|
210
|
+
// by GitHub; truncate defensively so a long title doesn't 422.
|
|
211
|
+
if (conclusion === 'in_progress') {
|
|
212
|
+
const description = title.length > 140 ? title.slice(0, 139) + '…' : title;
|
|
213
|
+
await octokit.rest.repos.createCommitStatus({
|
|
214
|
+
owner: ctx.owner,
|
|
215
|
+
repo: ctx.repo,
|
|
216
|
+
sha: ctx.headSha,
|
|
217
|
+
state: 'pending',
|
|
218
|
+
context: 'Delfini Docs Drift Check',
|
|
219
|
+
description,
|
|
220
|
+
...(detailsUrl !== undefined ? { target_url: detailsUrl } : {}),
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
await octokit.rest.checks.create({
|
|
225
|
+
owner: ctx.owner,
|
|
226
|
+
repo: ctx.repo,
|
|
227
|
+
head_sha: ctx.headSha,
|
|
228
|
+
name: 'Delfini Docs Drift Check',
|
|
229
|
+
status: 'completed',
|
|
230
|
+
conclusion,
|
|
231
|
+
output: { title, summary },
|
|
232
|
+
...(detailsUrl !== undefined ? { details_url: detailsUrl } : {}),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
function extractShasFromIssueComment(_prUrl) {
|
|
236
|
+
// SHAs are not available on issue_comment events directly.
|
|
237
|
+
// The caller must fetch them from the PR API after context creation.
|
|
238
|
+
// Returning empty strings signals that SHAs need to be resolved.
|
|
239
|
+
return {};
|
|
240
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { readDocsViaGitTrees, readDocsAtHeadViaGitTrees } from './doc-reader.js';
|
|
2
|
+
export type { DocFile, DocsReadResult, ReadDocsOptions, ScopeSource } from './doc-reader.js';
|
|
3
|
+
export { parseFrontMatter, stripFrontMatter } from './doc-exclusion.js';
|
|
4
|
+
export type { ExcludedDoc, ExclusionReason, FrontMatterResult } from './doc-exclusion.js';
|
|
5
|
+
export { buildAnalysisInput } from './analysis-input.js';
|
|
6
|
+
export type { BuildAnalysisInputOptions } from './analysis-input.js';
|
|
7
|
+
export { classifyPr } from './smart-skip.js';
|
|
8
|
+
export type { SmartSkipOptions, SmartSkipResult } from './smart-skip.js';
|
|
9
|
+
export { buildUnifiedDiff } from './diff-builder.js';
|
|
10
|
+
export { DELFINI_PR_COMMENT_MARKER, createCheckStatus, getFileContent, getPrContext, isForbiddenError, listChangedFiles, postOrUpdatePrComment, readDocs, } from './github-client-shared.js';
|
|
11
|
+
export type { ChangedFile, PrContext } from './github-client-shared.js';
|
|
12
|
+
export { readPipelineInputs } from './pipeline-inputs.js';
|
|
13
|
+
export type { Enforcement, PipelineDeps, PipelineInputs } from './pipeline-inputs.js';
|
|
14
|
+
export { createOrchestrator } from './adapters/factory.js';
|
|
15
|
+
export { SingleCallOrchestrator, loadTemplate as loadPromptTemplate, } from './adapters/single-call/orchestrator.js';
|
|
16
|
+
export { createChatModel } from './adapters/single-call/model.js';
|
|
17
|
+
export type { LLMProvider } from './adapters/single-call/model.js';
|
|
18
|
+
export type { AnalysisOrchestrator } from './ports/orchestrator.js';
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAA;AAChF,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAE5F,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACvE,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAEzF,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,YAAY,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAA;AAEpE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAExE,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAEpD,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,QAAQ,GACT,MAAM,2BAA2B,CAAA;AAClC,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AAEvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAErF,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAC1D,OAAO,EACL,sBAAsB,EACtB,YAAY,IAAI,kBAAkB,GACnC,MAAM,wCAAwC,CAAA;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA;AACjE,YAAY,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAA;AAElE,YAAY,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @delfini/action-core — single barrel (project rule: named exports only;
|
|
2
|
+
// barrel only at packages/*/src/index.ts).
|
|
3
|
+
//
|
|
4
|
+
// Story P3.9.2a (Lite/Full artifact split, Mechanism A): the shared
|
|
5
|
+
// analysis-pipeline core of the Delfini GitHub Action, consumed by the public
|
|
6
|
+
// Lite artifact (apps/action) and the Full artifact (apps/action-full;
|
|
7
|
+
// re-homed into delfini-web at P3.9.4) — algorithm parity by construction.
|
|
8
|
+
//
|
|
9
|
+
// V1 stability posture: internal shared core, published for transparency and
|
|
10
|
+
// delfini-web consumption — no semver API-stability promise (see README.md).
|
|
11
|
+
export { readDocsViaGitTrees, readDocsAtHeadViaGitTrees } from './doc-reader.js';
|
|
12
|
+
export { parseFrontMatter, stripFrontMatter } from './doc-exclusion.js';
|
|
13
|
+
export { buildAnalysisInput } from './analysis-input.js';
|
|
14
|
+
export { classifyPr } from './smart-skip.js';
|
|
15
|
+
export { buildUnifiedDiff } from './diff-builder.js';
|
|
16
|
+
export { DELFINI_PR_COMMENT_MARKER, createCheckStatus, getFileContent, getPrContext, isForbiddenError, listChangedFiles, postOrUpdatePrComment, readDocs, } from './github-client-shared.js';
|
|
17
|
+
export { readPipelineInputs } from './pipeline-inputs.js';
|
|
18
|
+
export { createOrchestrator } from './adapters/factory.js';
|
|
19
|
+
export { SingleCallOrchestrator, loadTemplate as loadPromptTemplate, } from './adapters/single-call/orchestrator.js';
|
|
20
|
+
export { createChatModel } from './adapters/single-call/model.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { GitHub } from '@actions/github/lib/utils';
|
|
2
|
+
import type { AnalysisOrchestrator } from './ports/orchestrator.js';
|
|
3
|
+
type Octokit = InstanceType<typeof GitHub>;
|
|
4
|
+
export type Enforcement = 'required' | 'warning';
|
|
5
|
+
export interface PipelineInputs {
|
|
6
|
+
docScope: string[];
|
|
7
|
+
enforcement: Enforcement;
|
|
8
|
+
githubToken: string;
|
|
9
|
+
/**
|
|
10
|
+
* Story P3.7.2 / FR151 — opt-in diff pre-filter. Default `false` — the
|
|
11
|
+
* assembled analysis input is byte-identical to the pre-story baseline.
|
|
12
|
+
* Optional in the type so test fixtures that build a minimal PipelineInputs
|
|
13
|
+
* keep compiling; `readPipelineInputs` always sets it explicitly.
|
|
14
|
+
*/
|
|
15
|
+
enableDiffPreFilter?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface PipelineDeps {
|
|
18
|
+
octokit: Octokit;
|
|
19
|
+
orchestrator?: AnalysisOrchestrator;
|
|
20
|
+
}
|
|
21
|
+
export declare function readPipelineInputs(): PipelineInputs;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=pipeline-inputs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline-inputs.d.ts","sourceRoot":"","sources":["../src/pipeline-inputs.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAA;AAGvD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAA;AAUnE,KAAK,OAAO,GAAG,YAAY,CAAC,OAAO,MAAM,CAAC,CAAA;AAE1C,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,SAAS,CAAA;AAEhD,MAAM,WAAW,cAAc;IAS7B,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,WAAW,EAAE,WAAW,CAAA;IACxB,WAAW,EAAE,MAAM,CAAA;IACnB;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAA;IAChB,YAAY,CAAC,EAAE,oBAAoB,CAAA;CACpC;AAED,wBAAgB,kBAAkB,IAAI,cAAc,CA2CnD"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as core from '@actions/core';
|
|
2
|
+
import { normalizeDocScope } from '@delfini/drift-engine';
|
|
3
|
+
export function readPipelineInputs() {
|
|
4
|
+
// Story P2.7 — Lite-mode doc-scope input is `doc_scope` (canonical name
|
|
5
|
+
// shared with the drift-engine algebra, CLI, FR88g API response, DB column,
|
|
6
|
+
// and Web settings). ADR-2026-06-01 / Story P2.6 — `doc_scope` accepts a
|
|
7
|
+
// newline- or comma-delimited list (action.yml inputs are strings only). A
|
|
8
|
+
// single bare path still works. The split list is fed through
|
|
9
|
+
// `normalizeDocScope` from `@delfini/drift-engine` (POSIX, trim, trailing-
|
|
10
|
+
// slash strip, dedupe, `./` / `..` resolution all handled there — do NOT
|
|
11
|
+
// re-implement here).
|
|
12
|
+
//
|
|
13
|
+
// The default `['docs/']` keeps Lite's user-observable default identical to
|
|
14
|
+
// pre-P2.6 (single string `'docs/'`) — only the type changes. The defensive
|
|
15
|
+
// collapse-to-empty fallback covers the user-types-",,,," case so an
|
|
16
|
+
// accidentally-empty normalized list never silently turns into a zero-scope
|
|
17
|
+
// run (which would skip every PR via FR57(b)).
|
|
18
|
+
const rawDocScope = core.getInput('doc_scope');
|
|
19
|
+
const entries = rawDocScope.length > 0 ? rawDocScope.split(/[\n,]/) : ['docs/'];
|
|
20
|
+
const normalized = normalizeDocScope(entries);
|
|
21
|
+
// A non-empty input that normalises to nothing (e.g. " ", ",,," , "\n\n")
|
|
22
|
+
// is almost certainly a typo, not an intentional default request. Warn so the
|
|
23
|
+
// operator can tell "I omitted doc_scope → default" apart from "my doc_scope
|
|
24
|
+
// collapsed → default" before silently analysing `docs/` (Story P2.6).
|
|
25
|
+
if (rawDocScope.length > 0 && normalized.length === 0) {
|
|
26
|
+
core.warning(`doc_scope "${rawDocScope}" contains no valid doc-scope entries after ` +
|
|
27
|
+
'normalisation — falling back to the default "docs/". Check for stray ' +
|
|
28
|
+
'delimiters or whitespace-only entries.');
|
|
29
|
+
}
|
|
30
|
+
// Fallback flows through `normalizeDocScope` so the collapse-to-default path
|
|
31
|
+
// yields the exact same value (`['docs']`) as the omitted-input path — no
|
|
32
|
+
// trailing-slash inconsistency between the two default routes.
|
|
33
|
+
const docScope = normalized.length > 0 ? normalized : normalizeDocScope(['docs/']);
|
|
34
|
+
const rawEnforcement = (core.getInput('enforcement') || 'warning').toLowerCase();
|
|
35
|
+
const enforcement = rawEnforcement === 'required' ? 'required' : 'warning';
|
|
36
|
+
const githubToken = process.env.GITHUB_TOKEN ?? core.getInput('github_token');
|
|
37
|
+
// Story P3.7.2 / FR151 — `enable_diff_prefilter` action.yml input. Default
|
|
38
|
+
// false: any value other than the literal "true" (case-insensitive) leaves
|
|
39
|
+
// the gate off so the pre-story behaviour is the path of least resistance.
|
|
40
|
+
const enableDiffPreFilter = core.getInput('enable_diff_prefilter').toLowerCase() === 'true';
|
|
41
|
+
return { docScope, enforcement, githubToken, enableDiffPreFilter };
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/ports/orchestrator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAE1E,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;CACvD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface SmartSkipOptions {
|
|
2
|
+
docScope: string[];
|
|
3
|
+
skipTestFiles?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface SmartSkipResult {
|
|
6
|
+
shouldSkip: boolean;
|
|
7
|
+
reason: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function classifyPr(changedFiles: string[], options: SmartSkipOptions): SmartSkipResult;
|
|
10
|
+
//# sourceMappingURL=smart-skip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smart-skip.d.ts","sourceRoot":"","sources":["../src/smart-skip.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAS/B,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,OAAO,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;CACf;AA8DD,wBAAgB,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,gBAAgB,GAAG,eAAe,CA+C7F"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { isFileInDocScope } from '@delfini/drift-engine';
|
|
2
|
+
const categoryLabels = {
|
|
3
|
+
dependency: 'dependency update',
|
|
4
|
+
ci: 'CI config change',
|
|
5
|
+
generated: 'generated file',
|
|
6
|
+
node_modules: 'node_modules change',
|
|
7
|
+
test: 'test file change',
|
|
8
|
+
};
|
|
9
|
+
const DEPENDENCY_BASENAMES = new Set([
|
|
10
|
+
'package.json',
|
|
11
|
+
'pnpm-lock.yaml',
|
|
12
|
+
'package-lock.json',
|
|
13
|
+
'yarn.lock',
|
|
14
|
+
]);
|
|
15
|
+
function basename(filePath) {
|
|
16
|
+
const parts = filePath.split('/');
|
|
17
|
+
return parts[parts.length - 1] ?? filePath;
|
|
18
|
+
}
|
|
19
|
+
function classifyFile(filePath, skipTestFiles) {
|
|
20
|
+
if (filePath.includes('node_modules/')) {
|
|
21
|
+
return 'node_modules';
|
|
22
|
+
}
|
|
23
|
+
if (DEPENDENCY_BASENAMES.has(basename(filePath))) {
|
|
24
|
+
return 'dependency';
|
|
25
|
+
}
|
|
26
|
+
if (filePath.startsWith('.github/') && (filePath.endsWith('.yml') || filePath.endsWith('.yaml'))) {
|
|
27
|
+
return 'ci';
|
|
28
|
+
}
|
|
29
|
+
if (filePath.endsWith('.generated.ts') || filePath.endsWith('.gen.ts')) {
|
|
30
|
+
return 'generated';
|
|
31
|
+
}
|
|
32
|
+
if (skipTestFiles &&
|
|
33
|
+
(filePath.endsWith('.test.ts') || filePath.endsWith('.test.tsx') || filePath.endsWith('.spec.ts'))) {
|
|
34
|
+
return 'test';
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function formatReason(counts) {
|
|
39
|
+
const parts = [];
|
|
40
|
+
for (const [category, count] of counts) {
|
|
41
|
+
const label = categoryLabels[category];
|
|
42
|
+
parts.push(`${count} ${label}${count > 1 ? 's' : ''}`);
|
|
43
|
+
}
|
|
44
|
+
return parts.join(', ');
|
|
45
|
+
}
|
|
46
|
+
export function classifyPr(changedFiles, options) {
|
|
47
|
+
if (changedFiles.length === 0) {
|
|
48
|
+
return { shouldSkip: true, reason: 'No changed files detected' };
|
|
49
|
+
}
|
|
50
|
+
const skipTestFiles = options.skipTestFiles ?? false;
|
|
51
|
+
const counts = new Map();
|
|
52
|
+
let docInScopeCount = 0;
|
|
53
|
+
for (const filePath of changedFiles) {
|
|
54
|
+
// Story P2.6 — shared in-scope predicate (picomatch@4 dialect via
|
|
55
|
+
// `@delfini/drift-engine`). Replaces the legacy private `isDocFile`
|
|
56
|
+
// prefix check so smart-skip and the Lite reader's git-trees matcher
|
|
57
|
+
// can never silently disagree on the same scope.
|
|
58
|
+
if (isFileInDocScope(filePath, options.docScope)) {
|
|
59
|
+
docInScopeCount += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const category = classifyFile(filePath, skipTestFiles);
|
|
63
|
+
if (category === null) {
|
|
64
|
+
return { shouldSkip: false, reason: 'Business-logic changes detected' };
|
|
65
|
+
}
|
|
66
|
+
counts.set(category, (counts.get(category) ?? 0) + 1);
|
|
67
|
+
}
|
|
68
|
+
let structurallyUninterestingCount = 0;
|
|
69
|
+
for (const count of counts.values()) {
|
|
70
|
+
structurallyUninterestingCount += count;
|
|
71
|
+
}
|
|
72
|
+
// FR57(b) v6.1 — every changed file is a doc-in-scope; smart-skip fires.
|
|
73
|
+
if (docInScopeCount > 0 && structurallyUninterestingCount === 0) {
|
|
74
|
+
const noun = docInScopeCount === 1 ? 'change' : 'changes';
|
|
75
|
+
return { shouldSkip: true, reason: `${docInScopeCount} doc-only ${noun} in doc scope` };
|
|
76
|
+
}
|
|
77
|
+
// FR57(a) — pure structurally-uninteresting changes; existing skip path.
|
|
78
|
+
if (docInScopeCount === 0) {
|
|
79
|
+
return { shouldSkip: true, reason: formatReason(counts) };
|
|
80
|
+
}
|
|
81
|
+
// Mixed doc-in-scope + structurally-uninteresting. The two skip checks
|
|
82
|
+
// are independent (FR57 v6.1) — a mixed PR satisfies neither, so analysis
|
|
83
|
+
// runs.
|
|
84
|
+
return { shouldSkip: false, reason: 'Mixed doc and non-doc changes detected' };
|
|
85
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@delfini/action-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"description": "Shared analysis-pipeline core of the Delfini GitHub Action (doc reader, smart-skip, analysis-input assembly, single-call orchestrator adapters, shared GitHub client, pipeline input reader). Internal shared core published for transparency and delfini-web consumption — no semver API-stability promise in V1.",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@actions/core": "^1.11.1",
|
|
23
|
+
"@actions/github": "^6.0.0",
|
|
24
|
+
"@langchain/anthropic": "^1.3.29",
|
|
25
|
+
"@langchain/core": "^1.1.45",
|
|
26
|
+
"@langchain/openai": "^1.4.5",
|
|
27
|
+
"gray-matter": "^4.0.3",
|
|
28
|
+
"@delfini/drift-engine": "0.1.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.10.2",
|
|
32
|
+
"typescript": "^5.7.2",
|
|
33
|
+
"vitest": "^3.0.5"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -b && node scripts/copy-prompt.mjs",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"lint": "eslint src",
|
|
39
|
+
"test": "vitest run"
|
|
40
|
+
}
|
|
41
|
+
}
|