@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.
Files changed (39) hide show
  1. package/README.md +37 -0
  2. package/dist/adapters/factory.d.ts +3 -0
  3. package/dist/adapters/factory.d.ts.map +1 -0
  4. package/dist/adapters/factory.js +2 -0
  5. package/dist/adapters/single-call/model.d.ts +4 -0
  6. package/dist/adapters/single-call/model.d.ts.map +1 -0
  7. package/dist/adapters/single-call/model.js +46 -0
  8. package/dist/adapters/single-call/orchestrator.d.ts +10 -0
  9. package/dist/adapters/single-call/orchestrator.d.ts.map +1 -0
  10. package/dist/adapters/single-call/orchestrator.js +97 -0
  11. package/dist/adapters/single-call/prompt.md +360 -0
  12. package/dist/analysis-input.d.ts +18 -0
  13. package/dist/analysis-input.d.ts.map +1 -0
  14. package/dist/analysis-input.js +60 -0
  15. package/dist/diff-builder.d.ts +16 -0
  16. package/dist/diff-builder.d.ts.map +1 -0
  17. package/dist/diff-builder.js +35 -0
  18. package/dist/doc-exclusion.d.ts +17 -0
  19. package/dist/doc-exclusion.d.ts.map +1 -0
  20. package/dist/doc-exclusion.js +85 -0
  21. package/dist/doc-reader.d.ts +34 -0
  22. package/dist/doc-reader.d.ts.map +1 -0
  23. package/dist/doc-reader.js +172 -0
  24. package/dist/github-client-shared.d.ts +27 -0
  25. package/dist/github-client-shared.d.ts.map +1 -0
  26. package/dist/github-client-shared.js +240 -0
  27. package/dist/index.d.ts +19 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +20 -0
  30. package/dist/pipeline-inputs.d.ts +23 -0
  31. package/dist/pipeline-inputs.d.ts.map +1 -0
  32. package/dist/pipeline-inputs.js +42 -0
  33. package/dist/ports/orchestrator.d.ts +5 -0
  34. package/dist/ports/orchestrator.d.ts.map +1 -0
  35. package/dist/ports/orchestrator.js +1 -0
  36. package/dist/smart-skip.d.ts +10 -0
  37. package/dist/smart-skip.d.ts.map +1 -0
  38. package/dist/smart-skip.js +85 -0
  39. package/package.json +41 -0
