muse-crew 0.14.4 → 0.14.6

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.
@@ -1,154 +0,0 @@
1
- // publish-content.js — shared deterministic content-judgment primitives.
2
- //
3
- // Parent publish verification (lib/verify-publish.js) and unknown-recovery
4
- // classification (lib/classify-publish-absence.js) both judge the same
5
- // question — "does the on-disk source contain the publish commit's diff?" —
6
- // against the same diff parser, findings parser, and collision-exemption
7
- // rules. Two copies of the collision logic already diverged once (2026-09-15,
8
- // task 00bca4b8); this module is the single source of truth.
9
- //
10
- // ESM (imported by verify-publish.js and classify-publish-absence.js).
11
- // Pure and deterministic: no wall-clock reads, no randomness, no I/O except
12
- // the git subprocesses in makeOldCounter (read-only).
13
-
14
- import { execFileSync } from "node:child_process";
15
-
16
- export const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
17
-
18
- // Parse `git diff base commit` into per-file added/removed lines.
19
- // Byte-identical split to build-readback-request.js and the old
20
- // verify-publish.js inline parser (the verifier looks raw lines up
21
- // verbatim, so all consumers must agree on the split).
22
- export function parseDiff(diffText) {
23
- const expected = new Map(); // path -> { added: [], removed: [], isBinary: bool }
24
- let curFile = null;
25
- for (const line of diffText.split("\n")) {
26
- if (line.startsWith("diff --git")) {
27
- const m = line.match(/^diff --git a\/(.+) b\/(.+)$/);
28
- curFile = m ? m[2] : "unknown";
29
- expected.set(curFile, { added: [], removed: [], isBinary: false });
30
- } else if (curFile && line.startsWith("Binary files ")) {
31
- expected.get(curFile).isBinary = true;
32
- } else if (curFile && line.startsWith("+") && !line.startsWith("+++")) {
33
- expected.get(curFile).added.push(line.slice(1));
34
- } else if (curFile && line.startsWith("-") && !line.startsWith("---")) {
35
- expected.get(curFile).removed.push(line.slice(1));
36
- }
37
- }
38
- return expected;
39
- }
40
-
41
- // Parse the machine-readable findings block:
42
- // FILE: <path>
43
- // ADDED: <line> :: PRESENT|ABSENT
44
- // REMOVED: <line> :: PRESENT|ABSENT
45
- // END_FILE
46
- export function parseFindings(text) {
47
- const findings = new Map(); // path -> { added: Map(line->verdict), removed: Map(line->verdict) }
48
- let cur = null;
49
- let malformed = null;
50
- for (const rawLine of text.split("\n")) {
51
- const line = rawLine.trimEnd();
52
- if (line.startsWith("FILE: ")) {
53
- cur = { added: new Map(), removed: new Map() };
54
- findings.set(line.slice(6).trim(), cur);
55
- } else if (line === "END_FILE") {
56
- cur = null;
57
- } else if (cur && (line.startsWith("ADDED: ") || line.startsWith("REMOVED: "))) {
58
- const kind = line.startsWith("ADDED: ") ? "added" : "removed";
59
- const rest = line.slice(kind === "added" ? 7 : 9);
60
- const sep = rest.lastIndexOf(" :: ");
61
- if (sep < 0) { malformed = `malformed finding line: ${line.slice(0, 80)}`; break; }
62
- const content = rest.slice(0, sep);
63
- const verdict = rest.slice(sep + 4).trim();
64
- if (verdict !== "PRESENT" && verdict !== "ABSENT") {
65
- malformed = `bad verdict: ${verdict}`;
66
- break;
67
- }
68
- cur[kind].set(content, verdict);
69
- }
70
- }
71
- return { findings, malformed };
72
- }
73
-
74
- // Whole-line occurrence counts of a file's content at <base>.
75
- // [] when the base is the empty tree or the file did not exist there.
76
- function oldTreeLines(repoPath, base, path) {
77
- if (base === EMPTY_TREE) return [];
78
- let text;
79
- try {
80
- text = execFileSync("git", ["-C", repoPath, "show", `${base}:${path}`], {
81
- encoding: "utf8", maxBuffer: 4 * 1024 * 1024,
82
- });
83
- } catch {
84
- return [];
85
- }
86
- if (text === "") return [];
87
- const lines = text.split("\n");
88
- if (lines[lines.length - 1] === "") lines.pop(); // drop the trailing-newline artifact
89
- return lines;
90
- }
91
-
92
- // Returns (path, line) => occurrences of line in path's old tree.
93
- // Cached per path. Read-only git subprocesses.
94
- export function makeOldCounter(repoPath, base) {
95
- const cache = new Map(); // path -> Map(line -> count)
96
- return (path, line) => {
97
- let counts = cache.get(path);
98
- if (counts === undefined) {
99
- counts = new Map();
100
- for (const l of oldTreeLines(repoPath, base, path)) {
101
- counts.set(l, (counts.get(l) || 0) + 1);
102
- }
103
- cache.set(path, counts);
104
- }
105
- return counts.get(line) || 0;
106
- };
107
- }
108
-
109
- // Split each file's diff lines into discriminating vs non-discriminating.
110
- //
111
- // A line has zero discriminating power when its presence/absence cannot
112
- // tell "hunk landed" from "hunk dropped":
113
- // - added line already in the old tree (colliding pre-existing line):
114
- // PRESENT whether or not the hunk landed.
115
- // - removed line occurring in the old tree strictly more times than the
116
- // diff removes it: PRESENT whether or not the removal happened.
117
- //
118
- // Returns Map(path -> {
119
- // addedDisc: [...], removedDisc: [...], // discriminating lines
120
- // unjudgeable: null | "binary-or-mode-or-rename" | "fully-colliding-added"
121
- // }).
122
- // unjudgeable marks files the line-based judge cannot certify at all:
123
- // a diff with no content lines (binary, mode-only, rename), or added
124
- // lines that ALL collide (their PRESENT findings prove nothing).
125
- export function discriminatingLines(diffMap, oldCount) {
126
- const out = new Map();
127
- for (const [path, exp] of diffMap) {
128
- const entry = { addedDisc: [], removedDisc: [], unjudgeable: null };
129
- if (exp.added.length === 0 && exp.removed.length === 0) {
130
- entry.unjudgeable = "binary-or-mode-or-rename";
131
- out.set(path, entry);
132
- continue;
133
- }
134
- for (const line of exp.added) {
135
- if (oldCount(path, line) === 0) entry.addedDisc.push(line);
136
- }
137
- // The diff may remove the same line more than once; the old tree must
138
- // account for every removal before a line counts as colliding.
139
- const removedBudget = new Map();
140
- for (const line of exp.removed) {
141
- removedBudget.set(line, (removedBudget.get(line) || 0) + 1);
142
- }
143
- for (const line of exp.removed) {
144
- if (oldCount(path, line) <= removedBudget.get(line)) {
145
- entry.removedDisc.push(line);
146
- }
147
- }
148
- if (exp.added.length > 0 && entry.addedDisc.length === 0) {
149
- entry.unjudgeable = "fully-colliding-added";
150
- }
151
- out.set(path, entry);
152
- }
153
- return out;
154
- }
@@ -1,195 +0,0 @@
1
- #!/usr/bin/env node
2
- // readback-disk.js — deterministic disk read-back sensor for parent publish
3
- // verification (docs/publish-verification.md).
4
- //
5
- // The read-back gap (2026-09-14): the platform removed `artifact_inspect`
6
- // and no agent-callable content read-back existed, so every artifact publish
7
- // parked at `publish: verification-requested` permanently. The platform's
8
- // artifact edits land in its on-disk working copy of the artifact source
9
- // (~/workspace/ts-spaces/<slug>/ — verified empirically 2026-09-15: added
10
- // lines present, removed lines absent across real platform commits), so a
11
- // deterministic local sensor can replace the removed inspection tool: no
12
- // LLM, no async handoff, no prose to parse.
13
- //
14
- // This script is the SENSOR; lib/verify-publish.js stays the judge. It
15
- // emits the exact machine-readable findings block the verifier already
16
- // parses:
17
- //
18
- // FILE: <path>
19
- // ADDED: <exact added line from the diff> :: PRESENT|ABSENT
20
- // REMOVED: <exact removed line from the diff> :: PRESENT|ABSENT
21
- // END_FILE
22
- //
23
- // one ADDED line per (+) diff line and one REMOVED line per (-) diff line,
24
- // in diff order. The verdict is PRESENT iff that exact line occurs in the
25
- // file's CURRENT on-disk source (whole-line match), ABSENT otherwise.
26
- //
27
- // Authority boundary: the sensor reads the platform's working copy of the
28
- // artifact source — the tree the hosted artifact is built/served from. If
29
- // the working copy is stale relative to a just-applied edit, the findings
30
- // honestly report ABSENT and the verifier fails CLOSED (parked, never a
31
- // false stamp). Staleness can only park, never certify.
32
- //
33
- // Usage:
34
- // node readback-disk.js --repo-path <path> --commit <sha> --base <sha>
35
- // --slug <artifact-slug> --task-id <uuid> [--spaces-root <dir>]
36
- //
37
- // --base is the previously-stamped provenance source_commit (or the
38
- // empty-tree sha 4b825dc642cb6eb9a060e54bf8d69288fbee4904 for a first
39
- // publish) — the SAME base the verifier uses. --spaces-root defaults to
40
- // ~/workspace/ts-spaces.
41
- //
42
- // Exit codes: 0 ok (findings block on stdout) · 2 usage/validation ·
43
- // 1 git/diff/read failure. On non-zero exit NOTHING is printed to stdout,
44
- // so a caller must never save stdout as a findings file.
45
- //
46
- // Security: the slug is charset-validated and the resolved space directory
47
- // must stay under the spaces root (realpath containment); every file path
48
- // from the diff is likewise contained before reading. A path that escapes
49
- // is a validation failure (exit 2), never a read.
50
-
51
- import { execFileSync } from "node:child_process";
52
- import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
53
- import { homedir } from "node:os";
54
- import { join, resolve } from "node:path";
55
-
56
- // --help: before required-arg parsing (shebang⇔CLI contract).
57
- if (process.argv.slice(2).includes("--help")) {
58
- console.log(
59
- "usage: node readback-disk.js --repo-path <path> --commit <sha> --base <sha> --task-id <uuid> --slug <artifact-slug> [--spaces-root <dir>]\n" +
60
- "the deterministic disk read-back SENSOR: emits the machine-readable findings block (FILE:/ADDED:/REMOVED:/END_FILE)"
61
- );
62
- process.exit(0);
63
- }
64
-
65
- const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
66
-
67
- function arg(name) {
68
- const i = process.argv.indexOf(name);
69
- if (i < 0 || i + 1 >= process.argv.length) {
70
- fail("usage", `${name} is required.`, 2);
71
- }
72
- return process.argv[i + 1];
73
- }
74
- function fail(code, message, exitCode) {
75
- // stderr only — stdout must stay empty on failure.
76
- process.stderr.write(JSON.stringify({ ok: false, error: code, message }) + "\n");
77
- process.exit(exitCode);
78
- }
79
-
80
- const repoPath = arg("--repo-path");
81
- const commit = arg("--commit");
82
- const base = arg("--base");
83
- const slug = arg("--slug");
84
- const taskId = arg("--task-id");
85
- const spacesRoot = process.argv.includes("--spaces-root")
86
- ? arg("--spaces-root")
87
- : join(homedir(), "workspace", "ts-spaces");
88
-
89
- if (!/^[0-9a-f]{40}$/.test(commit)) fail("usage", "commit must be a 40-char hex sha.", 2);
90
- if (!/^[0-9a-f]{40}$/.test(base)) fail("usage", "base must be a 40-char hex sha.", 2);
91
- if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(slug)) {
92
- fail("usage", `slug ${JSON.stringify(slug)} is not a safe artifact slug.`, 2);
93
- }
94
-
95
- // Resolve the space directory with realpath containment: the slug must not
96
- // escape the spaces root (no "..", no symlinks pointing out).
97
- let spaceDir;
98
- try {
99
- const rootReal = realpathSync(spacesRoot);
100
- const candReal = realpathSync(join(spacesRoot, slug));
101
- if (candReal !== rootReal && !candReal.startsWith(rootReal + "/")) {
102
- fail("usage", `slug ${JSON.stringify(slug)} resolves outside the spaces root.`, 2);
103
- }
104
- spaceDir = candReal;
105
- } catch (e) {
106
- fail("usage", `cannot resolve space dir for slug ${JSON.stringify(slug)}: ${e.message}`, 2);
107
- }
108
- try {
109
- if (!statSync(spaceDir).isDirectory()) fail("usage", `space dir ${spaceDir} is not a directory.`, 2);
110
- } catch (e) {
111
- fail("usage", `space dir ${spaceDir} unreadable: ${e.message}`, 2);
112
- }
113
-
114
- // The publish delta is base..commit — the same diff the verifier checks the
115
- // findings against. Refuse a base that is not an ancestor: certifying a
116
- // diff against an unrelated tree is worse than no read-back.
117
- if (base !== EMPTY_TREE) {
118
- try {
119
- execFileSync("git", ["-C", repoPath, "merge-base", "--is-ancestor", base, commit], { stdio: "ignore" });
120
- } catch (e) {
121
- fail("usage", `base ${base} is not an ancestor of commit ${commit} — refusing to read back an unrelated tree.`, 2);
122
- }
123
- }
124
-
125
- let diff;
126
- try {
127
- diff = execFileSync("git", ["-C", repoPath, "diff", base, commit, "--"], {
128
- encoding: "utf8", maxBuffer: 4 * 1024 * 1024,
129
- });
130
- } catch (e) {
131
- fail("git", `git diff failed: ${e.message}`, 1);
132
- }
133
- if (!diff.trim()) {
134
- fail("git", "empty diff for commit — nothing to read back.", 1);
135
- }
136
-
137
- // Parse the diff into per-file added/removed lines (byte-identical parser
138
- // to build-readback-request.js and verify-publish.js: the verifier looks
139
- // the raw lines up verbatim, so all three must agree on the split).
140
- const files = [];
141
- let current = null;
142
- for (const line of diff.split("\n")) {
143
- if (line.startsWith("diff --git")) {
144
- const m = line.match(/^diff --git a\/(.+) b\/(.+)$/);
145
- current = { path: m ? m[2] : "unknown", added: [], removed: [] };
146
- files.push(current);
147
- } else if (current && line.startsWith("+") && !line.startsWith("+++")) {
148
- current.added.push(line.slice(1));
149
- } else if (current && line.startsWith("-") && !line.startsWith("---")) {
150
- current.removed.push(line.slice(1));
151
- }
152
- }
153
-
154
- // Whole-line content of a disk file, normalized for the membership test.
155
- // Trailing-newline artifact popped (same convention as verify-publish.js
156
- // oldTreeLines); one trailing CR stripped per line so CRLF working copies
157
- // compare honestly against LF diffs. The RAW diff lines are still what get
158
- // emitted — the verifier looks those up verbatim.
159
- const norm = (s) => (s.endsWith("\r") ? s.slice(0, -1) : s);
160
- function diskLines(absPath) {
161
- if (!existsSync(absPath)) return new Set(); // missing file: every line ABSENT
162
- let text;
163
- try {
164
- text = readFileSync(absPath, "utf8");
165
- } catch (e) {
166
- fail("read", `cannot read ${absPath}: ${e.message}`, 1);
167
- }
168
- const lines = text.split("\n");
169
- if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
170
- return new Set(lines.map(norm));
171
- }
172
-
173
- const out = [];
174
- for (const f of files) {
175
- // Contain every diff path inside the space dir before reading.
176
- const abs = resolve(spaceDir, f.path);
177
- if (abs !== spaceDir && !abs.startsWith(spaceDir + "/")) {
178
- fail("usage", `diff path ${JSON.stringify(f.path)} escapes the space dir.`, 2);
179
- }
180
- const present = diskLines(abs);
181
- out.push(`FILE: ${f.path}`);
182
- for (const line of f.added) {
183
- out.push(`ADDED: ${line} :: ${present.has(norm(line)) ? "PRESENT" : "ABSENT"}`);
184
- }
185
- for (const line of f.removed) {
186
- out.push(`REMOVED: ${line} :: ${present.has(norm(line)) ? "PRESENT" : "ABSENT"}`);
187
- }
188
- out.push("END_FILE");
189
- }
190
-
191
- process.stdout.write(out.join("\n") + "\n");
192
- process.stderr.write(JSON.stringify({
193
- ok: true, slug, task_id: taskId, commit, base,
194
- files: files.map((f) => f.path),
195
- } ) + "\n");