rahman-resources 1.13.3 → 1.14.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.
@@ -8,6 +8,7 @@
8
8
  import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
9
9
  import { spawnSync } from "node:child_process";
10
10
  import path from "node:path";
11
+ import { loadSliceContract } from "../lib/load-contract.mjs";
11
12
 
12
13
  export function parseFlags(rest) {
13
14
  const positional = [];
@@ -51,26 +52,32 @@ const SLICE_ROOTS = [
51
52
  ["template-base", "frontend", "slices"],
52
53
  ];
53
54
 
54
- function resolveContractPath(repoRoot, slug) {
55
+ // The CURRENT contract is folded into slice.json — locate the slice dir by its
56
+ // slice.json (the .ts no longer exists in the working tree post-cutover).
57
+ function resolveSliceDir(repoRoot, slug) {
55
58
  for (const segs of SLICE_ROOTS) {
56
- const p = path.join(repoRoot, ...segs, slug, "slice.contract.ts");
57
- if (existsSync(p)) return p;
59
+ const dir = path.join(repoRoot, ...segs, slug);
60
+ if (existsSync(path.join(dir, "slice.json"))) return dir;
58
61
  }
59
62
  return null;
60
63
  }
61
64
 
65
+ // HISTORIC leg still pulls the OLD slice.contract.ts from git history, so the
66
+ // rel path is the .ts path — but gate on the slice's slice.json existing today
67
+ // (NOT the working-tree .ts, which is gone post-cutover).
62
68
  function resolveContractRelPath(repoRoot, slug) {
63
69
  for (const segs of SLICE_ROOTS) {
64
- const rel = [...segs, slug, "slice.contract.ts"].join("/");
65
- if (existsSync(path.join(repoRoot, rel))) return rel;
70
+ if (existsSync(path.join(repoRoot, ...segs, slug, "slice.json"))) {
71
+ return [...segs, slug, "slice.contract.ts"].join("/");
72
+ }
66
73
  }
67
74
  return null;
68
75
  }
69
76
 
70
77
  export function loadCurrentContract(repoRoot, slug) {
71
- const p = resolveContractPath(repoRoot, slug);
72
- if (!p) return null;
73
- return evalContract(repoRoot, p, null);
78
+ const dir = resolveSliceDir(repoRoot, slug);
79
+ if (!dir) return null;
80
+ return loadSliceContract(dir);
74
81
  }
75
82
 
76
83
  /**
package/bin/migrate.mjs CHANGED
@@ -80,7 +80,7 @@ export async function runMigrate(rest) {
80
80
  process.stderr.write(
81
81
  kleur.red(
82
82
  `migrate: cannot load current contract for "${slug}". ` +
83
- `Expected frontend/slices/${slug}/slice.contract.ts or template-base/frontend/slices/${slug}/slice.contract.ts.\n`,
83
+ `Expected a contract block in frontend/slices/${slug}/slice.json (or template-base/frontend/slices/${slug}/slice.json).\n`,
84
84
  ),
85
85
  );
86
86
  process.exit(1);
@@ -1,13 +1,14 @@
1
- // compose-solver-loader.mjs — contract discovery + tsx-eval loader.
1
+ // compose-solver-loader.mjs — contract discovery loader.
2
2
  //
3
3
  // Split out from compose-solver.mjs to keep the pure solver < 200 LOC.
4
- // I/O entry point only — discovers `slice.contract.ts` under known slice
5
- // roots and shells out to `npx tsx` to JSON-serialize each export.
4
+ // I/O entry point only — discovers slices under known roots and reads each
5
+ // folded contract from slice.json (`contract` block) via the shared adapter.
6
+ // No tsx subprocess: the contract lives in slice.json since the Phase-2 fold.
6
7
 
7
8
  import { readdir } from "node:fs/promises";
8
9
  import { existsSync } from "node:fs";
9
- import { spawnSync } from "node:child_process";
10
10
  import path from "node:path";
11
+ import { loadSliceContract } from "./load-contract.mjs";
11
12
 
12
13
  const SLICE_ROOT_GLOBS = [
13
14
  ["frontend", "slices"],
@@ -15,11 +16,10 @@ const SLICE_ROOT_GLOBS = [
15
16
  ];
16
17
 
17
18
  /**
18
- * Discover every `slice.contract.ts` under the kitab's known slice roots and
19
- * load them via `npx tsx`. Returns a Map<slug, SliceContract>. Contracts that
20
- * fail to load are silently skipped so a single broken file doesn't take
21
- * down the whole solver — `npm run validate:contracts` is the place to surface
22
- * those errors.
19
+ * Discover every slice with a folded contract under the kitab's known slice
20
+ * roots. Returns a Map<slug, SliceContract>. Slices without a slice.json
21
+ * contract block are silently skipped so a single uncontracted slice doesn't
22
+ * take down the solver — `npm run audit:slices` surfaces shape errors.
23
23
  *
24
24
  * @param {string} repoRoot Absolute path to the kitab repo root.
25
25
  * @returns {Promise<Map<string, import("./contract").SliceContract>>}
@@ -27,9 +27,8 @@ const SLICE_ROOT_GLOBS = [
27
27
  export async function loadAllContracts(repoRoot) {
28
28
  /** @type {Map<string, import("./contract").SliceContract>} */
29
29
  const out = new Map();
30
- const sliceFiles = await discoverContractFiles(repoRoot);
31
- for (const filePath of sliceFiles) {
32
- const contract = loadContractFile(repoRoot, filePath);
30
+ for (const dir of await discoverSliceDirs(repoRoot)) {
31
+ const contract = loadSliceContract(dir);
33
32
  if (contract && typeof contract.id === "string") {
34
33
  out.set(contract.id, contract);
35
34
  }
@@ -37,7 +36,7 @@ export async function loadAllContracts(repoRoot) {
37
36
  return out;
38
37
  }
39
38
 
40
- async function discoverContractFiles(repoRoot) {
39
+ async function discoverSliceDirs(repoRoot) {
41
40
  const found = [];
42
41
  for (const segs of SLICE_ROOT_GLOBS) {
43
42
  const root = path.join(repoRoot, ...segs);
@@ -46,34 +45,9 @@ async function discoverContractFiles(repoRoot) {
46
45
  for (const entry of entries) {
47
46
  if (!entry.isDirectory()) continue;
48
47
  if (entry.name.startsWith("_")) continue;
49
- const filePath = path.join(root, entry.name, "slice.contract.ts");
50
- if (existsSync(filePath)) found.push(filePath);
48
+ const dir = path.join(root, entry.name);
49
+ if (existsSync(path.join(dir, "slice.json"))) found.push(dir);
51
50
  }
52
51
  }
53
52
  return found;
54
53
  }
55
-
56
- /**
57
- * Dynamic-import a .ts contract file via `npx tsx -e` and JSON.parse the
58
- * stringified `contract` export. Returns null on any failure.
59
- */
60
- function loadContractFile(repoRoot, filePath) {
61
- const rel = "./" + path.relative(repoRoot, filePath);
62
- const code = [
63
- `import(${JSON.stringify(rel)})`,
64
- ` .then(m => { const c = m.contract || (m.default && m.default.contract); if (!c) { process.exit(2); } process.stdout.write(JSON.stringify(c)); })`,
65
- ` .catch(() => process.exit(3));`,
66
- ].join("\n");
67
- const res = spawnSync("npx", ["--no-install", "tsx", "-e", code], {
68
- cwd: repoRoot,
69
- encoding: "utf8",
70
- });
71
- if (res.status === 0 && res.stdout) {
72
- try {
73
- return JSON.parse(res.stdout);
74
- } catch {
75
- return null;
76
- }
77
- }
78
- return null;
79
- }
@@ -32,7 +32,7 @@ export function buildInitialCandidates({
32
32
  {
33
33
  type: "uncontracted",
34
34
  slug,
35
- detail: `Slice "${slug}" has no registered slice.contract.ts — accepted under allowUnknownSlices, but conflict checks are skipped for it.`,
35
+ detail: `Slice "${slug}" has no registered contract — accepted under allowUnknownSlices, but conflict checks are skipped for it.`,
36
36
  severity: "warning",
37
37
  },
38
38
  slug,
@@ -41,13 +41,13 @@ export function buildInitialCandidates({
41
41
  candidateSet.add(slug);
42
42
  candidateOrder.push(slug);
43
43
  notes.set(slug, "uncontracted");
44
- proof.push(`! ${slug}: accepted as uncontracted (no slice.contract.ts; skipping conflict checks)`);
44
+ proof.push(`! ${slug}: accepted as uncontracted (no contract (slice.json); skipping conflict checks)`);
45
45
  } else {
46
46
  record(
47
47
  {
48
48
  type: "missing-dep",
49
49
  slug,
50
- detail: `Contract not found for "${slug}" — no slice.contract.ts registered (strict mode).`,
50
+ detail: `Contract not found for "${slug}" — no registered contract (strict mode).`,
51
51
  severity: "blocker",
52
52
  },
53
53
  slug,
@@ -0,0 +1,33 @@
1
+ // load-contract.mjs — read a slice's folded composition contract from its
2
+ // slice.json (Phase 2 SSOT). Replaces parsing slice.contract.ts: the contract
3
+ // payload now lives under slice.json `contract`, with id/version omitted there
4
+ // (they are the slice.json scalars). This adapter reattaches them so callers
5
+ // get the same SliceContract shape the .ts used to export.
6
+ //
7
+ // Returns undefined when the slice has no slice.json or no contract block —
8
+ // callers treat that exactly like a missing slice.contract.ts before.
9
+
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import path from "node:path";
12
+
13
+ /**
14
+ * @param {string} sliceDir Absolute path to frontend/slices/<slug>/.
15
+ * @returns {object|undefined} { id, version, requires?, provides?, conflicts?,
16
+ * migrationFrom?, generalization? } or undefined.
17
+ */
18
+ export function loadSliceContract(sliceDir) {
19
+ return contractFromJson(path.join(sliceDir, "slice.json"));
20
+ }
21
+
22
+ /** Same, given a direct path to a slice.json file. */
23
+ export function contractFromJson(jsonPath) {
24
+ if (!existsSync(jsonPath)) return undefined;
25
+ let j;
26
+ try {
27
+ j = JSON.parse(readFileSync(jsonPath, "utf8"));
28
+ } catch {
29
+ return undefined;
30
+ }
31
+ if (!j.contract || typeof j.contract !== "object") return undefined;
32
+ return { id: j.slug, version: j.version, ...j.contract };
33
+ }
@@ -315,6 +315,10 @@
315
315
  "deprecated": {
316
316
  "type": "string",
317
317
  "description": "Successor slug. This slice was merged into the named slice as variants; install resolves there via manifest.aliases."
318
+ },
319
+ "contract": {
320
+ "type": "object",
321
+ "description": "Folded Phase-A composition contract (Phase 2 hybrid fold, 2026-06-21): requires/provides/conflicts/migrationFrom/generalization moved from slice.contract.ts. id+version omitted (== slug/version). requires.deps is verbatim pending per-slice normalization, so this is intentionally loose; strict invariants move to validate-slice when the .ts is retired."
318
322
  }
319
323
  }
320
324
  }
package/lib/snapshot.mjs CHANGED
@@ -1,13 +1,12 @@
1
1
  // snapshot.mjs — build a SliceSnapshot from a slice directory.
2
2
  //
3
3
  // Walks the directory recursively, keeps only typical slice file extensions,
4
- // reads `slice.contract.ts` when present and parses it (via `tsx` if
5
- // available, regex fallback otherwise). Skips node_modules, .kitab, and
6
- // dotfiles to keep snapshots stable across machines.
4
+ // and reads the folded composition contract from slice.json (`contract` block,
5
+ // id/version reattached from the slice.json scalars). Skips node_modules,
6
+ // .kitab, and dotfiles to keep snapshots stable across machines.
7
7
 
8
8
  import { readdir, readFile, stat } from "node:fs/promises";
9
9
  import { existsSync } from "node:fs";
10
- import { spawnSync } from "node:child_process";
11
10
  import path from "node:path";
12
11
 
13
12
  const INCLUDED_EXT = new Set([".ts", ".tsx", ".mjs", ".js", ".jsx", ".json", ".md", ".css"]);
@@ -33,18 +32,18 @@ export async function snapshotFromDir(slug, dir) {
33
32
  const files = {};
34
33
  await walk(dir, dir, files);
35
34
 
35
+ // The composition contract is folded into slice.json (`contract` block) since
36
+ // Phase 2; reattach id/version from the slice.json scalars to keep the same
37
+ // SliceContract shape the old slice.contract.ts exported.
36
38
  let contract;
37
- const contractPath = path.join(dir, "slice.contract.ts");
38
- if (existsSync(contractPath)) {
39
- contract = loadContract(contractPath) ?? regexLoadContract(await readFile(contractPath, "utf8"));
40
- }
41
-
42
- // Version preference: contract.version → slice.json.version → "0.0.0".
43
- let version = contract?.version ?? "0.0.0";
44
- if (!contract?.version && files["slice.json"]) {
39
+ let version = "0.0.0";
40
+ if (files["slice.json"]) {
45
41
  try {
46
42
  const meta = JSON.parse(files["slice.json"]);
47
43
  if (typeof meta.version === "string") version = meta.version;
44
+ if (meta.contract && typeof meta.contract === "object") {
45
+ contract = { id: meta.slug, version: meta.version, ...meta.contract };
46
+ }
48
47
  } catch {
49
48
  /* ignore */
50
49
  }
@@ -79,48 +78,3 @@ async function walk(root, dir, out) {
79
78
  out[rel] = await readFile(full, "utf8");
80
79
  }
81
80
  }
82
-
83
- /**
84
- * Mirror of validate-contract.mjs's tsx loader. Returns the parsed contract
85
- * or undefined on failure.
86
- */
87
- function loadContract(filePath) {
88
- const code = [
89
- `import(${JSON.stringify(filePath)})`,
90
- ` .then(m => { const c = m.contract || (m.default && m.default.contract); if (!c) { process.exit(2); } process.stdout.write(JSON.stringify(c)); })`,
91
- ` .catch(e => { process.stderr.write(String(e && e.message || e)); process.exit(3); });`,
92
- ].join("\n");
93
- try {
94
- const res = spawnSync("npx", ["--no-install", "tsx", "-e", code], {
95
- encoding: "utf8",
96
- });
97
- if (res.status === 0 && res.stdout) {
98
- return JSON.parse(res.stdout);
99
- }
100
- } catch {
101
- /* tsx missing or failed */
102
- }
103
- return undefined;
104
- }
105
-
106
- /**
107
- * Best-effort regex fallback: extract the literal-ish object passed to
108
- * `defineSliceContract({ ... })`. We can't fully parse TS, but for the kitab's
109
- * contracts (small static literals) a JSON5-ish coerce works for the basic
110
- * shape we need here.
111
- */
112
- function regexLoadContract(src) {
113
- const m = src.match(/defineSliceContract\s*\(\s*(\{[\s\S]*?\})\s*\)\s*;?\s*$/m);
114
- if (!m) return undefined;
115
- // Replace single quotes, strip trailing commas, quote keys.
116
- const body = m[1]
117
- .replace(/'([^'\\]*)'/g, '"$1"')
118
- .replace(/,(\s*[}\]])/g, "$1")
119
- .replace(/([{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/g, '$1"$2":')
120
- .replace(/\/\/.*$/gm, "");
121
- try {
122
- return JSON.parse(body);
123
- } catch {
124
- return undefined;
125
- }
126
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rahman-resources",
3
- "version": "1.13.3",
3
+ "version": "1.14.0",
4
4
  "description": "Rahman Resources (rr) — shadcn-style installer for vertical slices. `npx resources add <slug>` copies slice into your project's `slices/<slug>/`. You own the files.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,12 +29,12 @@
29
29
  "gen": "node scripts/gen-manifest.mjs",
30
30
  "validate": "node scripts/validate.mjs",
31
31
  "validate:slices": "node scripts/validate-slice.mjs",
32
- "validate:parity": "node scripts/validate-slice-parity.mjs",
32
+ "validate:parity": "node ../../scripts/features/gen-slice-catalog.mjs --check",
33
33
  "validate:structure": "node scripts/validate-structure.mjs",
34
- "validate:all": "node scripts/validate.mjs && node scripts/validate-slice.mjs --check && node scripts/validate-slice-parity.mjs && node scripts/validate-structure.mjs",
34
+ "validate:all": "node scripts/validate.mjs && node scripts/validate-slice.mjs --check && node ../../scripts/features/gen-slice-catalog.mjs --check && node scripts/validate-structure.mjs",
35
35
  "sync:skills": "node scripts/sync-skills.mjs",
36
36
  "sync:skills:check": "node scripts/sync-skills.mjs --check",
37
- "prepublishOnly": "node scripts/sync-skills.mjs --check && node scripts/validate.mjs && node scripts/validate-slice.mjs --check && node scripts/validate-slice-parity.mjs && node scripts/validate-structure.mjs"
37
+ "prepublishOnly": "node scripts/sync-skills.mjs --check && node scripts/validate.mjs && node scripts/validate-slice.mjs --check && node ../../scripts/features/gen-slice-catalog.mjs --check && node scripts/validate-structure.mjs"
38
38
  },
39
39
  "dependencies": {
40
40
  "kleur": "^4.1.5",