silvermoon 0.0.2 → 0.1.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.
@@ -2,15 +2,9 @@ import { lstat, readFile, readdir } from "node:fs/promises";
2
2
  import { relative, resolve } from "node:path";
3
3
 
4
4
  import { deriveIdeaState, isValidUlid, parseIdeaStatus } from "./ideas.js";
5
- import { gitObjectIdLength, runGit, worktreePathTree } from "./git.js";
5
+ import { inspectTreePaths, worktreeSnapshot } from "./git.js";
6
6
  import { IDEAS_ROOT, ideaPaths } from "./layout.js";
7
7
 
8
- const REVISION_BINDINGS = {
9
- approvedRevision: "idealRevision",
10
- implementationAcceptedRevision: "implementationRevision",
11
- deploymentAcceptedRevision: "deploymentRevision",
12
- };
13
-
14
8
  function displayPath(root, path) {
15
9
  return relative(root, path).replaceAll("\\", "/");
16
10
  }
@@ -28,91 +22,6 @@ async function metadata(path) {
28
22
  }
29
23
  }
30
24
 
31
- function validateRevisionObjects(root, statusPath, status, diagnostics) {
32
- for (const key of Object.keys(REVISION_BINDINGS)) {
33
- if (!status[key]) continue;
34
- const type = runGit(root, ["cat-file", "-t", status[key]]);
35
- if (!type.ok || type.stdout !== "tree") {
36
- diagnostics.push(error(
37
- "idea.revision.invalid-object",
38
- `${statusPath}#${key}`,
39
- `${key} must resolve to a Git tree object: ${status[key]}`,
40
- "Use the current corresponding world tree object ID from Git.",
41
- ));
42
- }
43
- }
44
- }
45
-
46
- function validateRevisionHistory(root, commit, idea, diagnostics) {
47
- for (const [statusKey, revisionKey] of Object.entries(REVISION_BINDINGS)) {
48
- const revision = idea.status[statusKey];
49
- if (!revision) continue;
50
- const history = runGit(root, [
51
- "log",
52
- "--reverse",
53
- "--format=%H",
54
- "-S",
55
- `${statusKey}: ${revision}`,
56
- commit,
57
- "--",
58
- idea.statusPath,
59
- ]);
60
- const evidenceCommit = history.ok
61
- ? history.stdout.split(/\r?\n/).filter(Boolean)[0]
62
- : null;
63
- if (!evidenceCommit) {
64
- const shallow = runGit(root, ["rev-parse", "--is-shallow-repository"]);
65
- diagnostics.push(error(
66
- shallow.ok && shallow.stdout === "true"
67
- ? "history.incomplete"
68
- : "idea.revision.history-missing",
69
- `${idea.statusPath}#${statusKey}`,
70
- `Primary history does not prove when ${statusKey} recorded ${revision}.`,
71
- "Fetch complete primary history or repair the decision through a forward commit.",
72
- ));
73
- continue;
74
- }
75
- const tree = runGit(root, ["rev-parse", `${evidenceCommit}:${idea.worlds[revisionKey].path}`]);
76
- if (!tree.ok || tree.stdout !== revision) {
77
- diagnostics.push(error(
78
- "idea.revision.history-mismatch",
79
- `${idea.statusPath}#${statusKey}`,
80
- `${statusKey} was not bound to its world tree in commit ${evidenceCommit}.`,
81
- "Record the decision only for the corresponding world tree in the same candidate commit.",
82
- ));
83
- }
84
- }
85
- }
86
-
87
- function validateCandidateRevisions(root, baseRevision, idea, diagnostics, objectIdLength) {
88
- let previous = {};
89
- if (baseRevision) {
90
- const previousSource = runGit(root, ["show", `${baseRevision}:${idea.statusPath}`]);
91
- if (previousSource.ok) {
92
- try {
93
- previous = parseIdeaStatus(previousSource.stdout + "\n", { objectIdLength });
94
- } catch {
95
- previous = {};
96
- }
97
- }
98
- }
99
- for (const [statusKey, revisionKey] of Object.entries(REVISION_BINDINGS)) {
100
- if (
101
- idea.status[statusKey] === previous[statusKey] ||
102
- idea.status[statusKey] === undefined
103
- ) continue;
104
- const expected = idea.revisions[revisionKey];
105
- if (idea.status[statusKey] !== expected) {
106
- diagnostics.push(error(
107
- "idea.revision.candidate-mismatch",
108
- `${idea.statusPath}#${statusKey}`,
109
- `${statusKey} must equal the corresponding world tree in the same candidate snapshot.`,
110
- `Use ${expected} for ${revisionKey} or omit the stale mutation.`,
111
- ));
112
- }
113
- }
114
- }
115
-
116
25
  async function requireDirectory(root, path, diagnostics) {
117
26
  const absolute = resolve(root, path);
118
27
  const value = await metadata(absolute);
@@ -184,23 +93,25 @@ async function rejectSymlinks(root, path, diagnostics) {
184
93
  }
185
94
  }
