@nurix/etna 0.19.79 → 0.19.81

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "fingerprint": "354c269e4457",
2
+ "fingerprint": "798dd0b4e94c",
3
3
  "harnesses": {
4
4
  "code": {
5
5
  "plugin": "code",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "code",
3
3
  "name": "code",
4
- "version": "3.10.5",
4
+ "version": "3.10.7",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -15,5 +15,5 @@
15
15
  "docs/memory/main.md",
16
16
  ".mcp.json"
17
17
  ],
18
- "digest": "3614b17652aa"
18
+ "digest": "3e27328dde24"
19
19
  }
@@ -31,7 +31,7 @@ import {
31
31
  recordTouched,
32
32
  } from "./lib/state.mjs";
33
33
  import { frozenStatus } from "./lib/frozen.mjs";
34
- import { approvedPackages } from "./lib/approved.mjs";
34
+ import { approvedPackages, rejectedPackages } from "./lib/approved.mjs";
35
35
  import {
36
36
  recordSession,
37
37
  liveSiblings,
@@ -484,15 +484,24 @@ function detectOutcomes(projectDir, meta, rows) {
484
484
  } catch (e) {
485
485
  continue;
486
486
  }
487
+ // A row is raised for a *rejected* package only — a decision already taken and violated.
488
+ // A package with no decision either way is not a defect: it is a dimension nobody has ruled
489
+ // on, harvested centrally by the fleet package census. Raising rows on absence turned the
490
+ // commit wall into a tax on every unlisted dependency and produced waivers, not decisions.
491
+ const rejected = rejectedPackages(projectDir);
487
492
  const names = [...Object.keys(manifest.dependencies || {}), ...Object.keys(manifest.devDependencies || {})];
488
493
  for (const name of names) {
489
- if (approved.has(name) || raised(`\`${name}\` (package-off-catalog`)) continue;
494
+ if (approved.has(name) || raised(`\`${name}\` (package-rejected`)) continue;
495
+ const no = rejected.get(name);
496
+ if (!no) continue;
490
497
  rows.push(
491
498
  formatAutoRow({
492
499
  ...meta,
493
- kind: "package-off-catalog",
494
- description: `${rel} declares \`${name}\` (package-off-catalog), absent from the approved table`,
495
- action: "remove the dependency, or add it to .claude/hooks/approved-packages.md by explicit user decision",
500
+ kind: "package-rejected",
501
+ description: `${rel} declares \`${name}\` (package-rejected), which the approved table rejects`,
502
+ action: no.use
503
+ ? `replace it with \`${no.use}\`, or change the decision in .claude/hooks/approved-packages.md`
504
+ : `use the choice ${no.rule} prescribes for this stack, or change the decision`,
496
505
  })
497
506
  );
498
507
  }
@@ -13,7 +13,7 @@ import { readQueue, countRows, askedThisSession, recordAsked } from "./lib/state
13
13
  import { frozenStatus } from "./lib/frozen.mjs";
14
14
  import { undischargedRows } from "./lib/ledger.mjs";
15
15
  import { appendEvent, unresolvedDenials, sessionLogPath, malformedEventLines } from "./lib/session-log.mjs";
16
- import { approvedPackages } from "./lib/approved.mjs";
16
+ import { approvedPackages, rejectedPackages } from "./lib/approved.mjs";
17
17
  import { claimActivity, releaseActivity, recentForeignTouches } from "./lib/claims.mjs";
18
18
 
19
19
  function readSafe(file) {
@@ -277,6 +277,7 @@ function packageGate(projectDir, command, sessionId) {
277
277
  if (!m) return null;
278
278
  const approved = approvedPackages(projectDir);
279
279
  if (approved.size === 0) return null; // no rules file installed — nothing to enforce
280
+ const rejected = rejectedPackages(projectDir);
280
281
  const raw = m[1] || "";
281
282
  // Test packages install at `latest`, outside the table's pin set (.claude/rules/testing.md).
282
283
  // Two dev-only installs are a decision already taken rather than a new one: a companion in an
@@ -291,16 +292,25 @@ function packageGate(projectDir, command, sessionId) {
291
292
  const args = raw.split(/\s+/).filter((a) => a && !a.startsWith("-") && PKG_SPEC.test(a));
292
293
  for (const arg of args) {
293
294
  const pkg = arg.startsWith("@") ? arg.split("@").slice(0, 2).join("@") : arg.split("@")[0];
295
+ if (approved.has(pkg)) continue;
294
296
  if (isDev && pkg.startsWith("@") && approvedScopes.has(pkg.split("/")[0])) continue;
295
- if (!approved.has(pkg)) {
297
+ // Approved wins over rejected: `jest` is banned for web and Express and prescribed for Expo,
298
+ // so a name in both is a prescription somewhere and must not be denied.
299
+ const no = rejected.get(pkg);
300
+ if (no) {
301
+ const instead = no.use
302
+ ? `Use \`${no.use}\`${no.version ? ` ${no.version}` : ""}${no.purpose ? ` — ${no.purpose}` : ""}.`
303
+ : `The prescribed choice for your stack is in ${no.rule}.`;
296
304
  return deny(
297
305
  projectDir,
298
306
  sessionId,
299
307
  "package-table",
300
308
  pkg,
301
- `BLOCKED — package table. \`${pkg}\` is not in the approved table (.claude/hooks/approved-packages.md; test packages: .claude/rules/testing.md). The table changes only by explicit user decision.`
309
+ `BLOCKED — \`${pkg}\` is rejected here. ${instead} Rejections are recorded in .claude/hooks/approved-packages.md; changing one is an explicit user decision.`
302
310
  );
303
311
  }
312
+ // No decision exists for this package — not approved, not rejected. Allowed, and harvested
313
+ // centrally by the fleet package census rather than blocked here.
304
314
  }
305
315
  // Every named package is approved — a package-table denial for one of them is a successful retry.
306
316
  for (const arg of args) {
@@ -313,15 +323,18 @@ function packageGate(projectDir, command, sessionId) {
313
323
  // The repo a Bash command acts on: an explicit `cd <dir>` in the command wins, then the
314
324
  // payload's cwd, then the session's home project — a session may operate across repos
315
325
  // (forge + consumer), and gating a commit against the wrong repo blocks or passes falsely.
326
+ // A candidate is normalized to its git toplevel: a cwd inside the repo (packages/studio)
327
+ // still gates — and writes the session log — against the repo root, not the subdirectory.
316
328
  function commandRoot(projectDir, payload, command) {
317
329
  const cdMatch = /(?:^|&&|;)\s*cd\s+"?([^"\s;&]+)"?/.exec(command || "");
318
330
  for (const candidate of [cdMatch && cdMatch[1], payload.cwd]) {
319
331
  if (!candidate || !path.isAbsolute(candidate)) continue;
320
332
  try {
321
- if (fs.statSync(candidate).isDirectory()) return candidate;
333
+ if (!fs.statSync(candidate).isDirectory()) continue;
322
334
  } catch (e) {
323
- /* fall through */
335
+ continue;
324
336
  }
337
+ return git(candidate, ["rev-parse", "--show-toplevel"]) || candidate;
325
338
  }
326
339
  return projectDir;
327
340
  }
@@ -56,3 +56,35 @@ export function approvedPackages(projectDir) {
56
56
  }
57
57
  return names;
58
58
  }
59
+
60
+ // A rejection is a decision already taken, and it names its replacement: the approved table's rows
61
+ // are `| package | version | use | don't use |`, so every name in a Don't-use cell points at that
62
+ // row's package. testing.md's framework table bans by stack without a single substitute, so those
63
+ // rejections carry the rule reference instead. Absence from both maps is not a rejection — it is a
64
+ // dimension nobody has ruled on, and the gate lets it through (the census harvests it centrally).
65
+ export function rejectedPackages(projectDir) {
66
+ const rejected = new Map();
67
+ const table = path.join(projectDir, ".claude", "hooks", "approved-packages.md");
68
+ for (const line of readSafe(table).split("\n")) {
69
+ const cells = line.split("|");
70
+ if (cells.length < 6) continue;
71
+ const pkg = /`([^`]+)`/.exec(cells[1] || "");
72
+ if (!pkg || pkg[1] === "Package" || pkg[1] === "---") continue;
73
+ const version = (/`?([^`|]+)`?/.exec((cells[2] || "").trim()) || [])[1] || "";
74
+ for (const m of (cells[4] || "").matchAll(/`(@?[a-z0-9][\w./-]*)`/g)) {
75
+ if (m[1] === pkg[1]) continue; // "`mermaid` via CDN" bans a delivery, not the row's own package
76
+ rejected.set(m[1], { use: pkg[1], version: version.trim(), purpose: (cells[3] || "").trim() });
77
+ }
78
+ }
79
+ const testing = path.join(projectDir, ".claude", "rules", "testing.md");
80
+ let banColumn = -1;
81
+ for (const line of readSafe(testing).split("\n")) {
82
+ banColumn = banColumnOf(line, banColumn);
83
+ if (banColumn < 0 || !line.trimStart().startsWith("|")) continue;
84
+ const cell = line.split("|")[banColumn] || "";
85
+ for (const m of cell.matchAll(/`(@?[a-z0-9][\w./-]*)`/g)) {
86
+ if (!rejected.has(m[1])) rejected.set(m[1], { rule: "testing.md § Frameworks by stack" });
87
+ }
88
+ }
89
+ return rejected;
90
+ }
@@ -33,9 +33,17 @@ How a package's `src/` is laid out. A single-runtime package drops `server/` and
33
33
 
34
34
  ## Packages
35
35
 
36
- **Only packages in the approved table** — `.claude/hooks/approved-packages.md`, enforced by the PreToolUse install gate, which names the file on a block. The table changes only by explicit user decision.
36
+ **The approved table is `.claude/hooks/approved-packages.md`**, enforced by the PreToolUse install gate. It records three states, and the gate treats them differently:
37
37
 
38
- Two **dev-only** installs are exempt, each a decision already taken rather than a new one: a scoped companion of an approved package (`@testing-library/react` is on the table, so `-D @testing-library/user-event` needs no row of its own) and `@types/*`. An unscoped or runtime install is always judged against the table.
38
+ | The package is… | What happens |
39
+ | --- | --- |
40
+ | in the **Package** column | installs |
41
+ | in a **Don't use** column | **blocked**, and the block names that row's replacement — `moment` answers "use `date-fns`" |
42
+ | in neither | **installs.** No decision exists, so there is nothing to enforce |
43
+
44
+ A rejection is a decision and binds; an absence is not one and never blocks work. Unlisted packages are harvested across the fleet by the package census, and become rows — or bans — by explicit user decision. **Never edit the table to unblock yourself**; a rejection changes only on the user's say-so.
45
+
46
+ Two **dev-only** installs are exempt even from the rejection check being reached: a scoped companion of an approved package (`@testing-library/react` is on the table, so `-D @testing-library/user-event` needs no row of its own) and `@types/*`.
39
47
 
40
48
  ## API Response Envelope
41
49
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "harness": "etna",
3
3
  "name": "etna",
4
- "version": "1.5.3",
4
+ "version": "1.5.4",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
8
8
  ".vscode/settings.json"
9
9
  ],
10
10
  "seeds": [],
11
- "digest": "4c78e401fa54"
11
+ "digest": "b76f5c26d6a9"
12
12
  }
@@ -33,9 +33,17 @@ How a package's `src/` is laid out. A single-runtime package drops `server/` and
33
33
 
34
34
  ## Packages
35
35
 
36
- **Only packages in the approved table** — `.claude/hooks/approved-packages.md`, enforced by the PreToolUse install gate, which names the file on a block. The table changes only by explicit user decision.
36
+ **The approved table is `.claude/hooks/approved-packages.md`**, enforced by the PreToolUse install gate. It records three states, and the gate treats them differently:
37
37
 
38
- Two **dev-only** installs are exempt, each a decision already taken rather than a new one: a scoped companion of an approved package (`@testing-library/react` is on the table, so `-D @testing-library/user-event` needs no row of its own) and `@types/*`. An unscoped or runtime install is always judged against the table.
38
+ | The package is… | What happens |
39
+ | --- | --- |
40
+ | in the **Package** column | installs |
41
+ | in a **Don't use** column | **blocked**, and the block names that row's replacement — `moment` answers "use `date-fns`" |
42
+ | in neither | **installs.** No decision exists, so there is nothing to enforce |
43
+
44
+ A rejection is a decision and binds; an absence is not one and never blocks work. Unlisted packages are harvested across the fleet by the package census, and become rows — or bans — by explicit user decision. **Never edit the table to unblock yourself**; a rejection changes only on the user's say-so.
45
+
46
+ Two **dev-only** installs are exempt even from the rejection check being reached: a scoped companion of an approved package (`@testing-library/react` is on the table, so `-D @testing-library/user-event` needs no row of its own) and `@types/*`.
39
47
 
40
48
  ## API Response Envelope
41
49
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.19.79",
3
+ "version": "0.19.81",
4
4
  "description": "Bootstrap a project with a Nurix harness — copies the harness's CLAUDE.md, skills, agents, rules, and hooks directly into the project from a bundled payload (no marketplace fetch, no network).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,5 +42,5 @@
42
42
  "agent",
43
43
  "plugin"
44
44
  ],
45
- "etnaFingerprint": "354c269e4457"
45
+ "etnaFingerprint": "798dd0b4e94c"
46
46
  }