+32
-0
| # Changelog | ||
| ## v0.5.5 — A store written on Windows opens everywhere | ||
| Found by inspecting a real user's store, not by a test. Includes everything from | ||
| 0.5.4, which never reached npm. | ||
| - **Manifest paths are now platform-independent.** `path.join` on Windows produced | ||
| `artifacts\name`, and that separator was written into `manifest.json` as permanent | ||
| metadata. On macOS and Linux it does not resolve — `artifacts\README.md` is not a | ||
| path there, it is a filename containing a backslash. Every artifact of a | ||
| Windows-written store was therefore unreachable on any other platform. Paths are | ||
| now always written POSIX-style and both separators are accepted on read, so | ||
| existing stores repair themselves at the next savepoint. | ||
| - **A file that cannot be carried forward is never dropped silently.** The failure | ||
| path swallowed the error and left the artifact out of the manifest without a word | ||
| — the exact silent loss StateArk exists to prevent, and it fired on every | ||
| cross-platform store. It now raises `carry_forward_failed`, naming the file and | ||
| the reason. | ||
| - **`state.md` opens with YAML frontmatter** in the shape the Open Knowledge Format | ||
| asks for: `type`, `title`, `description`, `timestamp`, `project`, `version`, | ||
| `tags`. Additive only — the Markdown body is unchanged and StateArk does not | ||
| depend on it, but Obsidian vaults, RAG pipelines and other agents can now filter | ||
| savepoints without a StateArk-specific parser. The store layout is deliberately | ||
| not restructured around OKF: the spec is at v0.1 and will still move. | ||
| - **The store README warns against hand-written savepoints** (from 0.5.4). A client | ||
| that could not reach the MCP tools invented a "documented manual fallback", | ||
| wrote the files itself and corrupted a timestamp in the process. There is no such | ||
| procedure. Writing by hand bypasses the lock, the atomic publish, the checksums | ||
| and every integrity check. | ||
| - Smoke suite: 10 new checks. Five rebuild a Windows-written store and prove the | ||
| artifacts survive; four cover the frontmatter, including a title full of YAML | ||
| metacharacters. | ||
| ## v0.5.3 — Several agents, one store | ||
@@ -4,0 +36,0 @@ |
| import { createClient } from "@supabase/supabase-js"; | ||
| import { readFile } from "node:fs/promises"; | ||
| import { resolveInside } from "./local-store.js"; | ||
| import { resolveInside, normalizeStoredPath } from "./local-store.js"; | ||
| const TEXT_KINDS = /^(code|prompt|schema|config)$/; | ||
@@ -70,3 +70,3 @@ const TEXT_EXT = /\.(md|txt|py|js|ts|tsx|jsx|json|sql|yaml|yml|toml|csv|xml|html|css|sh|ps1)$/i; | ||
| if (a.status === "stored" && a.relative_path) { | ||
| const full = resolveInside(sp.dir, a.relative_path); | ||
| const full = resolveInside(sp.dir, normalizeStoredPath(a.relative_path)); | ||
| if (!full) | ||
@@ -73,0 +73,0 @@ throw new Error(`Refusing to read artifact outside the savepoint: ${a.name}`); |
+67
-6
@@ -128,2 +128,19 @@ import { createHash, randomUUID } from "node:crypto"; | ||
| } | ||
| /** | ||
| * Manifest paths are always written POSIX-style. | ||
| * | ||
| * `path.join` on Windows produces `artifacts\name`, and that separator used to be | ||
| * persisted into manifest.json. Opening such a store on macOS or Linux then failed | ||
| * to resolve a single artifact: `artifacts\README.md` is not a path there, it is a | ||
| * filename containing a backslash. A store is meant to survive the move between | ||
| * machines, so the separator inside it cannot depend on the machine that wrote it. | ||
| */ | ||
| const artifactRelPath = (name) => path.posix.join("artifacts", name); | ||
| /** | ||
| * Read a manifest path written by any platform, including stores from before 0.5.5 | ||
| * that still contain backslashes. Tolerant on read, strict on write. | ||
| */ | ||
| export function normalizeStoredPath(rel) { | ||
| return String(rel ?? "").replace(/\\/g, "/"); | ||
| } | ||
| /** Resolve `rel` inside `base`, refusing anything that escapes it. */ | ||
@@ -518,3 +535,3 @@ export function resolveInside(base, rel) { | ||
| if (bytes) { | ||
| rel = path.join("artifacts", name); | ||
| rel = artifactRelPath(name); | ||
| const full = resolveInside(tmpDir, rel); | ||
@@ -566,4 +583,4 @@ if (!full) | ||
| continue; | ||
| const from = resolveInside(prev.dir, old.relative_path); | ||
| const rel = path.join("artifacts", old.name); | ||
| const from = resolveInside(prev.dir, normalizeStoredPath(old.relative_path)); | ||
| const rel = artifactRelPath(old.name); | ||
| const to = resolveInside(tmpDir, rel); | ||
@@ -581,3 +598,12 @@ if (!from || !to) | ||
| } | ||
| catch { /* source unreadable: nothing to carry, leave it out */ } | ||
| catch (e) { | ||
| // Never drop a file silently - that is the exact loss StateArk exists to | ||
| // prevent. Most likely cause: a store written on another platform, or an | ||
| // artifact deleted from the previous version by hand. | ||
| warnings.push({ | ||
| code: "carry_forward_failed", | ||
| artifact: old.name, | ||
| message: `"${old.name}" existed in ${prev.meta.version} but could not be carried forward (${e instanceof Error ? e.message : String(e)}). It is NOT part of this savepoint. Re-submit it if you still need it.`, | ||
| }); | ||
| } | ||
| } | ||
@@ -763,3 +789,3 @@ // ---- compare against the previous savepoint -------------------------- | ||
| } | ||
| const full = resolveInside(sp.dir, a.relative_path); | ||
| const full = resolveInside(sp.dir, normalizeStoredPath(a.relative_path)); | ||
| if (!full) | ||
@@ -787,3 +813,3 @@ return null; | ||
| const name = safeFilename(filename); | ||
| const rel = path.join("artifacts", name); | ||
| const rel = artifactRelPath(name); | ||
| const full = resolveInside(sp.dir, rel); | ||
@@ -899,4 +925,39 @@ if (!full) | ||
| } | ||
| /** Quote a value so it is a valid YAML scalar whatever the model put in it. */ | ||
| function yamlString(v) { | ||
| return `"${String(v ?? "").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/[\r\n]+/g, " ").trim()}"`; | ||
| } | ||
| /** | ||
| * YAML frontmatter, in the shape the Open Knowledge Format asks for. | ||
| * | ||
| * OKF (Google Cloud, v0.1) requires exactly one field, `type`, and leaves the rest | ||
| * to the producer. Adding it is additive: humans read the Markdown as before, | ||
| * StateArk does not depend on it, and Obsidian, RAG pipelines and other agents can | ||
| * suddenly filter savepoints by project, version or date without a custom parser. | ||
| * | ||
| * Deliberately only this. The OKF spec is young and will move; the store layout | ||
| * stays StateArk's own rather than being restructured around a v0.1 standard. | ||
| */ | ||
| function frontmatter(project, version, title, state, createdAt, warnings) { | ||
| const summary = (state.executive_summary ?? "").split(/(?<=[.!?])\s/)[0] ?? ""; | ||
| return [ | ||
| "---", | ||
| "type: project-state", | ||
| `title: ${yamlString(title ? `${project} ${version} — ${title}` : `${project} ${version}`)}`, | ||
| `description: ${yamlString(summary)}`, | ||
| `timestamp: ${yamlString(createdAt)}`, | ||
| `project: ${yamlString(project)}`, | ||
| `version: ${yamlString(version)}`, | ||
| `generated: ${yamlString("stateark")}`, | ||
| "tags:", | ||
| ` - ${yamlString("stateark")}`, | ||
| ` - ${yamlString(slugify(project))}`, | ||
| ...(warnings.length ? [` - ${yamlString("has-integrity-warnings")}`] : []), | ||
| "---", | ||
| "", | ||
| ]; | ||
| } | ||
| export function renderMarkdown(project, version, title, state, artifacts, createdAt, warnings = []) { | ||
| const l = [ | ||
| ...frontmatter(project, version, title, state, createdAt, warnings), | ||
| `# ${project} — ${version}`, "", | ||
@@ -903,0 +964,0 @@ `> StateArk Savepoint · ${createdAt}${title ? ` · ${title}` : ""}`, "", |
+1
-1
| { | ||
| "name": "stateark", | ||
| "version": "0.5.4", | ||
| "version": "0.5.5", | ||
| "mcpName": "io.github.Askrion/stateark", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+25
-1
@@ -98,2 +98,26 @@ # StateArk | ||
| Stores are platform-independent: a savepoint written on Windows opens on macOS and | ||
| Linux and the other way round. | ||
| ## Readable by other tools | ||
| Every `state.md` opens with YAML frontmatter in the shape the | ||
| [Open Knowledge Format](https://cloud.google.com/blog/products/data-analytics/how-the-open-knowledge-format-can-improve-data-sharing) | ||
| asks for — `type`, `title`, `description`, `timestamp`, `project`, `version`, `tags`: | ||
| ```yaml | ||
| --- | ||
| type: project-state | ||
| title: "Harbour Pricing v1.4 — pricing tiers final" | ||
| timestamp: "2026-08-28T09:00:00.000Z" | ||
| project: "Harbour Pricing" | ||
| version: "v1.4" | ||
| tags: ["stateark", "harbour-pricing"] | ||
| --- | ||
| ``` | ||
| Drop your store into an Obsidian vault or point a RAG pipeline at it and savepoints | ||
| are filterable by project, version or date without a StateArk-specific parser. The | ||
| Markdown body is unchanged, and StateArk does not depend on the frontmatter. | ||
| ## Licence | ||
@@ -151,3 +175,3 @@ | ||
| npm run typecheck | ||
| npm test # 97 checks against a throwaway store — run this first | ||
| npm test # 107 checks against a throwaway store — run this first | ||
| npm start | ||
@@ -154,0 +178,0 @@ ``` |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
187833
3.36%2889
2.16%354
7.27%