@jpkme/diff-review 0.1.5 → 0.1.6

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 (67) hide show
  1. package/README.md +1 -1
  2. package/bin/diff-review.mjs +9 -1
  3. package/lib/check-report.mjs +213 -0
  4. package/lib/check-report.test.mjs +46 -0
  5. package/lib/prompt.mjs +16 -3
  6. package/package.json +1 -1
  7. package/web/build/client/_app/immutable/chunks/{CO0mUwMJ.js → Ber_0nz7.js} +1 -1
  8. package/web/build/client/_app/immutable/chunks/Ber_0nz7.js.br +2 -0
  9. package/web/build/client/_app/immutable/chunks/Ber_0nz7.js.gz +0 -0
  10. package/web/build/client/_app/immutable/chunks/{CdHZDAAo.js → DRga3k74.js} +22 -14
  11. package/web/build/client/_app/immutable/chunks/DRga3k74.js.br +0 -0
  12. package/web/build/client/_app/immutable/chunks/DRga3k74.js.gz +0 -0
  13. package/web/build/client/_app/immutable/chunks/{DzqTpz7_.js → zvcaSoGP.js} +2 -2
  14. package/web/build/client/_app/immutable/chunks/zvcaSoGP.js.br +0 -0
  15. package/web/build/client/_app/immutable/chunks/zvcaSoGP.js.gz +0 -0
  16. package/web/build/client/_app/immutable/entry/{app.BRzz8J51.js → app.BP_R_kTF.js} +2 -2
  17. package/web/build/client/_app/immutable/entry/app.BP_R_kTF.js.br +0 -0
  18. package/web/build/client/_app/immutable/entry/app.BP_R_kTF.js.gz +0 -0
  19. package/web/build/client/_app/immutable/entry/start.lXpYdwbt.js +1 -0
  20. package/web/build/client/_app/immutable/entry/start.lXpYdwbt.js.br +1 -0
  21. package/web/build/client/_app/immutable/entry/start.lXpYdwbt.js.gz +0 -0
  22. package/web/build/client/_app/immutable/nodes/{1.ws8Khan5.js → 1.CGJk77oo.js} +1 -1
  23. package/web/build/client/_app/immutable/nodes/1.CGJk77oo.js.br +3 -0
  24. package/web/build/client/_app/immutable/nodes/1.CGJk77oo.js.gz +0 -0
  25. package/web/build/client/_app/immutable/nodes/{2.CsD5xqRB.js → 2.Dl2xIvoV.js} +1 -1
  26. package/web/build/client/_app/immutable/nodes/2.Dl2xIvoV.js.br +0 -0
  27. package/web/build/client/_app/immutable/nodes/2.Dl2xIvoV.js.gz +0 -0
  28. package/web/build/client/_app/immutable/nodes/{3.ByTmpJQZ.js → 3.Bn5l-P1e.js} +1 -1
  29. package/web/build/client/_app/immutable/nodes/3.Bn5l-P1e.js.br +0 -0
  30. package/web/build/client/_app/immutable/nodes/3.Bn5l-P1e.js.gz +0 -0
  31. package/web/build/client/_app/version.json +1 -1
  32. package/web/build/client/_app/version.json.br +0 -0
  33. package/web/build/client/_app/version.json.gz +0 -0
  34. package/web/build/handler.js +3 -3
  35. package/web/build/index.js +3 -3
  36. package/web/build/server/chunks/1-CRg9pswO.js +7 -0
  37. package/web/build/server/chunks/{1-qrn24yN5.js.map → 1-CRg9pswO.js.map} +1 -1
  38. package/web/build/server/chunks/{2-C7_Loqyn.js → 2-BmYzresl.js} +2 -2
  39. package/web/build/server/chunks/{2-C7_Loqyn.js.map → 2-BmYzresl.js.map} +1 -1
  40. package/web/build/server/chunks/{3-DZ5PBhOc.js → 3-DaeIJXI7.js} +2 -2
  41. package/web/build/server/chunks/{3-DZ5PBhOc.js.map → 3-DaeIJXI7.js.map} +1 -1
  42. package/web/build/server/chunks/{handler-DD4vP3bU.js → handler-2eew4UOl.js} +3 -3
  43. package/web/build/server/chunks/{handler-DD4vP3bU.js.map → handler-2eew4UOl.js.map} +1 -1
  44. package/web/build/server/chunks/{index.js-IhFPHwmp.js → index.js-DbiNsix3.js} +2 -2
  45. package/web/build/server/chunks/{index.js-IhFPHwmp.js.map → index.js-DbiNsix3.js.map} +1 -1
  46. package/web/build/server/chunks/{manifest.js-Dk3RuAA5.js → manifest.js-BYNXhn6E.js} +6 -6
  47. package/web/build/server/chunks/{manifest.js-Dk3RuAA5.js.map → manifest.js-BYNXhn6E.js.map} +1 -1
  48. package/web/build/server/chunks/{remote-1g4ayag-BRL-uaKE.js → remote-1g4ayag-DESnyPRn.js} +2 -2
  49. package/web/build/server/chunks/{remote-1g4ayag-BRL-uaKE.js.map → remote-1g4ayag-DESnyPRn.js.map} +1 -1
  50. package/web/build/client/_app/immutable/chunks/CO0mUwMJ.js.br +0 -2
  51. package/web/build/client/_app/immutable/chunks/CO0mUwMJ.js.gz +0 -0
  52. package/web/build/client/_app/immutable/chunks/CdHZDAAo.js.br +0 -0
  53. package/web/build/client/_app/immutable/chunks/CdHZDAAo.js.gz +0 -0
  54. package/web/build/client/_app/immutable/chunks/DzqTpz7_.js.br +0 -0
  55. package/web/build/client/_app/immutable/chunks/DzqTpz7_.js.gz +0 -0
  56. package/web/build/client/_app/immutable/entry/app.BRzz8J51.js.br +0 -0
  57. package/web/build/client/_app/immutable/entry/app.BRzz8J51.js.gz +0 -0
  58. package/web/build/client/_app/immutable/entry/start.BSioRkqK.js +0 -1
  59. package/web/build/client/_app/immutable/entry/start.BSioRkqK.js.br +0 -0
  60. package/web/build/client/_app/immutable/entry/start.BSioRkqK.js.gz +0 -0
  61. package/web/build/client/_app/immutable/nodes/1.ws8Khan5.js.br +0 -1
  62. package/web/build/client/_app/immutable/nodes/1.ws8Khan5.js.gz +0 -0
  63. package/web/build/client/_app/immutable/nodes/2.CsD5xqRB.js.br +0 -0
  64. package/web/build/client/_app/immutable/nodes/2.CsD5xqRB.js.gz +0 -0
  65. package/web/build/client/_app/immutable/nodes/3.ByTmpJQZ.js.br +0 -0
  66. package/web/build/client/_app/immutable/nodes/3.ByTmpJQZ.js.gz +0 -0
  67. package/web/build/server/chunks/1-qrn24yN5.js +0 -7
