javi-forge 1.35.1 → 1.37.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.
@@ -65,14 +65,34 @@ function ancestorChain(leaf) {
65
65
  * manual-recovery guidance (never clobbers a concurrent change).
66
66
  */
67
67
  export async function runTransaction(input) {
68
+ // Seam validation (fail-closed): the topology is EITHER a `layout` OR the
69
+ // default Claude pair [asset, settings]. A caller with neither would deref
70
+ // `undefined.desired` deep in the engine — refuse up front with a clear error.
71
+ if (!input.layout && !(input.asset && input.settings)) {
72
+ throw new TxAbort("validate", "runTransaction requires `layout` or both `asset` and `settings`");
73
+ }
68
74
  const { secureFs, clock, nonce, projectDir } = input;
69
- const claudeDir = path.join(projectDir, ".claude");
70
- const hooksDir = path.join(claudeDir, "hooks");
75
+ // Container topology + write components. The default (no `layout`) is the exact
76
+ // Claude `.claude` → `.claude/hooks` nesting with [asset, settings]; a `layout`
77
+ // (Codex) supplies absolute containers parent-first and an ordered component
78
+ // list. Either way the proof primitives below are identical.
79
+ const containers = input.layout
80
+ ? input.layout.containers
81
+ : [
82
+ path.join(projectDir, ".claude"),
83
+ path.join(projectDir, ".claude", "hooks"),
84
+ ];
85
+ const components = input.layout
86
+ ? input.layout.components
87
+ : [
88
+ input.asset,
89
+ input.settings,
90
+ ];
71
91
  // The dirs the tool OWNS: their children include the executed asset and the
72
92
  // settings it is referenced from. Fixed and known to the core regardless of
73
93
  // the per-run write plan; each existing member is proved on EVERY anyWrite run
74
94
  // (Round-4/5 / JDA-401 + JDB5-001).
75
- const managedContainers = new Set([claudeDir, hooksDir]);
95
+ const managedContainers = new Set(containers);
76
96
  const heldByPath = new Map();
77
97
  const heldOrder = [];
78
98
  const createdDirs = [];
@@ -80,7 +100,12 @@ export async function runTransaction(input) {
80
100
  const committed = [];
81
101
  const backups = [];
82
102
  const needsWrite = (c) => c.desired !== null;
83
- const anyWrite = needsWrite(input.asset) || needsWrite(input.settings);
103
+ const anyWrite = components.some(needsWrite);
104
+ // A component writes into `container` when its parent dir IS the container or a
105
+ // descendant of it — used to decide which absent containers to create.
106
+ const writesInto = (container) => components.some((c) => needsWrite(c) &&
107
+ (path.dirname(c.path) === container ||
108
+ path.dirname(c.path).startsWith(`${container}${path.sep}`)));
84
109
  // The uniform per-held-dir gate. It runs the LENIENT ancestor ACL predicate on
85
110
  // EVERY held dir — including managed containers, which are ALSO proved strict by
86
111
  // `proveManagedContainer` right after they are gated (in ensureManagedContainer)
@@ -168,23 +193,23 @@ export async function runTransaction(input) {
168
193
  // pre-commit); one that is absent is CREATED only when a child is written
169
194
  // into it this run, else left alone (nothing to secure).
170
195
  if (anyWrite) {
171
- const projectHandle = heldByPath.get(projectDir);
172
- // .claude always ensured on anyWrite (holds settings; grandparent of
173
- // the asset). createIfAbsent=true never returns null (opens, creates, or
174
- // throws) → narrow non-null before passing as parent (JDA6-003).
175
- const claudeHandle = await ensureManagedContainer(projectHandle, claudeDir,
176
- /* createIfAbsent */ true);
177
- if (!claudeHandle) {
178
- throw new TxAbort(`container ${claudeDir}`, "unexpected null handle");
196
+ // Ensure every managed container PARENT-FIRST. Each container's parent is
197
+ // already held (its dirname was gated in preflight or ensured by an earlier
198
+ // iteration). A container is CREATED only when a component writes into it
199
+ // or a descendant container this run; otherwise it is proved IF present,
200
+ // else left alone (JDB5-001). This generalizes the former hardcoded
201
+ // `.claude` → `.claude/hooks` pair without changing any proof primitive.
202
+ for (const container of containers) {
203
+ const parentHandle = heldByPath.get(path.dirname(container));
204
+ if (!parentHandle) {
205
+ throw new TxAbort(`container ${container}`, "parent chain not held");
206
+ }
207
+ await ensureManagedContainer(parentHandle, container,
208
+ /* createIfAbsent */ writesInto(container));
179
209
  }
180
- // .claude/hooks: create when the asset writes into it; otherwise prove
181
- // IF it exists (settings-only repair must still secure the hook's
182
- // container — JDB5-001).
183
- await ensureManagedContainer(claudeHandle, hooksDir,
184
- /* createIfAbsent */ needsWrite(input.asset));
185
210
  }
186
- // --- CAPTURE + (FORCED) BACKUP + STAGE, asset then settings ---
187
- for (const component of [input.asset, input.settings]) {
211
+ // --- CAPTURE + (FORCED) BACKUP + STAGE, in component order ---
212
+ for (const component of components) {
188
213
  if (!needsWrite(component))
189
214
  continue;
190
215
  const parentPath = path.dirname(component.path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javi-forge",
3
- "version": "1.35.1",
3
+ "version": "1.37.0",
4
4
  "description": "Project scaffolding and AI-ready CI bootstrap",
5
5
  "type": "module",
6
6
  "bin": {