@hassangameryt/freeflow 0.6.0 → 0.7.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.
Files changed (122) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.codex-plugin/plugin.json +4 -5
  4. package/README.md +39 -55
  5. package/capabilities/cognitive-routing/SKILL.md +288 -130
  6. package/command-surface.json +0 -24
  7. package/hooks/freeflow-runtime-context.mjs +42 -477
  8. package/hooks/hooks.json +1 -26
  9. package/hooks/tests/check-runtime-context-hook.sh +108 -319
  10. package/package.json +4 -2
  11. package/pi-extension/dist/cognitive-routing/commands.js +3 -3
  12. package/pi-extension/dist/cognitive-routing/config.js +16 -20
  13. package/pi-extension/dist/cognitive-routing/controller.js +478 -48
  14. package/pi-extension/dist/cognitive-routing/history.js +12 -0
  15. package/pi-extension/dist/cognitive-routing/host.js +74 -12
  16. package/pi-extension/dist/cognitive-routing/pi-routing-session.js +414 -0
  17. package/pi-extension/dist/cognitive-routing/pi-session-control.js +320 -0
  18. package/pi-extension/dist/cognitive-routing/tool.js +1 -0
  19. package/pi-extension/dist/cognitive-routing/types.js +1 -1
  20. package/pi-extension/dist/index.js +236 -219
  21. package/pi-extension/dist/runtime/runtime-context.js +126 -286
  22. package/pi-extension/dist/settings/settings-ui.js +52 -336
  23. package/pi-extension/src/cognitive-routing/commands.ts +15 -5
  24. package/pi-extension/src/cognitive-routing/config.ts +16 -20
  25. package/pi-extension/src/cognitive-routing/controller.ts +561 -51
  26. package/pi-extension/src/cognitive-routing/history.ts +12 -0
  27. package/pi-extension/src/cognitive-routing/host.ts +110 -16
  28. package/pi-extension/src/cognitive-routing/pi-routing-session.ts +538 -0
  29. package/pi-extension/src/cognitive-routing/pi-session-control.ts +444 -0
  30. package/pi-extension/src/cognitive-routing/tool.ts +1 -0
  31. package/pi-extension/src/cognitive-routing/types.ts +1 -1
  32. package/pi-extension/src/index.ts +249 -239
  33. package/pi-extension/src/runtime/runtime-context.ts +129 -305
  34. package/pi-extension/src/settings/settings-ui.ts +53 -371
  35. package/pi-extension/tests/cognitive-routing/commands.test.js +1 -1
  36. package/pi-extension/tests/cognitive-routing/controller.test.js +69 -40
  37. package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +128 -57
  38. package/pi-extension/tests/cognitive-routing/pi-normal-lifecycle.test.js +434 -0
  39. package/pi-extension/tests/cognitive-routing/pi-session-control.test.js +174 -0
  40. package/pi-extension/tests/cognitive-routing/pi-session-history.test.js +91 -0
  41. package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +47 -4
  42. package/pi-extension/tests/integration/mode-free-core.test.js +218 -0
  43. package/pi-extension/tests/integration/pi-extension.test.js +430 -2478
  44. package/pi-extension/tests/integration/prompt-architecture.test.js +124 -79
  45. package/pi-extension/tests/integration/unaffected-regressions.test.js +411 -0
  46. package/runtime/prompts/cognitive-routing.md +5 -4
  47. package/runtime/prompts/conversation-history.md +1 -1
  48. package/runtime/prompts/core.md +77 -6
  49. package/runtime/prompts/interaction-contract.md +6 -2
  50. package/skills/action-selection/SKILL.md +93 -44
  51. package/skills/action-selection/references/trajectory-stalls.md +71 -0
  52. package/skills/bypass/SKILL.md +109 -39
  53. package/skills/commit-work/SKILL.md +94 -55
  54. package/skills/commit-work/references/git-state-edges.md +94 -0
  55. package/skills/commit-work/references/simple-push.md +92 -0
  56. package/skills/commit-work/references/staging-decisions.md +79 -46
  57. package/skills/decision-gate/SKILL.md +80 -22
  58. package/skills/design-for-depth/SKILL.md +208 -43
  59. package/skills/design-for-depth/references/design-pressure-signals.md +108 -240
  60. package/skills/design-for-depth/references/interface-design-loop.md +77 -97
  61. package/skills/design-for-depth/references/module-and-dependency-design.md +147 -0
  62. package/skills/design-for-depth/references/software-design-philosophy.md +127 -173
  63. package/skills/design-for-depth/references/state-and-failure-boundaries.md +136 -0
  64. package/skills/diagnose-failure/SKILL.md +198 -52
  65. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +19 -5
  66. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +13 -5
  67. package/skills/discuss/SKILL.md +118 -76
  68. package/skills/discuss/references/discussion-continuity.md +84 -32
  69. package/skills/evaluate-skill/SKILL.md +166 -52
  70. package/skills/evaluate-skill/references/evaluation-definition-schema.md +2 -2
  71. package/skills/evaluate-skill/references/evaluation-design.md +56 -30
  72. package/skills/evaluate-skill/references/execution-and-evidence.md +1 -1
  73. package/skills/evaluate-skill/references/review-and-revision.md +62 -21
  74. package/skills/execute-work/SKILL.md +98 -59
  75. package/skills/execute-work/references/code-practices.md +4 -4
  76. package/skills/execute-work/references/execute-work-edges.md +42 -22
  77. package/skills/review-artifact/SKILL.md +132 -71
  78. package/skills/review-artifact/references/adjudicate-artifact-review.md +101 -0
  79. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +52 -41
  80. package/skills/review-work/SKILL.md +115 -72
  81. package/skills/review-work/references/adjudicate-work-review.md +97 -0
  82. package/skills/review-work/references/independent-work-reviewer-contract.md +40 -35
  83. package/skills/review-work/references/security-risk-lens.md +17 -15
  84. package/skills/setup-freeflow/SKILL.md +14 -17
  85. package/skills/setup-freeflow/references/activation-contract.md +24 -30
  86. package/skills/setup-freeflow/references/host-setup.md +6 -7
  87. package/skills/simplify-code/SKILL.md +97 -39
  88. package/skills/simplify-code/references/simplification-patterns.md +1 -1
  89. package/skills/tdd/SKILL.md +108 -41
  90. package/skills/tdd/references/test-design.md +1 -1
  91. package/skills/track-work/SKILL.md +133 -300
  92. package/skills/track-work/references/working-record-format.md +515 -0
  93. package/skills/track-work/scripts/lib/document.mjs +713 -0
  94. package/skills/track-work/scripts/lib/format.mjs +166 -0
  95. package/skills/track-work/scripts/lib/store.mjs +233 -0
  96. package/skills/track-work/scripts/lib/transitions.mjs +790 -0
  97. package/skills/track-work/scripts/lib/views.mjs +27 -430
  98. package/skills/track-work/scripts/working-record.mjs +292 -98
  99. package/skills/verify-work/SKILL.md +46 -20
  100. package/skills/verify-work/references/browser-runtime-evidence.md +1 -1
  101. package/skills/verify-work/references/integration-evidence.md +1 -7
  102. package/skills/verify-work/references/performance-evidence.md +1 -1
  103. package/skills/workflow/SKILL.md +110 -80
  104. package/skills/workflow/references/domain-skill-composition.md +24 -18
  105. package/skills/write-skill/SKILL.md +218 -62
  106. package/skills/write-skill/references/activation-boundaries.md +63 -21
  107. package/skills/write-skill/references/agent-first-instructions.md +61 -27
  108. package/skills/write-skill/references/progressive-disclosure.md +91 -41
  109. package/skills/write-skill/references/skill-dependency-graphs.md +190 -0
  110. package/runtime/prompts/skills.md +0 -65
  111. package/skills/mode-contract/SKILL.md +0 -104
  112. package/skills/track-work/scripts/lib/codec.mjs +0 -570
  113. package/skills/track-work/scripts/lib/commands.mjs +0 -568
  114. package/skills/track-work/scripts/lib/contract.mjs +0 -303
  115. package/skills/track-work/scripts/lib/model.mjs +0 -506
  116. package/skills/track-work/scripts/lib/operations.mjs +0 -965
  117. package/skills/track-work/scripts/lib/record-store.mjs +0 -264
  118. package/skills/track-work/scripts/lib/result.mjs +0 -37
  119. package/skills/track-work/scripts/lib/rewrites.mjs +0 -740
  120. package/skills/track-work/scripts/lib/schema.mjs +0 -277
  121. package/skills/track-work/scripts/lib/source-inventory.mjs +0 -162
  122. package/skills/track-work/scripts/lib/workspace.mjs +0 -134
