@piagent/platform 1.2.8 → 1.2.9
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/CHANGELOG.md +33 -0
- package/README.md +1 -1
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/context-engine.md +11 -1
- package/docs/distribution-standard.md +7 -7
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +1 -0
- package/docs/release-install-policy.md +6 -6
- package/docs/security-threat-model.md +2 -1
- package/docs/team-onboarding.md +2 -2
- package/docs-site/architecture.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/context-engine.html +1 -1
- package/docs-site/index.html +1 -1
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/package.json +1 -1
- package/packages/piagent-core/capabilities/capability-core.js +3 -0
- package/packages/piagent-core/extensions/context-engine.js +149 -71
- package/packages/piagent-core/extensions/context-index-policy.js +136 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +67 -67
- package/packages/piagent-core/package.json +1 -1
- package/scripts/context-engine.mjs +22 -20
- package/scripts/update-global.mjs +1 -1
- package/scripts/verify-local.sh +2 -0
|
@@ -3,6 +3,13 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
|
|
6
|
+
import {
|
|
7
|
+
contextIndexExcludeDigest,
|
|
8
|
+
contextIndexExcludePolicyVersion,
|
|
9
|
+
normalizeContextIndexExcludePatterns
|
|
10
|
+
} from "./context-index-policy.js";
|
|
11
|
+
import { matchesProtectedPath } from "./policy-core.js";
|
|
12
|
+
|
|
6
13
|
const INDEX_SCHEMA_VERSION = 2;
|
|
7
14
|
const TELEMETRY_SCHEMA_VERSION = 1;
|
|
8
15
|
const RRF_K = 60;
|
|
@@ -64,6 +71,13 @@ function clampInteger(value, fallback, min, max) {
|
|
|
64
71
|
return Math.max(min, Math.min(max, Math.trunc(number)));
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
function requireExplicitExcludePatterns(options, operation) {
|
|
75
|
+
if (!Array.isArray(options?.excludePatterns)) {
|
|
76
|
+
throw new TypeError(`${operation} requires an explicit excludePatterns array`);
|
|
77
|
+
}
|
|
78
|
+
return normalizeContextIndexExcludePatterns(options.excludePatterns);
|
|
79
|
+
}
|
|
80
|
+
|
|
67
81
|
export function estimateContextTokens(value) {
|
|
68
82
|
return Math.max(0, Math.ceil(String(value ?? "").length / 4));
|
|
69
83
|
}
|
|
@@ -184,41 +198,28 @@ function shouldIndexPath(relativePath, options) {
|
|
|
184
198
|
const segments = rel.split("/");
|
|
185
199
|
if (segments.some((segment) => IGNORED_SEGMENTS.has(segment))) return false;
|
|
186
200
|
if (SECRET_FILE_PATTERNS.some((pattern) => pattern.test(rel))) return false;
|
|
187
|
-
if ((options.excludePatterns ?? [])
|
|
201
|
+
if (matchesProtectedPath(rel, options.excludePatterns ?? [])) return false;
|
|
188
202
|
const name = path.posix.basename(rel);
|
|
189
203
|
return SOURCE_NAMES.has(name) || SOURCE_EXTENSIONS.has(path.posix.extname(name).toLowerCase());
|
|
190
204
|
}
|
|
191
205
|
|
|
192
|
-
function
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
let source = "";
|
|
196
|
-
for (let index = 0; index < normalized.length; index += 1) {
|
|
197
|
-
const char = normalized[index];
|
|
198
|
-
if (char === "*" && normalized[index + 1] === "*") {
|
|
199
|
-
if (normalized[index + 2] === "/") {
|
|
200
|
-
source += "(?:.*/)?";
|
|
201
|
-
index += 2;
|
|
202
|
-
} else {
|
|
203
|
-
source += ".*";
|
|
204
|
-
index += 1;
|
|
205
|
-
}
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
if (char === "*") {
|
|
209
|
-
source += "[^/]*";
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
if (char === "?") {
|
|
213
|
-
source += "[^/]";
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
source += /[.+^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
|
|
217
|
-
}
|
|
206
|
+
function projectFileInfo(cwd, relativePath, root = fs.realpathSync.native(cwd), directoryCache = new Map()) {
|
|
207
|
+
const expected = path.resolve(root, relativePath);
|
|
208
|
+
if (expected !== root && !expected.startsWith(`${root}${path.sep}`)) return undefined;
|
|
218
209
|
try {
|
|
219
|
-
|
|
210
|
+
const directPath = path.resolve(cwd, relativePath);
|
|
211
|
+
const directParent = path.dirname(directPath);
|
|
212
|
+
let canonicalParent = directoryCache.get(directParent);
|
|
213
|
+
if (canonicalParent === undefined) {
|
|
214
|
+
canonicalParent = fs.realpathSync.native(directParent);
|
|
215
|
+
directoryCache.set(directParent, canonicalParent);
|
|
216
|
+
}
|
|
217
|
+
if (canonicalParent !== path.dirname(expected)) return undefined;
|
|
218
|
+
const direct = fs.lstatSync(directPath);
|
|
219
|
+
if (direct.isSymbolicLink() || !direct.isFile()) return undefined;
|
|
220
|
+
return { absolute: expected, stat: direct };
|
|
220
221
|
} catch {
|
|
221
|
-
return
|
|
222
|
+
return undefined;
|
|
222
223
|
}
|
|
223
224
|
}
|
|
224
225
|
|
|
@@ -463,11 +464,14 @@ export function classifyContextTask(prompt) {
|
|
|
463
464
|
}
|
|
464
465
|
|
|
465
466
|
export async function buildContextIndexV2(cwd, options = {}) {
|
|
467
|
+
const excludePatterns = requireExplicitExcludePatterns(options, "buildContextIndexV2");
|
|
466
468
|
const startedAt = Date.now();
|
|
469
|
+
const projectRoot = fs.realpathSync.native(cwd);
|
|
470
|
+
const directoryCache = new Map();
|
|
467
471
|
const maxFiles = clampInteger(options.maxFiles, DEFAULT_MAX_FILES, 1, 50_000);
|
|
468
472
|
const maxFileBytes = clampInteger(options.maxFileBytes, DEFAULT_MAX_FILE_BYTES, 1_024, 2 * 1024 * 1024);
|
|
469
473
|
const listed = gitFileList(cwd) ?? walkedFileList(cwd, maxFiles);
|
|
470
|
-
const candidates = listed.filter((file) => shouldIndexPath(file, options)).slice(0, maxFiles).sort();
|
|
474
|
+
const candidates = listed.filter((file) => shouldIndexPath(file, { ...options, excludePatterns })).slice(0, maxFiles).sort();
|
|
471
475
|
const db = await openDatabase(cwd);
|
|
472
476
|
const existing = new Map(db.prepare("SELECT path, hash, bytes, mtime_ms FROM files").all().map((row) => [row.path, row]));
|
|
473
477
|
const current = new Set();
|
|
@@ -480,18 +484,12 @@ export async function buildContextIndexV2(cwd, options = {}) {
|
|
|
480
484
|
const skippedPaths = [];
|
|
481
485
|
|
|
482
486
|
for (const relativePath of candidates) {
|
|
483
|
-
const
|
|
484
|
-
if (!
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (stat.size > maxFileBytes) {
|
|
490
|
-
skippedLarge += 1;
|
|
491
|
-
skippedPaths.push(relativePath);
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
|
-
} catch {
|
|
487
|
+
const file = projectFileInfo(cwd, relativePath, projectRoot, directoryCache);
|
|
488
|
+
if (!file) continue;
|
|
489
|
+
const { absolute, stat } = file;
|
|
490
|
+
if (stat.size > maxFileBytes) {
|
|
491
|
+
skippedLarge += 1;
|
|
492
|
+
skippedPaths.push(relativePath);
|
|
495
493
|
continue;
|
|
496
494
|
}
|
|
497
495
|
const previous = existing.get(relativePath);
|
|
@@ -575,7 +573,9 @@ export async function buildContextIndexV2(cwd, options = {}) {
|
|
|
575
573
|
symbolCount: counts.symbols,
|
|
576
574
|
importCount: counts.imports,
|
|
577
575
|
maxFileBytes,
|
|
578
|
-
excludePatterns: JSON.stringify(
|
|
576
|
+
excludePatterns: JSON.stringify(excludePatterns),
|
|
577
|
+
excludeDigest: contextIndexExcludeDigest(excludePatterns),
|
|
578
|
+
excludePolicyVersion: contextIndexExcludePolicyVersion(),
|
|
579
579
|
skippedPaths: JSON.stringify(skippedPaths)
|
|
580
580
|
});
|
|
581
581
|
db.exec("COMMIT");
|
|
@@ -599,10 +599,26 @@ export async function buildContextIndexV2(cwd, options = {}) {
|
|
|
599
599
|
}
|
|
600
600
|
}
|
|
601
601
|
|
|
602
|
-
export async function contextIndexV2Status(cwd) {
|
|
602
|
+
export async function contextIndexV2Status(cwd, options = {}) {
|
|
603
603
|
const paths = contextEnginePaths(cwd);
|
|
604
|
+
const projectRoot = fs.realpathSync.native(cwd);
|
|
605
|
+
const directoryCache = new Map();
|
|
606
|
+
const expectedExcludePatterns = options.excludePatterns === undefined
|
|
607
|
+
? undefined
|
|
608
|
+
: normalizeContextIndexExcludePatterns(options.excludePatterns);
|
|
609
|
+
const expectedExcludeDigest = expectedExcludePatterns
|
|
610
|
+
? contextIndexExcludeDigest(expectedExcludePatterns)
|
|
611
|
+
: undefined;
|
|
604
612
|
if (!fs.existsSync(paths.database)) {
|
|
605
|
-
return {
|
|
613
|
+
return {
|
|
614
|
+
schemaVersion: INDEX_SCHEMA_VERSION,
|
|
615
|
+
exists: false,
|
|
616
|
+
path: normalizeRelative(path.relative(cwd, paths.database)),
|
|
617
|
+
stale: false,
|
|
618
|
+
policyStale: false,
|
|
619
|
+
expectedExcludeDigest,
|
|
620
|
+
warnings: ["index-v2 missing"]
|
|
621
|
+
};
|
|
606
622
|
}
|
|
607
623
|
const db = await openDatabase(cwd, { create: false });
|
|
608
624
|
try {
|
|
@@ -612,24 +628,29 @@ export async function contextIndexV2Status(cwd) {
|
|
|
612
628
|
const symbols = Number(db.prepare("SELECT COUNT(*) count FROM symbols").get().count);
|
|
613
629
|
const imports = Number(db.prepare("SELECT COUNT(*) count FROM imports").get().count);
|
|
614
630
|
const indexedPaths = new Set(fileRows.map((row) => row.path));
|
|
631
|
+
let storedExcludePatterns = [];
|
|
632
|
+
try {
|
|
633
|
+
const parsed = JSON.parse(metadata.excludePatterns ?? "[]");
|
|
634
|
+
if (Array.isArray(parsed)) storedExcludePatterns = normalizeContextIndexExcludePatterns(parsed);
|
|
635
|
+
} catch {
|
|
636
|
+
storedExcludePatterns = [];
|
|
637
|
+
}
|
|
638
|
+
const activeExcludePatterns = expectedExcludePatterns ?? storedExcludePatterns;
|
|
639
|
+
const storedExcludeDigest = metadata.excludeDigest;
|
|
640
|
+
const policyStale = expectedExcludeDigest !== undefined
|
|
641
|
+
&& (typeof storedExcludeDigest !== "string" || storedExcludeDigest !== expectedExcludeDigest);
|
|
615
642
|
const statChanged = [];
|
|
616
643
|
for (const row of fileRows) {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
644
|
+
if (!shouldIndexPath(row.path, { excludePatterns: activeExcludePatterns })) continue;
|
|
645
|
+
const file = projectFileInfo(cwd, row.path, projectRoot, directoryCache);
|
|
646
|
+
if (
|
|
647
|
+
!file
|
|
648
|
+
|| file.stat.size !== Number(row.bytes)
|
|
649
|
+
|| Math.abs(file.stat.mtimeMs - Number(row.mtime_ms)) >= 0.5
|
|
650
|
+
) {
|
|
623
651
|
statChanged.push(row.path);
|
|
624
652
|
}
|
|
625
653
|
}
|
|
626
|
-
let excludePatterns = [];
|
|
627
|
-
try {
|
|
628
|
-
const parsed = JSON.parse(metadata.excludePatterns ?? "[]");
|
|
629
|
-
if (Array.isArray(parsed)) excludePatterns = parsed.filter((value) => typeof value === "string");
|
|
630
|
-
} catch {
|
|
631
|
-
excludePatterns = [];
|
|
632
|
-
}
|
|
633
654
|
let skippedPaths = new Set();
|
|
634
655
|
try {
|
|
635
656
|
const parsed = JSON.parse(metadata.skippedPaths ?? "[]");
|
|
@@ -639,9 +660,12 @@ export async function contextIndexV2Status(cwd) {
|
|
|
639
660
|
}
|
|
640
661
|
const listed = gitFileList(cwd);
|
|
641
662
|
const newlyVisible = listed
|
|
642
|
-
? listed.filter((file) => !indexedPaths.has(file) && !skippedPaths.has(file) && shouldIndexPath(file, { excludePatterns }))
|
|
663
|
+
? listed.filter((file) => !indexedPaths.has(file) && !skippedPaths.has(file) && shouldIndexPath(file, { excludePatterns: activeExcludePatterns }))
|
|
643
664
|
: [];
|
|
644
665
|
const stalePaths = [...new Set([...statChanged, ...newlyVisible])];
|
|
666
|
+
const warnings = [];
|
|
667
|
+
if (policyStale) warnings.push("context index exclusion policy changed");
|
|
668
|
+
if (stalePaths.length > 0) warnings.push(`${stalePaths.length} changed file(s) since the last index build`);
|
|
645
669
|
return {
|
|
646
670
|
schemaVersion: Number(metadata.schemaVersion ?? INDEX_SCHEMA_VERSION),
|
|
647
671
|
exists: true,
|
|
@@ -650,15 +674,37 @@ export async function contextIndexV2Status(cwd) {
|
|
|
650
674
|
files,
|
|
651
675
|
symbols,
|
|
652
676
|
imports,
|
|
653
|
-
stale: stalePaths.length > 0,
|
|
677
|
+
stale: policyStale || stalePaths.length > 0,
|
|
678
|
+
policyStale,
|
|
679
|
+
excludePolicyVersion: metadata.excludePolicyVersion,
|
|
680
|
+
excludeDigest: storedExcludeDigest,
|
|
681
|
+
expectedExcludeDigest,
|
|
654
682
|
stalePaths: stalePaths.slice(0, 20),
|
|
655
|
-
warnings
|
|
683
|
+
warnings
|
|
656
684
|
};
|
|
657
685
|
} finally {
|
|
658
686
|
db.close();
|
|
659
687
|
}
|
|
660
688
|
}
|
|
661
689
|
|
|
690
|
+
export async function ensureContextIndexV2(cwd, options = {}) {
|
|
691
|
+
const excludePatterns = requireExplicitExcludePatterns(options, "ensureContextIndexV2");
|
|
692
|
+
let status = await contextIndexV2Status(cwd, { excludePatterns });
|
|
693
|
+
const reason = options.refresh
|
|
694
|
+
? "refresh"
|
|
695
|
+
: status.policyStale
|
|
696
|
+
? "exclusion-policy"
|
|
697
|
+
: !status.exists && options.rebuildMissing
|
|
698
|
+
? "missing"
|
|
699
|
+
: undefined;
|
|
700
|
+
let build;
|
|
701
|
+
if (reason) {
|
|
702
|
+
build = await buildContextIndexV2(cwd, { ...options, excludePatterns });
|
|
703
|
+
status = await contextIndexV2Status(cwd, { excludePatterns });
|
|
704
|
+
}
|
|
705
|
+
return { status, rebuilt: Boolean(reason), reason, build, excludePatterns };
|
|
706
|
+
}
|
|
707
|
+
|
|
662
708
|
function addRankedList(scores, rows, source, weight = 1) {
|
|
663
709
|
rows.forEach((row, index) => {
|
|
664
710
|
const filePath = row.path ?? row.file_path;
|
|
@@ -783,8 +829,11 @@ function retrievalFeedback(cwd) {
|
|
|
783
829
|
}
|
|
784
830
|
|
|
785
831
|
export async function searchContextIndexV2(cwd, query, options = {}) {
|
|
832
|
+
const excludePatterns = requireExplicitExcludePatterns(options, "searchContextIndexV2");
|
|
786
833
|
const limit = clampInteger(options.limit, 12, 1, 50);
|
|
787
|
-
const
|
|
834
|
+
const projectRoot = fs.realpathSync.native(cwd);
|
|
835
|
+
const directoryCache = new Map();
|
|
836
|
+
const status = await contextIndexV2Status(cwd, { excludePatterns });
|
|
788
837
|
if (!status.exists) return { query, terms: [], confidence: "none", results: [], status };
|
|
789
838
|
const terms = tokenizeQuery(query);
|
|
790
839
|
const explicitPaths = queryPathCandidates(query);
|
|
@@ -792,7 +841,8 @@ export async function searchContextIndexV2(cwd, query, options = {}) {
|
|
|
792
841
|
const db = await openDatabase(cwd, { create: false });
|
|
793
842
|
try {
|
|
794
843
|
const scores = new Map();
|
|
795
|
-
const
|
|
844
|
+
const pathAllowed = (filePath) => shouldIndexPath(filePath, { excludePatterns });
|
|
845
|
+
const allFiles = db.prepare("SELECT path, mtime_ms FROM files").all().filter((file) => pathAllowed(file.path));
|
|
796
846
|
const explicitRows = [];
|
|
797
847
|
for (const file of allFiles) {
|
|
798
848
|
const basename = path.posix.basename(file.path).toLowerCase();
|
|
@@ -809,7 +859,7 @@ export async function searchContextIndexV2(cwd, query, options = {}) {
|
|
|
809
859
|
lexicalRows = db.prepare(`
|
|
810
860
|
SELECT path, bm25(file_fts, 0.0, 1.0, 2.2) lexical_score
|
|
811
861
|
FROM file_fts WHERE file_fts MATCH ? ORDER BY lexical_score LIMIT ?
|
|
812
|
-
`).all(ftsQuery(terms), Math.max(limit * 4, 30));
|
|
862
|
+
`).all(ftsQuery(terms), Math.max(limit * 4, 30)).filter((row) => pathAllowed(row.path));
|
|
813
863
|
addRankedList(scores, lexicalRows, "lexical", 1.5);
|
|
814
864
|
}
|
|
815
865
|
|
|
@@ -820,7 +870,7 @@ export async function searchContextIndexV2(cwd, query, options = {}) {
|
|
|
820
870
|
ORDER BY CASE WHEN lower(name) = ? THEN 0 ELSE 1 END, length(name) LIMIT ?
|
|
821
871
|
`);
|
|
822
872
|
for (const term of terms.slice(0, 8)) {
|
|
823
|
-
symbolRows.push(...symbolStatement.all(term, `%${term}%`, term, Math.max(limit * 2, 20)));
|
|
873
|
+
symbolRows.push(...symbolStatement.all(term, `%${term}%`, term, Math.max(limit * 2, 20)).filter((row) => pathAllowed(row.file_path)));
|
|
824
874
|
}
|
|
825
875
|
const dedupedSymbols = [...new Map(symbolRows.map((row) => [`${row.file_path}:${row.line}`, row])).values()];
|
|
826
876
|
addRankedList(scores, dedupedSymbols, "symbol", 2.2);
|
|
@@ -841,11 +891,19 @@ export async function searchContextIndexV2(cwd, query, options = {}) {
|
|
|
841
891
|
addRankedList(scores, feedbackRows, "feedback", 0.45);
|
|
842
892
|
|
|
843
893
|
const graphSeeds = new Map([...scores.entries()].map(([filePath, value]) => [filePath, value.score]));
|
|
844
|
-
const importRows = db.prepare("SELECT file_path, target_path FROM imports WHERE target_path IS NOT NULL")
|
|
894
|
+
const importRows = db.prepare("SELECT file_path, target_path FROM imports WHERE target_path IS NOT NULL")
|
|
895
|
+
.all()
|
|
896
|
+
.filter((row) => pathAllowed(row.file_path) && pathAllowed(row.target_path));
|
|
845
897
|
const graphRows = personalizedPageRank(importRows, graphSeeds).slice(0, Math.max(limit * 3, 30));
|
|
846
898
|
addRankedList(scores, graphRows, "graph", 1);
|
|
847
899
|
|
|
848
|
-
const ranked = [...scores.values()]
|
|
900
|
+
const ranked = [...scores.values()]
|
|
901
|
+
.filter((candidate) => (
|
|
902
|
+
shouldIndexPath(candidate.path, { excludePatterns })
|
|
903
|
+
&& projectFileInfo(cwd, candidate.path, projectRoot, directoryCache)
|
|
904
|
+
))
|
|
905
|
+
.sort((left, right) => right.score - left.score)
|
|
906
|
+
.slice(0, limit);
|
|
849
907
|
const symbolLookup = db.prepare("SELECT name, kind, line, end_line, signature FROM symbols WHERE file_path = ? ORDER BY line LIMIT 12");
|
|
850
908
|
const results = ranked.map((candidate) => {
|
|
851
909
|
const matchingSymbols = candidate.rows
|
|
@@ -897,9 +955,15 @@ function lineRangesForFile(text, result, terms, includeCode) {
|
|
|
897
955
|
}
|
|
898
956
|
|
|
899
957
|
export async function buildContextPack(cwd, query, options = {}) {
|
|
958
|
+
const excludePatterns = requireExplicitExcludePatterns(options, "buildContextPack");
|
|
900
959
|
const budgetTokens = clampInteger(options.budgetTokens, DEFAULT_PACK_TOKENS, 200, 50_000);
|
|
901
960
|
const includeCode = options.includeCode !== false;
|
|
902
|
-
const
|
|
961
|
+
const projectRoot = fs.realpathSync.native(cwd);
|
|
962
|
+
const directoryCache = new Map();
|
|
963
|
+
const search = await searchContextIndexV2(cwd, query, {
|
|
964
|
+
limit: clampInteger(options.limit, 16, 1, 50),
|
|
965
|
+
excludePatterns
|
|
966
|
+
});
|
|
903
967
|
const selected = [];
|
|
904
968
|
const mapBudget = Math.min(1_200, Math.max(40, Math.trunc(budgetTokens * 0.2)));
|
|
905
969
|
const mapLines = [];
|
|
@@ -935,7 +999,9 @@ export async function buildContextPack(cwd, query, options = {}) {
|
|
|
935
999
|
for (const result of search.results) {
|
|
936
1000
|
let text;
|
|
937
1001
|
try {
|
|
938
|
-
|
|
1002
|
+
const file = projectFileInfo(cwd, result.path, projectRoot, directoryCache);
|
|
1003
|
+
if (!file) continue;
|
|
1004
|
+
text = fs.readFileSync(file.absolute, "utf8");
|
|
939
1005
|
} catch {
|
|
940
1006
|
continue;
|
|
941
1007
|
}
|
|
@@ -979,7 +1045,10 @@ export async function buildContextPack(cwd, query, options = {}) {
|
|
|
979
1045
|
}
|
|
980
1046
|
|
|
981
1047
|
export async function buildTestImpact(cwd, changedFiles = [], options = {}) {
|
|
982
|
-
const
|
|
1048
|
+
const excludePatterns = requireExplicitExcludePatterns(options, "buildTestImpact");
|
|
1049
|
+
const projectRoot = fs.realpathSync.native(cwd);
|
|
1050
|
+
const directoryCache = new Map();
|
|
1051
|
+
const status = await contextIndexV2Status(cwd, { excludePatterns });
|
|
983
1052
|
const normalizedChanged = [...new Set((changedFiles.length > 0 ? changedFiles : gitChangedFiles(cwd)).map(normalizeRelative).filter(Boolean))];
|
|
984
1053
|
if (!status.exists) return { changedFiles: normalizedChanged, impactedFiles: [], tests: [], status };
|
|
985
1054
|
const db = await openDatabase(cwd, { create: false });
|
|
@@ -993,13 +1062,22 @@ export async function buildTestImpact(cwd, changedFiles = [], options = {}) {
|
|
|
993
1062
|
const current = queue.shift();
|
|
994
1063
|
if (current.depth >= maxDepth) continue;
|
|
995
1064
|
for (const row of reverseStatement.all(current.filePath)) {
|
|
1065
|
+
if (
|
|
1066
|
+
!shouldIndexPath(row.file_path, { excludePatterns })
|
|
1067
|
+
|| !projectFileInfo(cwd, row.file_path, projectRoot, directoryCache)
|
|
1068
|
+
) continue;
|
|
996
1069
|
if (seen.has(row.file_path)) continue;
|
|
997
1070
|
seen.add(row.file_path);
|
|
998
1071
|
impacted.push({ path: row.file_path, via: current.filePath, depth: current.depth + 1 });
|
|
999
1072
|
queue.push({ filePath: row.file_path, depth: current.depth + 1 });
|
|
1000
1073
|
}
|
|
1001
1074
|
}
|
|
1002
|
-
const allFiles = db.prepare("SELECT path FROM files").all()
|
|
1075
|
+
const allFiles = db.prepare("SELECT path FROM files").all()
|
|
1076
|
+
.map((row) => row.path)
|
|
1077
|
+
.filter((filePath) => (
|
|
1078
|
+
shouldIndexPath(filePath, { excludePatterns })
|
|
1079
|
+
&& projectFileInfo(cwd, filePath, projectRoot, directoryCache)
|
|
1080
|
+
));
|
|
1003
1081
|
const stemTerms = normalizedChanged.flatMap((filePath) => {
|
|
1004
1082
|
const basename = path.posix.basename(filePath, path.posix.extname(filePath)).replace(/\.(?:test|spec)$/, "");
|
|
1005
1083
|
return basename.split(/[-_.]/).filter((term) => term.length >= 3);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { resolveProjectProfileDocument } from "../capabilities/project-profile.js";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CONTEXT_INDEX_PATH = ".pi/context-index.json";
|
|
8
|
+
const CONTEXT_ENGINE_STATE_PATTERN = ".pi/piagent-state/**";
|
|
9
|
+
const EXCLUDE_POLICY_VERSION = 1;
|
|
10
|
+
|
|
11
|
+
function stringArray(value) {
|
|
12
|
+
return Array.isArray(value)
|
|
13
|
+
? value.filter((item) => typeof item === "string" && item.trim().length > 0)
|
|
14
|
+
: [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function uniqueStrings(values) {
|
|
18
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function compareCodePoints(left, right) {
|
|
22
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizePattern(value) {
|
|
26
|
+
return String(value ?? "")
|
|
27
|
+
.trim()
|
|
28
|
+
.replaceAll("\\", "/")
|
|
29
|
+
.replace(/^\.\/+/, "")
|
|
30
|
+
.replace(/\/+/g, "/");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readJsonObject(file, label, { optional = false } = {}) {
|
|
34
|
+
if (!fs.existsSync(file)) {
|
|
35
|
+
if (optional) return undefined;
|
|
36
|
+
throw new Error(`${label} is missing at ${file}`);
|
|
37
|
+
}
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(`${label} is not valid JSON: ${file}`);
|
|
43
|
+
}
|
|
44
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
45
|
+
throw new Error(`${label} must contain a JSON object: ${file}`);
|
|
46
|
+
}
|
|
47
|
+
return parsed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function contextIndexStatePath(profile) {
|
|
51
|
+
const configured = profile?.contextIndex?.path;
|
|
52
|
+
return typeof configured === "string" && configured.trim()
|
|
53
|
+
? configured.trim()
|
|
54
|
+
: DEFAULT_CONTEXT_INDEX_PATH;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function effectiveProtectedPaths(policy = {}, profile = {}) {
|
|
58
|
+
const policyProtectedPaths = stringArray(policy.protectedPaths);
|
|
59
|
+
const baseReadProtectedPaths = Array.isArray(policy.shellProtectedPaths)
|
|
60
|
+
? stringArray(policy.shellProtectedPaths)
|
|
61
|
+
: policyProtectedPaths;
|
|
62
|
+
const profileProtectedPaths = stringArray(profile.protectedPaths);
|
|
63
|
+
const profileShellProtectedPaths = Array.isArray(profile.shellProtectedPaths)
|
|
64
|
+
? stringArray(profile.shellProtectedPaths)
|
|
65
|
+
: profileProtectedPaths;
|
|
66
|
+
const readOnlyPaths = stringArray(profile.readOnlyPaths);
|
|
67
|
+
const contextIndexPath = contextIndexStatePath(profile);
|
|
68
|
+
return {
|
|
69
|
+
readProtectedPaths: uniqueStrings([
|
|
70
|
+
...baseReadProtectedPaths,
|
|
71
|
+
...profileProtectedPaths,
|
|
72
|
+
contextIndexPath
|
|
73
|
+
]),
|
|
74
|
+
writeProtectedPaths: uniqueStrings([
|
|
75
|
+
...policyProtectedPaths,
|
|
76
|
+
...profileProtectedPaths,
|
|
77
|
+
...readOnlyPaths,
|
|
78
|
+
contextIndexPath
|
|
79
|
+
]),
|
|
80
|
+
shellProtectedPaths: uniqueStrings([
|
|
81
|
+
...baseReadProtectedPaths,
|
|
82
|
+
...profileShellProtectedPaths,
|
|
83
|
+
...readOnlyPaths,
|
|
84
|
+
contextIndexPath
|
|
85
|
+
]),
|
|
86
|
+
readOnlyPaths: uniqueStrings(readOnlyPaths)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function normalizeContextIndexExcludePatterns(patterns) {
|
|
91
|
+
return uniqueStrings(stringArray(patterns).map(normalizePattern)).sort(compareCodePoints);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function contextIndexExcludePatterns(policy, profile) {
|
|
95
|
+
const effective = effectiveProtectedPaths(policy, profile);
|
|
96
|
+
return normalizeContextIndexExcludePatterns([
|
|
97
|
+
...effective.readProtectedPaths,
|
|
98
|
+
...effective.writeProtectedPaths,
|
|
99
|
+
...effective.shellProtectedPaths,
|
|
100
|
+
CONTEXT_ENGINE_STATE_PATTERN
|
|
101
|
+
]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function contextIndexExcludeDigest(patterns) {
|
|
105
|
+
return crypto
|
|
106
|
+
.createHash("sha256")
|
|
107
|
+
.update(JSON.stringify({
|
|
108
|
+
version: EXCLUDE_POLICY_VERSION,
|
|
109
|
+
patterns: normalizeContextIndexExcludePatterns(patterns)
|
|
110
|
+
}))
|
|
111
|
+
.digest("hex");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function contextIndexExcludePolicyVersion() {
|
|
115
|
+
return EXCLUDE_POLICY_VERSION;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function loadProjectContextIndexPolicy(platformRoot, projectPath, options = {}) {
|
|
119
|
+
const policyPath = path.join(platformRoot, "packages", "piagent-core", "policies", "base-policy.json");
|
|
120
|
+
const policy = readJsonObject(policyPath, "Piagent base policy");
|
|
121
|
+
const explicitProfilePath = typeof options.profilePath === "string" && options.profilePath.trim()
|
|
122
|
+
? path.resolve(options.profilePath)
|
|
123
|
+
: undefined;
|
|
124
|
+
const profilePath = explicitProfilePath ?? path.join(projectPath, ".pi", "piagent-profile.json");
|
|
125
|
+
const stored = readJsonObject(profilePath, "Piagent project profile", { optional: !explicitProfilePath });
|
|
126
|
+
const profile = stored
|
|
127
|
+
? resolveProjectProfileDocument(platformRoot, stored).profile
|
|
128
|
+
: {};
|
|
129
|
+
return {
|
|
130
|
+
policy,
|
|
131
|
+
profile,
|
|
132
|
+
profilePath,
|
|
133
|
+
profileSource: explicitProfilePath ? "env" : stored ? "project" : "fallback",
|
|
134
|
+
excludePatterns: contextIndexExcludePatterns(policy, profile)
|
|
135
|
+
};
|
|
136
|
+
}
|