@diffci.com/diffci 0.1.2 → 0.1.4
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/COMMERCIAL.md +35 -0
- package/LICENSE +671 -0
- package/README.md +76 -23
- package/SECURITY.md +61 -0
- package/SUPPORT.md +47 -0
- package/dist-client/src/client/observe.js +6 -5
- package/dist-client/src/client/workflow-guard.js +4 -0
- package/dist-client/src/planner/test-command.js +25 -1
- package/dist-client/src/repo/adapters/go.js +154 -0
- package/dist-client/src/repo/adapters/index.js +23 -0
- package/dist-client/src/repo/adapters/types.js +3 -0
- package/dist-client/src/repo/adapters/vue.js +63 -0
- package/dist-client/src/repo/graph.js +82 -21
- package/dist-client/src/repo/impact.js +11 -2
- package/dist-client/src/repo/test-discovery.js +6 -0
- package/docs/distribution.md +15 -4
- package/docs/language-support.md +70 -0
- package/package.json +13 -7
|
@@ -3,6 +3,7 @@ import { createTestFileMatcher, DEFAULT_TEST_FILE_MATCHER, testFileMatcherForPro
|
|
|
3
3
|
import { isBuiltin } from "node:module";
|
|
4
4
|
import { dirname, extname, join, normalize, relative, resolve, sep } from "node:path";
|
|
5
5
|
import ts from "typescript";
|
|
6
|
+
import { adapterFiles, REPOSITORY_ADAPTERS } from "./adapters/index.js";
|
|
6
7
|
import { analyzeRepository } from "./analyzer.js";
|
|
7
8
|
const SOURCE_EXTENSIONS = new Set([
|
|
8
9
|
".ts",
|
|
@@ -13,6 +14,7 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
13
14
|
".cjs",
|
|
14
15
|
".mts",
|
|
15
16
|
".cts",
|
|
17
|
+
".vue",
|
|
16
18
|
]);
|
|
17
19
|
const ASSET_EXTENSIONS = new Set([
|
|
18
20
|
".css",
|
|
@@ -323,7 +325,7 @@ export function discoverNestedTsconfigPaths(repoPath) {
|
|
|
323
325
|
// merged compiler options reproducible across filesystems (readdir order is OS-dependent).
|
|
324
326
|
return found.sort();
|
|
325
327
|
}
|
|
326
|
-
function createProgram(repoPath, fallbackSourceRoots = []) {
|
|
328
|
+
function createProgram(repoPath, fallbackSourceRoots = [], additionalSources = []) {
|
|
327
329
|
// Phase 01 F5 (2026-08-26). This was `ts.findConfigFile(repoPath, ...)`, which starts at repoPath
|
|
328
330
|
// and walks UP - so a repository cloned beneath any directory containing a tsconfig.json was
|
|
329
331
|
// silently analysed against that ANCESTOR's project instead of its own. Flagged as a known latent
|
|
@@ -440,6 +442,10 @@ function createProgram(repoPath, fallbackSourceRoots = []) {
|
|
|
440
442
|
options = { ...options, allowJs: true };
|
|
441
443
|
}
|
|
442
444
|
}
|
|
445
|
+
if (additionalSources.length) {
|
|
446
|
+
fileNames = [...new Set([...fileNames, ...additionalSources])];
|
|
447
|
+
options = { module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler, target: ts.ScriptTarget.ES2022, ...options, allowJs: true };
|
|
448
|
+
}
|
|
443
449
|
const program = ts.createProgram({
|
|
444
450
|
rootNames: fileNames,
|
|
445
451
|
options,
|
|
@@ -523,12 +529,44 @@ export async function buildDependencyGraph(options = {}) {
|
|
|
523
529
|
const start = process.hrtime.bigint();
|
|
524
530
|
const repoPath = options.repoPath ? resolve(options.repoPath) : process.cwd();
|
|
525
531
|
const profile = analyzeRepository(options);
|
|
532
|
+
const files = adapterFiles(repoPath, options.excludeDirs);
|
|
533
|
+
const context = { repoPath, files, profile };
|
|
534
|
+
const contributions = REPOSITORY_ADAPTERS.filter((adapter) => adapter.detect(context)).map((adapter) => adapter.analyze(context));
|
|
535
|
+
const adapterBlockers = contributions.flatMap((item) => item.blockers);
|
|
536
|
+
if (contributions.some((item) => item.id === "go") && files.some((file) => /\.(?:[cm]?[jt]sx?|vue)$/.test(file))) {
|
|
537
|
+
adapterBlockers.push("Mixed Go/JavaScript repositories require explicit cross-language dependencies; full validation required");
|
|
538
|
+
}
|
|
539
|
+
if (contributions.length && files.some((file) => /\.(?:py|rs|java|kt|cs|svelte|astro)$/.test(file))) {
|
|
540
|
+
adapterBlockers.push("Unmodeled languages alongside an adapter require full validation");
|
|
541
|
+
}
|
|
542
|
+
profile.adapters = contributions.map(({ id, version, blockers }) => ({ id, version, blockers }));
|
|
543
|
+
profile.goTestPackages = Object.assign({}, ...contributions.map((item) => item.testPackages));
|
|
544
|
+
profile.goTestEnvironment = contributions.find((item) => item.id === "go")?.executionEnv;
|
|
545
|
+
const adapterTests = contributions.flatMap((item) => item.testFiles);
|
|
546
|
+
profile.testFilePaths = [...new Set([...profile.testFilePaths, ...adapterTests])].sort();
|
|
547
|
+
if (profile.testUniverse) {
|
|
548
|
+
profile.testUniverse.discoveredTestFiles = profile.testFilePaths.length;
|
|
549
|
+
profile.testUniverse.blindSpot = (profile.testUniverse.declaredFrameworks.length > 0 || contributions.some((item) => item.id === "go")) && profile.testFilePaths.length === 0;
|
|
550
|
+
}
|
|
526
551
|
const entryPointPaths = new Set(profile.entryPoints.map((e) => e.path));
|
|
527
|
-
|
|
552
|
+
const vueSources = contributions.some((item) => item.id === "vue")
|
|
553
|
+
? files.filter((file) => /\.[cm]?[jt]sx?$/.test(file)).map((file) => join(repoPath, file)) : [];
|
|
554
|
+
// Compiler include/exclude controls typechecking, not the runner's test universe. Parse every
|
|
555
|
+
// discovered JS/TS test so source changes can reach tests outside the compiler's root files.
|
|
556
|
+
// Adding those tests only as leaf nodes silently loses their dependency edges (ky, 2026-09-19).
|
|
557
|
+
const testSources = profile.testFilePaths
|
|
558
|
+
.filter((file) => /\.[cm]?[jt]sx?$/.test(file))
|
|
559
|
+
.map((file) => join(repoPath, file));
|
|
560
|
+
let { program, options: compilerOptions, resolvedViaProjectReferences } = createProgram(repoPath, profile.sourceRoots, [...vueSources, ...testSources]);
|
|
528
561
|
let moduleResolutionCache = ts.createModuleResolutionCache(repoPath, (x) => x, compilerOptions);
|
|
529
562
|
const sourceFiles = program
|
|
530
563
|
.getSourceFiles()
|
|
531
564
|
.filter((sf) => sf.fileName && !sf.fileName.endsWith(".d.ts"));
|
|
565
|
+
for (const item of contributions) {
|
|
566
|
+
for (const virtual of item.virtualSources) {
|
|
567
|
+
sourceFiles.push(ts.createSourceFile(join(repoPath, virtual.path), virtual.source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
532
570
|
const internalSourcePaths = new Set();
|
|
533
571
|
for (const sf of sourceFiles) {
|
|
534
572
|
const rel = toRelativeInternal(repoPath, sf.fileName);
|
|
@@ -536,11 +574,14 @@ export async function buildDependencyGraph(options = {}) {
|
|
|
536
574
|
internalSourcePaths.add(rel);
|
|
537
575
|
}
|
|
538
576
|
}
|
|
539
|
-
const edges =
|
|
577
|
+
const edges = contributions.flatMap((item) => item.edges);
|
|
540
578
|
const unresolved = [];
|
|
541
579
|
const references = [];
|
|
542
|
-
const assetPaths = new Set();
|
|
543
|
-
const
|
|
580
|
+
const assetPaths = new Set(contributions.flatMap((item) => item.assetPaths));
|
|
581
|
+
for (const item of contributions)
|
|
582
|
+
for (const path of item.sourcePaths)
|
|
583
|
+
internalSourcePaths.add(path);
|
|
584
|
+
const edgeKeys = new Set(edges.map((edge) => `${edge.from}|${edge.to}|${edge.kind}`));
|
|
544
585
|
function addEdge(from, to, kind) {
|
|
545
586
|
const key = `${from}|${to}|${kind}`;
|
|
546
587
|
if (edgeKeys.has(key))
|
|
@@ -590,6 +631,20 @@ export async function buildDependencyGraph(options = {}) {
|
|
|
590
631
|
}
|
|
591
632
|
const specifierCategory = classifySpecifier(ref.specifier);
|
|
592
633
|
const resolvableSpecifier = specifierCategory === "relative" || specifierCategory === "absolute" || specifierCategory === "alias" ? stripImportQuery(ref.specifier) : ref.specifier;
|
|
634
|
+
// TypeScript may resolve a Vue import to a declaration shim. Preserve the actual SFC edge.
|
|
635
|
+
if (resolvableSpecifier.endsWith(".vue")) {
|
|
636
|
+
const compilerAliases = Object.entries(compilerOptions.paths ?? {}).map(([pattern, substitutions]) => ({ pattern, substitutions }));
|
|
637
|
+
const candidate = findAssetCandidate(sf.fileName, resolvableSpecifier, compilerAliases.length ? compilerAliases : profile.pathAliases, compilerOptions.baseUrl ?? repoPath);
|
|
638
|
+
const target = candidate && toRelativeInternal(repoPath, candidate);
|
|
639
|
+
if (target && internalSourcePaths.has(target)) {
|
|
640
|
+
addEdge(importerRel, target, ref.kind);
|
|
641
|
+
recordReference("internal-source", importerRel, ref);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
recordUnresolved(importerRel, ref, "Vue component not found in analyzed source inventory");
|
|
645
|
+
recordReference("unresolved", importerRel, ref);
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
593
648
|
const resolution = ts.resolveModuleName(resolvableSpecifier, sf.fileName, compilerOptions, ts.sys, moduleResolutionCache);
|
|
594
649
|
if (!resolution.resolvedModule || !resolution.resolvedModule.resolvedFileName) {
|
|
595
650
|
const category = classifySpecifier(ref.specifier);
|
|
@@ -637,21 +692,12 @@ export async function buildDependencyGraph(options = {}) {
|
|
|
637
692
|
}
|
|
638
693
|
}
|
|
639
694
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
// `profile.testFilePaths` (the separate, tsconfig-agnostic glob walk in analyzer.ts's discoverTests())
|
|
647
|
-
// already found them correctly. Source-ROOT discovery itself was already correct (the 2026-08-21
|
|
648
|
-
// zod/trpc fallback already lists `packages`/`crates` as roots for exactly this monorepo shape) - the
|
|
649
|
-
// gap was narrower: the graph never incorporated what that walk found. Fix: union in any test file
|
|
650
|
-
// discoverTests() found that the TS program's own file list missed, as an ADDITIONAL leaf node
|
|
651
|
-
// (isTest true; no import edges - we have no real resolution info for a file the type-checker was
|
|
652
|
-
// never asked to see, so dependency-graph traversal through it is honestly absent, not guessed at).
|
|
653
|
-
// This does NOT add Rust visibility of any kind - testFilePaths only ever contains files already
|
|
654
|
-
// matched by the JS/TS test-file patterns; a `.rs` test is never in it and stays "unknown" as before.
|
|
695
|
+
if (unresolved.some((ref) => ref.importer.endsWith(".vue") || stripImportQuery(ref.specifier).endsWith(".vue"))) {
|
|
696
|
+
adapterBlockers.push("Unresolved Vue dependencies require full validation");
|
|
697
|
+
}
|
|
698
|
+
profile.adapterBlockers = [...adapterBlockers];
|
|
699
|
+
// Keep adapter-provided test identities visible as well. JS/TS tests are parsed above; their
|
|
700
|
+
// imports must not be replaced by disconnected leaf nodes merely because tsconfig excludes them.
|
|
655
701
|
for (const testPath of profile.testFilePaths) {
|
|
656
702
|
if (!internalSourcePaths.has(testPath) && !assetPaths.has(testPath))
|
|
657
703
|
internalSourcePaths.add(testPath);
|
|
@@ -679,7 +725,7 @@ export async function buildDependencyGraph(options = {}) {
|
|
|
679
725
|
profile.stats.testFiles = graph.nodes.filter((n) => n.isTest).length;
|
|
680
726
|
const integrity = validateDependencyGraph(graph);
|
|
681
727
|
const dynamicUnresolvedCount = unresolved.filter((u) => u.dynamic).length;
|
|
682
|
-
const confidence = computeConfidence(unresolved.length, dynamicUnresolvedCount, integrity.criticalCount, profile.stats.sourceFiles, resolvedViaProjectReferences);
|
|
728
|
+
const confidence = adapterBlockers.length ? "UNSAFE" : computeConfidence(unresolved.length, dynamicUnresolvedCount, integrity.criticalCount, profile.stats.sourceFiles, resolvedViaProjectReferences);
|
|
683
729
|
const internalAssetEdges = edges.filter((e) => e.kind === "asset").length;
|
|
684
730
|
const externalReferences = references.filter((r) => r.resolution === "external-package").length;
|
|
685
731
|
const platformBuiltinReferences = references.filter((r) => r.resolution === "platform-builtin").length;
|
|
@@ -700,6 +746,7 @@ export async function buildDependencyGraph(options = {}) {
|
|
|
700
746
|
};
|
|
701
747
|
return {
|
|
702
748
|
graph,
|
|
749
|
+
adapterBlockers,
|
|
703
750
|
profile,
|
|
704
751
|
unresolved,
|
|
705
752
|
references,
|
|
@@ -713,6 +760,18 @@ export async function buildDependencyGraph(options = {}) {
|
|
|
713
760
|
resolvedViaProjectReferences,
|
|
714
761
|
};
|
|
715
762
|
}
|
|
763
|
+
/** Product eligibility includes native adapters; legacy TS corpus classification stays separate. */
|
|
764
|
+
export function classifyRepositoryProject(repoPath) {
|
|
765
|
+
const typescript = classifyTypeScriptProject(repoPath);
|
|
766
|
+
if (typescript.capable)
|
|
767
|
+
return typescript;
|
|
768
|
+
const files = adapterFiles(repoPath);
|
|
769
|
+
if (files.includes("go.mod"))
|
|
770
|
+
return { capable: true, reason: "Go module (package-level analysis)" };
|
|
771
|
+
if (files.some((file) => file.endsWith(".vue")))
|
|
772
|
+
return { capable: true, reason: "Vue single-file components" };
|
|
773
|
+
return { capable: false, reason: "No TypeScript project, Vue components, or root Go module found" };
|
|
774
|
+
}
|
|
716
775
|
/**
|
|
717
776
|
* Can DiffCI build a dependency graph for this repository? (Phase 01 F3, 2026-08-26.)
|
|
718
777
|
*
|
|
@@ -899,6 +958,8 @@ function computeConfidence(unresolvedCount, dynamicUnresolvedCount, integrityCri
|
|
|
899
958
|
* narrowing only applies when unresolved/dynamic-unresolved imports are the actual reason, never when
|
|
900
959
|
* sourceFileCount or integrity triggered it. */
|
|
901
960
|
export function refineConfidenceForDelta(result, changedFiles) {
|
|
961
|
+
if (result.adapterBlockers?.length)
|
|
962
|
+
return "UNSAFE";
|
|
902
963
|
// Anything that was never UNSAFE in the first place passes through untouched - there is nothing to
|
|
903
964
|
// narrow, and re-deriving sourceFileCount/integrity from raw fields here (rather than trusting the
|
|
904
965
|
// already-computed confidence) would be both redundant and a real correctness risk: a caller's
|
|
@@ -3,7 +3,7 @@ import { refineConfidenceForDelta } from "./graph.js";
|
|
|
3
3
|
import { repositoryLayout, UNKNOWN_REPOSITORY_LAYOUT } from "./layout.js";
|
|
4
4
|
import { DEFAULT_TEST_FILE_MATCHER, matchesGlob as matchesTestGlob, testFileMatcherForProfile } from "./test-discovery.js";
|
|
5
5
|
import { resolveTestFixtureOwners } from "./test-fixture-ownership.js";
|
|
6
|
-
const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"]);
|
|
6
|
+
const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue", ".go"]);
|
|
7
7
|
const ASSET_EXTENSIONS = new Set([".css", ".scss", ".sass", ".less", ".json", ".jsonc", ".svg", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".bmp", ".woff", ".woff2", ".ttf", ".otf", ".eot", ".wasm", ".md", ".txt"]);
|
|
8
8
|
const NEXT_ENTRY_NAMES = new Set(["page", "layout", "route", "api", "loading", "error", "template", "not-found", "middleware", "generatemetadata", "generatestaticparams"]);
|
|
9
9
|
function isSourceFilePath(filePath) { return SOURCE_EXTENSIONS.has(extname(filePath).toLowerCase()); }
|
|
@@ -18,6 +18,10 @@ function isDocumentationFile(filePath, layout) { return layout.isDocumentationPa
|
|
|
18
18
|
function isConfigFile(filePath) {
|
|
19
19
|
const CONFIG_FILE_NAMES = new Set(["package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb", "bun.lock", "tsconfig.json", "tsconfig.base.json", "tsconfig.build.json", "jsconfig.json"]);
|
|
20
20
|
const base = posix.basename(filePath);
|
|
21
|
+
if (["go.mod", "go.sum", "go.work", "go.work.sum"].includes(base))
|
|
22
|
+
return true;
|
|
23
|
+
if (/^(?:vite|vue|nuxt)\.config\./.test(base))
|
|
24
|
+
return true;
|
|
21
25
|
if (CONFIG_FILE_NAMES.has(base))
|
|
22
26
|
return true;
|
|
23
27
|
if (base.startsWith("next.config"))
|
|
@@ -265,7 +269,12 @@ export class ImpactAnalyzer {
|
|
|
265
269
|
const changedImpacts = delta.files.map((file) => ({ file, category: classifyChangedFile(file, this.isTestFile, this.layout, options.repositoryFiles), reasons: changedFileReasons(file) }));
|
|
266
270
|
const evidence = [];
|
|
267
271
|
const riskSignals = [];
|
|
268
|
-
const fallbackReasons = [];
|
|
272
|
+
const fallbackReasons = [...(graphResult.adapterBlockers ?? [])];
|
|
273
|
+
for (const file of delta.files) {
|
|
274
|
+
if (allChangePaths(file).some((path) => /(?:^|\/)(?:go\.(?:mod|sum|work)|go\.work\.sum|(?:vite|vue|nuxt)\.config\.[^/]+)$/.test(path))) {
|
|
275
|
+
fallbackReasons.push(`Language/framework configuration changed: ${file.path}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
269
278
|
this.applyGlobalRiskRules(delta, riskSignals, fallbackReasons);
|
|
270
279
|
// Stage 1B fix (2026-08-21, docs/research/2026-08-21-stage1b-*.md): refine the graph's raw,
|
|
271
280
|
// delta-independent confidence to THIS delta's changed files - see refineConfidenceForDelta()'s doc
|
|
@@ -370,6 +370,12 @@ export function createTestFileMatcher(patterns, options = {}) {
|
|
|
370
370
|
/** The one place that turns a profile into a matcher, so analyzer discovery, graph node flags and
|
|
371
371
|
* impact classification cannot disagree about what a test is. */
|
|
372
372
|
export function testFileMatcherForProfile(profile) {
|
|
373
|
+
if (profile.goTestPackages && Object.keys(profile.goTestPackages).length) {
|
|
374
|
+
const jsMatcher = testFileMatcherForProfile({ ...profile, goTestPackages: undefined });
|
|
375
|
+
const matcher = ((path) => Object.hasOwn(profile.goTestPackages, path) || jsMatcher(path));
|
|
376
|
+
Object.defineProperty(matcher, "patterns", { value: jsMatcher.patterns });
|
|
377
|
+
return matcher;
|
|
378
|
+
}
|
|
373
379
|
if (!profile.testPatterns)
|
|
374
380
|
return DEFAULT_TEST_FILE_MATCHER;
|
|
375
381
|
return createTestFileMatcher(profile.testPatterns, {
|
package/docs/distribution.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
DiffCI has three install surfaces with the same initial contract: observe CI, write a report, and do
|
|
4
4
|
not change what the host repository runs.
|
|
5
5
|
|
|
6
|
+
The product model is open core. The npm CLI and basic GitHub Action are the open-source adoption path;
|
|
7
|
+
DiffCI Cloud adds hosted history, organization dashboards, policies, managed operations, and support.
|
|
8
|
+
See [`open-core-packaging.md`](open-core-packaging.md).
|
|
9
|
+
|
|
6
10
|
## GitHub App
|
|
7
11
|
|
|
8
12
|
The DiffCI Shadow GitHub App is the lowest-friction research and design-partner path. It receives
|
|
@@ -25,10 +29,10 @@ jobs:
|
|
|
25
29
|
- uses: actions/checkout@v4
|
|
26
30
|
with:
|
|
27
31
|
fetch-depth: 0
|
|
28
|
-
- uses: DiffCI/DiffCI.com@
|
|
32
|
+
- uses: DiffCI/DiffCI.com@v0.1.4
|
|
29
33
|
```
|
|
30
34
|
|
|
31
|
-
For the strongest supply-chain posture, pin the Action to a full commit SHA. `npx @diffci.com/diffci verify-workflow`
|
|
35
|
+
For the strongest supply-chain posture, pin the Action to a full commit SHA. `npx @diffci.com/diffci@latest verify-workflow`
|
|
32
36
|
checks that the job is dedicated, read-only, not required by other jobs, and unable to alter the rest
|
|
33
37
|
of CI.
|
|
34
38
|
|
|
@@ -37,8 +41,8 @@ of CI.
|
|
|
37
41
|
The CLI is the standalone npm package surface. The package name is `@diffci.com/diffci`:
|
|
38
42
|
|
|
39
43
|
```bash
|
|
40
|
-
npx @diffci.com/diffci observe
|
|
41
|
-
npx @diffci.com/diffci verify-workflow
|
|
44
|
+
npx @diffci.com/diffci@latest observe
|
|
45
|
+
npx @diffci.com/diffci@latest verify-workflow
|
|
42
46
|
```
|
|
43
47
|
|
|
44
48
|
`observe` writes a JSON report outside the checkout by default. It never runs, skips, cancels, or
|
|
@@ -79,3 +83,10 @@ brew install diffci
|
|
|
79
83
|
|
|
80
84
|
Those should ship only after the npm CLI and Action have signed releases, provenance, pinned build
|
|
81
85
|
workflows, and repeatable package verification.
|
|
86
|
+
|
|
87
|
+
## Supported OSS Channel
|
|
88
|
+
|
|
89
|
+
Tidelift belongs to the open-source package channel. It can provide maintenance, security, license, and
|
|
90
|
+
supply-chain assurance for the npm package without requiring a hosted DiffCI account. It should support
|
|
91
|
+
the OSS core rather than define a separate feature tier. The readiness checklist lives in
|
|
92
|
+
[`tidelift-package-support.md`](tidelift-package-support.md).
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Language and framework support
|
|
2
|
+
|
|
3
|
+
DiffCI now has an extensible repository-adapter boundary and initial Vue and Go support.
|
|
4
|
+
These additions propose selections in the existing observer; they do not enable production CI skipping.
|
|
5
|
+
|
|
6
|
+
| Surface | Implemented scope | Boundaries |
|
|
7
|
+
| --- | --- | --- |
|
|
8
|
+
| JavaScript / TypeScript | Existing dependency analysis and ten existing test-runner command mappings | Runner recognition is not a guarantee of complete framework semantics |
|
|
9
|
+
| Vue | SFC parsing with `@vue/compiler-sfc`; script imports, nested components, compiled template asset imports; propagation into importing JS/TS tests | Runtime component/directive resolution, preprocessors, external/custom SFC blocks, style URLs/imports, Nuxt conventions and glob imports require full validation |
|
|
10
|
+
| Go | One root module; native `go list` metadata; package-level transitive test selection, external test packages, embedded files; `go test -json` commands and result parsing | Workspaces/nested modules, inactive Go files, cgo/native objects, runtime plugins, generation/linkname, external local replacements and incomplete metadata require full validation |
|
|
11
|
+
| Mixed Go and JS/TS | Detected | Full validation until cross-language relationships are declared and modeled |
|
|
12
|
+
| Python, Svelte, Astro, Java/Kotlin, C#, Rust | No new semantic support in this release | Require additional adapters and qualification |
|
|
13
|
+
|
|
14
|
+
## Using the observer
|
|
15
|
+
|
|
16
|
+
Use the existing `observe` command and commit range options. The observer now admits a root
|
|
17
|
+
`go.mod` or Vue SFCs as well as a TypeScript project. Vue projects without a tsconfig have their
|
|
18
|
+
JS/TS files parsed alongside their components. Existing TS-only corpus eligibility remains separate:
|
|
19
|
+
this release does not silently enroll Go repositories in historical JS/TS research cohorts.
|
|
20
|
+
|
|
21
|
+
For Go, install a compatible Go toolchain on the **same host and build environment** used for analysis
|
|
22
|
+
and testing, and prepare the repository's dependencies first using its normal setup procedure.
|
|
23
|
+
DiffCI invokes `go list -mod=readonly -deps -test -json ./...` with `GOTOOLCHAIN=local`, `GOPROXY=off`,
|
|
24
|
+
`GOSUMDB=off`, and `GOWORK=off`. Missing toolchains or dependencies produce refusal/full fallback.
|
|
25
|
+
Go may populate its own build cache but must not modify the checkout. It does not run `go generate`.
|
|
26
|
+
Custom `GOFLAGS` currently require full validation. Discovered GOOS, GOARCH and CGO_ENABLED settings
|
|
27
|
+
are carried into the structured test command.
|
|
28
|
+
|
|
29
|
+
A selection such as `lib/value_test.go` becomes:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
go test -mod=readonly -json -count=1 ./lib
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
It runs the entire package. Consumers must honor the emitted `CommandSpec.env`, preserve the build
|
|
36
|
+
context, and check fallback/command-synthesis status before execution. Commands are not portable
|
|
37
|
+
between different GOOS/GOARCH/GOFLAGS contexts. The controlled runner command policies accept `go`;
|
|
38
|
+
hosted images still need a Go installation before Go workloads can run. No deployment is performed
|
|
39
|
+
by these code changes.
|
|
40
|
+
|
|
41
|
+
Go result parsing counts failing **packages**, records failing test names, and rejects incomplete
|
|
42
|
+
JSON streams. It does not invent test-file counts from package counts, so file-based economics
|
|
43
|
+
qualification may still report an unsupported denominator for Go.
|
|
44
|
+
|
|
45
|
+
## Extension points
|
|
46
|
+
|
|
47
|
+
- `src/repo/adapters/types.ts`: adapter context and graph contribution contract.
|
|
48
|
+
- `src/repo/adapters/index.ts`: framework/language adapter registry and bounded file inventory.
|
|
49
|
+
- `src/repo/adapters/vue.ts`: Vue compiler integration.
|
|
50
|
+
- `src/repo/adapters/go.ts`: native Go package metadata and conservative dependency model.
|
|
51
|
+
- `src/repo/adapters/go-test.ts`: structured Go outcome parsing.
|
|
52
|
+
- `src/planner/test-command.ts`: runner command routing, including verified Go package targets.
|
|
53
|
+
|
|
54
|
+
Adapters contribute source and asset nodes, dependency edges, virtual JS/TS source, runnable test
|
|
55
|
+
identities and explicit blockers. Blockers persist into the profile and graph result and cannot be
|
|
56
|
+
removed by delta-specific reachability refinement. Go graphs bypass the disk cache until every cache
|
|
57
|
+
caller provides a complete toolchain/build-context identity. The cache schema was bumped to prevent
|
|
58
|
+
reuse of graphs created before adapter support.
|
|
59
|
+
|
|
60
|
+
## Validation and qualification
|
|
61
|
+
|
|
62
|
+
`tests/repo/language-adapters.test.ts` covers transitive Vue selection, unsupported-feature fallbacks,
|
|
63
|
+
Go package imports and embeds, metadata failures, command routing, and incomplete output.
|
|
64
|
+
When Go is on PATH, its native integration test runs the observer against a real Git fixture,
|
|
65
|
+
compares successful full/subset test runs, and verifies the subset catches an introduced fault.
|
|
66
|
+
Without Go, that native test is explicitly skipped.
|
|
67
|
+
|
|
68
|
+
These are implementation checks, not prospective customer safety or savings evidence. Each new
|
|
69
|
+
ecosystem still needs real repository shadow observations and economic qualification before broader
|
|
70
|
+
support or savings claims are made.
|
package/package.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diffci.com/diffci",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "DiffCI - deterministic change-aware CI planning",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/DiffCI/DiffCI.com.git"
|
|
9
|
+
},
|
|
5
10
|
"private": false,
|
|
6
11
|
"type": "module",
|
|
7
12
|
"engines": {
|
|
@@ -18,7 +23,10 @@
|
|
|
18
23
|
"dist-client/src/repo",
|
|
19
24
|
"README.md",
|
|
20
25
|
"docs/distribution.md",
|
|
21
|
-
"docs/language-support.md"
|
|
26
|
+
"docs/language-support.md",
|
|
27
|
+
"SECURITY.md",
|
|
28
|
+
"SUPPORT.md",
|
|
29
|
+
"COMMERCIAL.md"
|
|
22
30
|
],
|
|
23
31
|
"exports": {
|
|
24
32
|
"./*": {
|
|
@@ -90,10 +98,12 @@
|
|
|
90
98
|
"qualify:universe": "tsx scripts/qualify-universe.ts",
|
|
91
99
|
"corpus:register": "tsx scripts/register-corpus-entry.ts",
|
|
92
100
|
"ci:benchmark": "tsx scripts/ci-inference-benchmark.ts",
|
|
93
|
-
"ci:reproduce": "tsx scripts/ci-reproduction.ts"
|
|
101
|
+
"ci:reproduce": "tsx scripts/ci-reproduction.ts",
|
|
102
|
+
"check:oss-boundary": "node scripts/check-oss-boundary.mjs"
|
|
94
103
|
},
|
|
95
104
|
"dependencies": {
|
|
96
105
|
"@types/node": "^22.15.12",
|
|
106
|
+
"@vue/compiler-sfc": "3.5.42",
|
|
97
107
|
"typescript": "^5.8.3",
|
|
98
108
|
"yaml": "^2.9.0"
|
|
99
109
|
},
|
|
@@ -114,9 +124,5 @@
|
|
|
114
124
|
"**/validate-*.test.*",
|
|
115
125
|
"scripts/**/*.test.mjs"
|
|
116
126
|
]
|
|
117
|
-
},
|
|
118
|
-
"repository": {
|
|
119
|
-
"type": "git",
|
|
120
|
-
"url": "https://github.com/DiffCI/DiffCI.com"
|
|
121
127
|
}
|
|
122
128
|
}
|