@aacombarro89/praxis 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.
Files changed (72) hide show
  1. package/dist/anchors.d.ts +36 -0
  2. package/dist/anchors.js +197 -0
  3. package/dist/anchors.js.map +1 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +4 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/emit.d.ts +51 -0
  8. package/dist/emit.js +253 -0
  9. package/dist/emit.js.map +1 -0
  10. package/dist/external.d.ts +61 -0
  11. package/dist/external.js +155 -0
  12. package/dist/external.js.map +1 -0
  13. package/dist/init.d.ts +45 -0
  14. package/dist/init.js +123 -0
  15. package/dist/init.js.map +1 -0
  16. package/dist/manifest.d.ts +36 -0
  17. package/dist/manifest.js +63 -0
  18. package/dist/manifest.js.map +1 -0
  19. package/dist/merge-json.d.ts +83 -0
  20. package/dist/merge-json.js +207 -0
  21. package/dist/merge-json.js.map +1 -0
  22. package/dist/merge.d.ts +53 -0
  23. package/dist/merge.js +89 -0
  24. package/dist/merge.js.map +1 -0
  25. package/dist/packages.d.ts +47 -0
  26. package/dist/packages.js +121 -0
  27. package/dist/packages.js.map +1 -0
  28. package/dist/permissions.d.ts +54 -0
  29. package/dist/permissions.js +83 -0
  30. package/dist/permissions.js.map +1 -0
  31. package/dist/plugins.d.ts +21 -0
  32. package/dist/plugins.js +88 -0
  33. package/dist/plugins.js.map +1 -0
  34. package/dist/program.d.ts +2 -0
  35. package/dist/program.js +201 -0
  36. package/dist/program.js.map +1 -0
  37. package/dist/sync.d.ts +37 -0
  38. package/dist/sync.js +45 -0
  39. package/dist/sync.js.map +1 -0
  40. package/package.json +40 -0
  41. package/packages/external/drawio/package.yaml +7 -0
  42. package/packages/external/drawio/plugins.yaml +13 -0
  43. package/packages/external/ponytail/package.yaml +7 -0
  44. package/packages/external/ponytail/plugins.yaml +13 -0
  45. package/packages/layer1/instruction-upkeep/commands/audit.md +41 -0
  46. package/packages/layer1/instruction-upkeep/package.yaml +8 -0
  47. package/packages/layer1/instruction-upkeep/rules.md +21 -0
  48. package/packages/layer1/karpathy-claude/package.yaml +5 -0
  49. package/packages/layer1/karpathy-claude/rules.md +35 -0
  50. package/packages/layer1/safe-permissions/package.yaml +5 -0
  51. package/packages/layer1/safe-permissions/permissions.yaml +26 -0
  52. package/packages/layer1/session-handoff/commands/handoff.md +185 -0
  53. package/packages/layer1/session-handoff/package.yaml +10 -0
  54. package/packages/layer1/session-handoff/rules.md +20 -0
  55. package/packages/layer1/upkeep/commands/upkeep.md +31 -0
  56. package/packages/layer1/upkeep/package.yaml +10 -0
  57. package/packages/layer1/upkeep/rules.md +12 -0
  58. package/packages/layer1/wiki-memory/commands/wiki.md +103 -0
  59. package/packages/layer1/wiki-memory/package.yaml +10 -0
  60. package/packages/layer1/wiki-memory/rules.md +49 -0
  61. package/packages/layer2/node-recipes/package.yaml +10 -0
  62. package/packages/layer2/node-recipes/rules.md +33 -0
  63. package/packages/layer2/node-testing/package.yaml +10 -0
  64. package/packages/layer2/node-testing/rules.md +33 -0
  65. package/packages/layer2/python-backend-recipes/package.yaml +10 -0
  66. package/packages/layer2/python-backend-recipes/rules.md +37 -0
  67. package/packages/layer2/python-testing/package.yaml +10 -0
  68. package/packages/layer2/python-testing/rules.md +33 -0
  69. package/packages/layer2/react-components/package.yaml +10 -0
  70. package/packages/layer2/react-components/rules.md +40 -0
  71. package/packages/layer2/react-testing/package.yaml +10 -0
  72. package/packages/layer2/react-testing/rules.md +35 -0