package/README.md CHANGED
@@ -55,7 +55,7 @@ npm start # mismo que el bin global
55
55
 
56
56
  1. Pegá la ruta (o URL) del repo y elegí branch / base — o lanzá `diff-review` / `diff-review --ui` desde el repo.
57
57
  2. Copiá el prompt (o el comando de skill `/diff-review`).
58
- 3. Corrélo en tu agente; que escriba `diff-rev_{branchTip}_{baseTip}.json` (también acepta nombres legacy).
58
+ 3. Corrélo en tu agente; que escriba `diff-rev_{branchTip}_{baseTip}.json` y lo valide con `npx --yes @jpkme/diff-review validate <archivo>` (también acepta nombres legacy).
59
59
  4. Arrastrá ese JSON a la dropzone (o dejá que el CLI lo importe solo). La UI hidrata los diffs con git.
60
60
 
61
61
  "Ver ejemplo" carga un reporte de muestra sin tocar un repo.
@@ -21,6 +21,7 @@ import { branchTips, getRemoteUrl, listBranches, tryResolveGitRoot } from '../li
21
21
  import { makeOutputFilename } from '../lib/output-name.mjs';
22
22
  import { buildPrompt } from '../lib/prompt.mjs';
23
23
  import { waitForReviewOutput } from '../lib/wait-output.mjs';
