@diffci.com/diffci 0.1.2 → 0.1.3

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 CHANGED
@@ -1,11 +1,32 @@
1
1
  # DiffCI
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@diffci.com/diffci.svg)](https://www.npmjs.com/package/@diffci.com/diffci)
4
+ [![npm provenance](https://img.shields.io/badge/npm-provenance-blue)](https://docs.npmjs.com/generating-provenance-statements)
5
+ [![GitHub Action](https://img.shields.io/badge/action-DiffCI%2FDiffCI.com%40v1-blue)](https://github.com/DiffCI/DiffCI.com)
6
+
3
7
  DiffCI is a deterministic, change-aware CI planner: given a commit or PR, it builds a real TypeScript
4
8
  dependency graph, computes what's actually reachable from the changed files, and proposes which CI
5
9
  tasks/tests could safely be skipped - without ever modifying production CI behavior itself. Every mode
6
10
  this repository currently implements is observe-and-compare only; nothing here can cancel, skip, or block
7
11
  a real CI run.
8
12
 
13
+ Try it in shadow mode:
14
+
15
+ ```bash
16
+ npx @diffci.com/diffci@latest observe
17
+ npx @diffci.com/diffci@latest verify-workflow
18
+ ```
19
+
20
+ Or install it as a non-blocking GitHub Action:
21
+
22
+ ```yaml
23
+ - uses: DiffCI/DiffCI.com@v1
24
+ ```
25
+
26
+ The promise is deliberately narrow: DiffCI observes your CI and reports what it would have selected.
27
+ It does not skip tests, cancel jobs, change required checks, or send reports anywhere unless you
28
+ explicitly configure an endpoint and token.
29
+
9
30
  **This repository moved out of the [DentalPresence.in](https://github.com/adityankale190895/DentalPresence.in)
10
31
  monorepo** (previously `diffci/` there) into its own repo on 2026-08-21, once the project outgrew being a
11
32
  subfolder. DentalPresence.in remains DiffCI's original dogfooding target - some code (the `planner`
@@ -15,6 +36,10 @@ real third-party repositories.
15
36
 
16
37
  ## Current state
17
38
 
39
+ Language expansion: initial Vue SFC and Go package-level analysis is implemented through repository
40
+ adapters. See [the support matrix and setup requirements](docs/language-support.md) for exact scope,
41
+ fallback behavior, and validation boundaries.
42
+
18
43
  Three completed research stages plus an in-progress prospective-validation stage, in order:
19
44
 
20
45
  - **Stage 0** - a 2,000-delta historical benchmark across 20 real repositories, run through a real
@@ -43,24 +68,51 @@ Every dated report behind these stages lives in [`docs/research/`](docs/research
43
68
  `2026-08-21-stage2-architecture.md` for the fullest current picture of what's built vs not, or the
44
69
  Stage 0/1A/1B reports for the historical-validation story.
45
70
 
71
+ For the next product milestone, see [`docs/alpha-readiness.md`](docs/alpha-readiness.md). It tracks the
72
+ private-alpha bar: install DiffCI, keep CI unchanged, collect real shadow observations, and render a
73
+ trustworthy potential-savings report.
74
+
46
75
  ## Architecture
47
76
 
48
- - `src/git/` - Git delta analysis (`analyzeGitDelta`): parses a commit range into a structured,
49
- serializable `GitDelta`. Project invariant: **failure to analyze must never be interpreted as
50
- permission to skip CI** - a failed analysis returns `{ success: false, error }` explicitly, never a
51
- silently-empty affected set.
52
- - `src/repo/` - the dependency graph engine (`buildDependencyGraph`, TypeScript-compiler-backed) and the
53
- impact analyzer (`ImpactAnalyzer`) that turns a graph + delta into a confidence-scored, fallback-aware
54
- impact result.
55
- - `src/planner/` - turns an impact result into an `ExecutionPlan` (`DefaultCIPlanner`): which tasks/tests
56
- run, which are skip-candidates, and why.
57
- - `src/research/` - the Stage 0/1 historical benchmark pipeline: repository sampling, the generic
58
- (non-DentalPresence-specific) PATH baseline, the opportunity classifier, historical GitHub CI evidence
59
- collection with flakiness detection, and the Cloudflare orchestrator (`src/research/cloudflare/`) that
60
- runs all of it at scale.
61
- - `src/shadow/` - the Stage 2 prospective pipeline: event identity (`event-identity.ts`), failure
62
- classification, ground-truth reconciliation (`reconcile.ts`) against real CI outcomes, and (written,
63
- not yet registered) GitHub App JWT/webhook code (`github-app.ts`).
77
+ DiffCI is now framed as an open-core product:
78
+
79
+ ```text
80
+ DiffCI
81
+ |
82
+ ├── Open-source core
83
+ | ├── DiffCI engine
84
+ | ├── CLI / npm package
85
+ | ├── Local analysis
86
+ | └── Basic GitHub Action
87
+ | |
88
+ | └── Tidelift package support
89
+ |
90
+ └── Commercial DiffCI
91
+ ├── Hosted service / DiffCI Cloud
92
+ ├── Organization dashboard
93
+ ├── Historical analytics
94
+ ├── Advanced CI/CD optimization
95
+ ├── Enterprise policies
96
+ ├── Managed runners
97
+ ├── Team features
98
+ └── Support / enterprise services
99
+ ```
100
+
101
+ The open-source core is the trust and adoption surface. It runs locally or in the host repository's own
102
+ CI, writes a report, and changes nothing about CI execution. Commercial DiffCI adds hosted history,
103
+ organization views, policy, managed operations, runners, and support. Tidelift belongs to the supported
104
+ open-source package path, not the hosted product feature boundary. See
105
+ [`docs/open-core-packaging.md`](docs/open-core-packaging.md) and
106
+ [`docs/tidelift-package-support.md`](docs/tidelift-package-support.md).
107
+
108
+ The source tree follows that split:
109
+
110
+ - `src/git/`, `src/repo/`, `src/planner/`, and `src/client/` are the installable OSS observer path.
111
+ - `action.yml` wraps the observer as a basic non-blocking GitHub Action.
112
+ - `src/research/` and `src/shadow/` run validation, GitHub App shadow observation, and reconciliation.
113
+ - `src/product/`, `src/auth/`, `src/billing/`, `src/ingest/`, `src/ledger/`, `src/runner/`, and
114
+ `src/usage/` are the commercial/control-plane layer.
115
+ - `docs/oss-boundary.md` records what is allowed into the npm package.
64
116
 
65
117
  ## Commands
66
118
 
@@ -88,11 +140,12 @@ DiffCI is intended to be installable as infrastructure, not only as a hosted sha
88
140
  ```
89
141
 
90
142
  ```bash
91
- npx @diffci.com/diffci observe
92
- npx @diffci.com/diffci verify-workflow
143
+ npx @diffci.com/diffci@latest observe
144
+ npx @diffci.com/diffci@latest verify-workflow
93
145
  ```
94
146
 
95
- The GitHub App remains the easiest shadow-mode entry point. The GitHub Action and npm CLI establish
96
- the OSS/package distribution path: DiffCI can become an explicit CI dependency while preserving the
97
- same observe-only contract. See [`docs/distribution.md`](docs/distribution.md) for the package and
98
- Action positioning.
147
+ The GitHub Action and npm CLI establish the OSS/package distribution path. The hosted GitHub App and
148
+ DiffCI Cloud build on that trust boundary for teams that want shared reports and history. See
149
+ [`docs/distribution.md`](docs/distribution.md) for the package and Action positioning,
150
+ [`docs/open-core-packaging.md`](docs/open-core-packaging.md) for the commercial split, and
151
+ [`docs/npm-adoption.md`](docs/npm-adoption.md) for copy-paste pilot material.
package/SECURITY.md ADDED
@@ -0,0 +1,61 @@
1
+ # Security policy
2
+
3
+ ## Supported project
4
+
5
+ This policy covers the DiffCI open-source core package, published as `@diffci.com/diffci`.
6
+
7
+ Covered surfaces:
8
+
9
+ - `diffci observe`
10
+ - `diffci verify-workflow`
11
+ - the basic GitHub Action in `action.yml`
12
+ - local report generation
13
+ - optional report submission to a configured endpoint and token
14
+
15
+ Commercial DiffCI, including DiffCI Cloud, hosted dashboards, managed runners, billing, organization
16
+ management, private report access, and enterprise policy features, is separate proprietary software and
17
+ is handled through commercial support channels.
18
+
19
+ ## Supported versions
20
+
21
+ The supported OSS package line is the latest published npm release. Security fixes are released as a
22
+ new npm version and Git tag.
23
+
24
+ DiffCI Core requires Node.js `>=22.5.0`.
25
+
26
+ ## Reporting a vulnerability
27
+
28
+ Do not open a public issue for a suspected vulnerability.
29
+
30
+ Report security issues by email to:
31
+
32
+ ```text
33
+ security@diffci.com
34
+ ```
35
+
36
+ Include:
37
+
38
+ - affected package/version;
39
+ - affected command or GitHub Action path;
40
+ - reproduction steps;
41
+ - expected impact;
42
+ - whether any token, report, repository data, or CI behavior is exposed or modified.
43
+
44
+ ## Response target
45
+
46
+ For the OSS package:
47
+
48
+ - acknowledge within 3 business days;
49
+ - provide an initial assessment within 7 business days;
50
+ - publish a fix or mitigation plan when the issue is confirmed.
51
+
52
+ ## Security boundaries
53
+
54
+ DiffCI Core is observation-only. A security issue is especially important if it causes the observer to:
55
+
56
+ - modify the repository checkout without reporting it;
57
+ - affect another CI job's conclusion;
58
+ - expose tokens or report submission credentials;
59
+ - submit a report to the wrong repository or organization;
60
+ - include source file contents when the report schema says it does not;
61
+ - bypass the OSS package boundary and include commercial/control-plane code.
package/SUPPORT.md ADDED
@@ -0,0 +1,47 @@
1
+ # Support policy
2
+
3
+ ## Open-source core
4
+
5
+ Supported package:
6
+
7
+ ```text
8
+ @diffci.com/diffci
9
+ ```
10
+
11
+ Supported surfaces:
12
+
13
+ - `diffci observe`
14
+ - `diffci verify-workflow`
15
+ - `diffci version`
16
+ - the basic GitHub Action in `action.yml`
17
+ - local JSON observation reports
18
+ - optional report submission to a configured endpoint and token
19
+
20
+ Supported runtime:
21
+
22
+ ```text
23
+ Node.js >=22.5.0
24
+ ```
25
+
26
+ The open-source support commitment is limited to the latest published npm release unless a security
27
+ advisory says otherwise.
28
+
29
+ ## Commercial DiffCI
30
+
31
+ Commercial DiffCI is separate proprietary software. It includes:
32
+
33
+ - DiffCI Cloud;
34
+ - hosted dashboards and private report access;
35
+ - organization/team management;
36
+ - historical analytics;
37
+ - enterprise policies;
38
+ - managed runners;
39
+ - billing, ledger, and support operations;
40
+ - deployment and control-plane infrastructure.
41
+
42
+ Those features are not included in Tidelift package support for the OSS dependency.
43
+
44
+ ## Tidelift boundary
45
+
46
+ Tidelift support, if accepted, should cover the open-source npm package as a dependency. It should not be
47
+ used to represent DiffCI Cloud, managed runners, or enterprise policy features as open source.
@@ -8,7 +8,8 @@
8
8
  *
9
9
  * Two properties are load-bearing, and both are enforced here rather than promised:
10
10
  *
11
- * READ-ONLY. The engine modules called below only read (verified: no write in src/repo, src/git). This
11
+ * READ-ONLY CHECKOUT. Analysis does not modify source; Go metadata can populate tool caches outside
12
+ * the checkout, with module edits and downloads disabled. This
12
13
  * function additionally records HEAD and `git status --porcelain` before and after itself, so a run that
13
14
  * DID dirty the tree says so in its own report instead of being discovered weeks later.
14
15
  *
@@ -22,7 +23,7 @@ import { execFileSync } from "node:child_process";
22
23
  import { createHash } from "node:crypto";
23
24
  import { relative, resolve } from "node:path";
24
25
  import { analyzeGitDelta } from "../git/git-diff.js";
25
- import { classifyTypeScriptProject, buildDependencyGraph } from "../repo/graph.js";
26
+ import { classifyRepositoryProject, buildDependencyGraph } from "../repo/graph.js";
26
27
  import { ImpactAnalyzer } from "../repo/impact.js";
27
28
  import { runPathBaseline } from "../planner/path-baseline.js";
28
29
  import { commandSpecToString, planSelectiveTestCommands } from "../planner/test-command.js";
@@ -145,12 +146,12 @@ export async function observe(options) {
145
146
  if (!resolved.ok)
146
147
  return finish("REFUSED", "context", { reason: resolved.reason });
147
148
  range = resolved.range;
148
- // The eligibility gate is asked of the graph builder itself (classifyTypeScriptProject), not of a
149
+ // The eligibility gate is asked of the graph builder itself (classifyRepositoryProject), not of a
149
150
  // separate list of conditions that can drift away from it. Phase 01 F3 is what that drift costs.
150
- const capability = classifyTypeScriptProject(repoPath);
151
+ const capability = classifyRepositoryProject(repoPath);
151
152
  if (!capability.capable) {
152
153
  return finish("REFUSED", "eligibility", {
153
- reason: `DiffCI can only analyse TypeScript/JavaScript projects today: ${capability.reason}`,
154
+ reason: `DiffCI supports TypeScript/JavaScript projects, Vue components, and root Go modules: ${capability.reason}`,
154
155
  });
155
156
  }
156
157
  const deltaResult = await analyzeGitDelta({ baseSha: range.baseSha, headSha: range.headSha, repoPath });
@@ -58,6 +58,29 @@ function buildCommand(framework, packageManager, configFile, paths) {
58
58
  * test-discovery.ts already models is carried through to execution rather than flattened.
59
59
  */
60
60
  export function planSelectiveTestCommands(profile, selectedPaths) {
61
+ const blockers = profile.adapterBlockers ?? profile.adapters?.flatMap((adapter) => adapter.blockers) ?? [];
62
+ if (blockers.length)
63
+ return { commands: [], groups: [], unroutedPaths: [...selectedPaths], refusalReason: blockers.join("; ") };
64
+ const goPaths = selectedPaths.filter((path) => path.endsWith(".go"));
65
+ if (goPaths.length) {
66
+ const packages = profile.goTestPackages ?? {};
67
+ const unclaimed = goPaths.filter((path) => !Object.hasOwn(packages, path));
68
+ if (unclaimed.length)
69
+ return { commands: [], groups: [], unroutedPaths: unclaimed, refusalReason: "Go test files require verified package metadata" };
70
+ const jsPlan = planSelectiveTestCommands(profile, selectedPaths.filter((path) => !path.endsWith(".go")));
71
+ if (jsPlan.refusalReason)
72
+ return jsPlan;
73
+ const targets = [...new Set(goPaths.map((path) => packages[path]))].sort();
74
+ if (targets.some((target) => target !== "." && (!target.startsWith("./") || target.split("/").includes("..") || /[\\\r\n]/.test(target)))) {
75
+ return { commands: [], groups: [], unroutedPaths: goPaths, refusalReason: "Invalid Go package target" };
76
+ }
77
+ const commandSpec = {
78
+ executable: "go", args: ["test", "-mod=readonly", "-json", "-count=1", ...targets],
79
+ env: { ...profile.goTestEnvironment, GOTOOLCHAIN: "local", GOPROXY: "off", GOSUMDB: "off", GOWORK: "off" },
80
+ };
81
+ const group = { runnerId: "go:test", label: "Go package tests", paths: [...goPaths].sort(), commandSpec };
82
+ return { commands: [...jsPlan.commands, commandSpec], groups: [...jsPlan.groups, group], unroutedPaths: [] };
83
+ }
61
84
  const paths = [...selectedPaths].sort();
62
85
  if (paths.length === 0)
63
86
  return { commands: [], groups: [], unroutedPaths: [] };
@@ -120,5 +143,6 @@ function shellEscape(arg) {
120
143
  return arg.replace(/([\s'"\\$|&;<>(){}\[\]*?#~`])/g, "\\$1");
121
144
  }
122
145
  export function commandSpecToString(spec) {
123
- return [spec.executable, ...spec.args.map(shellEscape)].join(" ");
146
+ const environment = Object.entries(spec.env ?? {}).map(([key, value]) => `${shellEscape(key)}=${shellEscape(value)}`);
147
+ return [...environment, spec.executable, ...spec.args.map(shellEscape)].join(" ");
124
148
  }
@@ -0,0 +1,154 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { isAbsolute, relative, resolve } from "node:path";
4
+ import { contribution } from "./types.js";
5
+ /** go list emits adjacent JSON objects, not a JSON array or JSONL. */
6
+ export function parseGoList(output) {
7
+ const packages = [];
8
+ let start = -1, depth = 0, quoted = false, escaped = false;
9
+ for (let i = 0; i < output.length; i++) {
10
+ const c = output[i];
11
+ if (quoted) {
12
+ if (escaped)
13
+ escaped = false;
14
+ else if (c === "\\")
15
+ escaped = true;
16
+ else if (c === '"')
17
+ quoted = false;
18
+ continue;
19
+ }
20
+ if (c === '"')
21
+ quoted = true;
22
+ else if (c === "{") {
23
+ if (depth++ === 0)
24
+ start = i;
25
+ }
26
+ else if (c === "}") {
27
+ if (--depth < 0)
28
+ throw new Error("Invalid go list output");
29
+ if (depth === 0) {
30
+ const pkg = JSON.parse(output.slice(start, i + 1));
31
+ if (typeof pkg.Dir !== "string" || typeof pkg.ImportPath !== "string")
32
+ throw new Error("Incomplete Go package metadata");
33
+ packages.push(pkg);
34
+ }
35
+ }
36
+ else if (depth === 0 && !/\s/.test(c))
37
+ throw new Error("Unexpected go list output");
38
+ }
39
+ if (depth || quoted || !packages.length)
40
+ throw new Error("Truncated or empty go list output");
41
+ return packages;
42
+ }
43
+ function internalPath(root, path) {
44
+ const rel = relative(root, resolve(path)).replace(/\\/g, "/");
45
+ return rel === ".." || rel.startsWith("../") || isAbsolute(rel) ? undefined : rel;
46
+ }
47
+ export function analyzeGoMetadata(context, output) {
48
+ const result = contribution(goAdapter);
49
+ const all = parseGoList(output);
50
+ if (all.some((pkg) => pkg.Module?.Replace?.Dir && internalPath(context.repoPath, pkg.Module.Replace.Dir) === undefined))
51
+ result.blockers.push("Go local module replacement lies outside the repository");
52
+ if (all.some((pkg) => pkg.Error || pkg.Incomplete || pkg.DepsErrors?.length))
53
+ result.blockers.push("Go package metadata contains errors");
54
+ const packages = all.filter((pkg) => !pkg.ForTest && !pkg.ImportPath.endsWith(".test") && !pkg.Standard && internalPath(context.repoPath, pkg.Dir) !== undefined);
55
+ const anchors = new Map();
56
+ const members = new Map();
57
+ for (const pkg of packages) {
58
+ const files = [...(pkg.GoFiles ?? []), ...(pkg.CgoFiles ?? []), ...(pkg.TestGoFiles ?? []), ...(pkg.XTestGoFiles ?? [])];
59
+ const paths = files.map((file) => internalPath(context.repoPath, resolve(pkg.Dir, file)));
60
+ if (paths.some((p) => p === undefined)) {
61
+ result.blockers.push("Go package contains files outside the repository");
62
+ continue;
63
+ }
64
+ const sources = paths;
65
+ if (!sources.length)
66
+ continue;
67
+ anchors.set(pkg.ImportPath, sources[0]);
68
+ members.set(pkg.ImportPath, sources);
69
+ result.sourcePaths.push(...sources);
70
+ if (pkg.CgoFiles?.length)
71
+ result.blockers.push(`Go ${pkg.ImportPath}: cgo dependencies require full validation`);
72
+ if (pkg.SFiles?.length || pkg.SysoFiles?.length)
73
+ result.blockers.push(`Go ${pkg.ImportPath}: native assembly/object dependencies require full validation`);
74
+ if ([...(pkg.Imports ?? []), ...(pkg.TestImports ?? []), ...(pkg.XTestImports ?? [])].includes("plugin"))
75
+ result.blockers.push(`Go ${pkg.ImportPath}: runtime plugins require full validation`);
76
+ for (const file of sources) {
77
+ if (/^\s*\/\/go:(?:generate|linkname)\b/m.test(readFileSync(resolve(context.repoPath, file), "utf8")))
78
+ result.blockers.push(`Go ${file}: generated or linked dependencies require full validation`);
79
+ }
80
+ const dir = internalPath(context.repoPath, pkg.Dir);
81
+ for (const test of [...(pkg.TestGoFiles ?? []), ...(pkg.XTestGoFiles ?? [])]) {
82
+ const path = internalPath(context.repoPath, resolve(pkg.Dir, test));
83
+ result.testFiles.push(path);
84
+ result.testPackages[path] = dir ? `./${dir}` : ".";
85
+ }
86
+ for (const file of [...(pkg.EmbedFiles ?? []), ...(pkg.TestEmbedFiles ?? []), ...(pkg.XTestEmbedFiles ?? [])]) {
87
+ const asset = internalPath(context.repoPath, resolve(pkg.Dir, file));
88
+ if (asset === undefined) {
89
+ result.blockers.push("Go embedded file escapes repository");
90
+ continue;
91
+ }
92
+ result.assetPaths.push(asset);
93
+ result.edges.push({ from: sources[0], to: asset, kind: "asset" });
94
+ }
95
+ }
96
+ for (const pkg of packages) {
97
+ const anchor = anchors.get(pkg.ImportPath);
98
+ if (!anchor)
99
+ continue;
100
+ // A package is the selection unit: every member shares package-level dependencies.
101
+ for (const file of members.get(pkg.ImportPath) ?? []) {
102
+ if (file !== anchor)
103
+ result.edges.push({ from: anchor, to: file, kind: "import" }, { from: file, to: anchor, kind: "import" });
104
+ }
105
+ for (const imported of [...(pkg.Imports ?? []), ...(pkg.TestImports ?? []), ...(pkg.XTestImports ?? [])]) {
106
+ const target = anchors.get(imported);
107
+ if (target && target !== anchor)
108
+ result.edges.push({ from: anchor, to: target, kind: "import" });
109
+ }
110
+ }
111
+ if (!result.sourcePaths.length)
112
+ result.blockers.push("Go analysis found no local packages");
113
+ // Ignored files/build tags, generation and testdata can change the runnable universe.
114
+ const modeled = new Set([...result.sourcePaths, ...result.assetPaths]);
115
+ if (context.files.some((file) => file.endsWith(".go") && !modeled.has(file)))
116
+ result.blockers.push("Go files outside the active build context require full validation");
117
+ return result;
118
+ }
119
+ export const goAdapter = {
120
+ id: "go", version: "1", kind: "language",
121
+ detect: ({ files }) => files.some((file) => file === "go.mod" || file.endsWith(".go")),
122
+ analyze(context) {
123
+ const failure = contribution(this);
124
+ if (!context.files.includes("go.mod") || context.files.some((f) => f === "go.work" || f.endsWith("/go.mod"))) {
125
+ failure.blockers.push("Go support requires one root go.mod; workspaces and nested modules require full validation");
126
+ return failure;
127
+ }
128
+ try {
129
+ const env = { ...process.env, GOTOOLCHAIN: "local", GOPROXY: "off", GOSUMDB: "off", GOWORK: "off" };
130
+ const buildEnv = JSON.parse(execFileSync("go", ["env", "-json", "GOOS", "GOARCH", "CGO_ENABLED", "GOFLAGS"], {
131
+ cwd: context.repoPath, encoding: "utf8", timeout: 10_000, maxBuffer: 1024 * 1024,
132
+ windowsHide: true, env, stdio: ["ignore", "pipe", "pipe"],
133
+ }));
134
+ if (["GOOS", "GOARCH", "CGO_ENABLED", "GOFLAGS"].some((key) => typeof buildEnv[key] !== "string") || buildEnv.GOFLAGS.trim()) {
135
+ failure.blockers.push("Go custom build flags or incomplete build context require full validation");
136
+ return failure;
137
+ }
138
+ // No repository code is executed and module manifests must not be modified.
139
+ const output = execFileSync("go", ["list", "-mod=readonly", "-deps", "-test", "-json", "./..."], {
140
+ cwd: context.repoPath, encoding: "utf8", timeout: 60_000, maxBuffer: 64 * 1024 * 1024,
141
+ windowsHide: true,
142
+ env,
143
+ stdio: ["ignore", "pipe", "pipe"],
144
+ });
145
+ const result = analyzeGoMetadata(context, output);
146
+ result.executionEnv = { GOOS: buildEnv.GOOS, GOARCH: buildEnv.GOARCH, CGO_ENABLED: buildEnv.CGO_ENABLED, GOFLAGS: "" };
147
+ return result;
148
+ }
149
+ catch {
150
+ failure.blockers.push("Go metadata unavailable: install Go and repository dependencies before analysis (go list must succeed offline)");
151
+ return failure;
152
+ }
153
+ },
154
+ };
@@ -0,0 +1,23 @@
1
+ import { readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { goAdapter } from "./go.js";
4
+ import { vueAdapter } from "./vue.js";
5
+ export const REPOSITORY_ADAPTERS = [vueAdapter, goAdapter];
6
+ /** Never follows symlinks or scans dependency/build output directories. */
7
+ export function adapterFiles(root, exclusions = []) {
8
+ const ignored = new Set([".git", "node_modules", "vendor", ".next", "dist", "build", "coverage", "tmp", "temp"]);
9
+ const files = [];
10
+ function walk(dir, prefix) {
11
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
12
+ const path = prefix ? `${prefix}/${entry.name}` : entry.name;
13
+ if (exclusions.some((excluded) => path === excluded || path.startsWith(`${excluded}/`)))
14
+ continue;
15
+ if (entry.isDirectory() && !ignored.has(entry.name))
16
+ walk(join(dir, entry.name), path);
17
+ else if (entry.isFile())
18
+ files.push(path);
19
+ }
20
+ }
21
+ walk(root, "");
22
+ return files.sort();
23
+ }
@@ -0,0 +1,3 @@
1
+ export function contribution(adapter) {
2
+ return { id: adapter.id, version: adapter.version, sourcePaths: [], assetPaths: [], edges: [], virtualSources: [], testFiles: [], testPackages: {}, blockers: [] };
3
+ }
@@ -0,0 +1,63 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { compileScript, compileTemplate, parse } from "@vue/compiler-sfc";
4
+ import { contribution } from "./types.js";
5
+ /** Explicit Vue SFC imports. Runtime component registries and preprocessors require full CI. */
6
+ export const vueAdapter = {
7
+ id: "vue", version: "1", kind: "framework",
8
+ detect: ({ files }) => files.some((file) => file.endsWith(".vue")),
9
+ analyze(context) {
10
+ const result = contribution(this);
11
+ const dependencies = [...context.profile.packageJson.dependencies, ...context.profile.packageJson.devDependencies];
12
+ if (dependencies.includes("nuxt") || context.files.some((file) => /(?:^|\/)nuxt\.config\./.test(file))) {
13
+ result.blockers.push("Nuxt implicit routes and auto-imports require a dedicated framework adapter");
14
+ }
15
+ for (const path of context.files.filter((file) => file.endsWith(".vue"))) {
16
+ result.sourcePaths.push(path);
17
+ const block = (reason) => result.blockers.push(`Vue ${path}: ${reason}`);
18
+ try {
19
+ const { descriptor, errors } = parse(readFileSync(join(context.repoPath, path), "utf8"), { filename: path });
20
+ if (errors.length)
21
+ block("component parse failed");
22
+ if (descriptor.customBlocks.length)
23
+ block("custom blocks require a framework plugin");
24
+ const blocks = [descriptor.script, descriptor.scriptSetup, descriptor.template, ...descriptor.styles].filter((b) => b !== null);
25
+ for (const b of blocks) {
26
+ if (b.src)
27
+ block("external SFC blocks are not yet modeled");
28
+ if (b.lang && !["js", "ts", "jsx", "tsx", "html", "css"].includes(b.lang))
29
+ block(`unsupported preprocessor ${b.lang}`);
30
+ }
31
+ const script = descriptor.script || descriptor.scriptSetup
32
+ ? compileScript(descriptor, { id: path }) : undefined;
33
+ let source = script?.content ?? "";
34
+ if (/\bimport\.meta\.glob(?:Eager)?\s*\(/.test(source))
35
+ block("glob imports require bundler dependency expansion");
36
+ if (descriptor.template && !descriptor.template.src && !descriptor.template.lang) {
37
+ const template = compileTemplate({
38
+ source: descriptor.template.content, filename: path, id: path,
39
+ compilerOptions: { bindingMetadata: script?.bindings },
40
+ });
41
+ if (template.errors.length)
42
+ block("template compilation failed");
43
+ // These calls represent dependencies supplied at runtime, outside the import graph.
44
+ if (/\b_resolve(?:DynamicComponent|Component|Directive)\s*\(/.test(template.code))
45
+ block("runtime component/directive resolution requires full validation");
46
+ source += `\n${template.code}`;
47
+ }
48
+ else if (descriptor.template)
49
+ block("external or preprocessed template requires full validation");
50
+ for (const style of descriptor.styles) {
51
+ // CSS imports/URLs may be rewritten by arbitrary bundler plugins. Do not guess.
52
+ if (/@import\b|url\s*\(/i.test(style.content))
53
+ block("style imports or URLs require full validation");
54
+ }
55
+ result.virtualSources.push({ path, source });
56
+ }
57
+ catch {
58
+ block("component could not be analyzed");
59
+ }
60
+ }
61
+ return result;
62
+ },
63
+ };