186
95
 
187
- function resolveTree({ gitRoot, root, snapshotTree }, path) {
188
- if (snapshotTree) {
189
- const resolved = runGit(gitRoot, ["rev-parse", `${snapshotTree}:${path}`]);
190
- if (!resolved.ok) throw new Error(resolved.stderr || `Cannot resolve ${path}`);
191
- return resolved.stdout;
96
+ function objectIdLength(tree) {
97
+ if (/^[0-9a-f]{40}$/.test(tree)) return 40;
98
+ if (/^[0-9a-f]{64}$/.test(tree)) return 64;
99
+ throw new Error(`Unsupported Git tree object ID: ${tree}`);
100
+ }
101
+
102
+ function requiredTree(objects, path) {
103
+ const inspected = objects.get(path);
104
+ if (!inspected || inspected.type !== "tree" || inspected.object === null) {
105
+ throw new Error(`${path} does not resolve to a Git tree`);
192
106
  }
193
- return worktreePathTree(root, path);
107
+ return inspected.object;
194
108
  }
195
109
 
196
110
  export async function inspectIdeaLayout({
197
- baseRevision,
198
111
  config: _config,
199
- historyCommit,
200
112
  root,
201
113
  gitRoot = root,
202
114
  snapshotTree,
203
- validateCandidate = false,
204
115
  }) {
205
116
  const diagnostics = [];
206
117
  const ideasRoot = resolve(root, IDEAS_ROOT);
@@ -228,9 +139,12 @@ export async function inspectIdeaLayout({
228
139
  };
229
140
  }
230
141
 
231
- let objectIdLength;
142
+ let resolvedSnapshotTree;
143
+ let repositoryObjectIdLength;
232
144
  try {
233
- objectIdLength = gitObjectIdLength(gitRoot);
145
+ resolvedSnapshotTree = snapshotTree
146
+ ?? worktreeSnapshot(gitRoot, { paths: [IDEAS_ROOT] }).tree;
147
+ repositoryObjectIdLength = objectIdLength(resolvedSnapshotTree);
234
148
  } catch (caught) {
235
149
  return {
236
150
  diagnostics: [error(
@@ -246,7 +160,30 @@ export async function inspectIdeaLayout({
246
160
  const aliases = new Map();
247
161
  const ideas = [];
248
162
  const caseNames = new Map();
249
- for (const entry of await readdir(ideasRoot, { withFileTypes: true })) {
163
+ const entries = (await readdir(ideasRoot, { withFileTypes: true }))
164
+ .sort((left, right) => left.name.localeCompare(right.name));
165
+ const ideaIds = new Set(
166
+ entries.filter((entry) => isValidUlid(entry.name)).map((entry) => entry.name),
167
+ );
168
+ const worldPaths = [...ideaIds].flatMap((id) => {
169
+ const paths = ideaPaths(id);
170
+ return [paths.idealPath, paths.innerPath, paths.outerPath];
171
+ });
172
+ let worldObjects;
173
+ try {
174
+ worldObjects = inspectTreePaths(gitRoot, resolvedSnapshotTree, worldPaths);
175
+ } catch (caught) {
176
+ return {
177
+ diagnostics: [error(
178
+ "idea.revision.unavailable",
179
+ IDEAS_ROOT,
180
+ caught.message,
181
+ "Ensure every world can be represented as a Git tree.",
182
+ )],
183
+ ideas: [],
184
+ };
185
+ }
186
+ for (const entry of entries) {
250
187
  const folderPath = resolve(ideasRoot, entry.name);
251
188
  const folderMetadata = await lstat(folderPath);
252
189
  const relativePath = displayPath(root, folderPath);
@@ -282,7 +219,9 @@ export async function inspectIdeaLayout({
282
219
  }
283
220
 
284
221
  const paths = ideaPaths(entry.name);
285
- for (const child of await readdir(folderPath, { withFileTypes: true })) {
222
+ const children = (await readdir(folderPath, { withFileTypes: true }))
223
+ .sort((left, right) => left.name.localeCompare(right.name));
224
+ for (const child of children) {
286
225
  if (
287
226
  child.name === "status.yaml" ||
288
227
  child.name === "ledger.md" ||
@@ -331,7 +270,7 @@ export async function inspectIdeaLayout({
331
270
  try {
332
271
  status = parseIdeaStatus(
333
272
  await readFile(resolve(root, paths.statusPath), "utf8"),
334
- { objectIdLength },
273
+ { objectIdLength: repositoryObjectIdLength },
335
274
  );
336
275
  } catch (caught) {
337
276
  diagnostics.push(error(
@@ -351,6 +290,14 @@ export async function inspectIdeaLayout({
351
290
  ));
352
291
  }
353
292
  if (status.alias !== undefined) {
293
+ if (ideaIds.has(status.alias)) {
294
+ diagnostics.push(error(
295
+ "idea.alias.id-collision",
296
+ `${paths.statusPath}#alias`,
297
+ `Alias ${status.alias} collides with an idea ULID.`,
298
+ "Choose an alias that is distinct from every idea ULID.",
299
+ ));
300
+ }
354
301
  const aliasOwner = aliases.get(status.alias);
355
302
  if (aliasOwner) {
356
303
  diagnostics.push(error(
@@ -374,15 +321,9 @@ export async function inspectIdeaLayout({
374
321
  let revisions;
375
322
  try {
376
323
  revisions = {
377
- idealRevision: resolveTree({ gitRoot, root, snapshotTree }, paths.idealPath),
378
- implementationRevision: resolveTree(
379
- { gitRoot, root, snapshotTree },
380
- paths.innerPath,
381
- ),
382
- deploymentRevision: resolveTree(
383
- { gitRoot, root, snapshotTree },
384
- paths.outerPath,
385
- ),
324
+ idealRevision: requiredTree(worldObjects, paths.idealPath),
325
+ implementationRevision: requiredTree(worldObjects, paths.innerPath),
326
+ deploymentRevision: requiredTree(worldObjects, paths.outerPath),
386
327
  };
387
328
  } catch (caught) {
388
329
  diagnostics.push(error(
@@ -394,23 +335,22 @@ export async function inspectIdeaLayout({
394
335
  continue;
395
336
  }
396
337
 
397
- validateRevisionObjects(gitRoot, paths.statusPath, status, diagnostics);
398
338
  const worlds = {
399
339
  idealRevision: {
400
340
  name: "Ideal World",
401
- displayName: "道心",
341
+ displayName: "理想世界",
402
342
  path: paths.idealPath,
403
343
  documentPath: paths.ideaDocumentPath,
404
344
  },
405
345
  implementationRevision: {
406
346
  name: "Inner World",
407
- displayName: "内景",
347
+ displayName: "主体世界",
408
348
  path: paths.innerPath,
409
349
  documentPath: paths.implementationDocumentPath,
410
350
  },
411
351
  deploymentRevision: {
412
352
  name: "Outer World",
413
- displayName: "现世",
353
+ displayName: "现实世界",
414
354
  path: paths.outerPath,
415
355
  documentPath: paths.deploymentDocumentPath,
416
356
  },
@@ -428,10 +368,6 @@ export async function inspectIdeaLayout({
428
368
  worlds,
429
369
  };
430
370
  if (status.alias !== undefined) idea.alias = status.alias;
431
- if (validateCandidate) {
432
- validateCandidateRevisions(gitRoot, baseRevision, idea, diagnostics, objectIdLength);
433
- }
434
- if (historyCommit) validateRevisionHistory(gitRoot, historyCommit, idea, diagnostics);
435
371
  ideas.push(idea);
436
372
  }
437
373