24
+ import { runValidateCli } from '../lib/check-report.mjs';
24
25
 
25
26
  const here = dirname(fileURLToPath(import.meta.url));
26
27
  const packageRoot = join(here, '..');
@@ -50,9 +51,11 @@ function printHelp() {
50
51
 
51
52
  Usage:
52
53
  diff-review [options]
54
+ diff-review validate <file.json>
53
55
 
54
56
  Por defecto (desde un repo git): elegís branch/base en la terminal (Clack),
55
57
  copia el prompt, espera el JSON y abre el viewer.
58
+ \`validate\` revisa el JSON y, si falla, dice cómo seguir (exit 0 = OK).
56
59
  Si pasás --branch y --base juntos, saltea los selects.
57
60
  Con --ui: abre la UI de inmediato (web-first).
58
61
 
@@ -403,7 +406,12 @@ async function runUiMode(opts, launch, launchCwd) {
403
406
  }
404
407
 
405
408
  async function main() {
406
- const opts = parseArgs(process.argv.slice(2));
409
+ const rawArgv = process.argv.slice(2);
410
+ if (rawArgv[0] === 'validate') {
411
+ process.exit(runValidateCli(rawArgv.slice(1)));
412
+ }
413
+
414
+ const opts = parseArgs(rawArgv);
407
415
  if (opts.help) {
408
416
  printHelp();
409
417
  process.exit(0);
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Valida un JSON de review y arma un texto accionable para el agente.
3
+ */
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { isAbsolute, resolve } from "node:path";
6
+ import { globMatch } from "./config.mjs";
7
+ import { extractBranchDiff } from "./extract.mjs";
8
+ import { tryResolveGitRoot } from "./git.mjs";
9
+ import { decodePayload } from "./json-payload.mjs";
10
+ import { REVIEW_SCHEMA, reviewIssues, validateAgainst } from "./validate.mjs";
11
+
12
+ const MISSING_LIMIT = 40;
13
+
14
+ /**
15
+ * @param {unknown} payload
16
+ * @param {string[]} [paths] rutas del diff (sin ignorados)
17
+ * @returns {{ ok: boolean, errors: string[], notes: string[] }}
18
+ */
19
+ export function checkPayload(payload, paths = []) {
20
+ const errors = [...validateAgainst(REVIEW_SCHEMA, payload), ...reviewIssues(payload)];
21
+ const notes = [];
22
+ const known = (paths || []).map((p) => String(p || "").replaceAll("\\", "/")).filter(Boolean);
23
+
24
+ if (known.length) {
25
+ const blocks = Array.isArray(payload?.blocks) ? payload.blocks : [];
26
+ const findings = Array.isArray(payload?.findings) ? payload.findings : [];
27
+ const skipped = Array.isArray(payload?.skipped) ? payload.skipped : [];
28
+ const skippedPatterns = skipped.map((s) => String(s?.file || "")).filter(Boolean);
29
+
30
+ const covers = (file) => {
31
+ const norm = String(file || "").replaceAll("\\", "/");
32
+ if (blocks.some((b) => String(b?.file || "").replaceAll("\\", "/") === norm)) return true;
33
+ return skippedPatterns.some((pattern) => {
34
+ const p = pattern.replaceAll("\\", "/");
35
+ return p === norm || globMatch(norm, p);
36
+ });
37
+ };
38
+
39
+ for (const block of blocks) {
40
+ const file = String(block?.file || "").replaceAll("\\", "/");
41
+ if (file && !known.includes(file)) {
42
+ errors.push(`el bloque ${block.id || file} apunta a un archivo que no está en el diff: ${file}`);
43
+ }
44
+ }
45
+ for (const finding of findings) {
46
+ const file = String(finding?.file || "").replaceAll("\\", "/");
47
+ if (file && !known.includes(file)) {
48
+ errors.push(
49
+ `el hallazgo ${finding.id || finding.what || file} apunta a un archivo fuera del diff: ${file}`
50
+ );
51
+ }
52
+ }
53
+
54
+ const missing = known.filter((file) => !covers(file));
55
+ if (missing.length) {
56
+ const shown = missing.slice(0, MISSING_LIMIT);
57
+ const rest = missing.length - shown.length;
58
+ errors.push(
59
+ `quedaron ${missing.length} archivo(s) del diff sin bloque ni skipped: ${shown.join(", ")}${
60
+ rest > 0 ? ` … y ${rest} más` : ""
61
+ }`
62
+ );
63
+ }
64
+ } else {
65
+ notes.push("No contrasté cobertura contra git (sin lista de paths del diff).");
66
+ }
67
+
68
+ return { ok: errors.length === 0, errors, notes };
69
+ }
70
+
71
+ /**
72
+ * @param {string} filePath
73
+ * @param {{ repo?: string, branch?: string, base?: string, cwd?: string }} [opts]
74
+ */
75
+ export function checkReportFile(filePath, opts = {}) {
76
+ const cwd = opts.cwd || process.cwd();
77
+ const abs = isAbsolute(filePath) ? filePath : resolve(cwd, filePath);
78
+ if (!existsSync(abs)) {
79
+ return {
80
+ ok: false,
81
+ file: abs,
82
+ errors: [`No existe el archivo: ${abs}`],
83
+ notes: [],
84
+ usage: true,
85
+ };
86
+ }
87
+
88
+ let payload;
89
+ try {
90
+ payload = decodePayload(readFileSync(abs, "utf8"));
91
+ } catch (err) {
92
+ return {
93
+ ok: false,
94
+ file: abs,
95
+ errors: [err instanceof Error ? err.message : String(err)],
96
+ notes: [],
97
+ };
98
+ }
99
+
100
+ const meta = payload && typeof payload === "object" ? payload.meta || {} : {};
101
+ const branch = String(opts.branch || meta.branch || "").trim();
102
+ const base = String(opts.base || meta.base || "develop").trim();
103
+ const repoHint = String(opts.repo || meta.repo || "").trim();
104
+ const repo =
105
+ (repoHint && !/^(https?:\/\/|git@|ssh:\/\/)/i.test(repoHint) && tryResolveGitRoot(repoHint)) ||
106
+ tryResolveGitRoot(cwd);
107
+
108
+ /** @type {string[]} */
109
+ let paths = [];
110
+ /** @type {string[]} */
111
+ const errors = [];
112
+
113
+ if (!branch) {
114
+ errors.push("Falta meta.branch (o --branch) para contrastar el diff.");
115
+ } else if (!repo) {
116
+ errors.push(
117
+ "No encontré un repo git local (meta.repo o el directorio actual). Sin eso no puedo verificar cobertura."
118
+ );
119
+ } else {
120
+ try {
121
+ const extracted = extractBranchDiff({ repo, branch, base });
122
+ paths = (extracted.files || []).map((f) => f.path);
123
+ } catch (err) {
124
+ errors.push(
125
+ `No pude listar el diff con git (${err instanceof Error ? err.message : String(err)}). Revisá meta.repo, meta.branch y meta.base.`
126
+ );
127
+ }
128
+ }
129
+
130
+ const result = checkPayload(payload, paths);
131
+ const allErrors = [...result.errors, ...errors];
132
+ return { ok: allErrors.length === 0, errors: allErrors, notes: result.notes, file: abs };
133
+ }
134
+
135
+ /**
136
+ * @param {string} file
137
+ * @param {string} command
138
+ * @param {{ ok: boolean, errors: string[], notes: string[] }} result
139
+ */
140
+ export function formatCheckReport(file, command, result) {
141
+ if (result.ok) {
142
+ const extra = result.notes?.length ? `\n${result.notes.map((n) => `Note: ${n}`).join("\n")}` : "";
143
+ return `OK: ${file}${extra}\nSchema and references passed. You can stop.`;
144
+ }
145
+
146
+ const problems = (result.errors || []).map((e) => `- ${e}`).join("\n");
147
+ const notes = (result.notes || []).map((n) => `Note: ${n}`).join("\n");
148
+ const lines = [
149
+ `INVALID: ${file}`,
150
+ "",
151
+ "The JSON is not ready. Edit this same file until the command prints OK. Do not create a new filename.",
152
+ "",
153
+ "Problems:",
154
+ problems || "- (unknown)",
155
+ ];
156
+ if (notes) lines.push("", notes);
157
+ lines.push(
158
+ "",
159
+ "How to continue:",
160
+ "- Fix every problem above in place. Keep existing meta/intent/groups/blocks/findings; only change what is wrong or missing.",
161
+ '- Every diff path must appear in "blocks" or "skipped" (a skipped glob counts).',
162
+ "- what/why/fix stay ≤120 chars; intent ≥40 chars; enums exactly as in the schema.",
163
+ "- Human-readable strings in Spanish.",
164
+ `- Re-run exactly: \`${command}\``,
165
+ "- Stop only when that command prints OK."
166
+ );
167
+ return lines.join("\n");
168
+ }
169
+
170
+ /**
171
+ * @param {string[]} argv args después de `validate`
172
+ * @param {{ stdout?: (s: string) => void, stderr?: (s: string) => void, cwd?: string }} [io]
173
+ * @returns {number} exit code
174
+ */
175
+ export function runValidateCli(argv, io = {}) {
176
+ const stdout = io.stdout || ((s) => console.log(s));
177
+ const stderr = io.stderr || ((s) => console.error(s));
178
+ const fileFlag = [];
179
+ /** @type {{ repo?: string, branch?: string, base?: string }} */
180
+ const opts = { cwd: io.cwd };
181
+
182
+ for (let i = 0; i < argv.length; i++) {
183
+ const a = argv[i];
184
+ if (a === "-h" || a === "--help") {
185
+ stdout(`Usage: npx --yes @jpkme/diff-review validate <file.json> [--repo <path>] [--branch <b>] [--base <b>]
186
+
187
+ Exit 0 and "OK" when the JSON matches the review contract (and diff coverage, when git can list paths).
188
+ Exit 1 with problems and how to continue otherwise.`);
189
+ return 0;
190
+ }
191
+ if (a === "--repo") opts.repo = argv[++i];
192
+ else if (a === "--branch") opts.branch = argv[++i];
193
+ else if (a === "--base") opts.base = argv[++i];
194
+ else if (a.startsWith("-")) {
195
+ stderr(`Opción desconocida: ${a}`);
196
+ return 2;
197
+ } else fileFlag.push(a);
198
+ }
199
+
200
+ const file = fileFlag[0];
201
+ if (!file) {
202
+ stderr("Falta el archivo JSON. Uso: diff-review validate <file.json>");
203
+ return 2;
204
+ }
205
+
206
+ const command = `npx --yes @jpkme/diff-review validate ${file}`;
207
+ const result = checkReportFile(file, opts);
208
+ const text = formatCheckReport(result.file || file, command, result);
209
+ if (result.ok) stdout(text);
210
+ else stderr(text);
211
+ if (result.usage) return 2;
212
+ return result.ok ? 0 : 1;
213
+ }
@@ -0,0 +1,46 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { checkPayload, formatCheckReport } from "./check-report.mjs";
4
+
5
+ const valid = () => ({
6
+ intent: "Agrega el ABM de depósitos locales end-to-end para dejar de administrarlos por SQL.",
7
+ groups: [{ id: "g1", kind: "feat", title: "CRUD de depósitos", intent: "base de todo" }],
8
+ blocks: [
9
+ {
10
+ id: "b1",
11
+ group: "g1",
12
+ file: "src/a.ts",
13
+ lines: "L1-20",
14
+ op: "add",
15
+ what: "controller nuevo",
16
+ why: "",
17
+ source: "code",
18
+ },
19
+ ],
20
+ findings: [],
21
+ skipped: [{ file: "**/*.lock", reason: "lockfile" }],
22
+ });
23
+
24
+ test("checkPayload acepta cobertura exacta y por glob", () => {
25
+ const result = checkPayload(valid(), ["src/a.ts", "bun.lock"]);
26
+ assert.equal(result.ok, true);
27
+ assert.deepEqual(result.errors, []);
28
+ });
29
+
30
+ test("checkPayload lista archivos del diff sin cubrir", () => {
31
+ const result = checkPayload(valid(), ["src/a.ts", "src/olvidado.ts"]);
32
+ assert.equal(result.ok, false);
33
+ assert.ok(result.errors.some((e) => e.includes("src/olvidado.ts")));
34
+ });
35
+
36
+ test("formatCheckReport explica cómo seguir", () => {
37
+ const text = formatCheckReport("diff-rev_a_b.json", "npx --yes @jpkme/diff-review validate diff-rev_a_b.json", {
38
+ ok: false,
39
+ errors: ["$.intent: texto demasiado corto"],
40
+ notes: [],
41
+ });
42
+ assert.match(text, /INVALID/);
43
+ assert.match(text, /How to continue/);
44
+ assert.match(text, /npx --yes @jpkme\/diff-review validate/);
45
+ assert.match(text, /same file/);
46
+ });
package/lib/prompt.mjs CHANGED
@@ -83,7 +83,14 @@ function workflowLocal(base, branch) {
83
83
  1. Resolve the merge-base of \`${base}\` and \`${branch}\`.
84
84
  2. Run exactly: \`git diff ${base}...${branch}\` (three-dot / merge-base diff). Do not use two-dot unless three-dot is impossible.
85
85
  3. Read enough surrounding code (types, callers, tests) to judge behavior — not only the hunk lines.
86
- 4. Emit the JSON file. Do not modify the repo.`;
86
+ 4. Emit the JSON file. Do not modify the repo.
87
+ 5. Validate it with the command in the output section. If it prints INVALID, follow "How to continue" and re-run until it prints OK.`;
88
+ }
89
+
90
+ /** Comando que el agente debe correr; npx por si el bin no está en PATH. */
91
+ export function validateCliCommand(filename) {
92
+ const file = String(filename || "diff-rev_<branchTip>_<baseTip>.json").trim();
93
+ return `npx --yes @jpkme/diff-review validate ${file}`;
87
94
  }
88
95
 
89
96
  function workflowUrl(base, branch) {
@@ -92,7 +99,8 @@ function workflowUrl(base, branch) {
92
99
  2. Resolve the merge-base of \`${base}\` and \`${branch}\`.
93
100
  3. Run exactly: \`git diff ${base}...${branch}\` (three-dot / merge-base diff). Do not use two-dot unless three-dot is impossible.
94
101
  4. Read enough surrounding code (types, callers, tests) to judge behavior — not only the hunk lines.
95
- 5. Emit the JSON file.`;
102
+ 5. Emit the JSON file.
103
+ 6. Validate it with the command in the output section. If it prints INVALID, follow "How to continue" and re-run until it prints OK.`;
96
104
  }
97
105
 
98
106
  /**
@@ -156,5 +164,10 @@ Include "meta" with the source/repo/branch/base above (and origin "remoteUrl" wh
156
164
 
157
165
  Do NOT include diffs or a "files" array — the tool computes them with git when opening the report. Do not invent hunks or paste patches into the JSON.
158
166
 
159
- When done, tell me that \`${outputFilename}\` was written.`;
167
+ After writing the file, validate it (npx so it works even if \`diff-review\` is not on PATH):
168
+
169
+ \`${validateCliCommand(outputFilename)}\`
170
+
171
+ - Exit 0 and a line starting with \`OK\` means the JSON is acceptable. Then tell me that \`${outputFilename}\` was written.
172
+ - Exit 1 prints \`INVALID\`, the problems, and how to continue. Fix the same file and re-run that exact command until it prints OK. Do not stop before OK.`;
160
173
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jpkme/diff-review",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Local UI to prompt a coding agent for a branch review and browse the result.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +1 @@
1
- import"./Dur-vdUf.js";import{c as e}from"./DzqTpz7_.js";var t={get data(){return e.data},get error(){return e.error},get form(){return e.form},get params(){return e.params},get route(){return e.route},get state(){return e.state},get status(){return e.status},get url(){return e.url}};export{t};
1
+ import"./Dur-vdUf.js";import{c as e}from"./zvcaSoGP.js";var t={get data(){return e.data},get error(){return e.error},get form(){return e.form},get params(){return e.params},get route(){return e.route},get state(){return e.state},get status(){return e.status},get url(){return e.url}};export{t};
@@ -0,0 +1,2 @@
1
+ %@��\��'��ǚU��)d�"*�ߎ�0�,�4 =�=�Ȧ�+3���_��澨#�
2
+ �Y ��M!^!�ƙ�9�8W"����:�|G����=�������<��`,�Z�Fxc��j��˾���K7�����an?