@@ -0,0 +1,207 @@
1
+ /**
2
+ * JSON-aware managed merge for `.claude/settings.json` (docs/wiki/merge-engine.md,
3
+ * decisions D10/D11/D18/D19). The markdown engine in `src/merge.ts` owns spans of *text*
4
+ * delimited by HTML-comment markers; that trick has **no JSON equivalent** — JSON
5
+ * has no comments. So this module is the structured-config analogue: Praxis owns
6
+ * only the **individual entries** it emits — permission rule strings, marketplace
7
+ * declarations, enabled-plugin keys — deep-merged into the host file, and records
8
+ * exactly which entries are its own in a self-contained top-level `_praxis` key
9
+ * (the JSON-native marker; D11 — no separate lockfile). One reconcile pass owns
10
+ * the whole file so two concerns (permissions, plugins) never compose into two
11
+ * separate writes of the same path (the same-file compose bug).
12
+ *
13
+ * Invariants (mirrors the markdown engine, verified by conformance):
14
+ * - User-added entries in the same structures survive untouched (per-entry
15
+ * ownership).
16
+ * - Entries Praxis previously emitted but no longer ships are removed on update.
17
+ * - If the user edited/removed one of Praxis's managed entries, that group is a
18
+ * **conflict, never a clobber** (D10): left exactly as-is, surfaced.
19
+ * - Idempotent: re-merging identical input is a byte-for-byte no-op.
20
+ * - Every other key in the file (model, env, defaultMode, …) is preserved.
21
+ *
22
+ * The `_praxis` key is a valid top-level addition: the Claude Code settings schema
23
+ * sets top-level `additionalProperties: true` (verified against
24
+ * json.schemastore.org/claude-code-settings.json), while `permissions` itself is
25
+ * `additionalProperties: false` — hence the marker lives as a top-level sibling of
26
+ * `permissions`/`extraKnownMarketplaces`/`enabledPlugins`, never inside them.
27
+ */
28
+ /** The three permission buckets Praxis manages, in stable emit order. */
29
+ export const BUCKETS = ["allow", "ask", "deny"];
30
+ function asStringArray(value) {
31
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
32
+ }
33
+ function isObject(v) {
34
+ return typeof v === "object" && v !== null && !Array.isArray(v);
35
+ }
36
+ /**
37
+ * Bring `.claude/settings.json` in line with the desired permissions and/or
38
+ * plugin-marketplace state in a single pass — the only function that may write
39
+ * this file, so permissions and plugins compose into one write instead of two
40
+ * operations racing on the same path. Splices only the entries Praxis owns;
41
+ * preserves user entries and every unrelated key; refuses to overwrite a group
42
+ * the user has edited (conflict). Pure; no I/O.
43
+ *
44
+ * @param existing the destination file's current text ("" if it does not exist)
45
+ * @param desired the permissions and/or plugins state Praxis wants to own
46
+ */
47
+ export function reconcileSettings(existing, desired) {
48
+ const root = parseRoot(existing);
49
+ const marker = isObject(root._praxis) ? root._praxis : {};
50
+ const managed = marker.managed ?? {};
51
+ const conflicts = [];
52
+ const newManaged = {};
53
+ if (desired.permissions) {
54
+ const recorded = managed.permissions ?? {};
55
+ const permissions = isObject(root.permissions)
56
+ ? root.permissions
57
+ : {};
58
+ const newRecorded = {};
59
+ for (const bucket of BUCKETS) {
60
+ const onDisk = asStringArray(permissions[bucket]);
61
+ const rec = asStringArray(recorded[bucket]);
62
+ const want = desired.permissions[bucket] ?? [];
63
+ // Conflict, never clobber (D10): the managed set is intact only if every
64
+ // rule Praxis last emitted is still present.
65
+ const intact = rec.every((r) => onDisk.includes(r));
66
+ if (!intact) {
67
+ conflicts.push(`permissions.${bucket}`);
68
+ newRecorded[bucket] = rec;
69
+ continue;
70
+ }
71
+ const recSet = new Set(rec);
72
+ const wantSet = new Set(want);
73
+ const merged = [];
74
+ for (const r of onDisk) {
75
+ if (!recSet.has(r) || wantSet.has(r))
76
+ merged.push(r);
77
+ }
78
+ for (const r of want) {
79
+ if (!merged.includes(r))
80
+ merged.push(r);
81
+ }
82
+ if (merged.length > 0)
83
+ permissions[bucket] = merged;
84
+ else
85
+ delete permissions[bucket];
86
+ if (want.length > 0)
87
+ newRecorded[bucket] = want;
88
+ }
89
+ if (Object.keys(permissions).length > 0)
90
+ root.permissions = permissions;
91
+ else
92
+ delete root.permissions;
93
+ if (Object.keys(newRecorded).length > 0)
94
+ newManaged.permissions = newRecorded;
95
+ }
96
+ else if (managed.permissions) {
97
+ // Not part of this sync — keep tracking what we previously owned untouched.
98
+ newManaged.permissions = managed.permissions;
99
+ }
100
+ if (desired.plugins) {
101
+ const recordedMarketplaces = managed.marketplaces ?? [];
102
+ const recordedPlugins = managed.plugins ?? [];
103
+ const marketplaces = isObject(root.extraKnownMarketplaces)
104
+ ? root.extraKnownMarketplaces
105
+ : {};
106
+ const enabledPlugins = isObject(root.enabledPlugins)
107
+ ? root.enabledPlugins
108
+ : {};
109
+ const marketplacesIntact = recordedMarketplaces.every((name) => name in marketplaces);
110
+ const pluginsIntact = recordedPlugins.every((key) => key in enabledPlugins);
111
+ if (!marketplacesIntact) {
112
+ // Conflict, never clobber (D10): leave extraKnownMarketplaces exactly as
113
+ // found on disk (including a present-but-empty `{}`), don't recompute it.
114
+ conflicts.push("plugins.marketplaces");
115
+ newManaged.marketplaces = recordedMarketplaces;
116
+ }
117
+ else {
118
+ const wantNames = new Set(desired.plugins.marketplaces.map((m) => m.name));
119
+ for (const name of Object.keys(marketplaces)) {
120
+ if (recordedMarketplaces.includes(name) && !wantNames.has(name))
121
+ delete marketplaces[name];
122
+ }
123
+ for (const m of desired.plugins.marketplaces) {
124
+ marketplaces[m.name] = { source: m.source };
125
+ }
126
+ if (Object.keys(marketplaces).length > 0)
127
+ root.extraKnownMarketplaces = marketplaces;
128
+ else
129
+ delete root.extraKnownMarketplaces;
130
+ if (desired.plugins.marketplaces.length > 0) {
131
+ newManaged.marketplaces = desired.plugins.marketplaces.map((m) => m.name);
132
+ }
133
+ }
134
+ if (!pluginsIntact) {
135
+ // Same conflict rule for enabledPlugins, independently of marketplaces.
136
+ conflicts.push("plugins.enable");
137
+ newManaged.plugins = recordedPlugins;
138
+ }
139
+ else {
140
+ const wantKeys = new Set(desired.plugins.enable);
141
+ for (const key of Object.keys(enabledPlugins)) {
142
+ if (recordedPlugins.includes(key) && !wantKeys.has(key))
143
+ delete enabledPlugins[key];
144
+ }
145
+ for (const key of desired.plugins.enable) {
146
+ enabledPlugins[key] = true;
147
+ }
148
+ if (Object.keys(enabledPlugins).length > 0)
149
+ root.enabledPlugins = enabledPlugins;
150
+ else
151
+ delete root.enabledPlugins;
152
+ if (desired.plugins.enable.length > 0)
153
+ newManaged.plugins = desired.plugins.enable;
154
+ }
155
+ }
156
+ else {
157
+ if (managed.marketplaces)
158
+ newManaged.marketplaces = managed.marketplaces;
159
+ if (managed.plugins)
160
+ newManaged.plugins = managed.plugins;
161
+ }
162
+ if (Object.keys(newManaged).length > 0) {
163
+ root._praxis = { managed: newManaged };
164
+ }
165
+ else {
166
+ delete root._praxis;
167
+ }
168
+ const text = `${JSON.stringify(root, null, 2)}\n`;
169
+ return { text, changed: text !== normalize(existing), conflicts };
170
+ }
171
+ /** Permissions-only convenience wrapper over `reconcileSettings` — kept because
172
+ * most call sites and tests only ever reconcile permissions. */
173
+ export function reconcilePermissions(existing, desired) {
174
+ return reconcileSettings(existing, { permissions: desired });
175
+ }
176
+ /** Parse the host file into a mutable object, preserving key order. Empty/blank
177
+ * → `{}`. Invalid JSON throws a readable error rather than risk a clobber. */
178
+ function parseRoot(existing) {
179
+ if (existing.trim() === "")
180
+ return {};
181
+ let parsed;
182
+ try {
183
+ parsed = JSON.parse(existing);
184
+ }
185
+ catch (err) {
186
+ throw new Error(`Cannot merge settings: the destination file is not valid JSON ` +
187
+ `(${err.message}). Fix or remove it, then re-run \`praxis sync\`.`);
188
+ }
189
+ if (!isObject(parsed)) {
190
+ throw new Error(`Cannot merge settings: the destination file must be a JSON object, ` +
191
+ `got ${Array.isArray(parsed) ? "an array" : typeof parsed}.`);
192
+ }
193
+ return parsed;
194
+ }
195
+ /** The byte-form a no-op write would produce for the given input, used to compute
196
+ * `changed` honestly when the input is already-formatted JSON. */
197
+ function normalize(existing) {
198
+ if (existing.trim() === "")
199
+ return "";
200
+ try {
201
+ return `${JSON.stringify(JSON.parse(existing), null, 2)}\n`;
202
+ }
203
+ catch {
204
+ return existing;
205
+ }
206
+ }
207
+ //# sourceMappingURL=merge-json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-json.js","sourceRoot":"","sources":["../src/merge-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,yEAAyE;AACzE,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAU,CAAC;AA+CzD,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC7F,CAAC;AAED,SAAS,QAAQ,CAAC,CAAU;IAC1B,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAClE,CAAC;AAWD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAgB,EAAE,OAAwB;IAC1E,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;IACjC,MAAM,MAAM,GAAiB,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAE,IAAI,CAAC,OAAwB,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1F,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC;IACrC,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,MAAM,UAAU,GAA4B,EAAE,CAAC;IAE/C,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;QACxB,MAAM,QAAQ,GAAG,OAAO,CAAC,WAAW,IAAI,EAAE,CAAC;QAC3C,MAAM,WAAW,GAA4B,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC;YACrE,CAAC,CAAE,IAAI,CAAC,WAAuC;YAC/C,CAAC,CAAC,EAAE,CAAC;QACP,MAAM,WAAW,GAAsC,EAAE,CAAC;QAE1D,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,MAAM,MAAM,GAAG,aAAa,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;YAClD,MAAM,GAAG,GAAG,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;YAC5C,MAAM,IAAI,GAAG,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YAE/C,yEAAyE;YACzE,6CAA6C;YAC7C,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;YACpD,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,SAAS,CAAC,IAAI,CAAC,eAAe,MAAM,EAAE,CAAC,CAAC;gBACxC,WAAW,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC;gBAC1B,SAAS;YACX,CAAC;YAED,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;YAC5B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;YAC9B,MAAM,MAAM,GAAa,EAAE,CAAC;YAC5B,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;gBACvB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;oBAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACvD,CAAC;YACD,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;gBACrB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;oBAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC1C,CAAC;YAED,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;gBAAE,WAAW,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;;gBAC/C,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC;YAChC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;gBAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;QAClD,CAAC;QAED,IAAI,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;;YACnE,OAAO,IAAI,CAAC,WAAW,CAAC;QAC7B,IAAI,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,UAAU,CAAC,WAAW,GAAG,WAAW,CAAC;IAChF,CAAC;SAAM,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;QAC/B,4EAA4E;QAC5E,UAAU,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAC/C,CAAC;IAED,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,oBAAoB,GAAG,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;QACxD,MAAM,eAAe,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;QAE9C,MAAM,YAAY,GAA4B,QAAQ,CAAC,IAAI,CAAC,sBAAsB,CAAC;YACjF,CAAC,CAAE,IAAI,CAAC,sBAAkD;YAC1D,CAAC,CAAC,EAAE,CAAC;QACP,MAAM,cAAc,GAA4B,QAAQ,CAAC,IAAI,CAAC,cAAc,CAAC;YAC3E,CAAC,CAAE,IAAI,CAAC,cAA0C;YAClD,CAAC,CAAC,EAAE,CAAC;QAEP,MAAM,kBAAkB,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,YAAY,CAAC,CAAC;QACtF,MAAM,aAAa,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,cAAc,CAAC,CAAC;QAE5E,IAAI,CAAC,kBAAkB,EAAE,CAAC;YACxB,yEAAyE;YACzE,0EAA0E;YAC1E,SAAS,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;YACvC,UAAU,CAAC,YAAY,GAAG,oBAAoB,CAAC;QACjD,CAAC;aAAM,CAAC;YACN,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YAC3E,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;gBAC7C,IAAI,oBAAoB,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;oBAAE,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC;YAC7F,CAAC;YACD,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;gBAC7C,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;YAC9C,CAAC;YACD,IAAI,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC;gBAAE,IAAI,CAAC,sBAAsB,GAAG,YAAY,CAAC;;gBAChF,OAAO,IAAI,CAAC,sBAAsB,CAAC;YACxC,IAAI,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC5C,UAAU,CAAC,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YAC5E,CAAC;QACH,CAAC;QAED,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,wEAAwE;YACxE,SAAS,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;YACjC,UAAU,CAAC,OAAO,GAAG,eAAe,CAAC;QACvC,CAAC;aAAM,CAAC;YACN,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YACjD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC;gBAC9C,IAAI,eAAe,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;oBAAE,OAAO,cAAc,CAAC,GAAG,CAAC,CAAC;YACtF,CAAC;YACD,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;gBACzC,cAAc,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;YAC7B,CAAC;YACD,IAAI,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,MAAM,GAAG,CAAC;gBAAE,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;;gBAC5E,OAAO,IAAI,CAAC,cAAc,CAAC;YAChC,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;gBAAE,UAAU,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC;QACrF,CAAC;IACH,CAAC;SAAM,CAAC;QACN,IAAI,OAAO,CAAC,YAAY;YAAE,UAAU,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;QACzE,IAAI,OAAO,CAAC,OAAO;YAAE,UAAU,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAC5D,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvC,IAAI,CAAC,OAAO,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC;IACzC,CAAC;SAAM,CAAC;QACN,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;IAClD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,KAAK,SAAS,CAAC,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC;AACpE,CAAC;AAED;iEACiE;AACjE,MAAM,UAAU,oBAAoB,CAAC,QAAgB,EAAE,OAAgB;IACrE,OAAO,iBAAiB,CAAC,QAAQ,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED;+EAC+E;AAC/E,SAAS,SAAS,CAAC,QAAgB;IACjC,IAAI,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,EAAE,CAAC;IACtC,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAChC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,gEAAgE;YAC9D,IAAK,GAAa,CAAC,OAAO,mDAAmD,CAChF,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,qEAAqE;YACnE,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,MAAM,GAAG,CAC/D,CAAC;IACJ,CAAC;IACD,OAAO,MAAiC,CAAC;AAC3C,CAAC;AAED;mEACmE;AACnE,SAAS,SAAS,CAAC,QAAgB;IACjC,IAAI,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,EAAE,CAAC;IACtC,IAAI,CAAC;QACH,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;IAC9D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,CAAC;IAClB,CAAC;AACH,CAAC"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The block-splice merge core (docs/wiki/merge-engine.md, decisions D7/D10/D11).
3
+ *
4
+ * Praxis owns only the text between its `praxis:begin`/`praxis:end` markers.
5
+ * The host file is never parsed as markdown — we locate our own machine markers
6
+ * and treat the content between them as OPAQUE text. This is the whole point of
7
+ * rejecting a markdown AST (D7): re-serializing a tree would normalize the user's
8
+ * prose. Here, every byte outside a managed span is preserved exactly.
9
+ */
10
+ /** A Praxis-managed region located in a host file. */
11
+ export interface ManagedBlock {
12
+ id: string;
13
+ /** Exact bytes between the markers, excluding the marker lines. Opaque. */
14
+ content: string;
15
+ /** sha256 recorded in the begin marker (`""` if the marker carried none). */
16
+ recordedHash: string;
17
+ /** Offset of the block start (begin marker) in the source text. */
18
+ start: number;
19
+ /** Offset just past the end marker in the source text. */
20
+ end: number;
21
+ }
22
+ export type BlockStatus = "absent" | "unchanged" | "user-edited";
23
+ /** sha256 (hex) of the content. Used to detect user edits inside a block. */
24
+ export declare function hashContent(content: string): string;
25
+ /** Render a full managed block, stamping the begin marker with a fresh hash. */
26
+ export declare function renderBlock(id: string, content: string): string;
27
+ /** All managed blocks in the text, in document order. */
28
+ export declare function findBlocks(text: string): ManagedBlock[];
29
+ /** The first managed block with the given id, wherever it sits in the file. */
30
+ export declare function findBlock(text: string, id: string): ManagedBlock | undefined;
31
+ /**
32
+ * absent — no such block in the file.
33
+ * unchanged — on-disk content still matches the recorded hash (Praxis's last
34
+ * emission is intact; safe to update).
35
+ * user-edited — on-disk content differs from the recorded hash; the user (or
36
+ * agent) edited managed content. Never overwrite (D10).
37
+ */
38
+ export declare function blockStatus(block: ManagedBlock | undefined): BlockStatus;
39
+ export interface ReconcileResult {
40
+ /** The resulting file text. */
41
+ text: string;
42
+ /** Whether any byte changed. */
43
+ changed: boolean;
44
+ /** Ids of blocks the user edited; left untouched, surfaced for resolution. */
45
+ conflicts: string[];
46
+ }
47
+ /**
48
+ * Bring the host text in line with the desired managed content, one block per
49
+ * entry of `desired` (id -> content). Splices only managed spans; appends blocks
50
+ * that are absent; refuses to overwrite blocks the user has edited (conflict).
51
+ * Idempotent: reconciling with already-applied content is a byte-for-byte no-op.
52
+ */
53
+ export declare function reconcile(text: string, desired: Record<string, string>): ReconcileResult;
package/dist/merge.js ADDED
@@ -0,0 +1,89 @@
1
+ import { createHash } from "node:crypto";
2
+ const beginMarker = (id, hash) => `<!-- praxis:begin ${id} sha256=${hash} (managed by praxis - edit praxis.yaml then run: praxis sync) -->`;
3
+ const endMarker = (id) => `<!-- praxis:end ${id} -->`;
4
+ // Match our own fences only. Group 1: id. Group 2: begin-marker attributes
5
+ // (carries sha256). Group 3: opaque content. The id backreference (\1) ties the
6
+ // end marker to its begin marker; content is non-greedy so the nearest end wins.
7
+ const BLOCK_RE = /<!--\s*praxis:begin\s+([A-Za-z0-9._-]+)([^>]*?)-->[^\n]*\r?\n([\s\S]*?)\r?\n?<!--\s*praxis:end\s+\1\s*-->/g;
8
+ function extractHash(attrs) {
9
+ const m = /sha256=([A-Za-z0-9]+)/.exec(attrs);
10
+ return m?.[1] ?? "";
11
+ }
12
+ /** sha256 (hex) of the content. Used to detect user edits inside a block. */
13
+ export function hashContent(content) {
14
+ return createHash("sha256").update(content, "utf8").digest("hex");
15
+ }
16
+ /** Render a full managed block, stamping the begin marker with a fresh hash. */
17
+ export function renderBlock(id, content) {
18
+ return `${beginMarker(id, hashContent(content))}\n${content}\n${endMarker(id)}`;
19
+ }
20
+ /** All managed blocks in the text, in document order. */
21
+ export function findBlocks(text) {
22
+ const blocks = [];
23
+ BLOCK_RE.lastIndex = 0;
24
+ let m;
25
+ while ((m = BLOCK_RE.exec(text)) !== null) {
26
+ blocks.push({
27
+ id: m[1] ?? "",
28
+ recordedHash: extractHash(m[2] ?? ""),
29
+ content: m[3] ?? "",
30
+ start: m.index,
31
+ end: m.index + m[0].length,
32
+ });
33
+ }
34
+ return blocks;
35
+ }
36
+ /** The first managed block with the given id, wherever it sits in the file. */
37
+ export function findBlock(text, id) {
38
+ return findBlocks(text).find((b) => b.id === id);
39
+ }
40
+ /**
41
+ * absent — no such block in the file.
42
+ * unchanged — on-disk content still matches the recorded hash (Praxis's last
43
+ * emission is intact; safe to update).
44
+ * user-edited — on-disk content differs from the recorded hash; the user (or
45
+ * agent) edited managed content. Never overwrite (D10).
46
+ */
47
+ export function blockStatus(block) {
48
+ if (!block)
49
+ return "absent";
50
+ return block.recordedHash === hashContent(block.content)
51
+ ? "unchanged"
52
+ : "user-edited";
53
+ }
54
+ /**
55
+ * Bring the host text in line with the desired managed content, one block per
56
+ * entry of `desired` (id -> content). Splices only managed spans; appends blocks
57
+ * that are absent; refuses to overwrite blocks the user has edited (conflict).
58
+ * Idempotent: reconciling with already-applied content is a byte-for-byte no-op.
59
+ */
60
+ export function reconcile(text, desired) {
61
+ let out = text;
62
+ let changed = false;
63
+ const conflicts = [];
64
+ for (const [id, content] of Object.entries(desired)) {
65
+ const block = findBlock(out, id);
66
+ if (!block) {
67
+ out = appendBlock(out, id, content);
68
+ changed = true;
69
+ continue;
70
+ }
71
+ if (blockStatus(block) === "user-edited") {
72
+ conflicts.push(id);
73
+ continue; // never clobber a user edit (D10)
74
+ }
75
+ if (block.content !== content) {
76
+ out = out.slice(0, block.start) + renderBlock(id, content) + out.slice(block.end);
77
+ changed = true;
78
+ }
79
+ }
80
+ return { text: out, changed, conflicts };
81
+ }
82
+ function appendBlock(text, id, content) {
83
+ const block = renderBlock(id, content);
84
+ if (text.length === 0)
85
+ return `${block}\n`;
86
+ const sep = text.endsWith("\n") ? "\n" : "\n\n";
87
+ return `${text}${sep}${block}\n`;
88
+ }
89
+ //# sourceMappingURL=merge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.js","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA2BzC,MAAM,WAAW,GAAG,CAAC,EAAU,EAAE,IAAY,EAAU,EAAE,CACvD,qBAAqB,EAAE,WAAW,IAAI,mEAAmE,CAAC;AAE5G,MAAM,SAAS,GAAG,CAAC,EAAU,EAAU,EAAE,CAAC,mBAAmB,EAAE,MAAM,CAAC;AAEtE,2EAA2E;AAC3E,gFAAgF;AAChF,iFAAiF;AACjF,MAAM,QAAQ,GACZ,4GAA4G,CAAC;AAE/G,SAAS,WAAW,CAAC,KAAa;IAChC,MAAM,CAAC,GAAG,uBAAuB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC9C,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AACtB,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,WAAW,CAAC,OAAe;IACzC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACpE,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,WAAW,CAAC,EAAU,EAAE,OAAe;IACrD,OAAO,GAAG,WAAW,CAAC,EAAE,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC,KAAK,OAAO,KAAK,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC;AAClF,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,MAAM,GAAmB,EAAE,CAAC;IAClC,QAAQ,CAAC,SAAS,GAAG,CAAC,CAAC;IACvB,IAAI,CAAyB,CAAC;IAC9B,OAAO,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC1C,MAAM,CAAC,IAAI,CAAC;YACV,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE;YACd,YAAY,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACrC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE;YACnB,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,GAAG,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM;SAC3B,CAAC,CAAC;IACL,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,EAAU;IAChD,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,KAA+B;IACzD,IAAI,CAAC,KAAK;QAAE,OAAO,QAAQ,CAAC;IAC5B,OAAO,KAAK,CAAC,YAAY,KAAK,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC;QACtD,CAAC,CAAC,WAAW;QACb,CAAC,CAAC,aAAa,CAAC;AACpB,CAAC;AAWD;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CACvB,IAAY,EACZ,OAA+B;IAE/B,IAAI,GAAG,GAAG,IAAI,CAAC;IACf,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,MAAM,SAAS,GAAa,EAAE,CAAC;IAE/B,KAAK,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACpD,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAEjC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,GAAG,GAAG,WAAW,CAAC,GAAG,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC;YACpC,OAAO,GAAG,IAAI,CAAC;YACf,SAAS;QACX,CAAC;QACD,IAAI,WAAW,CAAC,KAAK,CAAC,KAAK,aAAa,EAAE,CAAC;YACzC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACnB,SAAS,CAAC,kCAAkC;QAC9C,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;YAC9B,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,WAAW,CAAC,EAAE,EAAE,OAAO,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAClF,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;AAC3C,CAAC;AAED,SAAS,WAAW,CAAC,IAAY,EAAE,EAAU,EAAE,OAAe;IAC5D,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;IACvC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,KAAK,IAAI,CAAC;IAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;IAChD,OAAO,GAAG,IAAI,GAAG,GAAG,GAAG,KAAK,IAAI,CAAC;AACnC,CAAC"}
@@ -0,0 +1,47 @@
1
+ import { z } from "zod";
2
+ declare const ARTIFACTS: readonly ["rules", "permissions", "commands", "plugins"];
3
+ export type Artifact = (typeof ARTIFACTS)[number];
4
+ export declare const packageManifestSchema: z.ZodObject<{
5
+ name: z.ZodString;
6
+ layer: z.ZodEnum<{
7
+ layer1: "layer1";
8
+ layer2: "layer2";
9
+ decision: "decision";
10
+ external: "external";
11
+ }>;
12
+ provides: z.ZodArray<z.ZodEnum<{
13
+ rules: "rules";
14
+ permissions: "permissions";
15
+ commands: "commands";
16
+ plugins: "plugins";
17
+ }>>;
18
+ requires: z.ZodDefault<z.ZodArray<z.ZodString>>;
19
+ conflicts: z.ZodDefault<z.ZodArray<z.ZodString>>;
20
+ stack: z.ZodOptional<z.ZodEnum<{
21
+ "python-backend": "python-backend";
22
+ node: "node";
23
+ react: "react";
24
+ }>>;
25
+ }, z.core.$strict>;
26
+ export type PackageManifest = z.infer<typeof packageManifestSchema>;
27
+ export interface ResolvedPackage extends PackageManifest {
28
+ /** Absolute path to the package directory (where its artifacts live). */
29
+ dir: string;
30
+ }
31
+ /** Parse + validate a `package.yaml`. `dirName` is the directory the file was
32
+ * found in; the declared `name` must match it. */
33
+ export declare function parsePackageManifest(yamlText: string, dirName: string): PackageManifest;
34
+ /** Every installed package, keyed by name. Scans `packages/<layer>/<pkg>/package.yaml`. */
35
+ export declare function availablePackages(): Map<string, ResolvedPackage>;
36
+ /**
37
+ * Resolve the selected package names against what's available, enforcing the
38
+ * composition rule (docs/wiki/packages-and-emit.md): every name must exist, every `requires` must also be
39
+ * selected, no two selected packages may `conflict`, and a package targeting a
40
+ * stack is only valid when the repo declares that stack. Pure — `available` and
41
+ * `declaredStacks` are injected so the logic is testable without the filesystem.
42
+ * Throws an agent-actionable error on the first problem.
43
+ */
44
+ export declare function resolve(selected: string[], available: Map<string, ResolvedPackage>, declaredStacks?: string[]): ResolvedPackage[];
45
+ /** Resolve the selected packages against the installed set (filesystem-backed). */
46
+ export declare function resolvePackages(selected: string[], declaredStacks?: string[]): ResolvedPackage[];
47
+ export {};
@@ -0,0 +1,121 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { parse as parseYaml } from "yaml";
5
+ import { z } from "zod";
6
+ import { STACKS } from "./manifest.js";
7
+ /**
8
+ * The package loader (docs/wiki/packages-and-emit.md, decisions D20/D21). Each package carries
9
+ * a `package.yaml` declaring what it is and **provides**; the loader discovers all
10
+ * installed packages, validates the set selected in `praxis.yaml`, and resolves
11
+ * `requires`/`conflicts`.
12
+ *
13
+ * This replaces filesystem *probing* (try to load each artifact, keep what isn't
14
+ * undefined) with a *declared* contract: a package says what it provides, and an
15
+ * unknown package name fails loudly rather than emitting nothing (D4's "typos
16
+ * fail loud", extended to packages).
17
+ */
18
+ const PACKAGES_DIR = new URL("../packages/", import.meta.url);
19
+ // What a package can contribute. Closed vocabulary; each maps to an emitter path.
20
+ const ARTIFACTS = ["rules", "permissions", "commands", "plugins"];
21
+ // "external" holds thin packages that declare a Claude Code plugin marketplace
22
+ // (packages/external/<pkg>/) rather than authoring methodology content directly.
23
+ const LAYERS = ["layer1", "layer2", "decision", "external"];
24
+ export const packageManifestSchema = z.strictObject({
25
+ name: z.string().min(1),
26
+ layer: z.enum(LAYERS),
27
+ provides: z.array(z.enum(ARTIFACTS)).min(1),
28
+ requires: z.array(z.string().min(1)).default([]),
29
+ conflicts: z.array(z.string().min(1)).default([]),
30
+ // Layer 2 packages declare the stack they target; a repo only gets them when it
31
+ // declares that stack in praxis.yaml `stacks`. Layer 1 / external packages omit it.
32
+ stack: z.enum(STACKS).optional(),
33
+ });
34
+ /** Parse + validate a `package.yaml`. `dirName` is the directory the file was
35
+ * found in; the declared `name` must match it. */
36
+ export function parsePackageManifest(yamlText, dirName) {
37
+ let raw;
38
+ try {
39
+ raw = parseYaml(yamlText);
40
+ }
41
+ catch (err) {
42
+ throw new Error(`package.yaml in "${dirName}" is not valid YAML: ${err.message}`);
43
+ }
44
+ const result = packageManifestSchema.safeParse(raw);
45
+ if (!result.success) {
46
+ const issues = result.error.issues
47
+ .map((i) => ` - ${i.path.map(String).join(".") || "(root)"}: ${i.message}`)
48
+ .join("\n");
49
+ throw new Error(`package.yaml in "${dirName}" is invalid:\n${issues}`);
50
+ }
51
+ if (result.data.name !== dirName) {
52
+ throw new Error(`package.yaml name "${result.data.name}" must match its directory "${dirName}".`);
53
+ }
54
+ return result.data;
55
+ }
56
+ /** Every installed package, keyed by name. Scans `packages/<layer>/<pkg>/package.yaml`. */
57
+ export function availablePackages() {
58
+ const root = fileURLToPath(PACKAGES_DIR);
59
+ const out = new Map();
60
+ for (const layer of readdirSync(root, { withFileTypes: true })) {
61
+ if (!layer.isDirectory())
62
+ continue;
63
+ const layerDir = join(root, layer.name);
64
+ for (const pkg of readdirSync(layerDir, { withFileTypes: true })) {
65
+ if (!pkg.isDirectory())
66
+ continue;
67
+ const dir = join(layerDir, pkg.name);
68
+ const manifestPath = join(dir, "package.yaml");
69
+ if (!existsSync(manifestPath))
70
+ continue; // a directory without a manifest is not a package
71
+ const manifest = parsePackageManifest(readFileSync(manifestPath, "utf8"), pkg.name);
72
+ out.set(manifest.name, { ...manifest, dir });
73
+ }
74
+ }
75
+ return out;
76
+ }
77
+ /**
78
+ * Resolve the selected package names against what's available, enforcing the
79
+ * composition rule (docs/wiki/packages-and-emit.md): every name must exist, every `requires` must also be
80
+ * selected, no two selected packages may `conflict`, and a package targeting a
81
+ * stack is only valid when the repo declares that stack. Pure — `available` and
82
+ * `declaredStacks` are injected so the logic is testable without the filesystem.
83
+ * Throws an agent-actionable error on the first problem.
84
+ */
85
+ export function resolve(selected, available, declaredStacks = []) {
86
+ const resolved = selected.map((name) => {
87
+ const pkg = available.get(name);
88
+ if (!pkg) {
89
+ throw new Error(`praxis.yaml lists package "${name}" but no such package is installed.\n` +
90
+ `Available packages: ${[...available.keys()].sort().join(", ") || "(none)"}.`);
91
+ }
92
+ return pkg;
93
+ });
94
+ const selectedSet = new Set(selected);
95
+ const stackSet = new Set(declaredStacks);
96
+ for (const pkg of resolved) {
97
+ if (pkg.stack && !stackSet.has(pkg.stack)) {
98
+ throw new Error(`Package "${pkg.name}" targets the "${pkg.stack}" stack, which praxis.yaml does not declare.\n` +
99
+ `Add "${pkg.stack}" to \`stacks\` in praxis.yaml, or remove the package — a Layer 2 ` +
100
+ `recipe applies only when its stack is wanted (D15).`);
101
+ }
102
+ for (const req of pkg.requires) {
103
+ if (!selectedSet.has(req)) {
104
+ throw new Error(`Package "${pkg.name}" requires "${req}", which is not in praxis.yaml.\n` +
105
+ `Add "${req}" to packages — a rule without its required support is broken (D21).`);
106
+ }
107
+ }
108
+ for (const con of pkg.conflicts) {
109
+ if (selectedSet.has(con)) {
110
+ throw new Error(`Packages "${pkg.name}" and "${con}" conflict and cannot be installed together.\n` +
111
+ `Remove one from praxis.yaml.`);
112
+ }
113
+ }
114
+ }
115
+ return resolved;
116
+ }
117
+ /** Resolve the selected packages against the installed set (filesystem-backed). */
118
+ export function resolvePackages(selected, declaredStacks = []) {
119
+ return resolve(selected, availablePackages(), declaredStacks);
120
+ }
121
+ //# sourceMappingURL=packages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"packages.js","sourceRoot":"","sources":["../src/packages.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import { z } from "zod";
2
+ declare const CAPABILITIES: readonly ["read-repo", "edit-repo", "run-dev-scripts", "read-only-git", "git-commit", "git-push", "install-deps", "destructive-delete", "force-push", "read-secrets", "global-install"];
3
+ export type Capability = (typeof CAPABILITIES)[number];
4
+ export declare const policySchema: z.ZodObject<{
5
+ allow: z.ZodDefault<z.ZodArray<z.ZodEnum<{
6
+ "read-repo": "read-repo";
7
+ "edit-repo": "edit-repo";
8
+ "run-dev-scripts": "run-dev-scripts";
9
+ "read-only-git": "read-only-git";
10
+ "git-commit": "git-commit";
11
+ "git-push": "git-push";
12
+ "install-deps": "install-deps";
13
+ "destructive-delete": "destructive-delete";
14
+ "force-push": "force-push";
15
+ "read-secrets": "read-secrets";
16
+ "global-install": "global-install";
17
+ }>>>;
18
+ ask: z.ZodDefault<z.ZodArray<z.ZodEnum<{
19
+ "read-repo": "read-repo";
20
+ "edit-repo": "edit-repo";
21
+ "run-dev-scripts": "run-dev-scripts";
22
+ "read-only-git": "read-only-git";
23
+ "git-commit": "git-commit";
24
+ "git-push": "git-push";
25
+ "install-deps": "install-deps";
26
+ "destructive-delete": "destructive-delete";
27
+ "force-push": "force-push";
28
+ "read-secrets": "read-secrets";
29
+ "global-install": "global-install";
30
+ }>>>;
31
+ deny: z.ZodDefault<z.ZodArray<z.ZodEnum<{
32
+ "read-repo": "read-repo";
33
+ "edit-repo": "edit-repo";
34
+ "run-dev-scripts": "run-dev-scripts";
35
+ "read-only-git": "read-only-git";
36
+ "git-commit": "git-commit";
37
+ "git-push": "git-push";
38
+ "install-deps": "install-deps";
39
+ "destructive-delete": "destructive-delete";
40
+ "force-push": "force-push";
41
+ "read-secrets": "read-secrets";
42
+ "global-install": "global-install";
43
+ }>>>;
44
+ }, z.core.$strict>;
45
+ export type Policy = z.infer<typeof policySchema>;
46
+ /** Parse + validate neutral policy text. Throws a readable, agent-actionable
47
+ * error naming each problem — mirrors parseManifest in src/manifest.ts. */
48
+ export declare function parsePolicy(yamlText: string): Policy;
49
+ /** The neutral policy for a content package, or undefined if it provides none.
50
+ * Scans packages/<layer>/<pkg>/permissions.yaml — mirrors loadPackageSource. */
51
+ export declare function loadPolicy(pkg: string): Policy | undefined;
52
+ /** All capabilities referenced by a policy, across every bucket. */
53
+ export declare function policyCapabilities(policy: Policy): Capability[];
54
+ export {};