qualia-framework 6.2.9 → 6.2.10

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.
Files changed (72) hide show
  1. package/README.md +14 -11
  2. package/agents/builder.md +7 -7
  3. package/agents/planner.md +39 -3
  4. package/agents/research-synthesizer.md +1 -1
  5. package/agents/researcher.md +3 -3
  6. package/agents/roadmapper.md +7 -7
  7. package/agents/verifier.md +18 -6
  8. package/agents/visual-evaluator.md +8 -7
  9. package/bin/cli.js +111 -14
  10. package/bin/contract-runner.js +219 -0
  11. package/bin/host-adapters.js +66 -0
  12. package/bin/install.js +99 -152
  13. package/bin/plan-contract.js +99 -2
  14. package/bin/planning-hygiene.js +262 -0
  15. package/bin/runtime-manifest.js +32 -0
  16. package/bin/state-ledger.js +184 -0
  17. package/bin/state.js +299 -20
  18. package/bin/trust-score.js +276 -0
  19. package/docs/onboarding.html +5 -4
  20. package/guide.md +3 -2
  21. package/package.json +1 -1
  22. package/qualia-design/design-rubric.md +17 -5
  23. package/qualia-design/frontend.md +5 -1
  24. package/qualia-design/graphics.md +47 -0
  25. package/rules/command-output.md +35 -0
  26. package/skills/qualia/SKILL.md +10 -10
  27. package/skills/qualia-build/SKILL.md +20 -14
  28. package/skills/qualia-debug/SKILL.md +16 -8
  29. package/skills/qualia-discuss/SKILL.md +10 -10
  30. package/skills/qualia-doctor/SKILL.md +140 -0
  31. package/skills/qualia-feature/SKILL.md +23 -21
  32. package/skills/qualia-fix/SKILL.md +216 -0
  33. package/skills/qualia-flush/SKILL.md +9 -9
  34. package/skills/qualia-handoff/SKILL.md +9 -9
  35. package/skills/qualia-help/SKILL.md +3 -3
  36. package/skills/qualia-hook-gen/SKILL.md +1 -1
  37. package/skills/qualia-idk/SKILL.md +4 -4
  38. package/skills/qualia-issues/SKILL.md +2 -2
  39. package/skills/qualia-learn/SKILL.md +10 -10
  40. package/skills/qualia-map/SKILL.md +2 -2
  41. package/skills/qualia-milestone/SKILL.md +15 -15
  42. package/skills/qualia-new/REFERENCE.md +9 -9
  43. package/skills/qualia-new/SKILL.md +14 -14
  44. package/skills/qualia-optimize/REFERENCE.md +1 -1
  45. package/skills/qualia-optimize/SKILL.md +23 -16
  46. package/skills/qualia-pause/SKILL.md +2 -2
  47. package/skills/qualia-plan/SKILL.md +23 -13
  48. package/skills/qualia-polish/REFERENCE.md +14 -14
  49. package/skills/qualia-polish/SKILL.md +64 -19
  50. package/skills/qualia-polish/scripts/loop.mjs +3 -3
  51. package/skills/qualia-polish/scripts/score.mjs +9 -3
  52. package/skills/qualia-postmortem/SKILL.md +9 -9
  53. package/skills/qualia-report/SKILL.md +23 -23
  54. package/skills/qualia-research/SKILL.md +5 -5
  55. package/skills/qualia-resume/SKILL.md +4 -4
  56. package/skills/qualia-review/SKILL.md +28 -12
  57. package/skills/qualia-road/SKILL.md +18 -5
  58. package/skills/qualia-ship/SKILL.md +22 -22
  59. package/skills/qualia-skill-new/SKILL.md +13 -13
  60. package/skills/qualia-test/SKILL.md +5 -5
  61. package/skills/qualia-triage/SKILL.md +1 -1
  62. package/skills/qualia-verify/SKILL.md +37 -23
  63. package/skills/qualia-vibe/SKILL.md +13 -10
  64. package/skills/qualia-vibe/scripts/extract.mjs +1 -1
  65. package/skills/zoho-workflow/SKILL.md +1 -1
  66. package/templates/help.html +12 -10
  67. package/tests/bin.test.sh +34 -4
  68. package/tests/install-smoke.test.sh +22 -2
  69. package/tests/lib.test.sh +290 -0
  70. package/tests/runner.js +3 -0
  71. package/tests/skills.test.sh +4 -4
  72. package/tests/state.test.sh +65 -3
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  // Plan contract validator + helpers. See docs/plan-contract.md.
3
3
  //
