mirai-graph 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +19 -0
- package/CHANGELOG.md +30 -0
- package/CITATION.cff +2 -1
- package/README.md +21 -2
- package/ROADMAP.md +11 -16
- package/docs/adoption/cli.md +16 -1
- package/examples/project-technology-context-traversal/README.md +17 -0
- package/examples/project-technology-context-traversal/graph/objects.json +78 -0
- package/examples/project-technology-context-traversal/graph/relations.json +47 -0
- package/examples/project-technology-context-traversal/graph.json +30 -0
- package/examples/project-technology-context-traversal/selection.example.json +29 -0
- package/examples/project-technology-context-traversal/sources/announcement.md +3 -0
- package/examples/project-technology-context-traversal/sources/release-guide.md +3 -0
- package/examples/project-technology-context-traversal/sources/release-smoke.md +3 -0
- package/examples/project-technology-context-traversal/sources/rollback-policy.md +3 -0
- package/package.json +4 -3
- package/packages/cli/project-technology.js +43 -3
- package/packages/cli/validate-context-traversal.js +232 -0
- package/packages/cli/validate-mirai-graph.js +21 -2
- package/packages/project-technology/context-traversal.js +589 -0
- package/packages/project-technology/index.js +23 -40
- package/releases/1.1.0.md +36 -0
- package/releases/README.md +2 -0
- package/schemas/context-pack.schema.json +54 -1
- package/schemas/context-traversal-receipt.schema.json +64 -0
- package/standard/context-pack-generation.md +38 -1
- package/standard/object-model.md +11 -2
- package/standard/project-technology.md +37 -1
- package/standard/relation-model.md +11 -1
- package/standard/test-suite.md +5 -1
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { spawnSync } = require("child_process");
|
|
7
|
+
const { readJson, validateManifest } = require("../cli/graph-manifest");
|
|
8
|
+
|
|
9
|
+
const RECEIPT_CONTRACT = "context-traversal-receipt";
|
|
10
|
+
const RECEIPT_VERSION = "1.0.0";
|
|
11
|
+
const REQUIRED_RELATIONS = new Set(["requires", "governed_by", "validated_by"]);
|
|
12
|
+
const STRUCTURAL_RELATIONS = new Set(["contains", "specializes"]);
|
|
13
|
+
const SOURCE_RELATIONS = new Set(["documented_by", "implemented_by"]);
|
|
14
|
+
const SUPPORTED_RELATIONS = new Set([
|
|
15
|
+
...STRUCTURAL_RELATIONS,
|
|
16
|
+
...REQUIRED_RELATIONS,
|
|
17
|
+
...SOURCE_RELATIONS,
|
|
18
|
+
"hands_off_to",
|
|
19
|
+
"conflicts_with",
|
|
20
|
+
]);
|
|
21
|
+
const TERMINAL_KINDS = new Set(["resource", "source", "check", "gate", "constraint"]);
|
|
22
|
+
const ACTIVE_READINESS = new Set(["ready", "accepted", "implemented", "validated", "operating", "evolving"]);
|
|
23
|
+
const BLOCKING_READINESS = new Set(["blocked", "deprecated", "stale", "retired", "superseded"]);
|
|
24
|
+
const SECRET_PARTS = [".env", "credential", "secret", "token", "password", "cookie", "private-key", "id_rsa", ".pem", ".p12"];
|
|
25
|
+
|
|
26
|
+
function sortValue(value) {
|
|
27
|
+
if (Array.isArray(value)) return value.map(sortValue);
|
|
28
|
+
if (!value || typeof value !== "object") return value;
|
|
29
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortValue(value[key])]));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function canonicalBytes(value) {
|
|
33
|
+
return `${JSON.stringify(sortValue(value), null, 2)}\n`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function digest(value) {
|
|
37
|
+
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(typeof value === "string" ? value : canonicalBytes(value));
|
|
38
|
+
return `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function unique(values) {
|
|
42
|
+
return [...new Set(values.filter((value) => typeof value === "string" && value.trim()).map((value) => value.trim()))].sort();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function git(repo, ...args) {
|
|
46
|
+
const completed = spawnSync("git", args, { cwd: repo, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
47
|
+
return completed.status === 0 ? completed.stdout.trim() : "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function gitBlob(repo, relative) {
|
|
51
|
+
if (!relative || !git(repo, "ls-files", "--error-unmatch", "--", relative)) return null;
|
|
52
|
+
const completed = spawnSync("git", ["show", `HEAD:${relative}`], { cwd: repo, encoding: null, maxBuffer: 64 * 1024 * 1024 });
|
|
53
|
+
return completed.status === 0 ? completed.stdout : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function revisionBound(repo, relative) {
|
|
57
|
+
if (!gitBlob(repo, relative)) return false;
|
|
58
|
+
return spawnSync("git", ["diff", "--quiet", "HEAD", "--", relative], { cwd: repo }).status === 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function safeRelative(value) {
|
|
62
|
+
if (typeof value !== "string" || !value.trim() || path.isAbsolute(value)) return false;
|
|
63
|
+
const lowered = value.toLowerCase();
|
|
64
|
+
if (value.split(/[\\/]+/).includes("..")) return false;
|
|
65
|
+
if (lowered.startsWith("source/private/") || lowered.includes("/source/private/")) return false;
|
|
66
|
+
return !SECRET_PARTS.some((part) => lowered.includes(part));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function safeRef(value) {
|
|
70
|
+
if (typeof value !== "string" || !value.trim() || value.length > 512) return false;
|
|
71
|
+
if (/^(repo|https):\/\//.test(value)) return !SECRET_PARTS.some((part) => value.toLowerCase().includes(part));
|
|
72
|
+
return safeRelative(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function loadEntry(repo, relative, kind, output, visited = new Set()) {
|
|
76
|
+
if (!safeRelative(relative) || visited.has(relative)) return;
|
|
77
|
+
visited.add(relative);
|
|
78
|
+
const absolute = path.join(repo, relative);
|
|
79
|
+
if (!fs.existsSync(absolute) || fs.lstatSync(absolute).isSymbolicLink() || !fs.statSync(absolute).isFile()) return;
|
|
80
|
+
let payload;
|
|
81
|
+
try { payload = readJson(absolute); } catch (_) { output.blockers.push(`invalid_${kind}_entry`); return; }
|
|
82
|
+
const add = (item) => {
|
|
83
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return;
|
|
84
|
+
output[kind].push({ value: item, relative, sha256: digest(fs.readFileSync(absolute)) });
|
|
85
|
+
};
|
|
86
|
+
if (Array.isArray(payload)) payload.forEach(add);
|
|
87
|
+
else if (Array.isArray(payload[kind])) payload[kind].forEach(add);
|
|
88
|
+
else {
|
|
89
|
+
const files = kind === "objects" ? payload.object_files : payload.relation_files;
|
|
90
|
+
if (Array.isArray(files)) {
|
|
91
|
+
for (const child of files) {
|
|
92
|
+
const next = child.startsWith("graph/") ? child : path.posix.join(path.posix.dirname(relative), child);
|
|
93
|
+
loadEntry(repo, next, kind, output, visited);
|
|
94
|
+
}
|
|
95
|
+
} else add(payload);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readGraph(repoArg) {
|
|
100
|
+
const repo = path.resolve(repoArg || ".");
|
|
101
|
+
const manifestPath = path.join(repo, "graph.json");
|
|
102
|
+
const output = { objects: [], relations: [], blockers: [] };
|
|
103
|
+
if (!fs.existsSync(manifestPath) || fs.lstatSync(manifestPath).isSymbolicLink()) {
|
|
104
|
+
return { repo, manifest: null, revision: null, graphDigest: null, ...output, blockers: ["graph_manifest_missing_or_unsafe"] };
|
|
105
|
+
}
|
|
106
|
+
let manifest;
|
|
107
|
+
try { manifest = readJson(manifestPath); } catch (_) {
|
|
108
|
+
return { repo, manifest: null, revision: null, graphDigest: null, ...output, blockers: ["graph_manifest_invalid"] };
|
|
109
|
+
}
|
|
110
|
+
output.blockers.push(...validateManifest(manifest, repo));
|
|
111
|
+
for (const relative of manifest.graph?.objects || []) loadEntry(repo, relative, "objects", output);
|
|
112
|
+
for (const relative of manifest.graph?.relations || []) loadEntry(repo, relative, "relations", output);
|
|
113
|
+
const objects = new Map();
|
|
114
|
+
for (const record of output.objects) {
|
|
115
|
+
const id = String(record.value.id || "").trim();
|
|
116
|
+
if (!id || objects.has(id)) { output.blockers.push(id ? "duplicate_object_id" : "object_id_missing"); continue; }
|
|
117
|
+
objects.set(id, record);
|
|
118
|
+
}
|
|
119
|
+
const relations = [];
|
|
120
|
+
const relationIds = new Set();
|
|
121
|
+
for (const record of output.relations) {
|
|
122
|
+
const relation = record.value;
|
|
123
|
+
const id = String(relation.id || "").trim();
|
|
124
|
+
if (!id || relationIds.has(id)) { output.blockers.push(id ? "duplicate_relation_id" : "relation_id_missing"); continue; }
|
|
125
|
+
relationIds.add(id);
|
|
126
|
+
if (!SUPPORTED_RELATIONS.has(relation.type)) continue;
|
|
127
|
+
if (!objects.has(relation.source) || !objects.has(relation.target)) { output.blockers.push("relation_endpoint_missing"); continue; }
|
|
128
|
+
relations.push({ ...relation, _record: record });
|
|
129
|
+
}
|
|
130
|
+
const revision = git(repo, "rev-parse", "HEAD") || null;
|
|
131
|
+
for (const relative of unique([
|
|
132
|
+
"graph.json",
|
|
133
|
+
...[...objects.values()].map((record) => record.relative),
|
|
134
|
+
...relations.map((relation) => relation._record.relative),
|
|
135
|
+
])) if (!revisionBound(repo, relative)) output.blockers.push("graph_source_not_revision_bound");
|
|
136
|
+
const graphPayload = {
|
|
137
|
+
manifest: { id: manifest.id, scope: manifest.scope, profiles: manifest.profiles, graph: manifest.graph },
|
|
138
|
+
objects: [...objects.values()].map((record) => record.value).sort((a, b) => a.id.localeCompare(b.id)),
|
|
139
|
+
relations: relations.map(({ _record, ...relation }) => relation).sort((a, b) => a.id.localeCompare(b.id)),
|
|
140
|
+
revision,
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
repo,
|
|
144
|
+
manifest,
|
|
145
|
+
revision,
|
|
146
|
+
graphDigest: digest(graphPayload),
|
|
147
|
+
objects,
|
|
148
|
+
relations,
|
|
149
|
+
blockers: unique(output.blockers),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function tokenize(value) {
|
|
154
|
+
return unique(String(value || "").toLowerCase().split(/[^a-zа-я0-9_]+/i).filter((item) => item.length > 2));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function readinessOf(object) {
|
|
158
|
+
return String(object.readiness || object.lifecycle || object.status || "unknown").toLowerCase();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function outgoing(graph, id) {
|
|
162
|
+
return graph.relations.filter((relation) => relation.source === id);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function sourcePassport(graph, record, value) {
|
|
166
|
+
const raw = [...(Array.isArray(value.source_refs) ? value.source_refs : []), ...(Array.isArray(value.evidence) ? value.evidence : [])];
|
|
167
|
+
const declared = new Map();
|
|
168
|
+
for (const item of raw) {
|
|
169
|
+
const ref = typeof item === "string" ? item : item?.ref;
|
|
170
|
+
if (!safeRef(ref) || declared.has(ref)) continue;
|
|
171
|
+
declared.set(ref, typeof item === "object" && item ? item : { ref });
|
|
172
|
+
}
|
|
173
|
+
return [...declared].sort(([left], [right]) => left.localeCompare(right)).map(([ref, metadata]) => {
|
|
174
|
+
if (/^(repo|https):\/\//.test(ref)) return {
|
|
175
|
+
ref,
|
|
176
|
+
availability: metadata.availability || "declared",
|
|
177
|
+
revision: metadata.revision || graph.revision,
|
|
178
|
+
sha256: metadata.sha256 || metadata.digest || null,
|
|
179
|
+
};
|
|
180
|
+
const absolute = path.join(graph.repo, ref);
|
|
181
|
+
if (!fs.existsSync(absolute) || fs.lstatSync(absolute).isSymbolicLink() || !fs.statSync(absolute).isFile()) {
|
|
182
|
+
return { ref, availability: "unavailable", revision: graph.revision, sha256: null };
|
|
183
|
+
}
|
|
184
|
+
const blob = gitBlob(graph.repo, ref);
|
|
185
|
+
return { ref, availability: blob ? "available" : "unbound", revision: graph.revision, sha256: digest(blob || fs.readFileSync(absolute)) };
|
|
186
|
+
}).concat([{ ref: record.relative, availability: "available", revision: graph.revision, sha256: record.sha256 }]);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function nodePassport(graph, id) {
|
|
190
|
+
const record = graph.objects.get(id);
|
|
191
|
+
if (!record) return null;
|
|
192
|
+
const value = record.value;
|
|
193
|
+
const children = outgoing(graph, id);
|
|
194
|
+
const computedExpandable = children.length > 0;
|
|
195
|
+
const policy = value.expansion_policy || (TERMINAL_KINDS.has(value.kind) ? "terminal" : "conditional");
|
|
196
|
+
const declaredSourceCount = [...(Array.isArray(value.source_refs) ? value.source_refs : []), ...(Array.isArray(value.evidence) ? value.evidence : [])].length;
|
|
197
|
+
const sources = sourcePassport(graph, record, value);
|
|
198
|
+
const sensitiveMetadata = secretLike({ title: value.title, summary: value.summary, negative_boundaries: value.negative_boundaries, non_goals: value.non_goals });
|
|
199
|
+
return {
|
|
200
|
+
id,
|
|
201
|
+
kind: value.kind || "unknown",
|
|
202
|
+
title: sensitiveMetadata ? "[redacted]" : (value.title || id),
|
|
203
|
+
summary: sensitiveMetadata ? "[redacted]" : (value.summary || ""),
|
|
204
|
+
owner: value.owner || value.owner_id || graph.manifest.owner || null,
|
|
205
|
+
readiness: readinessOf(value),
|
|
206
|
+
authority: value.authority || value.tz_role || "reference",
|
|
207
|
+
expandable: computedExpandable,
|
|
208
|
+
child_kinds: unique(children.map((relation) => graph.objects.get(relation.target)?.value.kind).filter(Boolean)),
|
|
209
|
+
expansion_policy: policy,
|
|
210
|
+
applicability: value.applicability || null,
|
|
211
|
+
negative_boundaries: unique(value.negative_boundaries || value.non_goals || []),
|
|
212
|
+
source_refs: sources,
|
|
213
|
+
omitted_source_ref_count: Math.max(0, declaredSourceCount - (sources.length - 1)),
|
|
214
|
+
sensitive_metadata_detected: sensitiveMetadata,
|
|
215
|
+
access_requirements: (Array.isArray(value.access_requirements) ? value.access_requirements : []).map((item) => ({
|
|
216
|
+
ref: typeof item === "string" ? item : item.ref,
|
|
217
|
+
status: typeof item === "object" && item ? (item.status || "unknown") : "unknown",
|
|
218
|
+
permission: typeof item === "object" && item ? (item.permission || null) : null,
|
|
219
|
+
})).filter((item) => safeRef(item.ref)),
|
|
220
|
+
required_relations: unique(children.filter((relation) => REQUIRED_RELATIONS.has(relation.type)).map((relation) => relation.id)),
|
|
221
|
+
optional_relations: unique(children.filter((relation) => !REQUIRED_RELATIONS.has(relation.type)).map((relation) => relation.id)),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function relationPassport(relation) {
|
|
226
|
+
return {
|
|
227
|
+
id: relation.id,
|
|
228
|
+
type: relation.type,
|
|
229
|
+
source: relation.source,
|
|
230
|
+
target: relation.target,
|
|
231
|
+
readiness: relation.readiness || "unknown",
|
|
232
|
+
required: REQUIRED_RELATIONS.has(relation.type),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function scoreObject(object, taskTokens) {
|
|
237
|
+
const tokens = tokenize([object.id, object.kind, object.title, object.summary, ...(object.tags || []), ...(object.aliases || [])].join(" "));
|
|
238
|
+
const overlap = taskTokens.filter((token) => tokens.includes(token)).length;
|
|
239
|
+
return Math.min(1, overlap * 0.2 + (object.priority === "required" ? 0.1 : 0));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function receiptDigest(receipt) {
|
|
243
|
+
const clone = { ...receipt };
|
|
244
|
+
delete clone.traversal_digest;
|
|
245
|
+
return digest(clone);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function result(phase, status, extra = {}) {
|
|
249
|
+
return {
|
|
250
|
+
schema_version: RECEIPT_VERSION,
|
|
251
|
+
operation_id: `mirai.project_technology.context.${phase}`,
|
|
252
|
+
operation_mode: "read_only",
|
|
253
|
+
status,
|
|
254
|
+
changed: false,
|
|
255
|
+
blockers: [],
|
|
256
|
+
warnings: [],
|
|
257
|
+
next_action: "none",
|
|
258
|
+
...extra,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function baseReceipt(graph, task) {
|
|
263
|
+
return {
|
|
264
|
+
contract: RECEIPT_CONTRACT,
|
|
265
|
+
contract_version: RECEIPT_VERSION,
|
|
266
|
+
repository_id: graph.manifest?.id || path.basename(graph.repo),
|
|
267
|
+
task: { text: task, digest: digest(task) },
|
|
268
|
+
graph: { revision: graph.revision, digest: graph.graphDigest },
|
|
269
|
+
visited_ids: [],
|
|
270
|
+
expanded_ids: [],
|
|
271
|
+
frontier_ids: [],
|
|
272
|
+
candidates: [],
|
|
273
|
+
nodes: [],
|
|
274
|
+
relations: [],
|
|
275
|
+
selection_history: [],
|
|
276
|
+
blockers: [],
|
|
277
|
+
warnings: [],
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function discoverContext(repository, task, options = {}) {
|
|
282
|
+
const graph = readGraph(repository);
|
|
283
|
+
const text = String(task || "").trim();
|
|
284
|
+
const receipt = baseReceipt(graph, text);
|
|
285
|
+
const taskTokens = tokenize(text);
|
|
286
|
+
const incomingNavigational = new Set(graph.relations.filter((relation) => !["hands_off_to", "conflicts_with"].includes(relation.type)).map((relation) => relation.target));
|
|
287
|
+
let roots = [...graph.objects.keys()].filter((id) => !incomingNavigational.has(id));
|
|
288
|
+
if (roots.length === 0) roots = [...graph.objects.keys()];
|
|
289
|
+
const directScores = new Map([...graph.objects.entries()].map(([id, record]) => [id, scoreObject(record.value, taskTokens)]));
|
|
290
|
+
const descendantScore = (root) => {
|
|
291
|
+
const queue = [root]; const seen = new Set(); let best = 0;
|
|
292
|
+
while (queue.length) {
|
|
293
|
+
const id = queue.shift(); if (seen.has(id)) continue; seen.add(id);
|
|
294
|
+
best = Math.max(best, directScores.get(id) || 0);
|
|
295
|
+
for (const relation of outgoing(graph, id)) if (STRUCTURAL_RELATIONS.has(relation.type)) queue.push(relation.target);
|
|
296
|
+
}
|
|
297
|
+
return best;
|
|
298
|
+
};
|
|
299
|
+
const limit = Math.max(1, Math.min(50, Number(options.maxObjects || options.maxCandidates || 12)));
|
|
300
|
+
const allRanked = roots.map((id) => ({ id, score: Math.max(directScores.get(id) || 0, descendantScore(id)) }))
|
|
301
|
+
.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
302
|
+
const positive = allRanked.filter((item) => item.score > 0);
|
|
303
|
+
const ranked = (positive.length ? positive : allRanked).slice(0, limit);
|
|
304
|
+
if (!positive.length && ranked.length) receipt.warnings.push("no_lexical_match_candidates_unranked");
|
|
305
|
+
receipt.candidates = ranked.map((item) => ({ id: item.id, score: item.score }));
|
|
306
|
+
receipt.visited_ids = ranked.map((item) => item.id).sort();
|
|
307
|
+
receipt.frontier_ids = receipt.visited_ids.filter((id) => nodePassport(graph, id)?.expandable);
|
|
308
|
+
receipt.nodes = receipt.visited_ids.map((id) => nodePassport(graph, id));
|
|
309
|
+
const extension = graph.manifest?.extensions?.["mirai.project_technology"];
|
|
310
|
+
const extensionReady = extension?.enabled === true && extension.contract_version === "1.0.0" && extension.context_policy === "task_scoped" && extension.source_boundary === "hybrid_sot";
|
|
311
|
+
receipt.blockers = unique([...graph.blockers, ...(extensionReady ? [] : ["project_technology_not_enabled"]), ...(ranked.length ? [] : ["no_verifiable_runtime_objects"])]);
|
|
312
|
+
receipt.traversal_digest = receiptDigest(receipt);
|
|
313
|
+
return result("discover", receipt.blockers.length ? "blocked" : "success", {
|
|
314
|
+
repository_id: receipt.repository_id,
|
|
315
|
+
traversal_receipt: receipt,
|
|
316
|
+
blockers: receipt.blockers,
|
|
317
|
+
next_action: receipt.blockers.length ? "repair the graph blockers" : "select and expand relevant candidates",
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function validateReceipt(graph, receipt) {
|
|
322
|
+
const blockers = [];
|
|
323
|
+
if (!receipt || receipt.contract !== RECEIPT_CONTRACT || receipt.contract_version !== RECEIPT_VERSION) blockers.push("context_traversal_receipt_invalid");
|
|
324
|
+
if (receipt?.repository_id !== graph.manifest?.id) blockers.push("context_repository_mismatch");
|
|
325
|
+
if (receipt?.graph?.revision !== graph.revision) blockers.push("context_graph_revision_mismatch");
|
|
326
|
+
if (receipt?.graph?.digest !== graph.graphDigest) blockers.push("context_graph_digest_mismatch");
|
|
327
|
+
if (receipt?.traversal_digest !== receiptDigest(receipt || {})) blockers.push("context_traversal_digest_mismatch");
|
|
328
|
+
return unique([...graph.blockers, ...blockers]);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function expandContext(repository, traversalReceipt, selectedIds, options = {}) {
|
|
332
|
+
const graph = readGraph(repository);
|
|
333
|
+
const receipt = JSON.parse(JSON.stringify(traversalReceipt || {}));
|
|
334
|
+
const blockers = validateReceipt(graph, receipt);
|
|
335
|
+
const selected = unique(Array.isArray(selectedIds) ? selectedIds : [selectedIds]);
|
|
336
|
+
for (const id of selected) {
|
|
337
|
+
if (!graph.objects.has(id)) { blockers.push("selected_context_node_missing"); continue; }
|
|
338
|
+
if (!(receipt.visited_ids || []).includes(id)) { blockers.push("selected_context_node_not_discovered"); continue; }
|
|
339
|
+
const children = outgoing(graph, id);
|
|
340
|
+
const alreadyExpanded = (receipt.expanded_ids || []).includes(id);
|
|
341
|
+
receipt.expanded_ids = unique([...(receipt.expanded_ids || []), id]);
|
|
342
|
+
receipt.visited_ids = unique([...(receipt.visited_ids || []), ...children.map((relation) => relation.target)]);
|
|
343
|
+
if (!alreadyExpanded) receipt.selection_history = [...(receipt.selection_history || []), {
|
|
344
|
+
selector: options.selector || "ai",
|
|
345
|
+
selected_ids: [id],
|
|
346
|
+
reason: options.reason || "selected for deeper context",
|
|
347
|
+
confidence: Number(options.confidence ?? 1),
|
|
348
|
+
}];
|
|
349
|
+
}
|
|
350
|
+
receipt.nodes = receipt.visited_ids.map((id) => nodePassport(graph, id)).filter(Boolean);
|
|
351
|
+
const visible = new Set(receipt.visited_ids);
|
|
352
|
+
receipt.relations = graph.relations.filter((relation) => visible.has(relation.source) && visible.has(relation.target)).map(relationPassport).sort((a, b) => a.id.localeCompare(b.id));
|
|
353
|
+
receipt.frontier_ids = receipt.visited_ids.filter((id) => nodePassport(graph, id)?.expandable && !receipt.expanded_ids.includes(id));
|
|
354
|
+
receipt.blockers = unique(blockers);
|
|
355
|
+
receipt.traversal_digest = receiptDigest(receipt);
|
|
356
|
+
return result("expand", receipt.blockers.length ? "blocked" : "success", {
|
|
357
|
+
repository_id: receipt.repository_id,
|
|
358
|
+
traversal_receipt: receipt,
|
|
359
|
+
blockers: receipt.blockers,
|
|
360
|
+
next_action: receipt.blockers.length ? "repair the traversal blockers" : "expand remaining selected branches or compile a selection",
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function normalizeSelection(selection, receipt) {
|
|
365
|
+
const source = selection && typeof selection === "object" ? selection : {};
|
|
366
|
+
return {
|
|
367
|
+
selector: ["ai", "human", "rule"].includes(source.selector) ? source.selector : "ai",
|
|
368
|
+
task_digest: source.task_digest || receipt.task?.digest || null,
|
|
369
|
+
graph_digest: source.graph_digest || receipt.graph?.digest || null,
|
|
370
|
+
result_owner_id: source.result_owner_id || null,
|
|
371
|
+
selected: (Array.isArray(source.selected) ? source.selected : []).map((item) => typeof item === "string" ? { id: item, reason: "selected", confidence: 1 } : item)
|
|
372
|
+
.filter((item) => item && typeof item.id === "string").sort((a, b) => a.id.localeCompare(b.id)),
|
|
373
|
+
rejected: (Array.isArray(source.rejected) ? source.rejected : []).map((item) => typeof item === "string" ? { id: item, reason: "not applicable" } : item)
|
|
374
|
+
.filter((item) => item && typeof item.id === "string").sort((a, b) => a.id.localeCompare(b.id)),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function requiredClosure(graph, initial) {
|
|
379
|
+
const closure = new Set(initial); const queue = [...initial]; const relations = [];
|
|
380
|
+
while (queue.length) {
|
|
381
|
+
const id = queue.shift();
|
|
382
|
+
for (const relation of outgoing(graph, id)) {
|
|
383
|
+
if (!REQUIRED_RELATIONS.has(relation.type)) continue;
|
|
384
|
+
relations.push(relation.id);
|
|
385
|
+
if (!closure.has(relation.target)) { closure.add(relation.target); queue.push(relation.target); }
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return { ids: [...closure].sort(), relationIds: unique(relations) };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function sourceClosure(graph, initial) {
|
|
392
|
+
const closure = new Set(initial); const queue = [...initial]; const relations = [];
|
|
393
|
+
while (queue.length) {
|
|
394
|
+
const id = queue.shift();
|
|
395
|
+
for (const relation of outgoing(graph, id)) {
|
|
396
|
+
const targetKind = graph.objects.get(relation.target)?.value.kind;
|
|
397
|
+
const structuralSource = STRUCTURAL_RELATIONS.has(relation.type) && ["resource", "source"].includes(targetKind);
|
|
398
|
+
if (!SOURCE_RELATIONS.has(relation.type) && !structuralSource) continue;
|
|
399
|
+
relations.push(relation.id);
|
|
400
|
+
if (!closure.has(relation.target)) { closure.add(relation.target); queue.push(relation.target); }
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return { ids: [...closure].sort(), relationIds: unique(relations) };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function requiredCycle(graph, ids) {
|
|
407
|
+
const allowed = new Set(ids); const visiting = new Set(); const visited = new Set();
|
|
408
|
+
function visit(id) {
|
|
409
|
+
if (visiting.has(id)) return true;
|
|
410
|
+
if (visited.has(id)) return false;
|
|
411
|
+
visiting.add(id);
|
|
412
|
+
for (const relation of outgoing(graph, id)) if (REQUIRED_RELATIONS.has(relation.type) && allowed.has(relation.target) && visit(relation.target)) return true;
|
|
413
|
+
visiting.delete(id); visited.add(id); return false;
|
|
414
|
+
}
|
|
415
|
+
return ids.some(visit);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function compileContext(repository, traversalReceipt, selectionInput, options = {}) {
|
|
419
|
+
const graph = readGraph(repository);
|
|
420
|
+
const receipt = traversalReceipt || {};
|
|
421
|
+
const blockers = validateReceipt(graph, receipt);
|
|
422
|
+
const selection = normalizeSelection(selectionInput, receipt);
|
|
423
|
+
const selectedIds = unique(selection.selected.map((item) => item.id));
|
|
424
|
+
const rejectedIds = new Set(selection.rejected.map((item) => item.id));
|
|
425
|
+
if (selection.task_digest !== receipt.task?.digest) blockers.push("selection_task_digest_mismatch");
|
|
426
|
+
if (selection.graph_digest !== graph.graphDigest) blockers.push("selection_graph_digest_mismatch");
|
|
427
|
+
if (!selectedIds.length) blockers.push("context_selection_empty");
|
|
428
|
+
if (!selection.result_owner_id) blockers.push("result_owner_missing");
|
|
429
|
+
for (const item of selection.selected) {
|
|
430
|
+
if (!graph.objects.has(item.id) || !receipt.visited_ids?.includes(item.id)) blockers.push("selected_context_node_not_discovered");
|
|
431
|
+
if (!item.reason || !Number.isFinite(Number(item.confidence))) blockers.push("selected_context_reason_or_confidence_missing");
|
|
432
|
+
}
|
|
433
|
+
for (const item of selection.rejected) if (!item.reason) blockers.push("rejected_context_reason_missing");
|
|
434
|
+
const required = requiredClosure(graph, selectedIds);
|
|
435
|
+
const sources = sourceClosure(graph, required.ids);
|
|
436
|
+
const closure = { ids: sources.ids, relationIds: unique([...required.relationIds, ...sources.relationIds]) };
|
|
437
|
+
const missingRequired = closure.ids.filter((id) => !receipt.visited_ids?.includes(id));
|
|
438
|
+
if (missingRequired.length) blockers.push("required_context_not_discovered");
|
|
439
|
+
if (requiredCycle(graph, required.ids)) blockers.push("required_context_cycle");
|
|
440
|
+
const included = new Set(closure.ids);
|
|
441
|
+
if (selection.result_owner_id && !closure.ids.some((id) => {
|
|
442
|
+
const node = nodePassport(graph, id);
|
|
443
|
+
return id === selection.result_owner_id || node?.owner === selection.result_owner_id;
|
|
444
|
+
})) blockers.push("result_owner_not_in_selected_context");
|
|
445
|
+
const unresolvedBranches = [];
|
|
446
|
+
for (const id of closure.ids) {
|
|
447
|
+
const passport = nodePassport(graph, id);
|
|
448
|
+
if (!passport) continue;
|
|
449
|
+
if (!ACTIVE_READINESS.has(passport.readiness)) blockers.push(`context_node_${passport.readiness}`);
|
|
450
|
+
if (passport.omitted_source_ref_count > 0) blockers.push("unsafe_source_reference_omitted");
|
|
451
|
+
if (passport.sensitive_metadata_detected) blockers.push("context_node_sensitive_metadata");
|
|
452
|
+
if (passport.expansion_policy === "terminal" && passport.expandable) blockers.push("terminal_node_has_children");
|
|
453
|
+
if (passport.expansion_policy === "expandable" && !passport.expandable) blockers.push("expandable_node_has_no_children");
|
|
454
|
+
for (const source of passport.source_refs) if (source.availability === "unavailable") blockers.push("required_source_unavailable");
|
|
455
|
+
for (const source of passport.source_refs) if (source.availability === "unbound") blockers.push("required_source_not_revision_bound");
|
|
456
|
+
for (const access of passport.access_requirements) if (!["available", "granted", "not_required"].includes(access.status)) blockers.push("context_access_not_available");
|
|
457
|
+
if (!passport.expandable) continue;
|
|
458
|
+
if (!receipt.expanded_ids?.includes(id)) { unresolvedBranches.push(id); continue; }
|
|
459
|
+
for (const relation of outgoing(graph, id).filter((item) => STRUCTURAL_RELATIONS.has(item.type))) {
|
|
460
|
+
if (included.has(relation.target) || rejectedIds.has(relation.target)) continue;
|
|
461
|
+
unresolvedBranches.push(relation.target);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (unresolvedBranches.length) blockers.push("context_branch_decision_incomplete");
|
|
465
|
+
for (const relation of graph.relations) {
|
|
466
|
+
if (relation.type === "conflicts_with" && included.has(relation.source) && included.has(relation.target)) blockers.push("context_selection_conflict");
|
|
467
|
+
}
|
|
468
|
+
const includedIds = [...included].sort();
|
|
469
|
+
const includedNodes = includedIds.map((id) => nodePassport(graph, id)).filter(Boolean);
|
|
470
|
+
const includedRelations = graph.relations.filter((relation) => included.has(relation.source) && included.has(relation.target)).map(relationPassport).sort((a, b) => a.id.localeCompare(b.id));
|
|
471
|
+
for (const relation of includedRelations) if (!ACTIVE_READINESS.has(String(relation.readiness).toLowerCase())) blockers.push(`context_relation_${String(relation.readiness).toLowerCase()}`);
|
|
472
|
+
const terminalSourceMap = new Map();
|
|
473
|
+
for (const node of includedNodes.filter((item) => !item.expandable || ["resource", "source"].includes(item.kind))) {
|
|
474
|
+
for (const source of node.source_refs) {
|
|
475
|
+
const key = `${source.ref}:${source.sha256 || ""}:${source.revision || ""}`;
|
|
476
|
+
const current = terminalSourceMap.get(key) || { ...source, node_ids: [] };
|
|
477
|
+
current.node_ids = unique([...current.node_ids, node.id]);
|
|
478
|
+
terminalSourceMap.set(key, current);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const terminalSources = [...terminalSourceMap.values()].sort((a, b) => a.ref.localeCompare(b.ref) || String(a.sha256).localeCompare(String(b.sha256)));
|
|
482
|
+
for (const source of terminalSources) if (!source.sha256 || !source.revision) blockers.push("required_source_not_hash_bound");
|
|
483
|
+
const contextBudget = Number(options.contextBudget || options.maxBytes || 256 * 1024);
|
|
484
|
+
const base = {
|
|
485
|
+
id: `context_pack.${digest(`${receipt.task?.digest}:${graph.graphDigest}`).slice(7, 23)}`,
|
|
486
|
+
task_id: receipt.task?.digest,
|
|
487
|
+
task_digest: receipt.task?.digest,
|
|
488
|
+
graph_digest: graph.graphDigest,
|
|
489
|
+
graph_revision: graph.revision,
|
|
490
|
+
source_graph: graph.manifest?.id,
|
|
491
|
+
generated_at: graph.revision ? `revision:${graph.revision}` : "revision:unavailable",
|
|
492
|
+
selected_paths: selection.selected,
|
|
493
|
+
rejected_paths: selection.rejected,
|
|
494
|
+
visited_nodes: unique(receipt.visited_ids || []),
|
|
495
|
+
stop_reasons: includedNodes.filter((node) => !node.expandable).map((node) => ({ id: node.id, reason: "terminal" })),
|
|
496
|
+
required_closure: { object_ids: includedIds, relation_ids: closure.relationIds },
|
|
497
|
+
terminal_sources: terminalSources,
|
|
498
|
+
processes: includedNodes.filter((node) => node.kind === "process").map((node) => node.id),
|
|
499
|
+
constraints: includedNodes.filter((node) => node.kind === "constraint").map((node) => node.id),
|
|
500
|
+
validators: includedNodes.filter((node) => ["check", "gate", "validator"].includes(node.kind)).map((node) => node.id),
|
|
501
|
+
included_objects: includedIds,
|
|
502
|
+
included_relations: includedRelations.map((relation) => relation.id),
|
|
503
|
+
evidence: unique(terminalSources.map((source) => source.ref)),
|
|
504
|
+
selection: { method: "client_selection_with_required_closure", selector: selection.selector, result_owner_id: selection.result_owner_id, task_tokens: tokenize(receipt.task?.text), object_explanations: selection.selected.map((item) => ({ id: item.id, relevance_score: Number(item.confidence), reasons: [item.reason] })), relation_explanations: includedRelations.map((relation) => ({ id: relation.id, reason: relation.required ? "required closure" : "selected path", source: relation.source, target: relation.target })) },
|
|
505
|
+
completeness: { status: "ready", unresolved_required_ids: unique(missingRequired), unresolved_branch_ids: unique(unresolvedBranches) },
|
|
506
|
+
omissions: selection.rejected.map((item) => ({ id: item.id, reason: item.reason })),
|
|
507
|
+
limitations: [],
|
|
508
|
+
runtime_contract: { generated_context_authorizes_write: false, raw_source_authoritative: true },
|
|
509
|
+
};
|
|
510
|
+
if (Buffer.byteLength(canonicalBytes(base)) > contextBudget) blockers.push("context_budget_insufficient_for_required_closure");
|
|
511
|
+
const uniqueBlockers = unique(blockers);
|
|
512
|
+
const decisionBlockers = new Set(["context_selection_empty", "result_owner_missing", "context_branch_decision_incomplete", "selected_context_reason_or_confidence_missing", "rejected_context_reason_missing"]);
|
|
513
|
+
const discoveryBlockers = new Set(["required_context_not_discovered"]);
|
|
514
|
+
let status = "ready";
|
|
515
|
+
if (uniqueBlockers.some((item) => discoveryBlockers.has(item))) status = "needs_more_discovery";
|
|
516
|
+
else if (uniqueBlockers.some((item) => decisionBlockers.has(item))) status = "needs_decision";
|
|
517
|
+
else if (uniqueBlockers.length) status = "blocked";
|
|
518
|
+
base.completeness.status = status;
|
|
519
|
+
base.limitations = uniqueBlockers;
|
|
520
|
+
base.context_pack_digest = digest(base);
|
|
521
|
+
return result("compile", status, {
|
|
522
|
+
repository_id: graph.manifest?.id,
|
|
523
|
+
context_pack: base,
|
|
524
|
+
blockers: uniqueBlockers,
|
|
525
|
+
next_action: status === "ready" ? "use the immutable context pack and record usage evidence" : status === "needs_more_discovery" ? "expand the missing required branches" : status === "needs_decision" ? "complete the bounded selection" : "repair the blocking graph or source condition",
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function secretLike(value) {
|
|
530
|
+
const text = JSON.stringify(value || {});
|
|
531
|
+
return /(?:ghp_|github_pat_|bearer\s+)[a-z0-9_-]{12,}/i.test(text)
|
|
532
|
+
|| /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/.test(text)
|
|
533
|
+
|| /(?:^|["'\s/])\.env(?:["'\s/]|$)/i.test(text);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function verifyContext(repository, contextPack, usageEvidence, options = {}) {
|
|
537
|
+
const graph = readGraph(repository);
|
|
538
|
+
const blockers = [...graph.blockers];
|
|
539
|
+
if (!contextPack || contextPack.context_pack_digest !== digest(Object.fromEntries(Object.entries(contextPack || {}).filter(([key]) => key !== "context_pack_digest")))) blockers.push("context_pack_digest_mismatch");
|
|
540
|
+
if (contextPack?.graph_digest !== graph.graphDigest || contextPack?.graph_revision !== graph.revision) blockers.push("context_pack_graph_mismatch");
|
|
541
|
+
if (contextPack?.completeness?.status !== "ready") blockers.push("context_pack_not_ready");
|
|
542
|
+
if (contextPack?.runtime_contract?.generated_context_authorizes_write !== false) blockers.push("generated_context_write_authority_forbidden");
|
|
543
|
+
const evidence = Array.isArray(usageEvidence) ? usageEvidence : usageEvidence?.usage || [];
|
|
544
|
+
if (!Array.isArray(evidence) || evidence.length === 0) blockers.push("usage_evidence_missing");
|
|
545
|
+
const coveredSources = new Set(); const coveredValidators = new Set(); const coveredProcesses = new Set();
|
|
546
|
+
for (const item of Array.isArray(evidence) ? evidence : []) {
|
|
547
|
+
if (!item || typeof item !== "object" || item.context_pack_digest !== contextPack?.context_pack_digest) { blockers.push("usage_evidence_context_pack_mismatch"); continue; }
|
|
548
|
+
for (const field of ["source", "applied_rule", "decision", "surface", "validator", "outcome"]) if (!item[field]) blockers.push(`usage_evidence_${field}_missing`);
|
|
549
|
+
if (item.source) coveredSources.add(item.source);
|
|
550
|
+
if (item.validator) coveredValidators.add(item.validator);
|
|
551
|
+
if (item.process) coveredProcesses.add(item.process);
|
|
552
|
+
if (item.applied_rule) coveredProcesses.add(item.applied_rule);
|
|
553
|
+
if (secretLike(item)) blockers.push("usage_evidence_contains_sensitive_data");
|
|
554
|
+
}
|
|
555
|
+
for (const source of contextPack?.terminal_sources || []) if (!coveredSources.has(source.ref)) blockers.push("mandatory_source_not_applied");
|
|
556
|
+
for (const source of contextPack?.terminal_sources || []) {
|
|
557
|
+
if (!safeRef(source.ref)) { blockers.push("context_pack_source_ref_unsafe"); continue; }
|
|
558
|
+
if (/^(repo|https):\/\//.test(source.ref)) continue;
|
|
559
|
+
const absolute = path.join(graph.repo, source.ref);
|
|
560
|
+
if (!fs.existsSync(absolute) || fs.lstatSync(absolute).isSymbolicLink() || !fs.statSync(absolute).isFile()) blockers.push("context_pack_source_unavailable");
|
|
561
|
+
else {
|
|
562
|
+
const blob = gitBlob(graph.repo, source.ref);
|
|
563
|
+
if (!blob || !revisionBound(graph.repo, source.ref) || digest(blob) !== source.sha256 || source.revision !== graph.revision) blockers.push("context_pack_source_revision_or_digest_mismatch");
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
for (const process of contextPack?.processes || []) if (!coveredProcesses.has(process)) blockers.push("mandatory_process_not_applied");
|
|
567
|
+
for (const validator of contextPack?.validators || []) if (!coveredValidators.has(validator)) blockers.push("mandatory_validator_not_applied");
|
|
568
|
+
if (secretLike(contextPack)) blockers.push("context_pack_contains_sensitive_data");
|
|
569
|
+
const uniqueBlockers = unique(blockers);
|
|
570
|
+
return result("verify", uniqueBlockers.length ? "blocked" : "success", {
|
|
571
|
+
repository_id: graph.manifest?.id,
|
|
572
|
+
context_pack_digest: contextPack?.context_pack_digest || null,
|
|
573
|
+
usage_evidence_digest: evidence.length ? digest(evidence) : null,
|
|
574
|
+
blockers: uniqueBlockers,
|
|
575
|
+
next_action: uniqueBlockers.length ? "repair the context or usage evidence" : "none",
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
module.exports = {
|
|
580
|
+
RECEIPT_CONTRACT,
|
|
581
|
+
REQUIRED_RELATIONS,
|
|
582
|
+
SUPPORTED_RELATIONS,
|
|
583
|
+
compileContext,
|
|
584
|
+
digest,
|
|
585
|
+
discoverContext,
|
|
586
|
+
expandContext,
|
|
587
|
+
readGraph,
|
|
588
|
+
verifyContext,
|
|
589
|
+
};
|