@penpair/marwes-ui 0.2.0 → 0.3.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.
package/dist/ownership.js CHANGED
@@ -1,66 +1,161 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { mkdir, open, readFile, unlink } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { atomicWriteFile, ensureGitignoreEntry, resolveProjectInput, resolveProjectOutput } from "@penpair/core";
4
+ import { atomicWriteFile, ensureGitignoreEntry, resolveProjectInput, resolveProjectOutput, } from "@penpair/core";
5
5
  import { canonical, normalizeValue } from "./tokens.js";
6
- const tokenMap = (sets) => new Map(sets.map(set => [set.name, new Map(set.tokens.map(token => [token.name, token]))]));
7
- const values = (token) => token ? { type: token.type, value: normalizeValue(token.type, token.value), description: token.description || "" } : { type: null, value: null, description: null };
6
+ const tokenMap = (sets) => new Map(sets.map((set) => [
7
+ set.name,
8
+ new Map(set.tokens.map((token) => [
9
+ token.name,
10
+ token,
11
+ ])),
12
+ ]));
13
+ const values = (token) => token
14
+ ? {
15
+ type: token.type,
16
+ value: normalizeValue(token.type, token.value),
17
+ description: token.description || "",
18
+ }
19
+ : {
20
+ type: null,
21
+ value: null,
22
+ description: null,
23
+ };
8
24
  /** Report each changed property against a verified ancestor; never guess a rename. */