4
- // Pure library no CLI dispatch. Required by state.js and by skills that
5
- // emit/consume `.planning/phase-{N}-contract.json`.
4
+ // Library + tiny CLI. Required by state.js and by skills that emit/consume
5
+ // `.planning/phase-{N}-contract.json`.
6
6
  //
7
7
  // Zero npm dependencies. Hand-rolled validator, ~100 LOC.
8
8
 
@@ -241,11 +241,108 @@ function checkDrift(contractPath, planMdPath) {
241
241
  return { ok: true, drift: stored !== current, stored, current };
242
242
  }
243
243
 
244
+ function readContractFile(contractPath) {
245
+ if (!fs.existsSync(contractPath)) {
246
+ return { ok: false, error: "CONTRACT_MISSING", message: `Contract file not found: ${contractPath}` };
247
+ }
248
+ const parsed = parseSafely(fs.readFileSync(contractPath, "utf8"));
249
+ if (!parsed.ok) {
250
+ return { ok: false, error: "CONTRACT_UNPARSEABLE", message: parsed.error };
251
+ }
252
+ return { ok: true, contract: parsed.value };
253
+ }
254
+
255
+ function cliUsage() {
256
+ console.error([
257
+ "Usage:",
258
+ " plan-contract.js validate <contract.json> [--json]",
259
+ " plan-contract.js drift <contract.json> <plan.md> [--json]",
260
+ " plan-contract.js hash <plan.md>",
261
+ ].join("\n"));
262
+ }
263
+
264
+ function printResult(payload, asJson) {
265
+ if (asJson) {
266
+ console.log(JSON.stringify(payload, null, 2));
267
+ return;
268
+ }
269
+ if (payload.ok) {
270
+ if (payload.action === "validate") console.log(`VALID ${payload.path}`);
271
+ else if (payload.action === "drift") console.log(payload.drift ? "DRIFT" : "NO_DRIFT");
272
+ else if (payload.action === "hash") console.log(payload.hash);
273
+ else console.log("OK");
274
+ return;
275
+ }
276
+ console.error(`${payload.error || "ERROR"}: ${payload.message || (payload.errors || []).join("; ")}`);
277
+ }
278
+
279
+ function main(argv) {
280
+ const cmd = argv[2];
281
+ const asJson = argv.includes("--json");
282
+ if (!cmd || cmd === "--help" || cmd === "-h") {
283
+ cliUsage();
284
+ return 2;
285
+ }
286
+
287
+ if (cmd === "validate") {
288
+ const contractPath = argv[3];
289
+ if (!contractPath || contractPath.startsWith("--")) {
290
+ cliUsage();
291
+ return 2;
292
+ }
293
+ const loaded = readContractFile(contractPath);
294
+ if (!loaded.ok) {
295
+ printResult({ ok: false, action: "validate", path: contractPath, ...loaded }, asJson);
296
+ return 2;
297
+ }
298
+ const errors = validate(loaded.contract);
299
+ const payload = { ok: errors.length === 0, action: "validate", path: contractPath, errors };
300
+ printResult(payload, asJson);
301
+ return payload.ok ? 0 : 1;
302
+ }
303
+
304
+ if (cmd === "drift") {
305
+ const contractPath = argv[3];
306
+ const planPath = argv[4];
307
+ if (!contractPath || !planPath || contractPath.startsWith("--") || planPath.startsWith("--")) {
308
+ cliUsage();
309
+ return 2;
310
+ }
311
+ const result = checkDrift(contractPath, planPath);
312
+ const payload = { ok: !!result.ok && !result.drift, action: "drift", path: contractPath, plan: planPath, ...result };
313
+ printResult(payload, asJson);
314
+ return payload.ok ? 0 : 1;
315
+ }
316
+
317
+ if (cmd === "hash") {
318
+ const planPath = argv[3];
319
+ if (!planPath || planPath.startsWith("--")) {
320
+ cliUsage();
321
+ return 2;
322
+ }
323
+ if (!fs.existsSync(planPath)) {
324
+ printResult({ ok: false, action: "hash", error: "PLAN_MISSING", message: `Plan file not found: ${planPath}` }, asJson);
325
+ return 2;
326
+ }
327
+ const hash = hashPlan(fs.readFileSync(planPath, "utf8"));
328
+ printResult({ ok: true, action: "hash", path: planPath, hash }, asJson);
329
+ return 0;
330
+ }
331
+
332
+ cliUsage();
333
+ return 2;
334
+ }
335
+
244
336
  module.exports = {
245
337
  SCHEMA_VERSION,
246
338
  validate,
247
339
  parseSafely,
248
340
  hashPlan,
249
341
  checkDrift,
342
+ readContractFile,
250
343
  findScopeReductionPhrases,
251
344
  };
