58 files: TCS canons+GIR+projections (LW-LINK/BROADCAST/NOTARY/SHENBI/TRANSLATION-INDEX/TCS-ONLY-MANDATE + teaching&promotion events), 7 executors, neural-chain v5 (28 neurons), executor translation index, 8 acceptance receipts, numbered push manifest ZY-LWS-001 mapping every file back to JZAO canonical paths+sha256. Authorized by ICE-GL∞ direct language 2026-08-25. Route: LOCAL_GIT_OSXKEYCHAIN+FETCH+REBASE+HEAD:main+NO_FORCE.
172 lines
7.6 KiB
JavaScript
172 lines
7.6 KiB
JavaScript
#!/usr/bin/env node
|
|
// Fifth Domain Notary: 公证工单系统 (canonical tool)
|
|
// Canon: FIFTH-DOMAIN-BROADCAST-TOWER/TCS-PROTOCOL-FIFTH-DOMAIN-NOTARY-0001.tcs
|
|
// Append-only hash ledger; tickets immutable except status transitions and appended notes/receipts.
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const TOWER = process.env.GH_FDT_ROOT ?? "/Volumes/JZAO/铸渊-ICE-GL-ZY001/FIFTH-DOMAIN-BROADCAST-TOWER";
|
|
const NOTARY = path.join(TOWER, "notary");
|
|
const LEDGER = path.join(NOTARY, "ledger.json");
|
|
const TICKETS = path.join(NOTARY, "tickets");
|
|
|
|
function sha256(value) { return crypto.createHash("sha256").update(value).digest("hex"); }
|
|
function readJson(file) { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } }
|
|
function atomicWriteJson(file, value) {
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
const tmp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
fs.renameSync(tmp, file);
|
|
}
|
|
function canonical(entry) { return JSON.stringify(entry); }
|
|
function fail(msg) { process.stderr.write(`FDT_NOTARY FAIL ${msg}\n`); process.exit(1); }
|
|
function safeToken(value, fallback = "unknown") {
|
|
const token = String(value ?? "").trim().replaceAll(/[^a-zA-Z0-9._-]/gu, "-");
|
|
return token || fallback;
|
|
}
|
|
const KINDS = new Set(["COLLABORATION", "QUESTION", "AUTHORIZATION_REQUEST", "NOTARIZED_STATEMENT", "HANDOFF"]);
|
|
|
|
function loadLedger() {
|
|
const ledger = readJson(LEDGER);
|
|
if (ledger && ledger.schema !== "guanghu.fifth-domain-notary-ledger/v1") fail("LEDGER_CORRUPT");
|
|
return ledger ?? { schema: "guanghu.fifth-domain-notary-ledger/v1", entries: [], head: null };
|
|
}
|
|
function appendLedger(action, ticketId, actor, body) {
|
|
const ledger = loadLedger();
|
|
const entry = {
|
|
seq: ledger.entries.length + 1,
|
|
ts: new Date().toISOString(),
|
|
action,
|
|
ticket_id: ticketId,
|
|
actor: safeToken(actor),
|
|
prev_hash: ledger.head,
|
|
body,
|
|
};
|
|
entry.entry_hash = sha256(canonical(entry));
|
|
ledger.entries.push(entry);
|
|
ledger.head = entry.entry_hash;
|
|
atomicWriteJson(LEDGER, ledger);
|
|
return entry;
|
|
}
|
|
function ticketPath(id) { return path.join(TICKETS, `${id}.json`); }
|
|
function loadTicket(id) {
|
|
const t = readJson(ticketPath(id));
|
|
if (!t || t.schema !== "guanghu.fifth-domain-notary-ticket/v1") fail(`TICKET_MISSING_OR_CORRUPT:${id}`);
|
|
return t;
|
|
}
|
|
function saveTicket(t) { atomicWriteJson(ticketPath(t.ticket_id), t); }
|
|
|
|
function todaySeq() {
|
|
const day = new Date().toISOString().slice(0, 10).replaceAll("-", "");
|
|
const ledger = loadLedger();
|
|
const ids = new Set(ledger.entries.map((e) => e.ticket_id).filter((id) => id?.startsWith(`FDT-${day}`)));
|
|
let n = ids.size + 1;
|
|
let id = `FDT-${day}-${String(n).padStart(3, "0")}`;
|
|
while (ids.has(id) || fs.existsSync(ticketPath(id))) {
|
|
n += 1;
|
|
id = `FDT-${day}-${String(n).padStart(3, "0")}`;
|
|
}
|
|
return id;
|
|
}
|
|
|
|
function valueAfter(argv, name) { const i = argv.indexOf(name); return i >= 0 ? argv[i + 1] : null; }
|
|
|
|
function create(argv) {
|
|
const by = valueAfter(argv, "--by");
|
|
const kind = valueAfter(argv, "--kind");
|
|
const subject = valueAfter(argv, "--subject");
|
|
const bodyFile = valueAfter(argv, "--body-file");
|
|
const audience = valueAfter(argv, "--audience") ?? "ANY_INSTANCE";
|
|
if (!by || !kind || !subject || !bodyFile) fail("usage: create --by SESSION --kind KIND --subject TEXT --body-file FILE [--audience A]");
|
|
if (!KINDS.has(kind)) fail(`INVALID_KIND:${kind}`);
|
|
let bodyText;
|
|
try { bodyText = fs.readFileSync(bodyFile, "utf8"); } catch { fail(`BODY_FILE_UNREADABLE:${bodyFile}`); }
|
|
const id = todaySeq();
|
|
const bodyPath = path.join(TICKETS, `${id}.body.md`);
|
|
fs.mkdirSync(TICKETS, { recursive: true });
|
|
fs.writeFileSync(bodyPath, bodyText, { encoding: "utf8", mode: 0o600 });
|
|
const ticket = {
|
|
schema: "guanghu.fifth-domain-notary-ticket/v1",
|
|
ticket_id: id,
|
|
kind,
|
|
subject,
|
|
audience,
|
|
creator: safeToken(by),
|
|
created_at: new Date().toISOString(),
|
|
status: "OPEN",
|
|
body_path: bodyPath,
|
|
body_sha256: sha256(bodyText),
|
|
history: [],
|
|
};
|
|
const entry = appendLedger("CREATE", id, by, { kind, subject, audience, body_sha256: ticket.body_sha256 });
|
|
ticket.history.push({ ts: entry.ts, action: "CREATE", actor: entry.actor, entry_hash: entry.entry_hash });
|
|
saveTicket(ticket);
|
|
process.stdout.write(`FDT CREATED ${id} kind=${kind} creator=${safeToken(by)} ledger_seq=${entry.seq} head=${entry.entry_hash.slice(0, 12)}\n`);
|
|
}
|
|
|
|
function transition(argv, action) {
|
|
const id = valueAfter(argv, "--id");
|
|
const by = valueAfter(argv, "--by");
|
|
const text = valueAfter(argv, "--text") ?? "";
|
|
if (!id || !by) fail(`usage: ${action.toLowerCase()} --id FDT-x --by SESSION [--text TEXT]`);
|
|
const t = loadTicket(id);
|
|
const allowed = { CLAIM: ["OPEN"], RECEIPT: ["CLAIMED", "OPEN"], CLOSE: ["RECEIPTED", "CLAIMED"], NOTE: ["OPEN", "CLAIMED", "RECEIPTED", "CLOSED"] };
|
|
if (!allowed[action].includes(t.status)) fail(`INVALID_TRANSITION:${t.status}->${action}`);
|
|
const entry = appendLedger(action, id, by, text ? { text } : {});
|
|
t.history.push({ ts: entry.ts, action, actor: entry.actor, text: text || undefined, entry_hash: entry.entry_hash });
|
|
if (action === "CLAIM") { t.status = "CLAIMED"; t.claimed_by = entry.actor; }
|
|
if (action === "RECEIPT") t.status = "RECEIPTED";
|
|
if (action === "CLOSE") t.status = "CLOSED";
|
|
saveTicket(t);
|
|
process.stdout.write(`FDT ${action} ${id} by=${entry.actor} status=${t.status} head=${entry.entry_hash.slice(0, 12)}\n`);
|
|
}
|
|
|
|
function list(argv) {
|
|
const status = valueAfter(argv, "--status");
|
|
let ids = [];
|
|
try { ids = fs.readdirSync(TICKETS).filter((n) => n.endsWith(".json")).sort(); } catch { /* empty */ }
|
|
for (const name of ids) {
|
|
const t = readJson(path.join(TICKETS, name));
|
|
if (!t || (status && t.status !== status)) continue;
|
|
process.stdout.write(`${t.ticket_id} [${t.status}] ${t.kind} "${t.subject.slice(0, 40)}" by=${t.creator} -> ${t.audience}\n`);
|
|
}
|
|
}
|
|
|
|
function verify() {
|
|
const ledger = readJson(LEDGER);
|
|
if (!ledger || ledger.schema !== "guanghu.fifth-domain-notary-ledger/v1") fail("LEDGER_MISSING_OR_CORRUPT");
|
|
let prev = null;
|
|
for (const entry of ledger.entries) {
|
|
const { entry_hash, ...rest } = entry;
|
|
if (entry.prev_hash !== prev) fail(`CHAIN_BREAK_AT_SEQ:${entry.seq}`);
|
|
if (sha256(canonical(rest)) !== entry_hash) fail(`HASH_MISMATCH_AT_SEQ:${entry.seq}`);
|
|
prev = entry_hash;
|
|
}
|
|
if (ledger.head !== prev) fail("HEAD_MISMATCH");
|
|
for (const name of fs.readdirSync(TICKETS).filter((n) => n.endsWith(".json"))) {
|
|
const t = readJson(path.join(TICKETS, name));
|
|
if (!t || !Array.isArray(t.history)) fail(`TICKET_CORRUPT:${name}`);
|
|
const body = fs.readFileSync(t.body_path, "utf8");
|
|
if (sha256(body) !== t.body_sha256) fail(`TICKET_BODY_TAMPERED:${t.ticket_id}`);
|
|
for (const h of t.history) {
|
|
if (!ledger.entries.some((e) => e.entry_hash === h.entry_hash)) fail(`TICKET_HISTORY_UNANCHORED:${t.ticket_id}:${h.action}`);
|
|
}
|
|
}
|
|
process.stdout.write(`FDT VERIFY PASS entries=${ledger.entries.length} head=${ledger.head.slice(0, 12)}\n`);
|
|
}
|
|
|
|
function main() {
|
|
const [command, ...argv] = process.argv.slice(2);
|
|
switch (command) {
|
|
case "create": create(argv); break;
|
|
case "claim": transition(argv, "CLAIM"); break;
|
|
case "receipt": transition(argv, "RECEIPT"); break;
|
|
case "note": transition(argv, "NOTE"); break;
|
|
case "close": transition(argv, "CLOSE"); break;
|
|
case "list": list(argv); break;
|
|
case "verify": verify(); break;
|
|
default: process.stderr.write("usage: fifth-domain-notary.mjs create|claim|receipt|note|close|list|verify\n"); process.exit(2);
|
|
}
|
|
}
|
|
main();
|