@@ -0,0 +1,166 @@
1
+ export const SCHEMA_VERSION = 4;
2
+
3
+ export const TOP_LEVEL_SECTIONS = ["Current Context", "Current Work", "Future Work", "History", "Notes"];
4
+ export const CONTEXT_HEADINGS = [
5
+ "Goal",
6
+ "What defines this task",
7
+ "Settled",
8
+ "Tentative",
9
+ "Open",
10
+ "Current direction",
11
+ "Boundaries",
12
+ ];
13
+ export const HISTORY_HEADINGS = ["Decisions", "Checkpoints", "Slices"];
14
+
15
+ export const TASK_STATES = new Set(["active", "paused", "completed", "abandoned"]);
16
+ export const SLICE_TYPES = new Set(["learning", "delivery", "deepening"]);
17
+ export const CURRENT_SLICE_STATES = new Set(["in_progress", "paused"]);
18
+ export const HISTORICAL_SLICE_STATES = new Set(["completed", "blocked", "abandoned"]);
19
+ export const CHECKPOINT_TYPES = new Set(["independent_review", "local_commit", "user_decision", "continuity"]);
20
+ export const FUTURE_CHECKPOINT_STATES = new Set(["proposed", "pending", "deferred"]);
21
+ export const HISTORICAL_CHECKPOINT_STATES = new Set(["completed", "cancelled", "replaced"]);
22
+ export const DECISION_STATES = new Set(["active", "superseded", "retired"]);
23
+
24
+ export const ACTIVE_SLICE_FIELDS = new Set([
25
+ "State",
26
+ "Type",
27
+ "Intended result",
28
+ "Authority source",
29
+ "Scope",
30
+ "Expected evidence",
31
+ "Stop condition",
32
+ "Starting state",
33
+ "Dependencies",
34
+ "Reopened from",
35
+ ]);
36
+ export const PROPOSED_SLICE_FIELDS = new Set(["State", "Type", "Intended result", "Expected evidence", "Dependencies"]);
37
+ export const FUTURE_CHECKPOINT_FIELDS = new Set(["State", "Type", "Condition", "Applies to"]);
38
+ export const HISTORICAL_CHECKPOINT_FIELDS = new Set([
39
+ "State",
40
+ "Type",
41
+ "Condition",
42
+ "Applies to",
43
+ "Result",
44
+ "Evidence",
45
+ "Task effect",
46
+ "Reason",
47
+ "Replaced by",
48
+ ]);
49
+ export const DECISION_FIELDS = new Set([
50
+ "State",
51
+ "Decision",
52
+ "Established by",
53
+ "Rationale",
54
+ "Source references",
55
+ "Consequences",
56
+ "Revisit when",
57
+ "Superseded by",
58
+ "Retired because",
59
+ ]);
60
+ export const HISTORICAL_SLICE_FIELDS = new Set([
61
+ "State",
62
+ "Type",
63
+ "Intended result",
64
+ "Authority source",
65
+ "Result",
66
+ "Evidence and limits",
67
+ "Task effect",
68
+ "Reopened from",
69
+ "Resume when",
70
+ "Reason",
71
+ "Residual effects",
72
+ ]);
73
+
74
+ export const IDS = {
75
+ slice: /^S-(\d{3,})$/,
76
+ checkpoint: /^C-(\d{3,})$/,
77
+ decision: /^D-(\d{3,})$/,
78
+ };
79
+
80
+ export function heading(level, title) {
81
+ return `${"#".repeat(level)} ${title}`;
82
+ }
83
+
84
+ export function canonicalLines(name, state = "active", lastUpdated = new Date().toISOString()) {
85
+ return [
86
+ `# Working Record: ${name}`,
87
+ "",
88
+ "Schema: 4",
89
+ `State: ${state}`,
90
+ `Last updated: ${lastUpdated}`,
91
+ "",
92
+ "## Current Context",
93
+ "",
94
+ "### Goal",
95
+ "",
96
+ "### What defines this task",
97
+ "",
98
+ "### Settled",
99
+ "",
100
+ "### Tentative",
101
+ "",
102
+ "### Open",
103
+ "",
104
+ "### Current direction",
105
+ "",
106
+ "### Boundaries",
107
+ "",
108
+ "## Current Work",
109
+ "",
110
+ "### Current Slice",
111
+ "",
112
+ "None",
113
+ "",
114
+ "### Next useful action",
115
+ "",
116
+ "## Future Work",
117
+ "",
118
+ "## History",
119
+ "",
120
+ "### Decisions",
121
+ "",
122
+ "### Checkpoints",
123
+ "",
124
+ "### Slices",
125
+ "",
126
+ "## Notes",
127
+ "",
128
+ ];
129
+ }
130
+
131
+ export function normalizeNewline(text) {
132
+ return text.includes("\r\n") ? "\r\n" : "\n";
133
+ }
134
+
135
+ export function splitLines(text) {
136
+ const newline = normalizeNewline(text);
137
+ return { lines: text.split(/\r?\n/), newline };
138
+ }
139
+
140
+ export function joinLines(lines, newline) {
141
+ return lines.join(newline);
142
+ }
143
+
144
+ export function parseId(id, kind) {
145
+ const pattern = IDS[kind];
146
+ return typeof id === "string" && pattern ? pattern.exec(id) : null;
147
+ }
148
+
149
+ export function nextId(usedIds, kind) {
150
+ let next = 1;
151
+ for (const id of usedIds) {
152
+ const match = parseId(id, kind);
153
+ if (match) next = Math.max(next, Number(match[1]) + 1);
154
+ }
155
+ return `${kind === "slice" ? "S" : kind === "checkpoint" ? "C" : "D"}-${String(next).padStart(3, "0")}`;
156
+ }
157
+
158
+ export function slugify(value) {
159
+ const slug = String(value)
160
+ .trim()
161
+ .toLowerCase()
162
+ .replace(/[^a-z0-9]+/g, "-")
163
+ .replace(/^-+|-+$/g, "");
164
+ if (!slug) throw new Error("Task name must contain at least one letter or digit");
165
+ return slug;
166
+ }
@@ -0,0 +1,233 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { execFile as execFileCallback } from "node:child_process";
3
+ import { lstat, mkdir, readdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
4
+ import { realpathSync } from "node:fs";
5
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
+ import { promisify } from "node:util";
7
+
8
+ const execFile = promisify(execFileCallback);
9
+
10
+ export class StoreError extends Error {
11
+ constructor(code, message, details = {}, exitCode = 1) {
12
+ super(message);
13
+ this.name = "StoreError";
14
+ this.code = code;
15
+ this.details = details;
16
+ this.exitCode = exitCode;
17
+ }
18
+ }
19
+
20
+ function fail(code, message, details = {}, exitCode = 1) {
21
+ throw new StoreError(code, message, details, exitCode);
22
+ }
23
+
24
+ async function gitCommand(root, args) {
25
+ try {
26
+ const result = await execFile("git", ["-C", root, ...args], { maxBuffer: 1024 * 1024 });
27
+ return { ok: true, stdout: result.stdout, stderr: result.stderr };
28
+ } catch (error) {
29
+ return { ok: false, code: error?.code, stdout: error?.stdout ?? "", stderr: error?.stderr ?? "" };
30
+ }
31
+ }
32
+
33
+ async function assertGitSafePath(root, path) {
34
+ const repository = await gitCommand(root, ["rev-parse", "--show-toplevel"]);
35
+ if (!repository.ok) return;
36
+ const repositoryRoot = canonicalPath(repository.stdout.trim());
37
+ const relativePath = relative(repositoryRoot, canonicalPath(path)).split(sep).join("/");
38
+ const ignored = await gitCommand(repositoryRoot, ["check-ignore", "--no-index", "-q", "--", relativePath]);
39
+ if (!ignored.ok) fail("unignored-path", `Record path is not ignored by Git: ${relativePath}`);
40
+ const tracked = await gitCommand(repositoryRoot, ["ls-files", "--error-unmatch", "--", relativePath]);
41
+ if (tracked.ok) fail("tracked-path", `Record path is already tracked by Git: ${relativePath}`);
42
+ }
43
+
44
+ function inside(root, candidate) {
45
+ const rootPath = resolve(root);
46
+ const candidatePath = resolve(candidate);
47
+ const relativePath = relative(rootPath, candidatePath);
48
+ return (
49
+ relativePath === "" || (!relativePath.startsWith(`..${sep}`) && relativePath !== ".." && !isAbsolute(relativePath))
50
+ );
51
+ }
52
+
53
+ async function assertNoSymlinkPath(target, stopAt) {
54
+ let current = resolve(target);
55
+ const boundary = resolve(stopAt);
56
+ while (inside(boundary, current)) {
57
+ try {
58
+ if ((await lstat(current)).isSymbolicLink())
59
+ fail("unsafe-symlink", `Mutable path contains a symlink: ${current}`);
60
+ } catch (error) {
61
+ if (error?.code !== "ENOENT") throw error;
62
+ }
63
+ if (current === boundary) break;
64
+ const parent = dirname(current);
65
+ if (parent === current) break;
66
+ current = parent;
67
+ }
68
+ }
69
+
70
+ export function taskRoot(root) {
71
+ return resolve(root, ".freeflow", "tasks");
72
+ }
73
+
74
+ function canonicalPath(path) {
75
+ const candidate = resolve(path);
76
+ try {
77
+ return realpathSync(candidate);
78
+ } catch {
79
+ const parent = dirname(candidate);
80
+ if (parent === candidate) return candidate;
81
+ try {
82
+ return join(realpathSync(parent), basename(candidate));
83
+ } catch {
84
+ return candidate;
85
+ }
86
+ }
87
+ }
88
+
89
+ export function resolveRecordPath(root, supplied) {
90
+ if (!supplied) fail("missing-record", "A record path is required");
91
+ const rootPath = canonicalPath(root);
92
+ const tasks = canonicalPath(taskRoot(rootPath));
93
+ const candidate = isAbsolute(supplied) ? resolve(supplied) : resolve(rootPath, supplied);
94
+ const recordPath = basename(candidate) === "record.md" ? candidate : join(candidate, "record.md");
95
+ const canonicalRecord = canonicalPath(recordPath);
96
+ if (!inside(tasks, canonicalRecord) || basename(canonicalRecord) !== "record.md")
97
+ fail("unsafe-path", `Record must be a record.md below ${tasks}`);
98
+ const taskDirectory = basename(dirname(canonicalRecord));
99
+ if (!/^task-\d{3}-[a-z0-9][a-z0-9-]*$/.test(taskDirectory))
100
+ fail("unsafe-path", `Invalid task directory: ${taskDirectory}`);
101
+ return canonicalRecord;
102
+ }
103
+
104
+ export async function assertRecordPath(root, recordPath) {
105
+ const path = resolveRecordPath(root, recordPath);
106
+ await assertNoSymlinkPath(path, canonicalPath(taskRoot(root)));
107
+ await assertGitSafePath(root, path);
108
+ return path;
109
+ }
110
+
111
+ export async function readRecord(root, recordPath) {
112
+ const path = await assertRecordPath(root, recordPath);
113
+ try {
114
+ return { path, text: await readFile(path, "utf8") };
115
+ } catch (error) {
116
+ if (error?.code === "ENOENT") fail("missing-record", `Record does not exist: ${path}`);
117
+ fail("record-read-failure", `Could not read record: ${path}`);
118
+ }
119
+ }
120
+
121
+ export async function readInput(inputPath, cwd = process.cwd()) {
122
+ if (!inputPath) return "";
123
+ if (inputPath === "-")
124
+ return new Promise((resolveInput, reject) => {
125
+ let text = "";
126
+ process.stdin.setEncoding("utf8");
127
+ process.stdin.on("data", (chunk) => {
128
+ text += chunk;
129
+ });
130
+ process.stdin.on("end", () => resolveInput(text));
131
+ process.stdin.on("error", reject);
132
+ });
133
+ try {
134
+ return await readFile(isAbsolute(inputPath) ? inputPath : resolve(cwd, inputPath), "utf8");
135
+ } catch {
136
+ fail("input-read-failure", `Could not read input: ${inputPath}`);
137
+ }
138
+ }
139
+
140
+ async function nextTaskNumber(tasks) {
141
+ let entries = [];
142
+ try {
143
+ entries = await readdir(tasks, { withFileTypes: true });
144
+ } catch (error) {
145
+ if (error?.code !== "ENOENT") throw error;
146
+ }
147
+ const used = new Set();
148
+ for (const entry of entries) {
149
+ const match = /^task-(\d{3})-/.exec(entry.name);
150
+ if (entry.isDirectory() && match) used.add(Number(match[1]));
151
+ }
152
+ let number = 1;
153
+ while (used.has(number)) number += 1;
154
+ return number;
155
+ }
156
+
157
+ export async function createRecordPath(root, taskName, slug) {
158
+ const tasks = taskRoot(root);
159
+ await assertGitSafePath(root, tasks);
160
+ await mkdir(tasks, { recursive: true });
161
+ await assertNoSymlinkPath(tasks, canonicalPath(root));
162
+ const number = await nextTaskNumber(tasks);
163
+ const directory = join(tasks, `task-${String(number).padStart(3, "0")}-${slug}`);
164
+ const path = join(directory, "record.md");
165
+ await assertGitSafePath(root, path);
166
+ try {
167
+ await mkdir(directory, { recursive: false });
168
+ } catch (error) {
169
+ if (error?.code === "EEXIST") fail("task-already-exists", `Task directory already exists: ${directory}`);
170
+ throw error;
171
+ }
172
+ return { path, taskName };
173
+ }
174
+
175
+ export async function writeInitialAtomically(root, recordPath, text) {
176
+ const path = await assertRecordPath(root, recordPath);
177
+ const temporaryPath = join(dirname(path), `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
178
+ let published = false;
179
+ try {
180
+ await writeFile(temporaryPath, text, { encoding: "utf8", flag: "wx" });
181
+ await rename(temporaryPath, path);
182
+ published = true;
183
+ const confirmedText = await readFile(path, "utf8");
184
+ if (confirmedText !== text) fail("publication-confirmation", "Published record differs from the candidate", {}, 2);
185
+ } catch (error) {
186
+ if (!published) await unlink(temporaryPath).catch(() => undefined);
187
+ if (error instanceof StoreError) throw error;
188
+ fail("publication-failure", `Could not publish record: ${error?.code ?? "write"}`, {}, published ? 2 : 1);
189
+ }
190
+ return { path, changed: true };
191
+ }
192
+
193
+ export async function writeAtomically(root, recordPath, sourceText, candidateText) {
194
+ const path = await assertRecordPath(root, recordPath);
195
+ let currentText;
196
+ try {
197
+ currentText = await readFile(path, "utf8");
198
+ } catch (error) {
199
+ if (error?.code === "ENOENT") fail("missing-record", `Record does not exist: ${path}`);
200
+ throw error;
201
+ }
202
+ if (currentText !== sourceText)
203
+ fail("stale-source", "Record changed while the operation was preparing; nothing was written");
204
+ if (candidateText === sourceText) return { path, changed: false };
205
+
206
+ const temporaryPath = join(dirname(path), `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
207
+ let published = false;
208
+ try {
209
+ await writeFile(temporaryPath, candidateText, { encoding: "utf8", flag: "wx" });
210
+ const beforeRename = await readFile(path, "utf8");
211
+ if (beforeRename !== sourceText) fail("stale-source", "Record changed before publication; nothing was written");
212
+ await rename(temporaryPath, path);
213
+ published = true;
214
+ let confirmedText;
215
+ try {
216
+ confirmedText = await readFile(path, "utf8");
217
+ } catch (error) {
218
+ fail("publication-confirmation", `Could not confirm published record: ${error?.code ?? "read"}`, {}, 2);
219
+ }
220
+ if (confirmedText !== candidateText)
221
+ fail("publication-confirmation", "Published record differs from the candidate", {}, 2);
222
+ } catch (error) {
223
+ if (!published) await unlink(temporaryPath).catch(() => undefined);
224
+ if (error instanceof StoreError) throw error;
225
+ fail("publication-failure", `Could not publish record: ${error?.code ?? "write"}`, {}, published ? 2 : 1);
226
+ }
227
+ return { path, changed: true };
228
+ }
229
+
230
+ export function displayPath(root, path) {
231
+ const relativePath = relative(resolve(root), resolve(path));
232
+ return relativePath && !relativePath.startsWith(`..${sep}`) && relativePath !== ".." ? relativePath : path;
233
+ }