@@ -0,0 +1,60 @@
1
+ import * as core from '@actions/core';
2
+ import * as github from '@actions/github';
3
+ import { buildUnifiedDiff } from './diff-builder.js';
4
+ import { filterDiff } from '@delfini/drift-engine';
5
+ // Story P2.2 (AC6) — extracted verbatim from pipeline.ts. The Lite pipeline
6
+ // reuses this without importing pipeline.ts as a value, which would pull the
7
+ // FR88g/FR88d module graph (config-client / stream-routing / intake-client)
8
+ // into lite-pipeline.ts's runtime graph and break the epic's "Lite never calls
9
+ // FR88g/FR88d" invariant. `buildUnifiedDiff` + the `@actions/github` context
10
+ // carry no FR88d coupling, so this is a clean shared module.
11
+ export function buildAnalysisInput(ctx, changedFiles, docs, options = {}) {
12
+ let diff = buildUnifiedDiff(changedFiles);
13
+ if (options.enableDiffPreFilter === true) {
14
+ const result = filterDiff(diff);
15
+ diff = result.keptDiff;
16
+ // One info line; no per-path logs to keep a large lockfile churn from
17
+ // flooding the Action log. Counts only — sufficient to diagnose under-
18
+ // or over-aggressive filtering without leaking diff content.
19
+ //
20
+ // Count BOTH droppedPaths and droppedHunks per reason: a file whose every
21
+ // hunk is whitespace-only / import-only is promoted to a path-level drop
22
+ // with that reason, so a path-bucket-only summary would render whole-file
23
+ // whitespace/import drops invisible (AC3 — drops must never be silently
24
+ // discarded).
25
+ const counts = countByReason([
26
+ ...result.droppedPaths.map((p) => p.reason),
27
+ ...result.droppedHunks.map((h) => h.reason),
28
+ ]);
29
+ core.info(`Delfini diff pre-filter dropped: lockfiles=${counts.lockfile} ` +
30
+ `generated=${counts.generated} vendored=${counts.vendored} ` +
31
+ `fixtures=${counts.fixture} whitespace=${counts['whitespace-only']} ` +
32
+ `import=${counts['import-only']}`);
33
+ }
34
+ const prTitle = github.context.payload.pull_request?.title ?? '';
35
+ return {
36
+ diff,
37
+ docs,
38
+ prMetadata: {
39
+ owner: ctx.owner,
40
+ repo: ctx.repo,
41
+ prNumber: ctx.pullNumber,
42
+ headSha: ctx.headSha,
43
+ baseSha: ctx.baseSha,
44
+ title: prTitle,
45
+ },
46
+ };
47
+ }
48
+ function countByReason(reasons) {
49
+ const out = {
50
+ lockfile: 0,
51
+ generated: 0,
52
+ vendored: 0,
53
+ fixture: 0,
54
+ 'whitespace-only': 0,
55
+ 'import-only': 0,
56
+ };
57
+ for (const r of reasons)
58
+ out[r]++;
59
+ return out;
60
+ }
@@ -0,0 +1,16 @@
1
+ import type { ChangedFile } from './github-client-shared.js';
2
+ /**
3
+ * Builds a unified-diff string from the GitHub API's per-file patch entries.
4
+ *
5
+ * GitHub's `pulls.listFiles` returns each file's hunks (the `@@ ... @@` content)
6
+ * but omits the `diff --git` / `--- a/… +++ b/…` preamble. We synthesise that
7
+ * preamble so the resulting string is a valid multi-file unified diff and so
8
+ * the single-call prompt-builder's `diff --git` counter (for `changedFileCount`)
9
+ * sees the right file count.
10
+ *
11
+ * Files without a `patch` (binary, renamed-only, or >GitHub's per-file size cap)
12
+ * are skipped entirely per the Story 3.2 design decision: we never lie to the
13
+ * LLM about what changed when we can't actually show it.
14
+ */
15
+ export declare function buildUnifiedDiff(files: ChangedFile[]): string;
16
+ //# sourceMappingURL=diff-builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-builder.d.ts","sourceRoot":"","sources":["../src/diff-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAE5D;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,MAAM,CAuB7D"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Builds a unified-diff string from the GitHub API's per-file patch entries.
3
+ *
4
+ * GitHub's `pulls.listFiles` returns each file's hunks (the `@@ ... @@` content)
5
+ * but omits the `diff --git` / `--- a/… +++ b/…` preamble. We synthesise that
6
+ * preamble so the resulting string is a valid multi-file unified diff and so
7
+ * the single-call prompt-builder's `diff --git` counter (for `changedFileCount`)
8
+ * sees the right file count.
9
+ *
10
+ * Files without a `patch` (binary, renamed-only, or >GitHub's per-file size cap)
11
+ * are skipped entirely per the Story 3.2 design decision: we never lie to the
12
+ * LLM about what changed when we can't actually show it.
13
+ */
14
+ export function buildUnifiedDiff(files) {
15
+ const parts = [];
16
+ for (const file of files) {
17
+ if (!file.patch)
18
+ continue;
19
+ parts.push(`diff --git a/${file.filename} b/${file.filename}`);
20
+ if (file.status === 'added') {
21
+ parts.push(`--- /dev/null`);
22
+ parts.push(`+++ b/${file.filename}`);
23
+ }
24
+ else if (file.status === 'removed') {
25
+ parts.push(`--- a/${file.filename}`);
26
+ parts.push(`+++ /dev/null`);
27
+ }
28
+ else {
29
+ parts.push(`--- a/${file.filename}`);
30
+ parts.push(`+++ b/${file.filename}`);
31
+ }
32
+ parts.push(file.patch);
33
+ }
34
+ return parts.join('\n');
35
+ }
@@ -0,0 +1,17 @@
1
+ export type ExclusionReason = 'front-matter';
2
+ export interface ExcludedDoc {
3
+ path: string;
4
+ reason: ExclusionReason;
5
+ detail?: string;
6
+ }
7
+ export interface FrontMatterResult {
8
+ ignore: boolean;
9
+ reason?: string;
10
+ body: string;
11
+ frontMatterLineCount: number;
12
+ }
13
+ type WarnFn = (message: string) => void;
14
+ export declare function parseFrontMatter(markdown: string, onWarn?: WarnFn): FrontMatterResult;
15
+ export declare function stripFrontMatter(markdown: string): string;
16
+ export {};
17
+ //# sourceMappingURL=doc-exclusion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doc-exclusion.d.ts","sourceRoot":"","sources":["../src/doc-exclusion.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,eAAe,GAAG,cAAc,CAAA;AAE5C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,eAAe,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,OAAO,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IAOZ,oBAAoB,EAAE,MAAM,CAAA;CAC7B;AAuBD,KAAK,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;AAEvC,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,MAAiB,GACxB,iBAAiB,CAuEnB;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMzD"}
@@ -0,0 +1,85 @@
1
+ import matter from 'gray-matter';
2
+ // Counts the number of lines occupied by the YAML front-matter block in `markdown`.
3
+ // Returns 0 when no front-matter is present. Robust to CRLF / LF; the closing
4
+ // `---` may sit on its own line or be followed by trailing whitespace — both
5
+ // shapes are matched. If a leading `---` is present but no closing `---` is
6
+ // found within a reasonable bound, returns 0 (defensive — prevents counting
7
+ // the entire file as front-matter on a malformed doc).
8
+ const FRONT_MATTER_OPEN_RE = /^---\r?\n/;
9
+ function countFrontMatterLines(markdown) {
10
+ if (!FRONT_MATTER_OPEN_RE.test(markdown))
11
+ return 0;
12
+ const lines = markdown.split(/\r?\n/);
13
+ // First line is the opening `---`. Search for the closing `---` from line 2.
14
+ for (let i = 1; i < lines.length; i++) {
15
+ if (lines[i] === '---') {
16
+ // i is 0-indexed; the closing `---` is on line `i + 1` (1-indexed),
17
+ // and the block occupies that many lines.
18
+ return i + 1;
19
+ }
20
+ }
21
+ return 0;
22
+ }
23
+ export function parseFrontMatter(markdown, onWarn = () => { }) {
24
+ // Compute the line count from the original markdown — independent of
25
+ // gray-matter's parse outcome — so a malformed YAML fallback still produces
26
+ // a useful `frontMatterLineCount` rather than `0`. (Counter-argument: a
27
+ // malformed YAML block is also a sign the structure is suspect; defaulting
28
+ // the count to 0 in the catch path matches the body fallback to original
29
+ // markdown — both say "treat the whole file as body".)
30
+ let parsed;
31
+ try {
32
+ parsed = matter(markdown);
33
+ }
34
+ catch (error) {
35
+ const reason = error instanceof Error ? error.message : String(error);
36
+ onWarn(`Failed to parse front-matter: ${reason}`);
37
+ return { ignore: false, body: markdown, frontMatterLineCount: 0 };
38
+ }
39
+ const data = parsed.data;
40
+ const body = parsed.content;
41
+ const frontMatterLineCount = countFrontMatterLines(markdown);
42
+ if (!('delfini' in data)) {
43
+ return { ignore: false, body, frontMatterLineCount };
44
+ }
45
+ const value = data.delfini;
46
+ // Shorthand: delfini: ignore | skip (or delfini: true)
47
+ if (typeof value === 'string') {
48
+ const normalized = value.trim().toLowerCase();
49
+ if (normalized === 'ignore' || normalized === 'skip') {
50
+ return { ignore: true, body, frontMatterLineCount };
51
+ }
52
+ onWarn(`Unrecognised front-matter value for 'delfini': "${value}" — expected "ignore" or "skip". Treating as not-ignored.`);
53
+ return { ignore: false, body, frontMatterLineCount };
54
+ }
55
+ if (typeof value === 'boolean') {
56
+ return { ignore: value, body, frontMatterLineCount };
57
+ }
58
+ if (value !== null && typeof value === 'object') {
59
+ const obj = value;
60
+ const ignoreFlag = obj.ignore;
61
+ const skipFlag = obj.skip;
62
+ const rawReason = obj.reason;
63
+ const reason = typeof rawReason === 'string' && rawReason.trim().length > 0
64
+ ? rawReason.trim()
65
+ : undefined;
66
+ if (ignoreFlag === true || skipFlag === true) {
67
+ return { ignore: true, reason, body, frontMatterLineCount };
68
+ }
69
+ if (ignoreFlag === false || skipFlag === false) {
70
+ return { ignore: false, body, frontMatterLineCount };
71
+ }
72
+ onWarn(`Front-matter 'delfini' object did not contain a boolean 'ignore' or 'skip' flag. Treating as not-ignored.`);
73
+ return { ignore: false, body, frontMatterLineCount };
74
+ }
75
+ onWarn(`Front-matter 'delfini' value has unsupported type (${typeof value}). Treating as not-ignored.`);
76
+ return { ignore: false, body, frontMatterLineCount };
77
+ }
78
+ export function stripFrontMatter(markdown) {
79
+ try {
80
+ return matter(markdown).content;
81
+ }
82
+ catch {
83
+ return markdown;
84
+ }
85
+ }
@@ -0,0 +1,34 @@
1
+ import type { GitHub } from '@actions/github/lib/utils';
2
+ import { type ExcludedDoc } from './doc-exclusion.js';
3
+ export interface DocFile {
4
+ path: string;
5
+ content: string;
6
+ frontMatterLineCount: number;
7
+ }
8
+ export interface DocsReadResult {
9
+ included: DocFile[];
10
+ excluded: ExcludedDoc[];
11
+ scope: ScopeSource;
12
+ }
13
+ export type ScopeSource = {
14
+ kind: 'docs_path';
15
+ docScope: string;
16
+ };
17
+ type Octokit = InstanceType<typeof GitHub>;
18
+ export type ReadDocsOptions = Record<string, never>;
19
+ export declare function readDocsViaGitTrees(octokit: Octokit, owner: string, repo: string, docScope: string[], ref: string, _options?: ReadDocsOptions): Promise<DocsReadResult>;
20
+ /**
21
+ * Lite-mode reader wrapper. Always reads at the PR head SHA (`ctx.headSha`),
22
+ * matching `github-client.ts:readDocs`'s rationale: walking at base would
23
+ * re-detect the same drift forever once Approve-and-Commit splices doc
24
+ * updates onto the PR branch (Story P2.6 carries the rationale forward;
25
+ * Lite mode has no Approve-and-Commit but the head-vs-base distinction is
26
+ * still the correct one for analysing "what the PR's current docs look like").
27
+ *
28
+ * Lives here rather than in `github-client.ts` to keep the new reader and its
29
+ * wrapper colocated. Story 3.12 retired `readDocsFromPath`; BOTH modes read
30
+ * via this git-trees path now (Full through `github-client.ts:readDocs`).
31
+ */
32
+ export declare function readDocsAtHeadViaGitTrees(octokit: Octokit, owner: string, repo: string, docScope: string[], headSha: string, options?: ReadDocsOptions): Promise<DocsReadResult>;
33
+ export {};
34
+ //# sourceMappingURL=doc-reader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doc-reader.d.ts","sourceRoot":"","sources":["../src/doc-reader.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAA;AAEvD,OAAO,EAEL,KAAK,WAAW,EACjB,MAAM,oBAAoB,CAAA;AAE3B,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IAIf,oBAAoB,EAAE,MAAM,CAAA;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,QAAQ,EAAE,WAAW,EAAE,CAAA;IACvB,KAAK,EAAE,WAAW,CAAA;CACnB;AAQD,MAAM,MAAM,WAAW,GAAG;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAA;AAEjE,KAAK,OAAO,GAAG,YAAY,CAAC,OAAO,MAAM,CAAC,CAAA;AAE1C,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;AAmHnD,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAAE,EAClB,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,eAAoB,GAC7B,OAAO,CAAC,cAAc,CAAC,CA6EzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAAE,EAClB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CAEzB"}
@@ -0,0 +1,172 @@
1
+ import * as core from '@actions/core';
2
+ import { isFileInDocScope } from '@delfini/drift-engine';
3
+ import { parseFrontMatter, } from './doc-exclusion.js';
4
+ // Story 3.12 (ADR-2026-06-01) — the per-directory `getContent` walk
5
+ // (`readDocsFromPath` + `collectDocs`) is retired. BOTH modes now read docs
6
+ // via `readDocsViaGitTrees` (ONE recursive git-trees call + the shared
7
+ // `isFileInDocScope` matcher). Full mode reaches it through
8
+ // `readDocsAtHeadViaGitTrees` (github-client.ts:readDocs); Lite mode reaches
9
+ // it directly (lite-pipeline.ts).
10
+ async function processFile(octokit, owner, repo, filePath, ref, out) {
11
+ const content = await fetchFileContent(octokit, owner, repo, filePath, ref);
12
+ if (content === null) {
13
+ return;
14
+ }
15
+ const frontMatter = parseFrontMatter(content, (message) => {
16
+ core.warning(`${filePath}: ${message}`);
17
+ });
18
+ if (frontMatter.ignore) {
19
+ out.excluded.push({
20
+ path: filePath,
21
+ reason: 'front-matter',
22
+ detail: frontMatter.reason,
23
+ });
24
+ return;
25
+ }
26
+ out.included.push({
27
+ path: filePath,
28
+ content: frontMatter.body,
29
+ frontMatterLineCount: frontMatter.frontMatterLineCount,
30
+ });
31
+ }
32
+ async function fetchFileContent(octokit, owner, repo, path, ref) {
33
+ try {
34
+ const { data } = await octokit.rest.repos.getContent({
35
+ owner,
36
+ repo,
37
+ path,
38
+ ref,
39
+ });
40
+ if (Array.isArray(data) || data.type !== 'file' || !data.content) {
41
+ return null;
42
+ }
43
+ return Buffer.from(data.content, 'base64').toString('utf-8');
44
+ }
45
+ catch (error) {
46
+ if (isNotFoundError(error)) {
47
+ return null;
48
+ }
49
+ throw error;
50
+ }
51
+ }
52
+ function isNotFoundError(error) {
53
+ return (typeof error === 'object' &&
54
+ error !== null &&
55
+ 'status' in error &&
56
+ error.status === 404);
57
+ }
58
+ // ----------------------------------------------------------------------------
59
+ // Story P2.6 / ADR-2026-06-01 — multi-path / glob doc-scope reader
60
+ // ----------------------------------------------------------------------------
61
+ //
62
+ // `readDocsViaGitTrees` is the new Lite-mode reader. It replaces the Lite
63
+ // pipeline's per-directory `getContent` walk (`readDocsFromPath`) with ONE
64
+ // recursive git-trees call, filters every blob path through the shared
65
+ // `isFileInDocScope` predicate from `@delfini/drift-engine` (picomatch@4
66
+ // dialect), then fetches only the matched `.md` blobs.
67
+ //
68
+ // Why git-trees: the Contents API has no glob support; the trees API is the
69
+ // only tractable way to glob a remote tree and is usually FEWER round-trips
70
+ // than the directory walk. Dialect parity with smart-skip is enforced by
71
+ // construction — the SAME `isFileInDocScope` runs against the SAME `string[]`
72
+ // scope on both sides (the 23-row dialect-parity fixture in
73
+ // `packages/drift-engine/__tests__/fixtures/doc-scope-dialect.json` gates it).
74
+ //
75
+ // Story 3.12 retired `readDocsFromPath` — Full mode now reaches this reader
76
+ // via `readDocsAtHeadViaGitTrees` (github-client.ts:readDocs), at full
77
+ // multi-path/glob parity with Lite mode.
78
+ const MAX_CONCURRENT_BLOB_FETCHES = 8;
79
+ const MARKDOWN_EXTENSIONS = ['.md', '.markdown'];
80
+ function isMarkdownPath(path) {
81
+ const lower = path.toLowerCase();
82
+ return MARKDOWN_EXTENSIONS.some((ext) => lower.endsWith(ext));
83
+ }
84
+ export async function readDocsViaGitTrees(octokit, owner, repo, docScope, ref, _options = {}) {
85
+ // Joined string for the internal `ScopeSource.docScope` display field — keeps
86
+ // the existing `{kind: 'docs_path'; docScope: string}` shape byte-identical
87
+ // for downstream log/display consumers (Story P2.6 AC4 default per the
88
+ // Questions for the User answer). `ScopeSource` is internal to apps/action;
89
+ // widening to `string | string[]` is a forward option for Story 3.12.
90
+ const scopeDisplay = docScope.join(', ');
91
+ const result = {
92
+ included: [],
93
+ excluded: [],
94
+ scope: { kind: 'docs_path', docScope: scopeDisplay },
95
+ };
96
+ // Empty scope -> empty result, never analyse. Matches FR57(b)'s "no
97
+ // doc-in-scope files" baseline; never fall through to a recursive call
98
+ // against an empty scope (would match every blob).
99
+ if (docScope.length === 0) {
100
+ return result;
101
+ }
102
+ let treeData;
103
+ try {
104
+ const response = await octokit.rest.git.getTree({
105
+ owner,
106
+ repo,
107
+ tree_sha: ref,
108
+ // The Octokit REST typing accepts the string '1' here (cast on the
109
+ // wire to a query param). Boolean `true` works at runtime but is not
110
+ // strictly typed.
111
+ recursive: '1',
112
+ });
113
+ treeData = response.data;
114
+ }
115
+ catch (error) {
116
+ if (isNotFoundError(error)) {
117
+ core.warning(`git tree for ref ${ref} not found — returning empty docs list`);
118
+ return result;
119
+ }
120
+ throw error;
121
+ }
122
+ // V1 truncated-tree contract: warn-and-proceed. A truncated tree means the
123
+ // user has a very large monorepo and the response hit GitHub's size cap;
124
+ // some docs may be missed. A correctness-first follow-up walks the tree
125
+ // subtree-by-subtree on truncation — backlogged.
126
+ if (treeData.truncated) {
127
+ core.warning(`git tree for ${ref} was truncated by GitHub; some docs may be missed. ` +
128
+ 'Consider narrowing docs_path or splitting the repo.');
129
+ }
130
+ const tree = (treeData.tree ?? []);
131
+ const matchedPaths = [];
132
+ for (const entry of tree) {
133
+ if (entry.type !== 'blob')
134
+ continue;
135
+ const path = entry.path;
136
+ if (typeof path !== 'string' || path.length === 0)
137
+ continue;
138
+ if (!isFileInDocScope(path, docScope))
139
+ continue;
140
+ // The predicate is path-shape-only (P3.6.1 AC5). The `.md` / `.markdown`
141
+ // restriction is an expander concern — apply it AFTER the in-scope check.
142
+ if (!isMarkdownPath(path))
143
+ continue;
144
+ matchedPaths.push(path);
145
+ }
146
+ // Concurrency-capped blob fetch. A repo with thousands of matched .md files
147
+ // could otherwise trip GitHub's secondary rate limit. The cap is a safety
148
+ // floor, not a perf knob.
149
+ for (let i = 0; i < matchedPaths.length; i += MAX_CONCURRENT_BLOB_FETCHES) {
150
+ const batch = matchedPaths.slice(i, i + MAX_CONCURRENT_BLOB_FETCHES);
151
+ await Promise.all(batch.map((path) => processFile(octokit, owner, repo, path, ref, result)));
152
+ }
153
+ // Stable output order — git-trees response order can vary; tests depend on
154
+ // determinism for snapshot-style assertions.
155
+ result.included.sort((a, b) => a.path.localeCompare(b.path));
156
+ return result;
157
+ }
158
+ /**
159
+ * Lite-mode reader wrapper. Always reads at the PR head SHA (`ctx.headSha`),
160
+ * matching `github-client.ts:readDocs`'s rationale: walking at base would
161
+ * re-detect the same drift forever once Approve-and-Commit splices doc
162
+ * updates onto the PR branch (Story P2.6 carries the rationale forward;
163
+ * Lite mode has no Approve-and-Commit but the head-vs-base distinction is
164
+ * still the correct one for analysing "what the PR's current docs look like").
165
+ *
166
+ * Lives here rather than in `github-client.ts` to keep the new reader and its
167
+ * wrapper colocated. Story 3.12 retired `readDocsFromPath`; BOTH modes read
168
+ * via this git-trees path now (Full through `github-client.ts:readDocs`).
169
+ */
170
+ export async function readDocsAtHeadViaGitTrees(octokit, owner, repo, docScope, headSha, options = {}) {
171
+ return readDocsViaGitTrees(octokit, owner, repo, docScope, headSha, options);
172
+ }
@@ -0,0 +1,27 @@
1
+ import type { GitHub } from '@actions/github/lib/utils';
2
+ import type { DocFile, DocsReadResult, ReadDocsOptions } from './doc-reader.js';
3
+ export type { DocFile, DocsReadResult, ReadDocsOptions };
4
+ export interface PrContext {
5
+ owner: string;
6
+ repo: string;
7
+ pullNumber: number;
8
+ headSha: string;
9
+ baseSha: string;
10
+ }
11
+ export interface ChangedFile {
12
+ filename: string;
13
+ status: string;
14
+ patch?: string;
15
+ }
16
+ type Octokit = InstanceType<typeof GitHub>;
17
+ export declare function getPrContext(): PrContext;
18
+ export declare function listChangedFiles(octokit: Octokit, ctx: PrContext): Promise<ChangedFile[]>;
19
+ export declare function getFileContent(octokit: Octokit, ctx: PrContext, path: string, ref: string): Promise<string | null>;
20
+ export declare function readDocs(octokit: Octokit, ctx: PrContext, docScope: string[], options?: ReadDocsOptions): Promise<DocsReadResult>;
21
+ export declare const DELFINI_PR_COMMENT_MARKER = "<!-- delfini-pr-comment -->";
22
+ export declare function postOrUpdatePrComment(octokit: Octokit, ctx: PrContext, body: string, options: {
23
+ marker: string;
24
+ }): Promise<void>;
25
+ export declare function isForbiddenError(error: unknown): boolean;
26
+ export declare function createCheckStatus(octokit: Octokit, ctx: PrContext, conclusion: 'in_progress' | 'success' | 'failure' | 'neutral' | 'action_required', title: string, summary: string, detailsUrl?: string): Promise<void>;
27
+ //# sourceMappingURL=github-client-shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github-client-shared.d.ts","sourceRoot":"","sources":["../src/github-client-shared.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAA;AAEvD,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAO/E,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,CAAA;AAExD,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,KAAK,OAAO,GAAG,YAAY,CAAC,OAAO,MAAM,CAAC,CAAA;AAE1C,wBAAgB,YAAY,IAAI,SAAS,CA4BxC;AAED,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,SAAS,GACb,OAAO,CAAC,WAAW,EAAE,CAAC,CAiCxB;AAED,wBAAsB,cAAc,CAClC,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAyBxB;AAED,wBAAsB,QAAQ,CAC5B,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,SAAS,EACd,QAAQ,EAAE,MAAM,EAAE,EAClB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CAezB;AAsBD,eAAO,MAAM,yBAAyB,gCAAgC,CAAA;AAEtE,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAC1B,OAAO,CAAC,IAAI,CAAC,CA8Ef;AAgBD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAExD;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,SAAS,EACd,UAAU,EAAE,aAAa,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,iBAAiB,EACjF,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAqCf"}