345
+
346
+ if (require.main === module) {
347
+ process.exit(main(process.argv));
348
+ }
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ // Scan and organize `.planning/` so project state stays navigable.
3
+
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ const ROOT_EXACT = new Set([
8
+ "PROJECT.md",
9
+ "PRODUCT.md",
10
+ "DESIGN.md",
11
+ "CONTEXT.md",
12
+ "JOURNEY.md",
13
+ "ROADMAP.md",
14
+ "REQUIREMENTS.md",
15
+ "STATE.md",
16
+ "tracking.json",
17
+ "project-discovery.md",
18
+ "agent-runs.jsonl",
19
+ ]);
20
+
21
+ const ROOT_PATTERNS = [
22
+ /^phase-\d+(-gaps)?-plan\.md$/,
23
+ /^phase-\d+-contract\.json$/,
24
+ /^phase-\d+-verification\.md$/,
25
+ /^phase-\d+-context\.md$/,
26
+ /^phase-\d+-research\.md$/,
27
+ /^phase-\d+-postmortem\.md$/,
28
+ /^phase-\d+-deviations\.json$/,
29
+ ];
30
+
31
+ const ALLOWED_DIRS = new Set([
32
+ "archive",
33
+ "assets",
34
+ "agent-runs",
35
+ "decisions",
36
+ "design",
37
+ "evidence",
38
+ "reports",
39
+ "research",
40
+ "snapshots",
41
+ ]);
42
+
43
+ function usage() {
44
+ return `planning-hygiene.js — keep .planning organized
45
+
46
+ Usage:
47
+ node planning-hygiene.js scan [--json] [--planning-dir .planning]
48
+ node planning-hygiene.js organize --write [--planning-dir .planning]
49
+
50
+ scan is read-only. organize requires --write and only moves loose artifacts.`;
51
+ }
52
+
53
+ function parseArgs(argv) {
54
+ const args = argv.slice(2);
55
+ const opts = {
56
+ command: args.find((arg) => !arg.startsWith("-")) || "scan",
57
+ json: args.includes("--json"),
58
+ write: args.includes("--write"),
59
+ planningDir: ".planning",
60
+ };
61
+
62
+ const dirIndex = args.indexOf("--planning-dir");
63
+ if (dirIndex !== -1 && args[dirIndex + 1]) {
64
+ opts.planningDir = args[dirIndex + 1];
65
+ }
66
+
67
+ if (args.includes("-h") || args.includes("--help")) {
68
+ opts.help = true;
69
+ }
70
+
71
+ return opts;
72
+ }
73
+
74
+ function isAllowedRootFile(name) {
75
+ return ROOT_EXACT.has(name) || ROOT_PATTERNS.some((pattern) => pattern.test(name));
76
+ }
77
+
78
+ function routeForLooseFile(name) {
79
+ if (/^DEBUG-\d{4}-\d{2}-\d{2}/.test(name)) return path.join("reports", "debug", name);
80
+ if (/^FIX-\d{4}-\d{2}-\d{2}/.test(name)) return path.join("reports", "fix", name);
81
+ if (name === "REVIEW.md" || /^REVIEW-/.test(name)) return path.join("reports", "review", name);
82
+ if (name === "OPTIMIZE.md" || /^OPTIMIZE-/.test(name)) return path.join("reports", "optimize", name);
83
+ if (/^REFACTOR-/.test(name)) return path.join("reports", "refactor", name);
84
+ if (/^polish-critique-/.test(name)) return path.join("reports", "polish", name);
85
+ if (/^vibe-after\./.test(name)) return path.join("assets", "vibe", name);
86
+ if (name === "DESIGN-extracted.md") return path.join("design", name);
87
+ return path.join("archive", "loose", name);
88
+ }
89
+
90
+ function listPlanningRoot(planningDir) {
91
+ if (!fs.existsSync(planningDir)) {
92
+ return { exists: false, entries: [] };
93
+ }
94
+ const entries = fs.readdirSync(planningDir, { withFileTypes: true })
95
+ .filter((entry) => entry.name !== ".DS_Store")
96
+ .map((entry) => ({
97
+ name: entry.name,
98
+ type: entry.isDirectory() ? "dir" : "file",
99
+ }))
100
+ .sort((a, b) => a.name.localeCompare(b.name));
101
+
102
+ return { exists: true, entries };
103
+ }
104
+
105
+ function scan(planningDir) {
106
+ const root = listPlanningRoot(planningDir);
107
+ if (!root.exists) {
108
+ return {
109
+ ok: false,
110
+ planning_dir: planningDir,
111
+ status: "missing",
112
+ loose: [],
113
+ unknown_dirs: [],
114
+ message: ".planning directory not found",
115
+ };
116
+ }
117
+
118
+ const loose = [];
119
+ const unknownDirs = [];
120
+
121
+ for (const entry of root.entries) {
122
+ if (entry.type === "dir") {
123
+ if (!ALLOWED_DIRS.has(entry.name)) {
124
+ unknownDirs.push({
125
+ path: entry.name,
126
+ suggested_path: path.join("archive", "loose", entry.name),
127
+ });
128
+ }
129
+ continue;
130
+ }
131
+
132
+ if (!isAllowedRootFile(entry.name)) {
133
+ loose.push({
134
+ path: entry.name,
135
+ suggested_path: routeForLooseFile(entry.name),
136
+ });
137
+ }
138
+ }
139
+
140
+ return {
141
+ ok: loose.length === 0 && unknownDirs.length === 0,
142
+ planning_dir: planningDir,
143
+ status: loose.length === 0 && unknownDirs.length === 0 ? "clean" : "needs_organizing",
144
+ loose,
145
+ unknown_dirs: unknownDirs,
146
+ };
147
+ }
148
+
149
+ function uniqueDestination(planningDir, relPath) {
150
+ const parsed = path.parse(relPath);
151
+ let candidate = relPath;
152
+ let index = 2;
153
+ while (fs.existsSync(path.join(planningDir, candidate))) {
154
+ candidate = path.join(parsed.dir, `${parsed.name}-${index}${parsed.ext}`);
155
+ index += 1;
156
+ }
157
+ return candidate;
158
+ }
159
+
160
+ function moveEntry(planningDir, fromRel, toRel) {
161
+ const from = path.join(planningDir, fromRel);
162
+ const safeToRel = uniqueDestination(planningDir, toRel);
163
+ const to = path.join(planningDir, safeToRel);
164
+ fs.mkdirSync(path.dirname(to), { recursive: true });
165
+ fs.renameSync(from, to);
166
+ return safeToRel;
167
+ }
168
+
169
+ function organize(planningDir, write) {
170
+ const result = scan(planningDir);
171
+ if (!result.ok && result.status === "missing") {
172
+ return result;
173
+ }
174
+
175
+ const moves = [];
176
+ for (const item of [...result.loose, ...result.unknown_dirs]) {
177
+ if (!write) {
178
+ moves.push({ from: item.path, to: item.suggested_path, moved: false });
179
+ continue;
180
+ }
181
+ const actual = moveEntry(planningDir, item.path, item.suggested_path);
182
+ moves.push({ from: item.path, to: actual, moved: true });
183
+ }
184
+
185
+ return {
186
+ ok: true,
187
+ planning_dir: planningDir,
188
+ status: write ? "organized" : "dry_run",
189
+ moves,
190
+ };
191
+ }
192
+
193
+ function printHuman(result) {
194
+ if (result.status === "missing") {
195
+ console.log(`planning hygiene: ${result.message}`);
196
+ return;
197
+ }
198
+
199
+ if (result.status === "clean") {
200
+ console.log(`planning hygiene: clean (${result.planning_dir})`);
201
+ return;
202
+ }
203
+
204
+ if (result.moves) {
205
+ console.log(`planning hygiene: ${result.status} (${result.moves.length} move${result.moves.length === 1 ? "" : "s"})`);
206
+ for (const move of result.moves) {
207
+ console.log(` ${move.moved ? "moved" : "would move"} ${move.from} -> ${move.to}`);
208
+ }
209
+ return;
210
+ }
211
+
212
+ const count = result.loose.length + result.unknown_dirs.length;
213
+ console.log(`planning hygiene: needs organizing (${count} item${count === 1 ? "" : "s"})`);
214
+ for (const item of result.loose) {
215
+ console.log(` loose file ${item.path} -> ${item.suggested_path}`);
216
+ }
217
+ for (const item of result.unknown_dirs) {
218
+ console.log(` loose dir ${item.path} -> ${item.suggested_path}`);
219
+ }
220
+ }
221
+
222
+ function main() {
223
+ const opts = parseArgs(process.argv);
224
+ if (opts.help) {
225
+ console.log(usage());
226
+ return 0;
227
+ }
228
+
229
+ let result;
230
+ if (opts.command === "scan") {
231
+ result = scan(opts.planningDir);
232
+ } else if (opts.command === "organize" || opts.command === "fix") {
233
+ result = organize(opts.planningDir, opts.write);
234
+ } else {
235
+ console.error(usage());
236
+ return 2;
237
+ }
238
+
239
+ if (opts.json) {
240
+ console.log(JSON.stringify(result, null, 2));
241
+ } else {
242
+ printHuman(result);
243
+ }
244
+
245
+ if (result.status === "missing") return 1;
246
+ if (opts.command === "scan" && !result.ok) return 1;
247
+ return 0;
248
+ }
249
+
250
+ if (require.main === module) {
251
+ process.exitCode = main();
252
+ }
253
+
254
+ module.exports = {
255
+ ALLOWED_DIRS,
256
+ ROOT_EXACT,
257
+ ROOT_PATTERNS,
258
+ isAllowedRootFile,
259
+ routeForLooseFile,
260
+ scan,
261
+ organize,
262
+ };
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ // Shared runtime manifest for installed Qualia bin scripts.
3
+
4
+ const RUNTIME_BIN_SCRIPTS = [
5
+ { file: "runtime-manifest.js", label: "runtime-manifest.js (shared install manifest)" },
6
+ { file: "host-adapters.js", label: "host-adapters.js (Claude/Codex path renderer)" },
7
+ { file: "state.js", label: "state.js (state machine)" },
8
+ { file: "qualia-ui.js", label: "qualia-ui.js (cosmetics library)" },
9
+ { file: "statusline.js", label: "statusline.js (status bar renderer)" },
10
+ { file: "knowledge.js", label: "knowledge.js (memory-layer loader)" },
11
+ { file: "knowledge-flush.js", label: "knowledge-flush.js (cron-runnable flush)" },
12
+ { file: "state-ledger.js", label: "state-ledger.js (hash-chained state event ledger)" },
13
+ { file: "plan-contract.js", label: "plan-contract.js (plan JSON validator)" },
14
+ { file: "contract-runner.js", label: "contract-runner.js (contract evidence runner)" },
15
+ { file: "agent-runs.js", label: "agent-runs.js (agent telemetry writer)" },
16
+ { file: "slop-detect.mjs", label: "slop-detect.mjs (anti-pattern scanner — runs pre-commit on frontend builds)" },
17
+ { file: "erp-retry.js", label: "erp-retry.js (ERP report retry queue — drained by session-start hook and erp-flush CLI)" },
18
+ { file: "report-payload.js", label: "report-payload.js (Framework -> ERP report payload builder)" },
19
+ { file: "project-snapshot.js", label: "project-snapshot.js (ERP/admin project progress snapshot)" },
20
+ { file: "trust-score.js", label: "trust-score.js (harness health scoring)" },
21
+ { file: "codex-goal.js", label: "codex-goal.js (Codex /goal objective + token-budget suggester)" },
22
+ { file: "planning-hygiene.js", label: "planning-hygiene.js (.planning organization scanner)" },
23
+ ];
24
+
25
+ function binFiles() {
26
+ return RUNTIME_BIN_SCRIPTS.map((script) => script.file);
27
+ }
28
+
29
+ module.exports = {
30
+ RUNTIME_BIN_SCRIPTS,
31
+ binFiles,
32
+ };
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ // State ledger — append-only, hash-chained events for Qualia state mutations.
3
+
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const crypto = require("crypto");
7
+
8
+ const SCHEMA_VERSION = 1;
9
+
10
+ function qualiaDir(cwd = process.cwd()) {
11
+ return path.join(cwd, ".planning", "qualia");
12
+ }
13
+
14
+ function ledgerPath(cwd = process.cwd()) {
15
+ return path.join(qualiaDir(cwd), "state.jsonl");
16
+ }
17
+
18
+ function ensureDir(p) {
19
+ if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
20
+ }
21
+
22
+ function stable(value) {
23
+ if (Array.isArray(value)) return value.map(stable);
24
+ if (value && typeof value === "object") {
25
+ const out = {};
26
+ for (const key of Object.keys(value).sort()) out[key] = stable(value[key]);
27
+ return out;
28
+ }
29
+ return value;
30
+ }
31
+
32
+ function stableJson(value) {
33
+ return JSON.stringify(stable(value));
34
+ }
35
+
36
+ function hash(value) {
37
+ return `sha256:${crypto.createHash("sha256").update(String(value)).digest("hex")}`;
38
+ }
39
+
40
+ function hashContent(content) {
41
+ if (content == null) return null;
42
+ return hash(content);
43
+ }
44
+
45
+ function newEventId() {
46
+ const ts = Date.now().toString(36).toUpperCase().padStart(10, "0");
47
+ const rand = crypto.randomBytes(6).toString("hex").toUpperCase();
48
+ return `${ts}${rand}`;
49
+ }
50
+
51
+ function readEvents(cwd = process.cwd()) {
52
+ const file = ledgerPath(cwd);
53
+ if (!fs.existsSync(file)) return [];
54
+ const events = [];
55
+ for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/).filter(Boolean)) {
56
+ try { events.push(JSON.parse(line)); } catch {}
57
+ }
58
+ return events;
59
+ }
60
+
61
+ function lastEvent(cwd = process.cwd()) {
62
+ const events = readEvents(cwd);
63
+ return events.length ? events[events.length - 1] : null;
64
+ }
65
+
66
+ function actor() {
67
+ return process.env.QUALIA_ACTOR ||
68
+ process.env.CLAUDE_USER_NAME ||
69
+ process.env.USER ||
70
+ process.env.USERNAME ||
71
+ "unknown";
72
+ }
73
+
74
+ function compactState(state) {
75
+ if (!state) return null;
76
+ return {
77
+ phase: state.phase,
78
+ status: state.status,
79
+ phase_name: state.phase_name,
80
+ total_phases: state.total_phases,
81
+ };
82
+ }
83
+
84
+ function compactTracking(tracking) {
85
+ if (!tracking) return null;
86
+ return {
87
+ project: tracking.project,
88
+ phase: tracking.phase,
89
+ status: tracking.status,
90
+ milestone: tracking.milestone,
91
+ milestone_name: tracking.milestone_name,
92
+ tasks_done: tracking.tasks_done,
93
+ tasks_total: tracking.tasks_total,
94
+ verification: tracking.verification,
95
+ report_seq: tracking.report_seq,
96
+ };
97
+ }
98
+
99
+ function eventHash(event) {
100
+ const clean = { ...event };
101
+ delete clean.event_hash;
102
+ return hash(stableJson(clean));
103
+ }
104
+
105
+ function append(cwd, input) {
106
+ const previous = lastEvent(cwd);
107
+ const event = {
108
+ schema_version: SCHEMA_VERSION,
109
+ event_id: input.event_id || newEventId(),
110
+ timestamp: input.timestamp || new Date().toISOString(),
111
+ actor: input.actor || actor(),
112
+ command: input.command || "",
113
+ action: input.action,
114
+ result: input.result || "success",
115
+ phase_before: input.phase_before,
116
+ phase_after: input.phase_after,
117
+ status_before: input.status_before,
118
+ status_after: input.status_after,
119
+ state_before: compactState(input.state_before),
120
+ state_after: compactState(input.state_after),
121
+ tracking_before: compactTracking(input.tracking_before),
122
+ tracking_after: compactTracking(input.tracking_after),
123
+ state_hash_before: hashContent(input.state_raw_before),
124
+ state_hash_after: hashContent(input.state_raw_after),
125
+ tracking_hash_before: hashContent(input.tracking_raw_before),
126
+ tracking_hash_after: hashContent(input.tracking_raw_after),
127
+ evidence_refs: Array.isArray(input.evidence_refs) ? input.evidence_refs : [],
128
+ previous_event_hash: previous ? previous.event_hash : null,
129
+ };
130
+
131
+ for (const key of Object.keys(event)) {
132
+ if (event[key] === undefined) delete event[key];
133
+ }
134
+ event.event_hash = eventHash(event);
135
+
136
+ ensureDir(qualiaDir(cwd));
137
+ fs.appendFileSync(ledgerPath(cwd), JSON.stringify(event) + "\n");
138
+ return { ok: true, event_id: event.event_id, event_hash: event.event_hash };
139
+ }
140
+
141
+ function validate(cwd = process.cwd()) {
142
+ const events = readEvents(cwd);
143
+ const errors = [];
144
+ let previousHash = null;
145
+ events.forEach((event, index) => {
146
+ if (event.previous_event_hash !== previousHash) {
147
+ errors.push(`event ${index + 1}: previous_event_hash mismatch`);
148
+ }
149
+ const expected = eventHash(event);
150
+ if (event.event_hash !== expected) {
151
+ errors.push(`event ${index + 1}: event_hash mismatch`);
152
+ }
153
+ previousHash = event.event_hash || null;
154
+ });
155
+ return { ok: errors.length === 0, count: events.length, errors };
156
+ }
157
+
158
+ function main(argv) {
159
+ const cmd = argv[2] || "validate";
160
+ if (cmd === "validate") {
161
+ const result = validate(process.cwd());
162
+ console.log(JSON.stringify(result, null, 2));
163
+ return result.ok ? 0 : 1;
164
+ }
165
+ if (cmd === "tail") {
166
+ const limit = parseInt(argv[3] || "10", 10);
167
+ console.log(JSON.stringify(readEvents(process.cwd()).slice(-limit), null, 2));
168
+ return 0;
169
+ }
170
+ console.error("Usage: state-ledger.js <validate|tail> [limit]");
171
+ return 2;
172
+ }
173
+
174
+ module.exports = {
175
+ SCHEMA_VERSION,
176
+ append,
177
+ validate,
178
+ readEvents,
179
+ ledgerPath,
180
+ hash,
181
+ stableJson,
182
+ };
183
+
184
+ if (require.main === module) process.exit(main(process.argv));