mirai-graph 1.4.0 → 1.6.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 +3 -0
- package/CHANGELOG.md +36 -0
- package/CITATION.cff +2 -2
- package/README.md +13 -8
- package/package.json +2 -2
- package/packages/cli/graph-manifest.js +8 -1
- package/packages/cli/project-technology.js +19 -0
- package/packages/cli/validate-local-target.js +149 -0
- package/packages/cli/validate-provider-archive.js +165 -0
- package/packages/project-technology/continuity.js +2 -0
- package/packages/project-technology/index.js +332 -15
- package/releases/1.5.0.md +37 -0
- package/releases/1.6.0.md +43 -0
- package/releases/README.md +4 -0
- package/schemas/project-technology-extension.schema.json +10 -1
- package/schemas/project-technology-target-export.schema.json +1 -0
- package/standard/project-technology.md +122 -0
|
@@ -36,7 +36,7 @@ const TARGET_EXPORT_KEYS = new Set([
|
|
|
36
36
|
"schema_version", "target_id", "semantic_digest", "provider_revision",
|
|
37
37
|
"decision_refs", "goal_binding", "requirement_bindings", "constraint_ids",
|
|
38
38
|
"non_goal_ids", "deferred_boundary_ids", "allowed_change_scope",
|
|
39
|
-
"architecture_contract", "execution_contract_digest",
|
|
39
|
+
"architecture_contract", "execution_contract_digest", "provider_graph_id",
|
|
40
40
|
]);
|
|
41
41
|
const SECRET_PARTS = [".env", "credential", "secret", "token", "password", "private-key", "id_rsa", ".pem", ".p12"];
|
|
42
42
|
const EXCLUDED_PARTS = new Set([".git", ".mirai-graph", ".simai", "node_modules", "vendor", "dist", "build", "coverage", "generated"]);
|
|
@@ -425,6 +425,7 @@ function readExport(filePath) {
|
|
|
425
425
|
try { payload = JSON.parse(bytes.toString("utf8").replace(/^\uFEFF/, "")); } catch (_) { return { export: {}, blockers: ["provider_export_invalid"] }; }
|
|
426
426
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return { export: {}, blockers: ["provider_export_invalid"] };
|
|
427
427
|
if (Object.keys(payload).some((key) => !TARGET_EXPORT_KEYS.has(key))) blockers.push("provider_export_not_bounded");
|
|
428
|
+
if (Object.hasOwn(payload, "provider_graph_id") && (typeof payload.provider_graph_id !== "string" || !REF_RE.test(payload.provider_graph_id))) blockers.push("provider_graph_id_invalid");
|
|
428
429
|
const identity = bindingValues(payload); blockers.push(...identity.blockers);
|
|
429
430
|
const executionFields = [
|
|
430
431
|
"decision_refs", "goal_binding", "requirement_bindings", "constraint_ids",
|
|
@@ -435,7 +436,7 @@ function readExport(filePath) {
|
|
|
435
436
|
const digest = sha256(canonicalBytes(normalized.contract));
|
|
436
437
|
if (!payload.execution_contract_digest) blockers.push("provider_execution_contract_digest_missing");
|
|
437
438
|
else if (payload.execution_contract_digest !== digest) blockers.push("provider_execution_contract_digest_mismatch");
|
|
438
|
-
return { export: { schema_version: "1.0.0", ...identity.values, ...normalized.contract, execution_contract_digest: digest }, blockers: [...new Set(blockers)].sort(), raw_sha256: sha256(bytes) };
|
|
439
|
+
return { export: { schema_version: "1.0.0", ...identity.values, ...normalized.contract, execution_contract_digest: digest, ...(payload.provider_graph_id ? { provider_graph_id: payload.provider_graph_id } : {}) }, blockers: [...new Set(blockers)].sort(), raw_sha256: sha256(bytes) };
|
|
439
440
|
}
|
|
440
441
|
|
|
441
442
|
function trackedFiles(repo) {
|
|
@@ -453,7 +454,42 @@ function safeTrackedFile(relative) {
|
|
|
453
454
|
|
|
454
455
|
function inventory(repo) {
|
|
455
456
|
const entries = [];
|
|
456
|
-
|
|
457
|
+
const blockers = [];
|
|
458
|
+
let files = trackedFiles(repo);
|
|
459
|
+
const revision = git(repo, "rev-parse", "--verify", "HEAD^{commit}") || null;
|
|
460
|
+
if (!revision) {
|
|
461
|
+
if (fs.existsSync(path.join(repo, ".git")) && !unbornGitBranch(repo)) blockers.push("inventory_git_unavailable");
|
|
462
|
+
else {
|
|
463
|
+
// Ordinary folders and immutable distributions have no Git index. Use
|
|
464
|
+
// only explicitly declared graph/raw sources, never scan arbitrary data.
|
|
465
|
+
const graph = readManifest(repo).manifest?.graph || {};
|
|
466
|
+
const refs = ["graph.json", ...(graph.source_of_truth || []), ...(graph.objects || []),
|
|
467
|
+
...(graph.relations || []), ...(graph.schemas || []), ...(graph.raw_sources || [])];
|
|
468
|
+
const found = new Set();
|
|
469
|
+
const visited = new Set();
|
|
470
|
+
function collect(relative) {
|
|
471
|
+
if (typeof relative !== "string" || !relative || path.isAbsolute(relative) || relative.includes("\\") || relative.includes(":") || relative.split("/").includes("..") || /[?*\[\]]/.test(relative)) {
|
|
472
|
+
blockers.push("inventory_declared_source_unsafe_or_unsupported"); return;
|
|
473
|
+
}
|
|
474
|
+
if (!safeTrackedFile(relative)) return;
|
|
475
|
+
if (visited.has(relative)) return;
|
|
476
|
+
visited.add(relative);
|
|
477
|
+
if (visited.size > 10000) { blockers.push("inventory_declared_source_budget_exceeded"); return; }
|
|
478
|
+
const absolute = path.join(repo, relative);
|
|
479
|
+
if (!fs.existsSync(absolute)) { blockers.push("inventory_declared_source_missing"); return; }
|
|
480
|
+
if (fs.lstatSync(absolute).isSymbolicLink() || !fs.realpathSync(absolute).startsWith(`${fs.realpathSync(repo)}${path.sep}`)) {
|
|
481
|
+
blockers.push("inventory_declared_source_unsafe_or_unsupported"); return;
|
|
482
|
+
}
|
|
483
|
+
if (fs.statSync(absolute).isDirectory()) {
|
|
484
|
+
for (const name of fs.readdirSync(absolute).sort()) collect(`${relative.replace(/\/$/, "")}/${name}`);
|
|
485
|
+
} else if (fs.statSync(absolute).isFile()) found.add(relative);
|
|
486
|
+
else blockers.push("inventory_declared_source_unsafe_or_unsupported");
|
|
487
|
+
}
|
|
488
|
+
refs.forEach(collect);
|
|
489
|
+
files = [...found];
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
for (const relative of files.filter(safeTrackedFile).sort()) {
|
|
457
493
|
const absolute = path.join(repo, relative);
|
|
458
494
|
if (!fs.existsSync(absolute) || fs.lstatSync(absolute).isSymbolicLink() || !fs.statSync(absolute).isFile()) continue;
|
|
459
495
|
const bytes = fs.readFileSync(absolute);
|
|
@@ -462,14 +498,28 @@ function inventory(repo) {
|
|
|
462
498
|
const payload = {
|
|
463
499
|
schema_version: "1.0.0",
|
|
464
500
|
repository_id: readManifest(repo).manifest?.id || path.basename(repo),
|
|
465
|
-
revision
|
|
501
|
+
revision,
|
|
466
502
|
files: entries,
|
|
467
503
|
};
|
|
468
504
|
payload.inventory_digest = sha256(canonicalBytes(payload), true);
|
|
505
|
+
if (blockers.length) payload.blockers = [...new Set(blockers)].sort();
|
|
469
506
|
return payload;
|
|
470
507
|
}
|
|
471
508
|
|
|
472
|
-
|
|
509
|
+
// A valid new repository has no HEAD commit yet. It may inventory declared
|
|
510
|
+
// sources, but must never impersonate a revision-bound provider. Missing tools,
|
|
511
|
+
// corrupt refs/index and detached invalid HEAD are not an unborn branch.
|
|
512
|
+
function unbornGitBranch(repo) {
|
|
513
|
+
const options = { cwd: repo, encoding: "utf8", env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } };
|
|
514
|
+
const branch = spawnSync("git", ["symbolic-ref", "--quiet", "HEAD"], options);
|
|
515
|
+
const ref = (branch.stdout || "").trim();
|
|
516
|
+
if (branch.status !== 0 || !ref.startsWith("refs/heads/")) return false;
|
|
517
|
+
const existing = spawnSync("git", ["show-ref", "--verify", "--quiet", ref], options);
|
|
518
|
+
if (existing.status !== 1 || existing.stderr) return false;
|
|
519
|
+
return spawnSync("git", ["status", "--porcelain=v1", "--untracked-files=no"], options).status === 0;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function contextTraversal(repoArg, options = {}) {
|
|
473
523
|
if (options.phase === "expand") return traversal.expandContext(repoArg, options.traversalReceipt, options.selectedIds, options);
|
|
474
524
|
if (options.phase === "compile") return traversal.compileContext(repoArg, options.traversalReceipt, options.selection, options);
|
|
475
525
|
if (options.phase === "verify") return traversal.verifyContext(repoArg, options.contextPack, options.usageEvidence, options);
|
|
@@ -492,10 +542,147 @@ function context(repoArg, options = {}) {
|
|
|
492
542
|
};
|
|
493
543
|
}
|
|
494
544
|
|
|
545
|
+
function context(repoArg, options = {}) {
|
|
546
|
+
const output = contextTraversal(repoArg, options);
|
|
547
|
+
const binding = targetBindingStatus(normalizeRepo(repoArg), options);
|
|
548
|
+
const blockers = [...(output.blockers || [])];
|
|
549
|
+
if (options.significantWork && binding.status !== "ready") blockers.push(...binding.blockers, "accepted_target_binding_required_for_significant_work");
|
|
550
|
+
if (options.significantWork && binding.source_kind === "local") blockers.push(...localVerificationBlockers(normalizeRepo(repoArg), options));
|
|
551
|
+
return { ...output, target_binding: binding, blockers: [...new Set(blockers)].sort(),
|
|
552
|
+
status: blockers.length ? "blocked" : output.status };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function localVerificationBlockers(repo, options) {
|
|
556
|
+
const current = continuity.status(repo, readManifest(repo).manifest, options);
|
|
557
|
+
if (!current.terminal_receipt) return ["local_target_verification_missing"];
|
|
558
|
+
return current.terminal_receipt.current_graph_digest === current.graph_digest ? [] : ["local_target_verification_stale"];
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Resolve a local target from the same canonical object reader used by context.
|
|
562
|
+
// Approval origin is a caller responsibility, just like archive-provider trust.
|
|
563
|
+
// Never infer that trust from the decision file or persist it as approval.
|
|
564
|
+
function localTargetStatus(repo, manifest, selection, options = {}) {
|
|
565
|
+
const blockers = [];
|
|
566
|
+
const blocked = () => ({ status: "blocked", enabled: true, source_kind: "local", blockers: [...new Set(blockers)].sort(), next_action: "review the local target and verify its owner acceptance" });
|
|
567
|
+
if (!selection || selection.kind !== "local" || Object.keys(selection).some(k => !["kind", "target_id", "acceptance_ref"].includes(k)) ||
|
|
568
|
+
!REF_RE.test(selection.target_id || "") || !REF_RE.test(selection.acceptance_ref || "")) {
|
|
569
|
+
blockers.push("local_target_selection_invalid"); return blocked();
|
|
570
|
+
}
|
|
571
|
+
const graph = traversal.readGraph(repo);
|
|
572
|
+
blockers.push(...graph.blockers);
|
|
573
|
+
if (!(graph.objects instanceof Map)) { blockers.push("local_target_graph_invalid"); return blocked(); }
|
|
574
|
+
const target = graph.objects.get(selection.target_id)?.value;
|
|
575
|
+
const decision = graph.objects.get(selection.acceptance_ref)?.value;
|
|
576
|
+
const decisionRecord = graph.objects.get(selection.acceptance_ref);
|
|
577
|
+
if (decisionRecord && (!decisionRecord.relative.startsWith("graph/specs/") || !localSafeFile(repo, decisionRecord.relative))) blockers.push("local_target_acceptance_source_unsafe");
|
|
578
|
+
if (!target || !["contract", "system"].includes(target.tz_role)) { blockers.push("local_target_missing_or_invalid"); return blocked(); }
|
|
579
|
+
const normalized = normalizeExecutionContract(target.provider_execution_contract);
|
|
580
|
+
blockers.push(...normalized.blockers);
|
|
581
|
+
const contract = normalized.contract;
|
|
582
|
+
const accepted = obj => obj && ACCEPTED_LIFECYCLES.has(obj.lifecycle) &&
|
|
583
|
+
(!obj.readiness || ACCEPTED_LIFECYCLES.has(obj.readiness));
|
|
584
|
+
if (!accepted(target)) blockers.push("local_target_not_accepted");
|
|
585
|
+
const refs = new Set([selection.target_id, contract.goal_binding?.goal_id,
|
|
586
|
+
...(contract.goal_binding?.done_when_ids || []), ...(contract.constraint_ids || []),
|
|
587
|
+
...(contract.non_goal_ids || []), ...(contract.deferred_boundary_ids || []),
|
|
588
|
+
...(contract.decision_refs || []), contract.architecture_contract?.contract_ref]);
|
|
589
|
+
for (const item of contract.requirement_bindings || []) {
|
|
590
|
+
refs.add(item.requirement_id);
|
|
591
|
+
for (const id of [...item.acceptance_ids, ...item.done_when_ids]) refs.add(id);
|
|
592
|
+
}
|
|
593
|
+
// The approval object cannot participate in the digest it signs.
|
|
594
|
+
refs.delete(selection.acceptance_ref); refs.delete(undefined);
|
|
595
|
+
const active = new Set(); const visited = new Set();
|
|
596
|
+
function visit(id) {
|
|
597
|
+
if (active.has(id)) { blockers.push("local_target_required_cycle"); return; }
|
|
598
|
+
if (visited.has(id)) return;
|
|
599
|
+
visited.add(id); active.add(id);
|
|
600
|
+
for (const relation of graph.relations.filter(r => r.source === id && traversal.REQUIRED_RELATIONS.has(r.type))) {
|
|
601
|
+
if (!ACCEPTED_LIFECYCLES.has(relation.lifecycle || relation.readiness)) blockers.push("local_target_relation_not_accepted");
|
|
602
|
+
if (relation.target === selection.acceptance_ref) continue;
|
|
603
|
+
refs.add(relation.target); visit(relation.target);
|
|
604
|
+
}
|
|
605
|
+
active.delete(id);
|
|
606
|
+
}
|
|
607
|
+
for (const id of [...refs]) visit(id);
|
|
608
|
+
const semanticObjects = []; const sources = [];
|
|
609
|
+
const excluded = new Set(["created_at", "updated_at", "semantic_digest", "execution_contract_digest", "content_revision", "evidence", "approval_ref"]);
|
|
610
|
+
for (const id of [...refs].sort()) {
|
|
611
|
+
const record = graph.objects.get(id);
|
|
612
|
+
if (!record) { blockers.push("local_target_required_object_missing"); continue; }
|
|
613
|
+
if (!accepted(record.value)) blockers.push("local_target_required_object_not_accepted");
|
|
614
|
+
if (!record.relative.startsWith("graph/specs/") || !localSafeFile(repo, record.relative)) { blockers.push("local_target_source_unsafe"); continue; }
|
|
615
|
+
const value = Object.fromEntries(Object.entries(record.value).filter(([key]) => !excluded.has(key)));
|
|
616
|
+
// Digests of referenced tools/evidence belong to content freshness, not meaning.
|
|
617
|
+
const sourceRefs = record.value.source_refs || [];
|
|
618
|
+
const evidenceRefs = record.value.evidence || [];
|
|
619
|
+
if (!Array.isArray(sourceRefs) || !Array.isArray(evidenceRefs) ||
|
|
620
|
+
sourceRefs.some(ref => typeof (typeof ref === "string" ? ref : ref?.ref) !== "string")) {
|
|
621
|
+
blockers.push("local_target_source_refs_invalid"); continue;
|
|
622
|
+
}
|
|
623
|
+
value.source_refs = sourceRefs.map(ref => typeof ref === "string" ? ref : ref.ref).sort();
|
|
624
|
+
semanticObjects.push(value);
|
|
625
|
+
for (const raw of [...sourceRefs, ...evidenceRefs]) {
|
|
626
|
+
const ref = typeof raw === "string" ? raw : raw?.ref;
|
|
627
|
+
if (!localSafeFile(repo, ref)) { blockers.push("local_target_source_unsafe_or_missing"); continue; }
|
|
628
|
+
const bytes = fs.readFileSync(path.join(repo, ref));
|
|
629
|
+
const actual = sha256(bytes, true);
|
|
630
|
+
if (typeof raw === "object" && (raw.sha256 || raw.digest) && (raw.sha256 || raw.digest) !== actual) blockers.push("local_target_source_digest_mismatch");
|
|
631
|
+
sources.push({ ref, sha256: actual });
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const relations = graph.relations.filter(r => refs.has(r.source) && refs.has(r.target)).map(({ _record, ...r }) =>
|
|
635
|
+
Object.fromEntries(Object.entries(r).filter(([k]) => !excluded.has(k))));
|
|
636
|
+
relations.sort((a,b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
637
|
+
if (relations.some(r => r.type === "conflicts_with")) blockers.push("local_target_conflict");
|
|
638
|
+
if (contract.goal_binding?.done_when_ids.some(id => !contract.requirement_bindings.some(r => r.done_when_ids.includes(id)))) blockers.push("local_target_done_when_uncovered");
|
|
639
|
+
const semanticDigest = sha256(canonicalBytes({ graph_id: manifest.id, target_id: selection.target_id, objects: semanticObjects, relations }), true);
|
|
640
|
+
const executionDigest = sha256(canonicalBytes(contract));
|
|
641
|
+
const contentRevision = sha256(canonicalBytes([...new Map(sources.map(s => [s.ref, s])).values()].sort((a,b) => a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0)), true);
|
|
642
|
+
if (target.semantic_digest !== semanticDigest) blockers.push("local_target_semantic_digest_mismatch");
|
|
643
|
+
if (target.content_revision !== contentRevision) blockers.push("local_target_content_stale");
|
|
644
|
+
if (!decision || decision.kind !== "decision" || decision.subtype !== "architecture_baseline_acceptance" || !accepted(decision)) blockers.push("local_target_acceptance_missing_or_invalid");
|
|
645
|
+
const architecture = contract.architecture_contract || {};
|
|
646
|
+
if (!contract.decision_refs?.includes(selection.acceptance_ref) || architecture.acceptance_ref !== selection.acceptance_ref) blockers.push("local_target_acceptance_ref_mismatch");
|
|
647
|
+
if (decision && (decision.graph_id !== manifest.id || decision.target_id !== selection.target_id ||
|
|
648
|
+
decision.owner_id !== architecture.architecture_owner_id || decision.semantic_digest !== semanticDigest ||
|
|
649
|
+
decision.execution_contract_digest !== executionDigest || !REF_RE.test(decision.approval_ref || ""))) blockers.push("local_target_acceptance_identity_mismatch");
|
|
650
|
+
const trust = options.localAcceptance;
|
|
651
|
+
if (!trust || Object.keys(trust).some(k => !["graphId", "targetId", "ownerId", "decisionSha256"].includes(k)) ||
|
|
652
|
+
trust.graphId !== manifest.id || trust.targetId !== selection.target_id || trust.ownerId !== architecture.architecture_owner_id ||
|
|
653
|
+
!decision || trust.decisionSha256 !== sha256(canonicalBytes(decision))) blockers.push("local_target_acceptance_unverified");
|
|
654
|
+
if (!contract.allowed_change_scope?.some(s => s.repository_id === manifest.id)) blockers.push("local_target_repository_scope_unapproved");
|
|
655
|
+
return { ...blocked(), status: blockers.length ? "blocked" : "ready", repository_id: manifest.id,
|
|
656
|
+
target_id: selection.target_id, acceptance_ref: selection.acceptance_ref,
|
|
657
|
+
semantic_digest: semanticDigest, content_revision: contentRevision,
|
|
658
|
+
execution_contract_digest: executionDigest, execution_contract: contract,
|
|
659
|
+
next_action: blockers.length ? "review the local target and verify its owner acceptance" : "none" };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function localSafeFile(repo, ref) {
|
|
663
|
+
if (typeof ref !== "string" || !ref || /[:\\\\]/.test(ref) || path.posix.isAbsolute(ref) || ref.split("/").some(p => ["..", ".", "source", "generated"].includes(p)) ||
|
|
664
|
+
SECRET_PARTS.some(p => ref.toLowerCase().includes(p))) return false;
|
|
665
|
+
let current = repo;
|
|
666
|
+
try {
|
|
667
|
+
for (const part of ref.split("/")) { current = path.join(current, part); if (fs.lstatSync(current).isSymbolicLink()) return false; }
|
|
668
|
+
return fs.statSync(current).isFile();
|
|
669
|
+
} catch (_) { return false; }
|
|
670
|
+
}
|
|
671
|
+
|
|
495
672
|
function targetBindingStatus(repo, options = {}) {
|
|
496
673
|
const manifest = readManifest(repo).manifest;
|
|
497
674
|
const root = runtimeRoot(repo, manifest, options);
|
|
498
675
|
const bindingPath = path.join(root, BINDING_FILE);
|
|
676
|
+
const selection = options.targetSource || manifest?.extensions?.[EXTENSION_KEY]?.target_source;
|
|
677
|
+
if (selection) {
|
|
678
|
+
if (fs.existsSync(bindingPath)) return { status: "blocked", enabled: true, source_kind: "conflict", blockers: ["local_external_target_conflict"], next_action: "explicitly reconcile local and external targets" };
|
|
679
|
+
const local = localTargetStatus(repo, manifest, selection, options);
|
|
680
|
+
if (manifest?.extensions?.[EXTENSION_KEY]?.enabled !== true) {
|
|
681
|
+
local.status = "blocked";
|
|
682
|
+
local.blockers = [...new Set([...local.blockers, "project_technology_disabled"])].sort();
|
|
683
|
+
}
|
|
684
|
+
return local;
|
|
685
|
+
}
|
|
499
686
|
if (!fs.existsSync(bindingPath)) return { status: "not_configured", enabled: false, blockers: [], next_action: "connect an exact target provider binding" };
|
|
500
687
|
let binding;
|
|
501
688
|
try { binding = readJson(bindingPath); } catch (_) { return { status: "blocked", enabled: true, blockers: ["target_binding_invalid"] }; }
|
|
@@ -521,6 +708,7 @@ function targetBindingStatus(repo, options = {}) {
|
|
|
521
708
|
provider_export_sha256: binding.provider_export_sha256 || null,
|
|
522
709
|
execution_contract: Object.fromEntries(["decision_refs", "goal_binding", "requirement_bindings", "constraint_ids", "non_goal_ids", "deferred_boundary_ids", "allowed_change_scope", "architecture_contract"].map((key) => [key, exported.export[key]])),
|
|
523
710
|
execution_contract_digest: exported.export.execution_contract_digest || null,
|
|
711
|
+
provider_graph_id: exported.export.provider_graph_id || null,
|
|
524
712
|
blockers: [...new Set(blockers)].sort(),
|
|
525
713
|
next_action: blockers.length ? "refresh or repair the exact provider binding" : "none",
|
|
526
714
|
};
|
|
@@ -535,7 +723,7 @@ function status(repoArg, options = {}) {
|
|
|
535
723
|
try { if (fs.existsSync(path.join(root, INVENTORY_FILE))) stored = readJson(path.join(root, INVENTORY_FILE)); } catch (_) { /* stale */ }
|
|
536
724
|
const current = inventory(repo);
|
|
537
725
|
const freshness = stored && stored.inventory_digest === current.inventory_digest ? "current" : stored ? "stale" : "missing";
|
|
538
|
-
const blockers = [...manifestState.blockers, ...ext.blockers];
|
|
726
|
+
const blockers = [...manifestState.blockers, ...ext.blockers, ...(current.blockers || [])];
|
|
539
727
|
if (!stored && legacyRuntimeState(repo).present) blockers.push("project_technology_host_state_migration_required");
|
|
540
728
|
if (ext.contract && ext.contract.enabled === false) blockers.push("project_technology_disabled");
|
|
541
729
|
if (freshness !== "current") blockers.push(`project_technology_inventory_${freshness}`);
|
|
@@ -548,7 +736,7 @@ function status(repoArg, options = {}) {
|
|
|
548
736
|
technology_freshness: freshness,
|
|
549
737
|
technology_digest: current.inventory_digest,
|
|
550
738
|
target_binding: binding,
|
|
551
|
-
continuity: continuity.status(repo, manifestState.manifest,
|
|
739
|
+
continuity: continuity.status(repo, manifestState.manifest, options),
|
|
552
740
|
blockers: [...new Set(blockers)].sort(),
|
|
553
741
|
next_action: ext.legacy ? "run enable --apply to migrate the legacy contract" : blockers.length ? "run plan, then the required transactional operation" : "none",
|
|
554
742
|
});
|
|
@@ -561,10 +749,11 @@ function explain(repoArg) {
|
|
|
561
749
|
});
|
|
562
750
|
}
|
|
563
751
|
|
|
564
|
-
function plan(repoArg) {
|
|
565
|
-
const state = status(repoArg);
|
|
752
|
+
function plan(repoArg, options = {}) {
|
|
753
|
+
const state = status(repoArg, options);
|
|
566
754
|
return result("plan", "read_only", "success", {
|
|
567
755
|
current_status: state.status,
|
|
756
|
+
target_binding: state.target_binding,
|
|
568
757
|
planned_changes: ["validate_graph_manifest", "migrate_legacy_extension_if_present", "migrate_project_local_runtime_to_host_state", "enable_public_contract", "build_safe_inventory", "verify_target_binding"],
|
|
569
758
|
blockers: state.blockers.filter((item) => !["project_technology_not_configured", "project_technology_migration_required", "project_technology_inventory_missing", "project_technology_inventory_stale"].includes(item)),
|
|
570
759
|
next_action: "rerun the selected operation with --apply",
|
|
@@ -573,6 +762,8 @@ function plan(repoArg) {
|
|
|
573
762
|
|
|
574
763
|
function preview(operation, repoArg, options = {}) {
|
|
575
764
|
const state = status(repoArg, options);
|
|
765
|
+
if (options.targetSource) return result(operation, "preview", "preview", { apply_required: true,
|
|
766
|
+
target_binding: state.target_binding, blockers: state.target_binding.blockers, next_action: "sync --apply with expected graph digest and independently verified acceptance" });
|
|
576
767
|
return result(operation, "preview", "preview", { apply_required: true, current_status: state.status, blockers: state.blockers.filter((item) => item.startsWith("graph_manifest_")), next_action: `${operation} --apply` });
|
|
577
768
|
}
|
|
578
769
|
|
|
@@ -580,11 +771,13 @@ function enable(repoArg, options = {}) {
|
|
|
580
771
|
const repo = normalizeRepo(repoArg);
|
|
581
772
|
const manifestState = readManifest(repo);
|
|
582
773
|
if (!manifestState.manifest || manifestState.blockers.length) return result("enable", "transactional", "fail", { blockers: manifestState.blockers, next_action: "repair graph.json" });
|
|
774
|
+
const inventoryPreflight = inventory(repo);
|
|
775
|
+
if (inventoryPreflight.blockers?.length) return result("enable", "transactional", "blocked", { blockers: inventoryPreflight.blockers, next_action: "repair declared graph sources" });
|
|
583
776
|
const manifest = structuredClone(manifestState.manifest);
|
|
584
777
|
const extensions = { ...(manifest.extensions || {}) };
|
|
585
778
|
const legacy = extensions[LEGACY_EXTENSION_KEY];
|
|
586
779
|
delete extensions[LEGACY_EXTENSION_KEY];
|
|
587
|
-
extensions[EXTENSION_KEY] = extensionContract();
|
|
780
|
+
extensions[EXTENSION_KEY] = { ...extensionContract(), ...(extensions[EXTENSION_KEY]?.target_source ? { target_source: extensions[EXTENSION_KEY].target_source } : {}) };
|
|
588
781
|
manifest.extensions = extensions;
|
|
589
782
|
const root = runtimeRoot(repo, manifest, options);
|
|
590
783
|
const runtimeMigration = migrateProjectLocalRuntime(repo, manifest, root);
|
|
@@ -615,12 +808,15 @@ function enable(repoArg, options = {}) {
|
|
|
615
808
|
|
|
616
809
|
function sync(repoArg, options = {}) {
|
|
617
810
|
const repo = normalizeRepo(repoArg);
|
|
811
|
+
if (options.targetSource) return syncLocalSelection(repo, options);
|
|
618
812
|
const manifestState = readManifest(repo);
|
|
619
813
|
const ext = extensionState(manifestState.manifest);
|
|
620
|
-
const blockers = [...manifestState.blockers, ...ext.blockers];
|
|
814
|
+
const blockers = [...manifestState.blockers, ...ext.blockers, ...(inventory(repo).blockers || [])];
|
|
621
815
|
const hostRoot = runtimeRoot(repo, manifestState.manifest, options);
|
|
622
816
|
if (legacyRuntimeState(repo).present && !fs.existsSync(path.join(hostRoot, INVENTORY_FILE))) blockers.push("project_technology_host_state_migration_required");
|
|
623
817
|
if (!ext.contract || ext.legacy || ext.contract.enabled !== true) blockers.push("project_technology_not_enabled");
|
|
818
|
+
const binding = targetBindingStatus(repo, options);
|
|
819
|
+
if (binding.source_kind && binding.status !== "ready") blockers.push(...binding.blockers);
|
|
624
820
|
if (blockers.length) return result("sync", "transactional", "fail", { blockers: [...new Set(blockers)].sort(), next_action: "enable or repair Project Technology" });
|
|
625
821
|
let continuityResult = null;
|
|
626
822
|
if (options.boundary) {
|
|
@@ -648,6 +844,56 @@ function sync(repoArg, options = {}) {
|
|
|
648
844
|
});
|
|
649
845
|
}
|
|
650
846
|
|
|
847
|
+
function syncLocalSelection(repo, options, remove = false) {
|
|
848
|
+
const operation = remove ? "disconnect" : "sync";
|
|
849
|
+
const manifestState = readManifest(repo);
|
|
850
|
+
const manifest = manifestState.manifest;
|
|
851
|
+
const ext = extensionState(manifest);
|
|
852
|
+
const binding = targetBindingStatus(repo, options);
|
|
853
|
+
const blockers = [...manifestState.blockers, ...ext.blockers, ...(remove ? [] : binding.blockers)];
|
|
854
|
+
if (!remove && ext.contract?.enabled !== true) blockers.push("project_technology_not_enabled");
|
|
855
|
+
if (options.boundary) blockers.push("local_target_selection_requires_separate_boundary");
|
|
856
|
+
const beforeDigest = continuity.graphDigest(repo);
|
|
857
|
+
if (!options.expectedGraphDigest || beforeDigest !== options.expectedGraphDigest) blockers.push("local_target_compare_and_swap_conflict");
|
|
858
|
+
if (blockers.length) return result(operation, "transactional", "blocked", { target_binding: binding, blockers: [...new Set(blockers)].sort() });
|
|
859
|
+
const root = runtimeRoot(repo, manifest, options);
|
|
860
|
+
const lease = continuity.lock(repo, root);
|
|
861
|
+
if (!lease.acquired) return result(operation, "transactional", "blocked", { blockers: ["continuity_lease_conflict"] });
|
|
862
|
+
const file = path.join(repo, "graph.json"); const inventoryPath = path.join(root, INVENTORY_FILE);
|
|
863
|
+
const before = fs.readFileSync(file);
|
|
864
|
+
const inventoryBefore = fs.existsSync(inventoryPath) ? fs.readFileSync(inventoryPath) : null;
|
|
865
|
+
try {
|
|
866
|
+
if (continuity.graphDigest(repo) !== beforeDigest) return result(operation, "transactional", "blocked", { blockers: ["local_target_compare_and_swap_conflict"] });
|
|
867
|
+
const next = structuredClone(manifest);
|
|
868
|
+
if (remove) delete next.extensions[EXTENSION_KEY].target_source;
|
|
869
|
+
else next.extensions[EXTENSION_KEY].target_source = options.targetSource;
|
|
870
|
+
if (canonicalBytes(next) === canonicalBytes(manifest)) return result(operation, "transactional", "success", { target_binding: binding });
|
|
871
|
+
const backup = path.join(root, "rollback", beforeDigest.slice(7), "graph.json");
|
|
872
|
+
atomicWrite(backup, before);
|
|
873
|
+
atomicWrite(file, canonicalBytes(next));
|
|
874
|
+
const inv = inventory(repo);
|
|
875
|
+
if (inv.blockers?.length) throw new Error("local_target_inventory_failed");
|
|
876
|
+
atomicWrite(inventoryPath, canonicalBytes(inv));
|
|
877
|
+
// No source write besides the selector is permitted in this transaction.
|
|
878
|
+
// Ignore expected Git dirtiness from the selector itself during readback.
|
|
879
|
+
if (!fs.readFileSync(file).equals(Buffer.from(canonicalBytes(next)))) throw new Error("local_target_readback_failed");
|
|
880
|
+
const readback = targetBindingStatus(repo, remove ? { ...options, targetSource: undefined } : options);
|
|
881
|
+
if (!remove && (readback.semantic_digest !== binding.semantic_digest || readback.content_revision !== binding.content_revision ||
|
|
882
|
+
readback.blockers.some(code => code !== "graph_source_not_revision_bound"))) throw new Error("local_target_source_changed_during_selection");
|
|
883
|
+
return result(operation, "transactional", "success", { changed: true, target_binding: readback,
|
|
884
|
+
rollback_ref: `host-local://rollback/${beforeDigest.slice(7)}/graph.json`, technology_digest: inv.inventory_digest });
|
|
885
|
+
} catch (_) {
|
|
886
|
+
let restored = false;
|
|
887
|
+
try {
|
|
888
|
+
atomicWrite(file, before);
|
|
889
|
+
if (inventoryBefore) atomicWrite(inventoryPath, inventoryBefore);
|
|
890
|
+
else if (fs.existsSync(inventoryPath)) fs.unlinkSync(inventoryPath);
|
|
891
|
+
restored = fs.readFileSync(file).equals(before);
|
|
892
|
+
} catch (_) { /* recovery required remains explicit */ }
|
|
893
|
+
return result(operation, "transactional", "fail", { blockers: [restored ? "local_target_rollback_applied" : "local_target_recovery_required"] });
|
|
894
|
+
} finally { continuity.releaseLock(lease); }
|
|
895
|
+
}
|
|
896
|
+
|
|
651
897
|
function provide(repoArg, options = {}) {
|
|
652
898
|
const repo = normalizeRepo(repoArg);
|
|
653
899
|
const identity = bindingValues(options);
|
|
@@ -661,7 +907,7 @@ function provide(repoArg, options = {}) {
|
|
|
661
907
|
blockers.push(...target.blockers);
|
|
662
908
|
if (blockers.length) return result("provide", "transactional", "fail", { blockers: [...new Set(blockers)].sort(), next_action: "repair the accepted target contract" });
|
|
663
909
|
const executionContractDigest = sha256(canonicalBytes(target.contract));
|
|
664
|
-
const payload = { schema_version: "1.0.0", ...identity.values, ...target.contract, execution_contract_digest: executionContractDigest };
|
|
910
|
+
const payload = { schema_version: "1.0.0", ...identity.values, ...target.contract, execution_contract_digest: executionContractDigest, provider_graph_id: manifestState.manifest.id };
|
|
665
911
|
const changed = atomicWrite(path.join(repo, EXPORT_FILE), canonicalBytes(payload));
|
|
666
912
|
return result("provide", "transactional", "success", { changed, export_ref: EXPORT_FILE, target_binding: payload });
|
|
667
913
|
}
|
|
@@ -671,6 +917,32 @@ function providerRootFor(exportPath) {
|
|
|
671
917
|
return completed.status === 0 ? completed.stdout.trim() : null;
|
|
672
918
|
}
|
|
673
919
|
|
|
920
|
+
// This is explicit consumer trust, NOT an assertion read from the provider.
|
|
921
|
+
// The caller must obtain it from authenticated, checksum-bound release metadata.
|
|
922
|
+
// Keeping it out of the export prevents an archive from authenticating itself.
|
|
923
|
+
function archiveProviderIdentity(source, exported, anchor) {
|
|
924
|
+
const fail = code => ({ blockers: [code], ancestors: [] });
|
|
925
|
+
if (!anchor || typeof anchor !== "object" || Array.isArray(anchor)) return fail("provider_archive_trust_invalid");
|
|
926
|
+
const keys = ["exportSha256", "graphId", "providerRevision", "ancestorRevisions"];
|
|
927
|
+
if (Object.keys(anchor).some(key => !keys.includes(key)) || keys.some(key => !Object.hasOwn(anchor, key))) return fail("provider_archive_trust_invalid");
|
|
928
|
+
if (!/^[0-9a-f]{64}$/.test(anchor.exportSha256 || "") ||
|
|
929
|
+
typeof anchor.graphId !== "string" || !REF_RE.test(anchor.graphId) ||
|
|
930
|
+
!REVISION_RE.test(anchor.providerRevision || "") ||
|
|
931
|
+
!Array.isArray(anchor.ancestorRevisions) || anchor.ancestorRevisions.length > 4096 ||
|
|
932
|
+
anchor.ancestorRevisions.some(rev => typeof rev !== "string" || !REVISION_RE.test(rev) || rev === anchor.providerRevision) ||
|
|
933
|
+
new Set(anchor.ancestorRevisions).size !== anchor.ancestorRevisions.length) return fail("provider_archive_trust_invalid");
|
|
934
|
+
try {
|
|
935
|
+
// Parent aliases such as macOS /var are allowed; the exact bytes are pinned.
|
|
936
|
+
if (fs.lstatSync(source).isSymbolicLink()) return fail("provider_archive_source_unsafe");
|
|
937
|
+
if (!fs.statSync(source).isFile() || fs.statSync(source).size > 1024 * 1024) return fail("provider_archive_source_unsafe");
|
|
938
|
+
const bytes = fs.readFileSync(source);
|
|
939
|
+
if (sha256(bytes) !== anchor.exportSha256 || canonicalBytes(JSON.parse(bytes)) !== canonicalBytes(exported)) return fail("provider_archive_export_digest_mismatch");
|
|
940
|
+
} catch (_) { return fail("provider_archive_source_unsafe"); }
|
|
941
|
+
if (exported.provider_graph_id !== anchor.graphId) return fail("provider_archive_graph_mismatch");
|
|
942
|
+
if (exported.provider_revision !== anchor.providerRevision) return fail("provider_archive_revision_mismatch");
|
|
943
|
+
return { blockers: [], ancestors: anchor.ancestorRevisions };
|
|
944
|
+
}
|
|
945
|
+
|
|
674
946
|
function connect(repoArg, options = {}) {
|
|
675
947
|
const repo = normalizeRepo(repoArg);
|
|
676
948
|
const source = path.resolve(String(options.source || ""));
|
|
@@ -681,14 +953,21 @@ function connect(repoArg, options = {}) {
|
|
|
681
953
|
const blockers = [...identity.blockers, ...read.blockers, ...manifestState.blockers, ...ext.blockers];
|
|
682
954
|
if (!ext.contract || ext.legacy || ext.contract.enabled !== true) blockers.push("project_technology_not_enabled");
|
|
683
955
|
for (const key of Object.keys(identity.values)) if (read.export[key] !== identity.values[key]) blockers.push(`${key}_mismatch`);
|
|
684
|
-
const
|
|
685
|
-
if (
|
|
956
|
+
const archiveRequested = Object.hasOwn(options, "providerArchive");
|
|
957
|
+
if (manifestState.manifest?.extensions?.[EXTENSION_KEY]?.target_source) blockers.push("local_external_target_conflict");
|
|
958
|
+
const archive = archiveRequested ? archiveProviderIdentity(source, read.export, options.providerArchive) : null;
|
|
959
|
+
const providerRoot = archiveRequested ? null : providerRootFor(source);
|
|
960
|
+
if (archive) blockers.push(...archive.blockers);
|
|
961
|
+
else if (!providerRoot) blockers.push("provider_revision_order_unverifiable");
|
|
686
962
|
else if (git(providerRoot, "rev-parse", "HEAD").toLowerCase() !== identity.values.provider_revision) blockers.push("provider_revision_does_not_match_head");
|
|
687
963
|
const root = runtimeRoot(repo, manifestState.manifest, options);
|
|
688
964
|
const currentPath = path.join(root, BINDING_FILE);
|
|
689
965
|
let current = null;
|
|
690
966
|
try { if (fs.existsSync(currentPath)) current = readJson(currentPath); } catch (_) { blockers.push("current_target_binding_invalid"); }
|
|
691
967
|
if (current) {
|
|
968
|
+
const currentExport = targetBindingStatus(repo, options);
|
|
969
|
+
const existingGraphId = currentExport.provider_graph_id;
|
|
970
|
+
if (existingGraphId && existingGraphId !== read.export.provider_graph_id) blockers.push("target_provider_graph_conflict");
|
|
692
971
|
if (!options.refreshBinding) {
|
|
693
972
|
const same = Object.keys(identity.values).every((key) => current[key] === identity.values[key]);
|
|
694
973
|
if (!same) blockers.push("target_provider_conflict", "target_provider_refresh_required");
|
|
@@ -703,7 +982,9 @@ function connect(repoArg, options = {}) {
|
|
|
703
982
|
const currentState = targetBindingStatus(repo, options);
|
|
704
983
|
if (currentState.status !== "ready") blockers.push(...currentState.blockers);
|
|
705
984
|
if (currentState.execution_contract_digest !== read.export.execution_contract_digest) blockers.push("provider_execution_contract_refresh_mismatch");
|
|
706
|
-
if (
|
|
985
|
+
if (archive) {
|
|
986
|
+
if (current.provider_revision !== identity.values.provider_revision && !archive.ancestors.includes(current.provider_revision)) blockers.push("provider_revision_not_forward");
|
|
987
|
+
} else if (!providerRoot) blockers.push("provider_revision_order_unverifiable");
|
|
707
988
|
else {
|
|
708
989
|
if (current.provider_revision !== identity.values.provider_revision) {
|
|
709
990
|
const ancestry = spawnSync("git", ["-C", providerRoot, "merge-base", "--is-ancestor", current.provider_revision, identity.values.provider_revision]);
|
|
@@ -735,6 +1016,7 @@ function connect(repoArg, options = {}) {
|
|
|
735
1016
|
|
|
736
1017
|
function disconnect(repoArg, options = {}) {
|
|
737
1018
|
const repo = normalizeRepo(repoArg);
|
|
1019
|
+
if (readManifest(repo).manifest?.extensions?.[EXTENSION_KEY]?.target_source) return syncLocalSelection(repo, options, true);
|
|
738
1020
|
const root = runtimeRoot(repo, readManifest(repo).manifest, options);
|
|
739
1021
|
const bindingPath = path.join(root, BINDING_FILE);
|
|
740
1022
|
if (!fs.existsSync(bindingPath)) return result("disconnect", "transactional", "success");
|
|
@@ -768,7 +1050,40 @@ function repair(repoArg, options = {}) {
|
|
|
768
1050
|
return sync(repo, options);
|
|
769
1051
|
}
|
|
770
1052
|
|
|
1053
|
+
function verifyProviderExport(repoArg, options = {}) {
|
|
1054
|
+
const repo = normalizeRepo(repoArg);
|
|
1055
|
+
const source = path.resolve(options.source || path.join(repo, EXPORT_FILE));
|
|
1056
|
+
const exported = readExport(source);
|
|
1057
|
+
const manifestState = readManifest(repo);
|
|
1058
|
+
const ext = extensionState(manifestState.manifest);
|
|
1059
|
+
const identity = bindingValues(exported.export);
|
|
1060
|
+
const blockers = [...exported.blockers, ...manifestState.blockers, ...ext.blockers, ...identity.blockers];
|
|
1061
|
+
if (options.significantWork) blockers.push("provider_export_verification_is_not_execution_authority");
|
|
1062
|
+
if (!trackedFiles(repo).includes("graph.json") || spawnSync("git", ["diff", "--quiet", "HEAD", "--", "graph.json"], { cwd: repo }).status !== 0) blockers.push("provider_manifest_not_revision_bound");
|
|
1063
|
+
if (!source.startsWith(`${repo}${path.sep}`)) blockers.push("provider_export_outside_repository");
|
|
1064
|
+
if (ext.contract?.enabled !== true || ext.legacy) blockers.push("project_technology_not_enabled");
|
|
1065
|
+
const head = git(repo, "rev-parse", "HEAD");
|
|
1066
|
+
if (head !== identity.values.provider_revision) blockers.push("provider_revision_does_not_match_head");
|
|
1067
|
+
if (exported.export.provider_graph_id !== manifestState.manifest?.id) blockers.push("provider_archive_graph_mismatch");
|
|
1068
|
+
const target = targetContract(repo, identity.values.target_id, identity.values.semantic_digest, manifestState.manifest);
|
|
1069
|
+
blockers.push(...target.blockers);
|
|
1070
|
+
if (sha256(canonicalBytes(target.contract)) !== exported.export.execution_contract_digest) blockers.push("provider_execution_contract_source_mismatch");
|
|
1071
|
+
// Bound supported ancestry; deeper histories need a narrower supported
|
|
1072
|
+
// release window rather than unbounded metadata in every consumer.
|
|
1073
|
+
const history = git(repo, "rev-list", "--max-count=4098", "HEAD").split("\n").filter(Boolean);
|
|
1074
|
+
if (history[0] !== head || history.length > 4097) blockers.push("provider_archive_ancestry_unverifiable_or_too_large");
|
|
1075
|
+
return result("verify", "read_only", blockers.length ? "blocked" : "success", {
|
|
1076
|
+
blockers: [...new Set(blockers)].sort(),
|
|
1077
|
+
provider_archive: blockers.length ? null : {
|
|
1078
|
+
exportSha256: exported.raw_sha256, graphId: manifestState.manifest.id,
|
|
1079
|
+
providerRevision: head, ancestorRevisions: history.slice(1),
|
|
1080
|
+
},
|
|
1081
|
+
next_action: blockers.length ? "repair the canonical provider export before packaging" : "seal this anchor in authenticated release metadata",
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
|
|
771
1085
|
function verify(repoArg, options = {}) {
|
|
1086
|
+
if (options.source) return verifyProviderExport(repoArg, options);
|
|
772
1087
|
const state = status(repoArg, options);
|
|
773
1088
|
const blockers = [...state.blockers];
|
|
774
1089
|
const repo = normalizeRepo(repoArg);
|
|
@@ -776,6 +1091,7 @@ function verify(repoArg, options = {}) {
|
|
|
776
1091
|
const continuityResult = continuity.verify(repo, manifest, options);
|
|
777
1092
|
if (continuity.continuityPolicy(manifest) === "task_boundary" || options.receiptDigest || options.significantWork) blockers.push(...continuityResult.blockers);
|
|
778
1093
|
if (options.significantWork && state.target_binding.status !== "ready") blockers.push("accepted_target_binding_required_for_significant_work");
|
|
1094
|
+
if (options.significantWork && state.target_binding.source_kind === "local") blockers.push(...localVerificationBlockers(repo, options));
|
|
779
1095
|
return result("verify", "read_only", blockers.length ? "blocked" : "success", {
|
|
780
1096
|
repository_id: state.repository_id,
|
|
781
1097
|
enabled: state.enabled,
|
|
@@ -835,6 +1151,7 @@ module.exports = {
|
|
|
835
1151
|
sync,
|
|
836
1152
|
targetBindingStatus,
|
|
837
1153
|
verify,
|
|
1154
|
+
verifyProviderExport,
|
|
838
1155
|
verifyArtifactRelease: artifacts.verifyArtifactRelease,
|
|
839
1156
|
verifyTechnologyCourse: technologyCourse.verifyTechnologyCourse,
|
|
840
1157
|
verifyContext: traversal.verifyContext,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Mirai Graph 1.5.0
|
|
2
|
+
|
|
3
|
+
Compatible additive release for existing 1.x consumers. Published to npm under
|
|
4
|
+
`legacy-1`; it does not replace the 2.x `latest` channel or migrate consumers.
|
|
5
|
+
|
|
6
|
+
## Changes
|
|
7
|
+
|
|
8
|
+
- Existing Project Technology connect/refresh can import a bounded provider
|
|
9
|
+
export without Git when an authenticated distributor supplies its exact
|
|
10
|
+
export digest, graph identity, revision and forward ancestry proof.
|
|
11
|
+
- Read-only source verification binds that proof to the accepted target and
|
|
12
|
+
real Git revision before packaging. Caller-supplied trust is not inferred
|
|
13
|
+
from an export's own claims and never grants permission to perform work.
|
|
14
|
+
- Folder inventory follows only explicitly declared graph and raw-source paths,
|
|
15
|
+
detects stale content and rejects unsafe or missing sources.
|
|
16
|
+
- CLI archive trust can be passed through stdin without a temporary trust file.
|
|
17
|
+
|
|
18
|
+
## Compatibility and safety
|
|
19
|
+
|
|
20
|
+
Existing Git provider operations, complete target and execution contracts,
|
|
21
|
+
transactional rollback and fail-closed behavior remain in force. No new profile,
|
|
22
|
+
registry, daemon or schema family is introduced. The graph manifest remains
|
|
23
|
+
2.0.0 and the Project Technology activation contract remains 1.0.0.
|
|
24
|
+
|
|
25
|
+
Archive metadata must come from an independently authenticated release; this
|
|
26
|
+
package does not establish a distributor's trust or copy private source content.
|
|
27
|
+
|
|
28
|
+
## Verification
|
|
29
|
+
|
|
30
|
+
`npm run release:check` runs the current suite, including 39 existing Project
|
|
31
|
+
Technology checks and 71 archive/source-proof checks. The archive cases cover
|
|
32
|
+
no-Git import, exact replay, forward refresh, downgrade, tamper, graph conflict,
|
|
33
|
+
missing trust, source inventory and read-only behavior.
|
|
34
|
+
|
|
35
|
+
Local validation uses macOS ARM64 with Node 22.23.2. Federation's native
|
|
36
|
+
Windows/macOS/Linux complete installation matrix is a separate acceptance gate;
|
|
37
|
+
this release does not claim that matrix or a Federation release has passed.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Mirai Graph 1.6.0 candidate
|
|
2
|
+
|
|
3
|
+
Status: local release preparation, not published or independently accepted.
|
|
4
|
+
This is an additive compatible 1.x release. Intended npm channel: `legacy-1`;
|
|
5
|
+
it must not move the separate 2.x `latest` channel.
|
|
6
|
+
|
|
7
|
+
## Result
|
|
8
|
+
|
|
9
|
+
An ordinary project folder can explicitly select its own accepted target without
|
|
10
|
+
Git or a self-connected external provider. The caller authenticates the human
|
|
11
|
+
approval and supplies an independent trust anchor; the graph cannot approve
|
|
12
|
+
itself. Existing external provider bindings remain supported and fail closed.
|
|
13
|
+
|
|
14
|
+
The same target identity is exposed in status, plan, context and verify. Selection
|
|
15
|
+
and disconnect reuse the existing transaction, lease, compare-and-swap, backup
|
|
16
|
+
and rollback. Semantic acceptance is distinct from source-content freshness.
|
|
17
|
+
Significant work still requires current task-boundary verification evidence.
|
|
18
|
+
|
|
19
|
+
A valid new Git branch can inventory declared sources before its first commit.
|
|
20
|
+
Corrupt refs/indexes and unavailable Git remain blocked. No commit revision is
|
|
21
|
+
invented and provider export still requires a real committed revision.
|
|
22
|
+
|
|
23
|
+
## Compatibility
|
|
24
|
+
|
|
25
|
+
- Manifest schema remains `2.0.0`; activation contract remains `1.0.0`.
|
|
26
|
+
- No new profile, engine, approval issuer or source-of-truth store.
|
|
27
|
+
- Local target selection is explicit; missing approval never falls back to ready.
|
|
28
|
+
- An offline synchronization conflict requires reconciliation. A filesystem
|
|
29
|
+
lease is not a distributed lock between disconnected copies.
|
|
30
|
+
- A new host may inspect the project, but cannot execute solely because a copied
|
|
31
|
+
folder declares itself accepted or contains a matching adjacent checksum.
|
|
32
|
+
|
|
33
|
+
## Verification and limits
|
|
34
|
+
|
|
35
|
+
The implementation commits passed the complete test suite and the existing
|
|
36
|
+
Windows/macOS/Ubuntu Project Technology, context and continuity CI matrix.
|
|
37
|
+
Release packaging is checked separately on the versioned artifact. These tests
|
|
38
|
+
do not prove the complete installer of any downstream application, a real shared
|
|
39
|
+
network filesystem or an employee's legacy installation.
|
|
40
|
+
|
|
41
|
+
Independent acceptance, immutable package publication and downstream exact-lock
|
|
42
|
+
installation are separate requirements. No tag, GitHub Release or npm package
|
|
43
|
+
is represented as published by this candidate document.
|
package/releases/README.md
CHANGED
|
@@ -15,6 +15,10 @@ Release notes must separate:
|
|
|
15
15
|
|
|
16
16
|
## Release Notes
|
|
17
17
|
|
|
18
|
+
- [v1.6.0 candidate](1.6.0.md) - local accepted targets and valid unborn Git
|
|
19
|
+
inventory; unpublished, intended for `legacy-1` only.
|
|
20
|
+
- [v1.5.0](1.5.0.md) - compatible 1.x authenticated archive providers and
|
|
21
|
+
declared-source inventory without Git (`legacy-1`, not npm `latest`).
|
|
18
22
|
- [v1.2.0](1.2.0.md) - portable task-boundary project continuity in Project
|
|
19
23
|
Technology.
|
|
20
24
|
- [v1.1.0](1.1.0.md) - universal sequential context traversal and usage
|
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
"enabled": { "type": "boolean" },
|
|
11
11
|
"context_policy": { "const": "task_scoped" },
|
|
12
12
|
"source_boundary": { "const": "hybrid_sot" },
|
|
13
|
-
"continuity_policy": { "enum": ["task_boundary"] }
|
|
13
|
+
"continuity_policy": { "enum": ["task_boundary"] },
|
|
14
|
+
"target_source": {
|
|
15
|
+
"type": "object", "additionalProperties": false,
|
|
16
|
+
"required": ["kind", "target_id", "acceptance_ref"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"kind": { "const": "local" },
|
|
19
|
+
"target_id": { "type": "string", "minLength": 2 },
|
|
20
|
+
"acceptance_ref": { "type": "string", "minLength": 2 }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
14
23
|
}
|
|
15
24
|
}
|