@piagent/platform 1.2.9 → 1.2.11
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 +179 -0
- package/README.md +29 -6
- package/adapters/backend-api/profile.json +1 -1
- package/adapters/be-readonly-fe/profile.json +2 -2
- package/adapters/data/profile.json +1 -1
- package/adapters/devops/profile.json +1 -3
- package/adapters/docs/profile.json +1 -1
- package/adapters/fullstack/profile.json +2 -4
- package/adapters/generic/profile.json +1 -1
- package/adapters/mobile/profile.json +1 -2
- package/adapters/node-typescript/profile.json +1 -3
- package/benchmarks/core-v1/suite.json +51 -0
- package/benchmarks/core-v1/tasks/discount-order/grade.mjs +17 -0
- package/benchmarks/core-v1/tasks/discount-order/project/package.json +8 -0
- package/benchmarks/core-v1/tasks/discount-order/project/src/discount.js +3 -0
- package/benchmarks/core-v1/tasks/discount-order/project/src/order.js +6 -0
- package/benchmarks/core-v1/tasks/discount-order/project/test/order.test.js +7 -0
- package/benchmarks/core-v1/tasks/discount-order/prompt.md +4 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/grade.mjs +17 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/project/package.json +8 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/project/src/invoice.js +3 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/project/test/invoice.test.js +7 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/prompt.md +3 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/grade.mjs +11 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/project/README.md +4 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/project/package.json +8 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/project/test/smoke.test.js +4 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/prompt.md +4 -0
- package/benchmarks/core-v1/tasks/targeted-session/grade.mjs +14 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/package.json +8 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/auth/session.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/billing/currency.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/catalog/search.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/notifications/channel.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/search/tokenize.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/test/session.test.js +7 -0
- package/benchmarks/core-v1/tasks/targeted-session/prompt.md +4 -0
- package/catalog/capabilities.json +12 -12
- package/docs/command-reference-vietnamese.md +15 -8
- package/docs/context-engine.md +56 -15
- package/docs/distribution-standard.md +11 -8
- package/docs/mcp-and-tools.md +20 -7
- package/docs/model-options.md +7 -10
- package/docs/operator-manual-vietnamese.md +102 -37
- package/docs/package-architecture-notes.md +3 -1
- package/docs/publishing-for-teams.md +5 -0
- package/docs/quality-benchmark.md +225 -58
- package/docs/quickstart-vietnamese.md +5 -0
- package/docs/release-install-policy.md +41 -7
- package/docs/runtime-harness-standard.md +47 -29
- package/docs/runtime-policy-design.md +23 -12
- package/docs/runtime-quality-baseline.md +31 -17
- package/docs/security-threat-model.md +7 -2
- package/docs/subagents-and-multiagent.md +1 -1
- package/docs/task-implementation-contract.md +113 -177
- package/docs/task-lifecycle-vietnamese.md +129 -125
- package/docs/team-onboarding.md +12 -4
- package/docs/usage-observability.md +15 -8
- package/docs-site/architecture.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/context-engine.html +8 -1
- package/docs-site/content/runtime.html +10 -0
- package/docs-site/content/security.html +1 -0
- package/docs-site/context-engine.html +9 -2
- 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 +11 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +2 -1
- package/docs-site/team.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/fixtures/task-contract.invalid.json +15 -1
- package/evals/fixtures/task-contract.valid.json +18 -2
- package/package.json +3 -2
- package/packages/piagent-core/README.md +43 -4
- package/packages/piagent-core/benchmark/benchmark-core.js +691 -0
- package/packages/piagent-core/extensions/context-engine.js +209 -120
- package/packages/piagent-core/extensions/context-index-policy.js +1 -1
- package/packages/piagent-core/extensions/guard-types.ts +35 -3
- package/packages/piagent-core/extensions/local-state-path.js +59 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +2073 -222
- package/packages/piagent-core/extensions/policy-core.js +11 -1
- package/packages/piagent-core/extensions/runtime-evidence.js +39 -19
- package/packages/piagent-core/extensions/state-retention.js +218 -0
- package/packages/piagent-core/extensions/task-lifecycle.js +138 -0
- package/packages/piagent-core/extensions/task-state.js +630 -0
- package/packages/piagent-core/mcp/mcp-config-layers.js +52 -6
- package/packages/piagent-core/mcp/mcp-tool-naming.js +12 -1
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/policies/base-policy.json +1 -0
- package/packages/piagent-core/prompts/be-to-fe.md +14 -22
- package/packages/piagent-core/prompts/platform-improve.md +12 -17
- package/packages/piagent-core/prompts/review.md +7 -11
- package/packages/piagent-core/prompts/scout.md +6 -10
- package/packages/piagent-core/prompts/task.md +11 -17
- package/packages/piagent-core/skills/piagent-ops/SKILL.md +12 -15
- package/packages/piagent-core/subagents/piagent-reviewer.md +3 -3
- package/schemas/task-contract.schema.json +267 -104
- package/scripts/benchmark-runner.mjs +1172 -0
- package/scripts/install-global.sh +1 -1
- package/scripts/migrate-project-state.mjs +94 -3
- package/scripts/piagent-cli.mjs +1 -1
- package/scripts/update-global.mjs +36 -3
- package/scripts/verify-local.sh +15 -0
- package/templates/project/.pi/task-contract.template.json +19 -2
- package/templates/project/AGENTS.md +10 -20
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
contextIndexExcludePolicyVersion,
|
|
9
9
|
normalizeContextIndexExcludePatterns
|
|
10
10
|
} from "./context-index-policy.js";
|
|
11
|
-
import { matchesProtectedPath } from "./policy-core.js";
|
|
11
|
+
import { matchesAnyPath, matchesProtectedPath } from "./policy-core.js";
|
|
12
|
+
import { appendJsonlBounded, readJsonlTail } from "./state-retention.js";
|
|
13
|
+
import { ensurePrivateStateDirectory, resolveLocalStatePath } from "./local-state-path.js";
|
|
12
14
|
|
|
13
15
|
const INDEX_SCHEMA_VERSION = 2;
|
|
14
16
|
const TELEMETRY_SCHEMA_VERSION = 1;
|
|
@@ -18,6 +20,7 @@ const DEFAULT_MAX_FILES = 8_000;
|
|
|
18
20
|
const DEFAULT_PACK_TOKENS = 6_000;
|
|
19
21
|
const MAX_TELEMETRY_EVENTS = 50_000;
|
|
20
22
|
const MAX_TELEMETRY_READ_BYTES = 32 * 1024 * 1024;
|
|
23
|
+
const MAX_TELEMETRY_FILE_BYTES = 32 * 1024 * 1024;
|
|
21
24
|
const SOURCE_EXTENSIONS = new Set([
|
|
22
25
|
".c", ".cc", ".cpp", ".cs", ".css", ".dart", ".ex", ".exs", ".go", ".h", ".hpp",
|
|
23
26
|
".html", ".java", ".js", ".jsx", ".kt", ".kts", ".lua", ".m", ".md", ".mdx",
|
|
@@ -47,7 +50,8 @@ const STOP_TERMS = new Set([
|
|
|
47
50
|
"about", "after", "agent", "before", "build", "change", "check", "code", "could", "file",
|
|
48
51
|
"from", "have", "into", "just", "make", "need", "please", "project", "should", "task",
|
|
49
52
|
"that", "them", "then", "there", "these", "this", "through", "update", "using", "want",
|
|
50
|
-
"what", "when", "where", "which", "with", "would"
|
|
53
|
+
"what", "when", "where", "which", "with", "would",
|
|
54
|
+
"anh", "cho", "cua", "em", "giup", "lam", "minh", "muon", "nay", "trong", "voi"
|
|
51
55
|
]);
|
|
52
56
|
|
|
53
57
|
let sqliteModulePromise;
|
|
@@ -79,7 +83,17 @@ function requireExplicitExcludePatterns(options, operation) {
|
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
export function estimateContextTokens(value) {
|
|
82
|
-
|
|
86
|
+
const text = String(value ?? "");
|
|
87
|
+
let asciiChars = 0;
|
|
88
|
+
let nonAsciiBytes = 0;
|
|
89
|
+
for (const character of text) {
|
|
90
|
+
if (character.codePointAt(0) <= 0x7f) asciiChars += 1;
|
|
91
|
+
else nonAsciiBytes += Buffer.byteLength(character, "utf8");
|
|
92
|
+
}
|
|
93
|
+
// ASCII prose/code averages close to four characters per token. UTF-8 text
|
|
94
|
+
// is estimated more conservatively so Vietnamese, CJK, and emoji packs do not
|
|
95
|
+
// silently exceed the provider budget.
|
|
96
|
+
return Math.max(0, Math.ceil(asciiChars / 4 + nonAsciiBytes / 2.5));
|
|
83
97
|
}
|
|
84
98
|
|
|
85
99
|
export function contextEnginePaths(cwd) {
|
|
@@ -92,6 +106,21 @@ export function contextEnginePaths(cwd) {
|
|
|
92
106
|
};
|
|
93
107
|
}
|
|
94
108
|
|
|
109
|
+
function hardenContextDatabaseArtifacts(cwd) {
|
|
110
|
+
const database = resolveLocalStatePath(cwd, contextEnginePaths(cwd).database, { label: "Context index database" });
|
|
111
|
+
for (const artifact of [database, `${database}-wal`, `${database}-shm`]) {
|
|
112
|
+
try {
|
|
113
|
+
const stat = fs.lstatSync(artifact);
|
|
114
|
+
if (!stat.isFile()) {
|
|
115
|
+
throw new Error(`Context index storage path must be a regular file: ${artifact}`);
|
|
116
|
+
}
|
|
117
|
+
fs.chmodSync(artifact, 0o600);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error?.code !== "ENOENT") throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
95
124
|
async function loadSqlite() {
|
|
96
125
|
if (!sqliteModulePromise) {
|
|
97
126
|
sqliteModulePromise = (async () => {
|
|
@@ -116,54 +145,91 @@ async function loadSqlite() {
|
|
|
116
145
|
|
|
117
146
|
async function openDatabase(cwd, { create = true } = {}) {
|
|
118
147
|
const paths = contextEnginePaths(cwd);
|
|
119
|
-
|
|
120
|
-
fs.
|
|
148
|
+
const database = resolveLocalStatePath(cwd, paths.database, { label: "Context index database" });
|
|
149
|
+
if (!create && !fs.existsSync(database)) return undefined;
|
|
150
|
+
if (create) ensurePrivateStateDirectory(cwd, paths.root, "Context index storage root");
|
|
151
|
+
else {
|
|
152
|
+
const existingRoot = resolveLocalStatePath(cwd, paths.root, { label: "Context index storage root", kind: "directory" });
|
|
153
|
+
try {
|
|
154
|
+
fs.chmodSync(existingRoot, 0o700);
|
|
155
|
+
} catch {
|
|
156
|
+
// Best effort on filesystems that do not expose POSIX modes.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
hardenContextDatabaseArtifacts(cwd);
|
|
160
|
+
const descriptor = fs.openSync(
|
|
161
|
+
database,
|
|
162
|
+
fs.constants.O_RDWR
|
|
163
|
+
| (create ? fs.constants.O_CREAT : 0)
|
|
164
|
+
| (fs.constants.O_NOFOLLOW ?? 0),
|
|
165
|
+
0o600
|
|
166
|
+
);
|
|
167
|
+
try {
|
|
168
|
+
fs.fchmodSync(descriptor, 0o600);
|
|
169
|
+
} finally {
|
|
170
|
+
fs.closeSync(descriptor);
|
|
171
|
+
}
|
|
121
172
|
const { DatabaseSync } = await loadSqlite();
|
|
122
|
-
const db = new DatabaseSync(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
173
|
+
const db = new DatabaseSync(database);
|
|
174
|
+
try {
|
|
175
|
+
db.exec(`
|
|
176
|
+
PRAGMA journal_mode = WAL;
|
|
177
|
+
PRAGMA synchronous = NORMAL;
|
|
178
|
+
PRAGMA foreign_keys = ON;
|
|
179
|
+
PRAGMA secure_delete = ON;
|
|
180
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
181
|
+
key TEXT PRIMARY KEY,
|
|
182
|
+
value TEXT NOT NULL
|
|
183
|
+
);
|
|
184
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
185
|
+
path TEXT PRIMARY KEY,
|
|
186
|
+
hash TEXT NOT NULL,
|
|
187
|
+
bytes INTEGER NOT NULL,
|
|
188
|
+
mtime_ms REAL NOT NULL,
|
|
189
|
+
language TEXT NOT NULL,
|
|
190
|
+
indexed_at TEXT NOT NULL
|
|
191
|
+
);
|
|
192
|
+
CREATE TABLE IF NOT EXISTS symbols (
|
|
193
|
+
id INTEGER PRIMARY KEY,
|
|
194
|
+
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
|
|
195
|
+
name TEXT NOT NULL,
|
|
196
|
+
kind TEXT NOT NULL,
|
|
197
|
+
line INTEGER NOT NULL,
|
|
198
|
+
end_line INTEGER NOT NULL,
|
|
199
|
+
signature TEXT NOT NULL
|
|
200
|
+
);
|
|
201
|
+
CREATE INDEX IF NOT EXISTS symbols_name_idx ON symbols(name);
|
|
202
|
+
CREATE INDEX IF NOT EXISTS symbols_file_idx ON symbols(file_path);
|
|
203
|
+
CREATE TABLE IF NOT EXISTS imports (
|
|
204
|
+
id INTEGER PRIMARY KEY,
|
|
205
|
+
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
|
|
206
|
+
specifier TEXT NOT NULL,
|
|
207
|
+
target_path TEXT,
|
|
208
|
+
line INTEGER NOT NULL
|
|
209
|
+
);
|
|
210
|
+
CREATE INDEX IF NOT EXISTS imports_file_idx ON imports(file_path);
|
|
211
|
+
CREATE INDEX IF NOT EXISTS imports_target_idx ON imports(target_path);
|
|
212
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS file_fts USING fts5(
|
|
213
|
+
path UNINDEXED,
|
|
214
|
+
body,
|
|
215
|
+
symbol_names,
|
|
216
|
+
tokenize = 'unicode61 remove_diacritics 2'
|
|
217
|
+
);
|
|
218
|
+
`);
|
|
219
|
+
const ftsSecureDelete = db.prepare(`
|
|
220
|
+
SELECT v
|
|
221
|
+
FROM file_fts_config
|
|
222
|
+
WHERE k = 'secure-delete'
|
|
223
|
+
`).get();
|
|
224
|
+
if (Number(ftsSecureDelete?.v) !== 1) {
|
|
225
|
+
db.exec("INSERT INTO file_fts(file_fts, rank) VALUES('secure-delete', 1)");
|
|
226
|
+
}
|
|
227
|
+
hardenContextDatabaseArtifacts(cwd);
|
|
228
|
+
return db;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
db.close();
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
167
233
|
}
|
|
168
234
|
|
|
169
235
|
function metadataMap(db) {
|
|
@@ -408,18 +474,30 @@ function resolveImportTarget(sourcePath, specifier, files) {
|
|
|
408
474
|
return candidates.map(normalizeRelative).find((candidate) => files.has(candidate));
|
|
409
475
|
}
|
|
410
476
|
|
|
477
|
+
function foldSearchSignal(value) {
|
|
478
|
+
return String(value ?? "")
|
|
479
|
+
.normalize("NFKD")
|
|
480
|
+
.replace(/\p{M}/gu, "")
|
|
481
|
+
.replace(/[đĐ]/g, (character) => character === "Đ" ? "D" : "d")
|
|
482
|
+
.toLowerCase();
|
|
483
|
+
}
|
|
484
|
+
|
|
411
485
|
function tokenizeQuery(query) {
|
|
412
486
|
const expanded = String(query ?? "")
|
|
413
487
|
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
414
488
|
.replaceAll("_", " ")
|
|
415
489
|
.replaceAll("-", " ");
|
|
416
|
-
const terms = expanded.match(/[
|
|
417
|
-
return [...new Set(terms
|
|
490
|
+
const terms = expanded.match(/[\p{L}][\p{L}\p{N}]{1,63}/gu) ?? [];
|
|
491
|
+
return [...new Set(terms
|
|
492
|
+
.map((term) => term.toLowerCase())
|
|
493
|
+
.filter((term) => !STOP_TERMS.has(foldSearchSignal(term))))].slice(0, 16);
|
|
418
494
|
}
|
|
419
495
|
|
|
420
496
|
function queryPathCandidates(query) {
|
|
421
|
-
const
|
|
422
|
-
return
|
|
497
|
+
const pattern = /(?:^|[\s"'`(])((?:\.{0,2}\/)?[\p{L}\p{N}_.@-]+(?:\/[\p{L}\p{N}_.@-]+)+|[\p{L}\p{N}_.@-]+\.[\p{L}\p{N}]{1,8}|\.[\p{L}\p{N}_@-]{1,32})(?=$|[\s"'`),:])/gu;
|
|
498
|
+
return [...String(query ?? "").matchAll(pattern)]
|
|
499
|
+
.map((match) => normalizeRelative(match[1]))
|
|
500
|
+
.filter(Boolean);
|
|
423
501
|
}
|
|
424
502
|
|
|
425
503
|
function ftsQuery(terms) {
|
|
@@ -441,18 +519,20 @@ function gitChangedFiles(cwd) {
|
|
|
441
519
|
export function classifyContextTask(prompt) {
|
|
442
520
|
const text = String(prompt ?? "").trim();
|
|
443
521
|
const lower = text.toLowerCase();
|
|
522
|
+
const folded = foldSearchSignal(text);
|
|
444
523
|
const paths = queryPathCandidates(text);
|
|
445
524
|
const terms = tokenizeQuery(text);
|
|
446
525
|
let workflow = "task";
|
|
447
526
|
if (/^\/?(?:scout|review|plan|discuss)\b/.test(lower)) workflow = lower.match(/^\/?([a-z-]+)/)?.[1] ?? "task";
|
|
448
|
-
if (/\b(onboard|profile setup|first-read)\b/.test(
|
|
449
|
-
if (/\b(commit|pull request
|
|
527
|
+
if (/\b(onboard|profile setup|first-read|cau hinh profile|khoi tao project)\b/.test(folded)) workflow = "onboard";
|
|
528
|
+
if (/\b(commit|pull request|pr|release|publish|deploy|phat hanh|trien khai)\b/.test(folded)) workflow = "release";
|
|
450
529
|
const usageIntent = /^\/?(?:usage|session)\b/.test(lower)
|
|
451
530
|
|| /\b(?:show|check|view|report|current|session|how many)\b.{0,48}\b(?:usage|tokens?|cost|context stats|efficiency)\b/.test(lower)
|
|
452
|
-
|| /\b(?:usage|tokens?|cost|context stats|efficiency)\b.{0,48}\b(?:show|check|view|report|current|session)\b/.test(lower)
|
|
531
|
+
|| /\b(?:usage|tokens?|cost|context stats|efficiency)\b.{0,48}\b(?:show|check|view|report|current|session)\b/.test(lower)
|
|
532
|
+
|| /\b(?:xem|kiem tra|bao cao|hien tai|phien)\b.{0,48}\b(?:usage|token|chi phi|context)\b/.test(folded);
|
|
453
533
|
if (usageIntent) workflow = "usage";
|
|
454
|
-
const highRisk = /\b(auth|authorization|credential|database|deploy|encryption|migration|payment|permission|production|release|secret|security|token)\b/.test(
|
|
455
|
-
const tiny = text.length < 220 && (paths.length > 0 || /\b(rename|typo|label|copy|one line|small)\b/.test(
|
|
534
|
+
const highRisk = /\b(auth|authorization|credential|database|deploy|encryption|migration|payment|permission|production|release|secret|security|token|bao mat|phan quyen|xac thuc|du lieu|co so du lieu|thanh toan|ma hoa|trien khai|phat hanh)\b/.test(folded);
|
|
535
|
+
const tiny = text.length < 220 && (paths.length > 0 || /\b(rename|typo|label|copy|one line|small|doi ten|sua chu|nhan|mot dong|nho)\b/.test(folded));
|
|
456
536
|
return {
|
|
457
537
|
workflow,
|
|
458
538
|
lane: highRisk ? "high-risk" : tiny ? "tiny" : "normal",
|
|
@@ -465,6 +545,7 @@ export function classifyContextTask(prompt) {
|
|
|
465
545
|
|
|
466
546
|
export async function buildContextIndexV2(cwd, options = {}) {
|
|
467
547
|
const excludePatterns = requireExplicitExcludePatterns(options, "buildContextIndexV2");
|
|
548
|
+
const excludeDigest = contextIndexExcludeDigest(excludePatterns);
|
|
468
549
|
const startedAt = Date.now();
|
|
469
550
|
const projectRoot = fs.realpathSync.native(cwd);
|
|
470
551
|
const directoryCache = new Map();
|
|
@@ -473,7 +554,10 @@ export async function buildContextIndexV2(cwd, options = {}) {
|
|
|
473
554
|
const listed = gitFileList(cwd) ?? walkedFileList(cwd, maxFiles);
|
|
474
555
|
const candidates = listed.filter((file) => shouldIndexPath(file, { ...options, excludePatterns })).slice(0, maxFiles).sort();
|
|
475
556
|
const db = await openDatabase(cwd);
|
|
557
|
+
const previousMetadata = metadataMap(db);
|
|
476
558
|
const existing = new Map(db.prepare("SELECT path, hash, bytes, mtime_ms FROM files").all().map((row) => [row.path, row]));
|
|
559
|
+
const purgeStaleContent = Boolean(previousMetadata.builtAt || existing.size > 0)
|
|
560
|
+
&& (previousMetadata.purgePending === "1" || previousMetadata.excludeDigest !== excludeDigest);
|
|
477
561
|
const current = new Set();
|
|
478
562
|
const currentHashes = new Map();
|
|
479
563
|
const changed = [];
|
|
@@ -525,8 +609,10 @@ export async function buildContextIndexV2(cwd, options = {}) {
|
|
|
525
609
|
}
|
|
526
610
|
|
|
527
611
|
const removed = [...existing.keys()].filter((file) => !current.has(file));
|
|
528
|
-
|
|
612
|
+
let transactionOpen = false;
|
|
529
613
|
try {
|
|
614
|
+
db.exec("BEGIN IMMEDIATE");
|
|
615
|
+
transactionOpen = true;
|
|
530
616
|
const deleteFile = db.prepare("DELETE FROM files WHERE path = ?");
|
|
531
617
|
const deleteFts = db.prepare("DELETE FROM file_fts WHERE path = ?");
|
|
532
618
|
for (const relativePath of [...removed, ...changed.map((item) => item.relativePath)]) {
|
|
@@ -574,12 +660,28 @@ export async function buildContextIndexV2(cwd, options = {}) {
|
|
|
574
660
|
importCount: counts.imports,
|
|
575
661
|
maxFileBytes,
|
|
576
662
|
excludePatterns: JSON.stringify(excludePatterns),
|
|
577
|
-
excludeDigest
|
|
663
|
+
excludeDigest,
|
|
578
664
|
excludePolicyVersion: contextIndexExcludePolicyVersion(),
|
|
665
|
+
purgePending: purgeStaleContent ? 1 : 0,
|
|
579
666
|
skippedPaths: JSON.stringify(skippedPaths)
|
|
580
667
|
});
|
|
581
668
|
db.exec("COMMIT");
|
|
582
|
-
|
|
669
|
+
transactionOpen = false;
|
|
670
|
+
if (purgeStaleContent) {
|
|
671
|
+
if (typeof options.onPurgeStart === "function") await options.onPurgeStart();
|
|
672
|
+
db.exec("INSERT INTO file_fts(file_fts) VALUES('rebuild')");
|
|
673
|
+
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
674
|
+
db.exec("VACUUM");
|
|
675
|
+
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
676
|
+
db.exec("BEGIN IMMEDIATE");
|
|
677
|
+
transactionOpen = true;
|
|
678
|
+
setMetadata(db, {
|
|
679
|
+
purgePending: 0,
|
|
680
|
+
purgedAt: nowIso()
|
|
681
|
+
});
|
|
682
|
+
db.exec("COMMIT");
|
|
683
|
+
transactionOpen = false;
|
|
684
|
+
}
|
|
583
685
|
return {
|
|
584
686
|
schemaVersion: INDEX_SCHEMA_VERSION,
|
|
585
687
|
builtAt: indexedAt,
|
|
@@ -590,26 +692,26 @@ export async function buildContextIndexV2(cwd, options = {}) {
|
|
|
590
692
|
removed: removed.length,
|
|
591
693
|
skippedLarge,
|
|
592
694
|
skippedBinary,
|
|
695
|
+
purgedStaleContent: purgeStaleContent,
|
|
593
696
|
...counts
|
|
594
697
|
};
|
|
595
698
|
} catch (error) {
|
|
596
|
-
db.exec("ROLLBACK");
|
|
597
|
-
db.close();
|
|
699
|
+
if (transactionOpen) db.exec("ROLLBACK");
|
|
598
700
|
throw error;
|
|
701
|
+
} finally {
|
|
702
|
+
db.close();
|
|
703
|
+
hardenContextDatabaseArtifacts(cwd);
|
|
599
704
|
}
|
|
600
705
|
}
|
|
601
706
|
|
|
602
707
|
export async function contextIndexV2Status(cwd, options = {}) {
|
|
708
|
+
const expectedExcludePatterns = requireExplicitExcludePatterns(options, "contextIndexV2Status");
|
|
709
|
+
const expectedExcludeDigest = contextIndexExcludeDigest(expectedExcludePatterns);
|
|
603
710
|
const paths = contextEnginePaths(cwd);
|
|
604
711
|
const projectRoot = fs.realpathSync.native(cwd);
|
|
605
712
|
const directoryCache = new Map();
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
: normalizeContextIndexExcludePatterns(options.excludePatterns);
|
|
609
|
-
const expectedExcludeDigest = expectedExcludePatterns
|
|
610
|
-
? contextIndexExcludeDigest(expectedExcludePatterns)
|
|
611
|
-
: undefined;
|
|
612
|
-
if (!fs.existsSync(paths.database)) {
|
|
713
|
+
const database = resolveLocalStatePath(cwd, paths.database, { label: "Context index database" });
|
|
714
|
+
if (!fs.existsSync(database)) {
|
|
613
715
|
return {
|
|
614
716
|
schemaVersion: INDEX_SCHEMA_VERSION,
|
|
615
717
|
exists: false,
|
|
@@ -617,6 +719,7 @@ export async function contextIndexV2Status(cwd, options = {}) {
|
|
|
617
719
|
stale: false,
|
|
618
720
|
policyStale: false,
|
|
619
721
|
expectedExcludeDigest,
|
|
722
|
+
purgePending: false,
|
|
620
723
|
warnings: ["index-v2 missing"]
|
|
621
724
|
};
|
|
622
725
|
}
|
|
@@ -628,20 +731,14 @@ export async function contextIndexV2Status(cwd, options = {}) {
|
|
|
628
731
|
const symbols = Number(db.prepare("SELECT COUNT(*) count FROM symbols").get().count);
|
|
629
732
|
const imports = Number(db.prepare("SELECT COUNT(*) count FROM imports").get().count);
|
|
630
733
|
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
734
|
const storedExcludeDigest = metadata.excludeDigest;
|
|
640
|
-
const
|
|
641
|
-
|
|
735
|
+
const purgePending = metadata.purgePending === "1";
|
|
736
|
+
const excludeDigestStale = typeof storedExcludeDigest !== "string"
|
|
737
|
+
|| storedExcludeDigest !== expectedExcludeDigest;
|
|
738
|
+
const policyStale = purgePending || excludeDigestStale;
|
|
642
739
|
const statChanged = [];
|
|
643
740
|
for (const row of fileRows) {
|
|
644
|
-
if (!shouldIndexPath(row.path, { excludePatterns:
|
|
741
|
+
if (!shouldIndexPath(row.path, { excludePatterns: expectedExcludePatterns })) continue;
|
|
645
742
|
const file = projectFileInfo(cwd, row.path, projectRoot, directoryCache);
|
|
646
743
|
if (
|
|
647
744
|
!file
|
|
@@ -660,11 +757,12 @@ export async function contextIndexV2Status(cwd, options = {}) {
|
|
|
660
757
|
}
|
|
661
758
|
const listed = gitFileList(cwd);
|
|
662
759
|
const newlyVisible = listed
|
|
663
|
-
? listed.filter((file) => !indexedPaths.has(file) && !skippedPaths.has(file) && shouldIndexPath(file, { excludePatterns:
|
|
760
|
+
? listed.filter((file) => !indexedPaths.has(file) && !skippedPaths.has(file) && shouldIndexPath(file, { excludePatterns: expectedExcludePatterns }))
|
|
664
761
|
: [];
|
|
665
762
|
const stalePaths = [...new Set([...statChanged, ...newlyVisible])];
|
|
666
763
|
const warnings = [];
|
|
667
|
-
if (
|
|
764
|
+
if (purgePending) warnings.push("context index storage purge pending");
|
|
765
|
+
if (excludeDigestStale) warnings.push("context index exclusion policy changed");
|
|
668
766
|
if (stalePaths.length > 0) warnings.push(`${stalePaths.length} changed file(s) since the last index build`);
|
|
669
767
|
return {
|
|
670
768
|
schemaVersion: Number(metadata.schemaVersion ?? INDEX_SCHEMA_VERSION),
|
|
@@ -676,6 +774,7 @@ export async function contextIndexV2Status(cwd, options = {}) {
|
|
|
676
774
|
imports,
|
|
677
775
|
stale: policyStale || stalePaths.length > 0,
|
|
678
776
|
policyStale,
|
|
777
|
+
purgePending,
|
|
679
778
|
excludePolicyVersion: metadata.excludePolicyVersion,
|
|
680
779
|
excludeDigest: storedExcludeDigest,
|
|
681
780
|
expectedExcludeDigest,
|
|
@@ -755,9 +854,9 @@ function personalizedPageRank(importRows, seeds, iterations = 8) {
|
|
|
755
854
|
}
|
|
756
855
|
|
|
757
856
|
function retrievalFeedback(cwd) {
|
|
758
|
-
const target = contextEnginePaths(cwd).telemetry;
|
|
759
857
|
let signature = "missing";
|
|
760
858
|
try {
|
|
859
|
+
const target = resolveLocalStatePath(cwd, contextEnginePaths(cwd).telemetry, { label: "Context telemetry" });
|
|
761
860
|
const stat = fs.statSync(target);
|
|
762
861
|
signature = `${stat.size}:${stat.mtimeMs}`;
|
|
763
862
|
} catch {
|
|
@@ -958,27 +1057,39 @@ export async function buildContextPack(cwd, query, options = {}) {
|
|
|
958
1057
|
const excludePatterns = requireExplicitExcludePatterns(options, "buildContextPack");
|
|
959
1058
|
const budgetTokens = clampInteger(options.budgetTokens, DEFAULT_PACK_TOKENS, 200, 50_000);
|
|
960
1059
|
const includeCode = options.includeCode !== false;
|
|
1060
|
+
if (options.includePatterns !== undefined && !Array.isArray(options.includePatterns)) {
|
|
1061
|
+
throw new TypeError("buildContextPack includePatterns must be an array when provided");
|
|
1062
|
+
}
|
|
1063
|
+
const includePatterns = options.includePatterns === undefined
|
|
1064
|
+
? undefined
|
|
1065
|
+
: [...new Set(options.includePatterns.map((value) => String(value).trim()).filter(Boolean))];
|
|
961
1066
|
const projectRoot = fs.realpathSync.native(cwd);
|
|
962
1067
|
const directoryCache = new Map();
|
|
963
1068
|
const search = await searchContextIndexV2(cwd, query, {
|
|
964
1069
|
limit: clampInteger(options.limit, 16, 1, 50),
|
|
965
1070
|
excludePatterns
|
|
966
1071
|
});
|
|
1072
|
+
const rankedResults = includePatterns === undefined
|
|
1073
|
+
? search.results
|
|
1074
|
+
: search.results.filter((result) => matchesAnyPath(result.path, includePatterns));
|
|
1075
|
+
const confidence = rankedResults.length > 0 ? search.confidence : "none";
|
|
967
1076
|
const selected = [];
|
|
968
1077
|
const mapBudget = Math.min(1_200, Math.max(40, Math.trunc(budgetTokens * 0.2)));
|
|
969
1078
|
const mapLines = [];
|
|
970
|
-
for (const result of
|
|
1079
|
+
for (const result of rankedResults) {
|
|
971
1080
|
const symbolNames = result.symbols.slice(0, 5).map((symbol) => `${symbol.kind} ${symbol.name}`).join(", ");
|
|
972
1081
|
const line = `- ${result.path}${symbolNames ? `: ${symbolNames}` : ""} [${result.sources.join("+")}]`;
|
|
973
1082
|
const cost = estimateContextTokens(line);
|
|
974
1083
|
if (estimateContextTokens(mapLines.join("\n")) + cost > mapBudget) break;
|
|
975
1084
|
mapLines.push(line);
|
|
976
1085
|
}
|
|
977
|
-
const footer =
|
|
1086
|
+
const footer = options.currentSnapshot === true
|
|
1087
|
+
? "Current-turn source snapshot: content was read from current non-protected project files after index freshness checks. Use it for the first edit; re-read only when a needed region is absent or an edit reports a mismatch."
|
|
1088
|
+
: "Use this as navigation evidence. Re-read current files before editing; index hits are advisory.";
|
|
978
1089
|
const renderHeader = () => [
|
|
979
1090
|
"Pi Context Pack v2",
|
|
980
1091
|
`queryHash: ${sha256(String(query)).slice(0, 12)}`,
|
|
981
|
-
`confidence: ${
|
|
1092
|
+
`confidence: ${confidence}`,
|
|
982
1093
|
`index: ${search.status.files ?? 0} files / ${search.status.symbols ?? 0} symbols${search.status.stale ? " / stale" : ""}`,
|
|
983
1094
|
"",
|
|
984
1095
|
"Repository map:",
|
|
@@ -996,7 +1107,7 @@ export async function buildContextPack(cwd, query, options = {}) {
|
|
|
996
1107
|
header = renderHeader();
|
|
997
1108
|
}
|
|
998
1109
|
|
|
999
|
-
for (const result of
|
|
1110
|
+
for (const result of rankedResults) {
|
|
1000
1111
|
let text;
|
|
1001
1112
|
try {
|
|
1002
1113
|
const file = projectFileInfo(cwd, result.path, projectRoot, directoryCache);
|
|
@@ -1031,13 +1142,13 @@ export async function buildContextPack(cwd, query, options = {}) {
|
|
|
1031
1142
|
const text = renderPack(selected);
|
|
1032
1143
|
return {
|
|
1033
1144
|
queryHash: sha256(String(query)),
|
|
1034
|
-
confidence
|
|
1145
|
+
confidence,
|
|
1035
1146
|
status: search.status,
|
|
1036
|
-
candidates:
|
|
1147
|
+
candidates: rankedResults.length,
|
|
1037
1148
|
selected: selected.map(({ text: _text, ...item }) => item),
|
|
1038
1149
|
estimatedTokens: estimateContextTokens(text),
|
|
1039
|
-
finderRecommended:
|
|
1040
|
-
finderRequest:
|
|
1150
|
+
finderRecommended: confidence === "none" || confidence === "low",
|
|
1151
|
+
finderRequest: confidence === "none" || confidence === "low"
|
|
1041
1152
|
? `Run one bounded read-only finder pass for: ${String(query).slice(0, 600)}. Return only paths, symbols, line ranges, evidence, and unknowns.`
|
|
1042
1153
|
: undefined,
|
|
1043
1154
|
text
|
|
@@ -1094,44 +1205,21 @@ export async function buildTestImpact(cwd, changedFiles = [], options = {}) {
|
|
|
1094
1205
|
|
|
1095
1206
|
export function appendContextTelemetry(cwd, event) {
|
|
1096
1207
|
const paths = contextEnginePaths(cwd);
|
|
1097
|
-
fs.mkdirSync(paths.root, { recursive: true });
|
|
1098
1208
|
const record = {
|
|
1099
1209
|
schemaVersion: TELEMETRY_SCHEMA_VERSION,
|
|
1100
1210
|
source: "piagent",
|
|
1101
1211
|
recordedAt: nowIso(),
|
|
1102
1212
|
...event
|
|
1103
1213
|
};
|
|
1104
|
-
|
|
1214
|
+
appendJsonlBounded(paths.telemetry, record, { maxBytes: MAX_TELEMETRY_FILE_BYTES, mode: 0o600, projectRoot: cwd });
|
|
1105
1215
|
return record;
|
|
1106
1216
|
}
|
|
1107
1217
|
|
|
1108
1218
|
export function readContextTelemetry(cwd, options = {}) {
|
|
1109
1219
|
const target = contextEnginePaths(cwd).telemetry;
|
|
1110
|
-
if (!fs.existsSync(target)) return [];
|
|
1111
1220
|
const limit = clampInteger(options.limit, MAX_TELEMETRY_EVENTS, 1, MAX_TELEMETRY_EVENTS);
|
|
1112
1221
|
const maxBytes = clampInteger(options.maxBytes, MAX_TELEMETRY_READ_BYTES, 64 * 1024, 256 * 1024 * 1024);
|
|
1113
|
-
|
|
1114
|
-
const start = Math.max(0, stat.size - maxBytes);
|
|
1115
|
-
const length = stat.size - start;
|
|
1116
|
-
const buffer = Buffer.allocUnsafe(length);
|
|
1117
|
-
const descriptor = fs.openSync(target, "r");
|
|
1118
|
-
try {
|
|
1119
|
-
fs.readSync(descriptor, buffer, 0, length, start);
|
|
1120
|
-
} finally {
|
|
1121
|
-
fs.closeSync(descriptor);
|
|
1122
|
-
}
|
|
1123
|
-
let text = buffer.toString("utf8");
|
|
1124
|
-
if (start > 0) text = text.slice(Math.max(0, text.indexOf("\n") + 1));
|
|
1125
|
-
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
1126
|
-
const events = [];
|
|
1127
|
-
for (const line of lines.slice(-limit)) {
|
|
1128
|
-
try {
|
|
1129
|
-
events.push(JSON.parse(line));
|
|
1130
|
-
} catch {
|
|
1131
|
-
// Ignore one incomplete tail record after an interrupted append.
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
return events;
|
|
1222
|
+
return readJsonlTail(target, { limit, maxBytes, projectRoot: cwd });
|
|
1135
1223
|
}
|
|
1136
1224
|
|
|
1137
1225
|
function ratio(numerator, denominator) {
|
|
@@ -1233,8 +1321,9 @@ export function buildContextEfficiencyReport(cwd, options = {}) {
|
|
|
1233
1321
|
recommendations
|
|
1234
1322
|
};
|
|
1235
1323
|
const paths = contextEnginePaths(cwd);
|
|
1236
|
-
|
|
1237
|
-
|
|
1324
|
+
ensurePrivateStateDirectory(cwd, paths.root, "Context report directory");
|
|
1325
|
+
const reportPath = resolveLocalStatePath(cwd, paths.report, { label: "Context efficiency report" });
|
|
1326
|
+
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, { mode: 0o600 });
|
|
1238
1327
|
return report;
|
|
1239
1328
|
}
|
|
1240
1329
|
|
|
@@ -6,7 +6,7 @@ import { resolveProjectProfileDocument } from "../capabilities/project-profile.j
|
|
|
6
6
|
|
|
7
7
|
const DEFAULT_CONTEXT_INDEX_PATH = ".pi/context-index.json";
|
|
8
8
|
const CONTEXT_ENGINE_STATE_PATTERN = ".pi/piagent-state/**";
|
|
9
|
-
const EXCLUDE_POLICY_VERSION =
|
|
9
|
+
const EXCLUDE_POLICY_VERSION = 2;
|
|
10
10
|
|
|
11
11
|
function stringArray(value) {
|
|
12
12
|
return Array.isArray(value)
|
|
@@ -179,8 +179,20 @@ export type WorkPlanStep = {
|
|
|
179
179
|
title: string;
|
|
180
180
|
role: OrchestrationRole;
|
|
181
181
|
mode: "read-only" | "single-writer" | "review";
|
|
182
|
-
status: "pending" | "in-progress" | "done" | "skipped";
|
|
182
|
+
status: "pending" | "in-progress" | "done" | "skipped" | "failed";
|
|
183
183
|
dependsOn?: string[];
|
|
184
|
+
note?: string;
|
|
185
|
+
updatedAt?: string;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export type TaskAttemptSummary = {
|
|
189
|
+
taskRunId: string;
|
|
190
|
+
attempt: number;
|
|
191
|
+
outcome: "pending" | "completed" | "blocked" | "partial" | "failed";
|
|
192
|
+
failedAt?: "research" | "plan" | "execute" | "verify" | "review";
|
|
193
|
+
reason?: string;
|
|
194
|
+
ruledOut?: string;
|
|
195
|
+
recordedAt: string;
|
|
184
196
|
};
|
|
185
197
|
|
|
186
198
|
export type OrchestrationPolicySettings = {
|
|
@@ -250,9 +262,18 @@ export type ProjectOnboardingSnapshot = {
|
|
|
250
262
|
};
|
|
251
263
|
|
|
252
264
|
export type TaskContract = {
|
|
265
|
+
schemaVersion: 2;
|
|
266
|
+
taskRunId: string;
|
|
253
267
|
taskId: string;
|
|
268
|
+
sessionId: string;
|
|
269
|
+
sessionName?: string;
|
|
270
|
+
changeMode: "source-change" | "read-only";
|
|
271
|
+
attempt: number;
|
|
272
|
+
maxAttempts: number;
|
|
273
|
+
previousAttempts: TaskAttemptSummary[];
|
|
254
274
|
summary: string;
|
|
255
275
|
riskLane: "tiny" | "normal" | "high-risk";
|
|
276
|
+
intakeMode?: "model" | "runtime";
|
|
256
277
|
expectedOutput: string;
|
|
257
278
|
acceptanceCriteria: string[];
|
|
258
279
|
scope: string[];
|
|
@@ -262,9 +283,10 @@ export type TaskContract = {
|
|
|
262
283
|
contextManifest: Array<{ path: string; reason: string }>;
|
|
263
284
|
memoryCitations: Array<{ path: string; reason: string }>;
|
|
264
285
|
mcpCapabilities: string[];
|
|
286
|
+
verifyGroup?: string;
|
|
265
287
|
verifyCommands: string[];
|
|
266
|
-
workPlan
|
|
267
|
-
reviewLenses
|
|
288
|
+
workPlan: WorkPlanStep[];
|
|
289
|
+
reviewLenses: ReviewLens[];
|
|
268
290
|
orchestration?: {
|
|
269
291
|
mode: OrchestrationMode;
|
|
270
292
|
subagents: "not-used" | "optional" | "used";
|
|
@@ -272,6 +294,11 @@ export type TaskContract = {
|
|
|
272
294
|
fieldGuidePath?: string;
|
|
273
295
|
modelRoles?: Record<"planner" | "worker" | "reviewer" | "watchdog", string>;
|
|
274
296
|
};
|
|
297
|
+
baselineChangedFiles: string[];
|
|
298
|
+
baselineFileDigests: Record<string, string>;
|
|
299
|
+
observedChangedFiles: string[];
|
|
300
|
+
finalWorkingTreeFiles: string[];
|
|
301
|
+
finalFileDigests: Record<string, string>;
|
|
275
302
|
changedFiles: string[];
|
|
276
303
|
verifyEvidence: Array<{
|
|
277
304
|
command: string;
|
|
@@ -282,6 +309,7 @@ export type TaskContract = {
|
|
|
282
309
|
observedAt?: string;
|
|
283
310
|
isError?: boolean;
|
|
284
311
|
matchedProfileCommand?: boolean;
|
|
312
|
+
workingTreeDigest?: string;
|
|
285
313
|
}>;
|
|
286
314
|
trace: {
|
|
287
315
|
outcome: "pending" | "completed" | "blocked" | "partial" | "failed";
|
|
@@ -289,6 +317,10 @@ export type TaskContract = {
|
|
|
289
317
|
notes?: string;
|
|
290
318
|
recordedAt?: string;
|
|
291
319
|
};
|
|
320
|
+
failedAt?: "research" | "plan" | "execute" | "verify" | "review";
|
|
321
|
+
failureReason?: string;
|
|
322
|
+
ruledOut?: string;
|
|
323
|
+
migratedFromSchemaVersion?: number;
|
|
292
324
|
createdAt: string;
|
|
293
325
|
updatedAt: string;
|
|
294
326
|
};
|