@twinsearth/w2m-dsh-plugin 1.9.16 → 1.9.17

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.
@@ -81,6 +81,16 @@ export function projectLedgerLine(line) {
81
81
  try { record = JSON.parse(line); } catch { return { changed: false, line }; }
82
82
  // Real ledger lines nest the envelope one level down: {type:'result.stored', result:{envelope:{...}}}.
83
83
  // Handle that shape first, then the flat result, then a bare envelope record.
84
+ // v2 R8: a legacy task record still carries the command itself. Project it to hash + length,
85
+ // so the scrub can bring old ledgers in line with "persist only metadata".
86
+ if (record?.type === 'task.created' && record.task && typeof record.task === 'object') {
87
+ const task = record.task;
88
+ if (!Array.isArray(task.command_argv)) return { changed: false, line };
89
+ task.command_argv_len = task.command_argv.length;
90
+ delete task.command_argv;
91
+ record.projection = { dropped: ['task.command_argv'], at: new Date().toISOString() };
92
+ return { changed: true, line: JSON.stringify(record) };
93
+ }
84
94
  let holder = null;
85
95
  if (record?.result && typeof record.result === 'object' && record.result.envelope && typeof record.result.envelope === 'object') holder = { parent: record.result, key: 'envelope' };
86
96
  else if (record?.result && typeof record.result === 'object') holder = { parent: record, key: 'result' };
@@ -614,10 +614,13 @@ export function renderReportMarkdown(agg, task = null) {
614
614
  L.push('');
615
615
  }
616
616
  if (task) {
617
- L.push('## 原始命令');
617
+ // R8: the markdown report is readable by any paired device token, so it must never carry the
618
+ // command itself. The hash is the command identity; the length is the only shape we expose.
619
+ L.push('## 命令指纹');
618
620
  L.push('');
619
621
  L.push('```');
620
- L.push(jcs(task.command_argv));
622
+ L.push('command_hash: ' + (typeof task.command_hash === 'string' ? task.command_hash : '-'));
623
+ L.push('command_argv_len: ' + (Array.isArray(task.command_argv) ? task.command_argv.length : '-'));
621
624
  L.push('```');
622
625
  L.push('');
623
626
  }
@@ -1,81 +1,94 @@
1
- /**
2
- * relay 状态清理(P0):旧 blob 清除 + ledger 内容投影重建。
3
- * 只处理已落盘的数据,不联网;dry-run 先报清单,apply 先备份再原子替换。
4
- */
5
- import {
6
- copyFileSync, existsSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync,
7
- } from 'node:fs';
8
- import { createHash } from 'node:crypto';
9
- import { join } from 'node:path';
10
-
11
- import { projectLedgerLine } from './p0-projection.mjs';
12
-
13
- export const LEDGER_CONTENT_FIELDS = ['stdout_head', 'stdout_tail', 'stderr_head', 'stderr_tail', 'command_argv', 'pipeline_hash_material', 'changed_files'];
14
- const sha256 = (buf) => createHash('sha256').update(buf).digest('hex');
15
-
16
-
17
- /** 清理 ledger:默认 dry-run;apply 时备份 .bak-<ts> 后原子替换。 */
18
- export function scrubLedger(ledgerPath, { dryRun = true, now = Date.now() } = {}) {
19
- if (!existsSync(ledgerPath)) return { ok: true, exists: false, changed: 0, lines: 0, backup: null };
20
- const text = readFileSync(ledgerPath, 'utf8');
21
- const lines = text.split('\n').filter((line) => line.trim() !== '');
22
- let changed = 0;
23
- const rewritten = lines.map((line) => {
24
- const projected = projectLedgerLine(line);
25
- if (projected.changed) changed += 1;
26
- return projected.line;
27
- });
28
- if (dryRun || changed === 0) return { ok: true, exists: true, changed, lines: lines.length, backup: null, dryRun };
29
- const backup = ledgerPath + '.bak-' + now;
30
- copyFileSync(ledgerPath, backup);
31
- const tmp = ledgerPath + '.tmp-' + process.pid;
32
- writeFileSync(tmp, rewritten.join('\n') + '\n', 'utf8');
33
- renameSync(tmp, ledgerPath);
34
- return { ok: true, exists: true, changed, lines: lines.length, backup, dryRun: false };
35
- }
36
-
37
- /** 清点/删除 relay blob 目录(内容寻址文件)。 */
38
- export function scrubBlobs(blobDir, { dryRun = true, now = Date.now() } = {}) {
39
- if (!existsSync(blobDir)) return { ok: true, exists: false, files: [], bytes: 0, deleted: 0, manifest: null };
40
- const files = [];
41
- let bytes = 0;
42
- for (const name of readdirSync(blobDir)) {
43
- const path = join(blobDir, name);
44
- if (!statSync(path).isFile()) continue;
45
- files.push(name);
46
- bytes += statSync(path).size;
47
- }
48
- const manifest = {
49
- at: new Date(now).toISOString(),
50
- files: files.map((name) => ({ name, sha256: sha256(readFileSync(join(blobDir, name))) })),
51
- };
52
- if (dryRun) return { ok: true, exists: true, files, bytes, deleted: 0, manifest, dryRun };
53
- for (const name of files) unlinkSync(join(blobDir, name));
54
- return { ok: true, exists: true, files, bytes, deleted: files.length, manifest, dryRun: false };
55
- }
56
-
57
- /** 验收:扫描目录中是否仍存在内容字段/数据面 marker。 */
58
- export function scanForContent(rootDir, markers = []) {
59
- const hits = [];
60
- const walk = (dir) => {
61
- for (const name of readdirSync(dir)) {
62
- // Backups/temporaries are not live state: they hold the bytes the scrub just moved out.
63
- if (/\.bak-|\.tmp-|\.corrupt-/.test(name)) continue;
64
- const path = join(dir, name);
65
- const stat = statSync(path);
66
- if (stat.isDirectory()) { walk(path); continue; }
67
- if (stat.size > 32 * 1024 * 1024) continue;
68
- // The projection metadata names the dropped fields; scanning it for those names would report
69
- // the scrub itself as a leak, so it is removed before the marker check.
70
- const text = readFileSync(path, 'utf8').replace(/"projection"\s*:\s*\{[^{}]*\}/g, '');
71
- for (const field of LEDGER_CONTENT_FIELDS) {
72
- if (text.includes('"' + field + '"')) hits.push({ file: path, marker: field });
73
- }
74
- for (const marker of markers) {
75
- if (marker !== '' && text.includes(marker)) hits.push({ file: path, marker });
76
- }
77
- }
78
- };
79
- if (existsSync(rootDir)) walk(rootDir);
80
- return { ok: hits.length === 0, hits };
81
- }
1
+ /**
2
+ * relay 状态清理(P0):旧 blob 清除 + ledger 内容投影重建。
3
+ * 只处理已落盘的数据,不联网;dry-run 先报清单,apply 先备份再原子替换。
4
+ */
5
+ import {
6
+ copyFileSync, existsSync, readdirSync, readFileSync, renameSync, rmdirSync, statSync, unlinkSync, writeFileSync,
7
+ } from 'node:fs';
8
+ import { createHash } from 'node:crypto';
9
+ import { join } from 'node:path';
10
+
11
+ import { projectLedgerLine } from './p0-projection.mjs';
12
+
13
+ export const LEDGER_CONTENT_FIELDS = ['stdout_head', 'stdout_tail', 'stderr_head', 'stderr_tail', 'command_argv', 'pipeline_hash_material', 'changed_files'];
14
+ const sha256 = (buf) => createHash('sha256').update(buf).digest('hex');
15
+
16
+
17
+ /** 清理 ledger:默认 dry-run;apply 时备份 .bak-<ts> 后原子替换。 */
18
+ export function scrubLedger(ledgerPath, { dryRun = true, now = Date.now() } = {}) {
19
+ if (!existsSync(ledgerPath)) return { ok: true, exists: false, changed: 0, lines: 0, backup: null };
20
+ const text = readFileSync(ledgerPath, 'utf8');
21
+ const lines = text.split('\n').filter((line) => line.trim() !== '');
22
+ let changed = 0;
23
+ const rewritten = lines.map((line) => {
24
+ const projected = projectLedgerLine(line);
25
+ if (projected.changed) changed += 1;
26
+ return projected.line;
27
+ });
28
+ if (dryRun || changed === 0) return { ok: true, exists: true, changed, lines: lines.length, backup: null, dryRun };
29
+ const backup = ledgerPath + '.bak-' + now;
30
+ copyFileSync(ledgerPath, backup);
31
+ const tmp = ledgerPath + '.tmp-' + process.pid;
32
+ writeFileSync(tmp, rewritten.join('\n') + '\n', 'utf8');
33
+ renameSync(tmp, ledgerPath);
34
+ return { ok: true, exists: true, changed, lines: lines.length, backup, dryRun: false };
35
+ }
36
+
37
+ /** 清点/删除 relay blob 目录(内容寻址;真实布局是 blobs/<aa>/<sha>,递归处理)。 */
38
+ export function scrubBlobs(blobDir, { dryRun = true, now = Date.now() } = {}) {
39
+ if (!existsSync(blobDir)) return { ok: true, exists: false, files: [], bytes: 0, deleted: 0, manifest: null };
40
+ const files = [];
41
+ let bytes = 0;
42
+ const walk = (dir, rel) => {
43
+ for (const name of readdirSync(dir)) {
44
+ const path = join(dir, name);
45
+ const stat = statSync(path);
46
+ const relPath = rel === '' ? name : rel + '/' + name;
47
+ if (stat.isDirectory()) { walk(path, relPath); continue; }
48
+ files.push(relPath);
49
+ bytes += stat.size;
50
+ }
51
+ };
52
+ walk(blobDir, '');
53
+ const manifest = {
54
+ at: new Date(now).toISOString(),
55
+ files: files.map((name) => ({ name, sha256: sha256(readFileSync(join(blobDir, name.split('/').join('/')))) })),
56
+ };
57
+ if (dryRun) return { ok: true, exists: true, files, bytes, deleted: 0, manifest, dryRun };
58
+ for (const name of files) unlinkSync(join(blobDir, name));
59
+ // Remove the empty sharding directories the files lived in, but never the blob root itself.
60
+ const prune = (dir) => {
61
+ for (const name of readdirSync(dir)) {
62
+ const path = join(dir, name);
63
+ if (statSync(path).isDirectory()) { prune(path); try { rmdirSync(path); } catch { /* not empty */ } }
64
+ }
65
+ };
66
+ prune(blobDir);
67
+ return { ok: true, exists: true, files, bytes, deleted: files.length, manifest, dryRun: false };
68
+ }
69
+
70
+ /** 验收:扫描目录中是否仍存在内容字段/数据面 marker。 */
71
+ export function scanForContent(rootDir, markers = []) {
72
+ const hits = [];
73
+ const walk = (dir) => {
74
+ for (const name of readdirSync(dir)) {
75
+ // Backups/temporaries are not live state: they hold the bytes the scrub just moved out.
76
+ if (/\.bak-|\.tmp-|\.corrupt-/.test(name)) continue;
77
+ const path = join(dir, name);
78
+ const stat = statSync(path);
79
+ if (stat.isDirectory()) { walk(path); continue; }
80
+ if (stat.size > 32 * 1024 * 1024) continue;
81
+ // The projection metadata names the dropped fields; scanning it for those names would report
82
+ // the scrub itself as a leak, so it is removed before the marker check.
83
+ const text = readFileSync(path, 'utf8').replace(/"projection"\s*:\s*\{[^{}]*\}/g, '');
84
+ for (const field of LEDGER_CONTENT_FIELDS) {
85
+ if (text.includes('"' + field + '"')) hits.push({ file: path, marker: field });
86
+ }
87
+ for (const marker of markers) {
88
+ if (marker !== '' && text.includes(marker)) hits.push({ file: path, marker });
89
+ }
90
+ }
91
+ };
92
+ if (existsSync(rootDir)) walk(rootDir);
93
+ return { ok: hits.length === 0, hits };
94
+ }