craftar 0.2.0 → 0.2.1

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.
package/dist/cli.js CHANGED
@@ -15,7 +15,7 @@ import { UnifyPlanSchema } from "./schema/index.js";
15
15
  process.stdout.on("error", (e) => { if (e.code === "EPIPE")
16
16
  process.exit(0); });
17
17
  const program = new Command();
18
- program.name("craftar").description("Craft, sync and convert AI-coding workspace harnesses.").version("0.2.0");
18
+ program.name("craftar").description("Craft, sync and convert AI-coding workspace harnesses.").version("0.2.1");
19
19
  /* ---------------------------------------------------------------- import */
20
20
  program
21
21
  .command("import")
@@ -13,17 +13,55 @@ export function fingerprintOf(meta, files) {
13
13
  delete m.origin;
14
14
  delete m.name;
15
15
  delete m.as;
16
- const parts = [JSON.stringify(m, Object.keys(m).sort())];
16
+ const parts = [JSON.stringify(sortedDeep(m))];
17
17
  for (const k of Object.keys(files).sort())
18
18
  parts.push(k, hashNormalized(files[k]));
19
19
  return hashNormalized(parts.join("\0"));
20
20
  }
21
+ /**
22
+ * A copy of `v` with object keys sorted at every depth, so `JSON.stringify` of it is canonical.
23
+ * Not `JSON.stringify(v, sortedKeys)`: a replacer array filters keys at every depth, which hashed
24
+ * an MCP `server: { command, args, env }` as `{}` and made two different servers look identical.
25
+ * For metadata with no nested object the output is byte-identical to that form, so those
26
+ * fingerprints — and the unify plans that record them — do not move. (One exception: integer-like
27
+ * keys, which only a hand-added unknown field can introduce, now follow the engine's numeric-first
28
+ * key order instead of the replacer's lexicographic one. The result is still canonical.)
29
+ * A YAML alias can make the parsed metadata cyclic; that is refused rather than overflowing.
30
+ */
31
+ function sortedDeep(v, ancestors = new Set()) {
32
+ if (v === null || typeof v !== "object")
33
+ return v;
34
+ if (ancestors.has(v))
35
+ throw new Error("ingredient metadata is cyclic (a YAML alias refers back to itself)");
36
+ if (Array.isArray(v)) {
37
+ ancestors.add(v);
38
+ const out = v.map((x) => sortedDeep(x, ancestors));
39
+ ancestors.delete(v);
40
+ return out;
41
+ }
42
+ const proto = Object.getPrototypeOf(v);
43
+ if (proto !== Object.prototype && proto !== null)
44
+ return v; // Date and friends keep their toJSON
45
+ ancestors.add(v);
46
+ // A null prototype, so a `__proto__` key is an ordinary key instead of setting the prototype.
47
+ const out = Object.create(null);
48
+ for (const k of Object.keys(v).sort())
49
+ out[k] = sortedDeep(v[k], ancestors);
50
+ ancestors.delete(v);
51
+ return out;
52
+ }
21
53
  export async function fingerprintDir(dir) {
22
- const meta = YAML.parse(await fs.readFile(path.join(dir, "ingredient.yaml"), "utf8"));
54
+ const metaFile = path.join(dir, "ingredient.yaml");
55
+ const meta = YAML.parse(await fs.readFile(metaFile, "utf8"));
23
56
  const files = {};
24
57
  for (const rel of await listFiles(dir))
25
58
  if (rel !== "ingredient.yaml")
26
59
  files[rel] = await fs.readFile(path.join(dir, rel));
27
- return fingerprintOf(meta, files);
60
+ try {
61
+ return fingerprintOf(meta, files);
62
+ }
63
+ catch (e) {
64
+ throw new Error(`${metaFile}: ${e.message}`);
65
+ }
28
66
  }
29
67
  //# sourceMappingURL=fingerprint.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../../src/core/fingerprint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,IAAa,EAAE,KAAsC;IACjF,MAAM,CAAC,GAA4B,EAAE,GAAI,IAAgC,EAAE,CAAC;IAC5E,OAAO,CAAC,CAAC,MAAM,CAAC;IAChB,OAAO,CAAC,CAAC,IAAI,CAAC;IACd,OAAO,CAAC,CAAC,EAAE,CAAC;IACZ,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACzD,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE;QAAE,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACnF,OAAO,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAW;IAC9C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IACtF,MAAM,KAAK,GAA2B,EAAE,CAAC;IACzC,KAAK,MAAM,GAAG,IAAI,MAAM,SAAS,CAAC,GAAG,CAAC;QAAE,IAAI,GAAG,KAAK,iBAAiB;YAAE,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IAC3H,OAAO,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AACpC,CAAC"}
1
+ {"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../../src/core/fingerprint.ts"],"names":[],"mappings":"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"}
@@ -89,9 +89,9 @@ function comparableMeta(meta) {
89
89
  * Ruling 28: the top-level `ingredient.yaml` fields on which base and variant differ, sorted.
90
90
  * `diffIngredients` leaves `ingredient.yaml` out, so without this a variant that differs only in
91
91
  * its metadata (an MCP `server`, an agent's `model` or `tools`) would resolve with zero decisions
92
- * and be deleted. Compared on the zod-validated `meta` with a real deep equality — never through
93
- * `fingerprintOf` or `JSON.stringify(x, keys)`, whose key-array replacer filters at every depth
94
- * and would see two different nested `server` objects as the same `{}`.
92
+ * and be deleted. Compared on the zod-validated `meta` with a real deep equality, field by field so
93
+ * the differing fields can be named — never through `JSON.stringify(x, keys)`, whose key-array
94
+ * replacer filters at every depth and sees two different nested `server` objects as the same `{}`.
95
95
  */
96
96
  export function metaDifferences(base, variant) {
97
97
  const a = comparableMeta(base.meta);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craftar",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Craft, sync and convert AI-coding workspace harnesses across clients and tools.",
5
5
  "license": "MIT",
6
6
  "type": "module",