9
25
  export function ownershipReport(desired, live, ancestor) {
10
- const wanted = tokenMap(desired.sets), actual = tokenMap(live.sets), old = tokenMap(ancestor?.sets ?? []);
26
+ const wanted = tokenMap(desired.sets);
27
+ const actual = tokenMap(live.sets);
28
+ const old = tokenMap(ancestor?.sets ?? []);
11
29
  const changes = [];
12
- const unmanagedSets = live.sets.filter(set => !wanted.has(set.name) && !old.has(set.name)).map(set => set.name);
30
+ const unmanagedSets = live.sets
31
+ .filter((set) => !wanted.has(set.name) && !old.has(set.name))
32
+ .map((set) => set.name);
13
33
  const unmanagedTokens = [];
14
- for (const setName of new Set([...wanted.keys(), ...old.keys()])) {
15
- for (const name of new Set([...wanted.get(setName)?.keys() ?? [], ...old.get(setName)?.keys() ?? []])) {
16
- const a = values(old.get(setName)?.get(name)), d = values(wanted.get(setName)?.get(name)), l = values(actual.get(setName)?.get(name));
17
- for (const property of ["type", "value", "description"]) {
34
+ for (const setName of new Set([
35
+ ...wanted.keys(),
36
+ ...old.keys(),
37
+ ])) {
38
+ for (const name of new Set([
39
+ ...(wanted.get(setName)?.keys() ?? []),
40
+ ...(old.get(setName)?.keys() ?? []),
41
+ ])) {
42
+ const a = values(old.get(setName)?.get(name));
43
+ const d = values(wanted.get(setName)?.get(name));
44
+ const l = values(actual.get(setName)?.get(name));
45
+ for (const property of [
46
+ "type",
47
+ "value",
48
+ "description",
49
+ ]) {
18
50
  const same = (x, y) => canonical(x) === canonical(y);
19
- if (same(d[property], l[property]))
51
+ if (same(d[property], l[property])) {
20
52
  continue;
53
+ }
21
54
  let kind;
22
55
  if (!ancestor || !old.get(setName)?.has(name)) {
23
56
  kind = !actual.get(setName)?.has(name) ? "upstream" : "untracked";
24
57
  }
25
58
  else {
26
- const upstream = !same(d[property], a[property]), designer = !same(l[property], a[property]);
59
+ const upstream = !same(d[property], a[property]);
60
+ const designer = !same(l[property], a[property]);
27
61
  kind = upstream && designer ? "conflict" : designer ? "designer" : "upstream";
28
62
  }
29
- changes.push({ set: setName, name, property, kind, ancestor: a[property], desired: d[property], live: l[property] });
63
+ changes.push({
64
+ set: setName,
65
+ name,
66
+ property,
67
+ kind,
68
+ ancestor: a[property],
69
+ desired: d[property],
70
+ live: l[property],
71
+ });
72
+ }
73
+ }
74
+ for (const name of actual.get(setName)?.keys() ?? []) {
75
+ if (!wanted.get(setName)?.has(name) && !old.get(setName)?.has(name)) {
76
+ unmanagedTokens.push({
77
+ set: setName,
78
+ name,
79
+ });
30
80
  }
31
81
  }
32
- for (const name of actual.get(setName)?.keys() ?? [])
33
- if (!wanted.get(setName)?.has(name) && !old.get(setName)?.has(name))
34
- unmanagedTokens.push({ set: setName, name });
35
82
  }
36
- const key = (theme) => canonical([theme.group, theme.name]);
37
- const previousThemes = new Map((ancestor?.themes ?? []).map(theme => [key(theme), theme]));
83
+ const key = (theme) => canonical([
84
+ theme.group,
85
+ theme.name,
86
+ ]);
87
+ const previousThemes = new Map((ancestor?.themes ?? []).map((theme) => [
88
+ key(theme),
89
+ theme,
90
+ ]));
38
91
  for (const theme of desired.themes) {
39
- const current = live.themes.find(t => key(t) === key(theme)), previous = previousThemes.get(key(theme));
40
- const managed = new Set([...desired.sets, ...ancestor?.sets ?? []].map(set => set.name));
41
- const orderedMembership = (sets) => sets === undefined ? null : sets.filter(name => managed.has(name)).sort();
42
- const a = orderedMembership(previous?.sets), d = orderedMembership(theme.sets), l = orderedMembership(current?.sets);
43
- if (canonical(d) === canonical(l))
92
+ const current = live.themes.find((t) => key(t) === key(theme));
93
+ const previous = previousThemes.get(key(theme));
94
+ const managed = new Set([
95
+ ...desired.sets,
96
+ ...(ancestor?.sets ?? []),
97
+ ].map((set) => set.name));
98
+ const orderedMembership = (sets) => sets === undefined ? null : sets.filter((name) => managed.has(name)).sort();
99
+ const a = orderedMembership(previous?.sets);
100
+ const d = orderedMembership(theme.sets);
101
+ const l = orderedMembership(current?.sets);
102
+ if (canonical(d) === canonical(l)) {
44
103
  continue;
45
- const kind = !previous ? !current ? "upstream" : "untracked" : canonical(l) === canonical(a) ? "upstream" : canonical(d) === canonical(a) ? "designer" : "conflict";
46
- changes.push({ set: "@themes", name: key(theme), property: "membership", kind, ancestor: a, desired: d, live: l });
104
+ }
105
+ const kind = !previous
106
+ ? !current
107
+ ? "upstream"
108
+ : "untracked"
109
+ : canonical(l) === canonical(a)
110
+ ? "upstream"
111
+ : canonical(d) === canonical(a)
112
+ ? "designer"
113
+ : "conflict";
114
+ changes.push({
115
+ set: "@themes",
116
+ name: key(theme),
117
+ property: "membership",
118
+ kind,
119
+ ancestor: a,
120
+ desired: d,
121
+ live: l,
122
+ });
47
123
  }
48
- return { changes, blockers: changes.filter(change => change.kind !== "upstream"), unmanagedSets, unmanagedTokens };
124
+ return {
125
+ changes,
126
+ blockers: changes.filter((change) => change.kind !== "upstream"),
127
+ unmanagedSets,
128
+ unmanagedTokens,
129
+ };
49
130
  }
50
131
  /** Destruction is conservative across all sets, including those outside adapter ownership. */
51
132
  export function assertSafeDestruction(actions, live, bindingCounts = {}) {
52
133
  for (const action of actions) {
53
- const names = action.kind === "removeTokens" ? action.names : action.kind === "replaceTokens" ? action.tokens.filter(token => live.sets.find(set => set.name === action.set)?.tokens.find(old => old.name === token.name)?.type !== token.type).map(token => token.name) : [];
134
+ const names = action.kind === "removeTokens"
135
+ ? action.names
136
+ : action.kind === "replaceTokens"
137
+ ? action.tokens
138
+ .filter((token) => live.sets
139
+ .find((set) => set.name === action.set)
140
+ ?.tokens.find((old) => old.name === token.name)?.type !== token.type)
141
+ .map((token) => token.name)
142
+ : [];
54
143
  for (const name of names) {
55
- if ((bindingCounts[name] ?? 0) > 0)
144
+ if ((bindingCounts[name] ?? 0) > 0) {
56
145
  throw new Error(`Cannot remove or change type of ${name}: bound to ${bindingCounts[name]} retained shape(s)`);
57
- for (const set of live.sets)
146
+ }
147
+ for (const set of live.sets) {
58
148
  for (const token of set.tokens) {
59
- if (set.name === action.set && token.name === name)
149
+ if (set.name ===
150
+ action.set &&
151
+ token.name === name) {
60
152
  continue;
61
- if (JSON.stringify(token.value).includes("{" + name + "}"))
153
+ }
154
+ if (JSON.stringify(token.value).includes("{" + name + "}")) {
62
155
  throw new Error(`Cannot remove or change type of ${name}: referenced by ${set.name}/${token.name}`);
156
+ }
63
157
  }
158
+ }
64
159
  }
65
160
  }
66
161
  }
@@ -68,27 +163,57 @@ const ledgerPath = (fileId) => `.penpair/token-sync/${createHash("sha256").updat
68
163
  export async function readSyncAncestor(root, fileId) {
69
164
  try {
70
165
  const record = JSON.parse(await readFile(await resolveProjectInput(root, ledgerPath(fileId)), "utf8"));
71
- if (record.schemaVersion !== 1 || record.fileId !== fileId || record.sha256 !== createHash("sha256").update(canonical(record.snapshot)).digest("hex") || ![1, 2].includes(record.snapshot?.schemaVersion) || !Array.isArray(record.snapshot?.sets) || !Array.isArray(record.snapshot?.themes))
166
+ if (record.schemaVersion !== 1 ||
167
+ record.fileId !== fileId ||
168
+ record.sha256 !== createHash("sha256").update(canonical(record.snapshot)).digest("hex") ||
169
+ ![
170
+ 1,
171
+ 2,
172
+ ].includes(record.snapshot?.schemaVersion) ||
173
+ !Array.isArray(record.snapshot?.sets) ||
174
+ !Array.isArray(record.snapshot?.themes)) {
72
175
  throw new Error("Invalid token-sync ancestor; inspect it before replacing it");
176
+ }
73
177
  return record.snapshot;
74
178
  }
75
179
  catch (error) {
76
- if (error.code === "ENOENT")
180
+ if (error.code === "ENOENT") {
77
181
  return undefined;
182
+ }
78
183
  throw error;
79
184
  }
80
185
  }
81
186
  export async function saveSyncAncestor(root, fileId, snapshot, expectedAncestor) {
82
- await ensureGitignoreEntry(root, ".penpair", { directory: true, comment: "penpair: private token synchronization evidence" });
187
+ await ensureGitignoreEntry(root, ".penpair", {
188
+ directory: true,
189
+ comment: "penpair: private token synchronization evidence",
190
+ });
83
191
  const destination = await resolveProjectOutput(root, ledgerPath(fileId));
84
- await mkdir(path.dirname(destination), { recursive: true, mode: 0o700 });
192
+ await mkdir(path.dirname(destination), {
193
+ recursive: true,
194
+ mode: 0o700,
195
+ });
85
196
  const lockPath = destination + ".lock";
86
- const lock = await open(lockPath, "wx", 0o600).catch(() => { throw new Error("Token ancestor is locked; inspect any interrupted local sync before retrying"); });
197
+ const lock = await open(lockPath, "wx", 0o600).catch(() => {
198
+ throw new Error("Token ancestor is locked; inspect any interrupted local sync before retrying");
199
+ });
87
200
  try {
88
201
  const current = await readSyncAncestor(root, fileId);
89
- if (current && canonical(current) !== canonical(snapshot) && canonical(current) !== canonical(expectedAncestor))
202
+ if (current &&
203
+ canonical(current) !== canonical(snapshot) &&
204
+ canonical(current) !== canonical(expectedAncestor)) {
90
205
  throw new Error("Token ancestor advanced concurrently; newer sync evidence was preserved");
91
- await atomicWriteFile(destination, JSON.stringify({ schemaVersion: 1, fileId, savedAt: new Date().toISOString(), sha256: createHash("sha256").update(canonical(snapshot)).digest("hex"), snapshot }, null, 2) + "\n", { replace: true, mode: 0o600 });
206
+ }
207
+ await atomicWriteFile(destination, JSON.stringify({
208
+ schemaVersion: 1,
209
+ fileId,
210
+ savedAt: new Date().toISOString(),
211
+ sha256: createHash("sha256").update(canonical(snapshot)).digest("hex"),
212
+ snapshot,
213
+ }, null, 2) + "\n", {
214
+ replace: true,
215
+ mode: 0o600,
216
+ });
92
217
  }
93
218
  finally {
94
219
  await lock.close();