framein 0.0.4 → 0.0.5

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.
package/dist/disagree.js CHANGED
@@ -1,85 +1,106 @@
1
- // Disagreement Protocol (F-LOOP-5, ADR-0008): bound model-vs-model debate so it converges instead
2
- // of looping. A proposal is met with at most `maxRounds` blocking challenges; the reviewer returns
3
- // CLAIMS + a required change (never edits — the lead keeps control); no agreement after the cap
4
- // escalates to the human with exactly two options. Pure state machine; the model-authored content
5
- // of each turn is the deferred live path.
6
- import { PLAIN } from './ui/theme.js';
7
- export const MAX_ROUNDS = 2;
8
- export function newDebate(topic, proposal, maxRounds = MAX_ROUNDS) {
9
- return { topic, entries: [{ kind: 'proposal', proposal }], maxRounds };
10
- }
11
- export function challengeCount(d) {
12
- return d.entries.filter((e) => e.kind === 'challenge').length;
13
- }
14
- function leadPosition(d) {
15
- for (let i = d.entries.length - 1; i >= 0; i--) {
16
- const e = d.entries[i];
17
- if (e.kind === 'revision')
18
- return e.revision.text || d.topic;
19
- if (e.kind === 'proposal')
20
- return e.proposal.text;
21
- }
22
- return d.topic;
23
- }
24
- function reviewerRequirement(d) {
25
- for (let i = d.entries.length - 1; i >= 0; i--) {
26
- const e = d.entries[i];
27
- if (e.kind === 'challenge' && e.challenge.verdict === 'challenge')
28
- return e.challenge.requiredChange ?? e.challenge.claim;
29
- }
30
- return undefined;
31
- }
32
- export function debateStatus(d) {
33
- const last = d.entries[d.entries.length - 1];
34
- const rounds = challengeCount(d);
35
- const escalate = () => ({
36
- state: 'escalate',
37
- reason: `no agreement after ${rounds} round${rounds === 1 ? '' : 's'} (max ${d.maxRounds})`,
38
- options: [`A: ${leadPosition(d)}`, `B: ${reviewerRequirement(d) ?? 'reviewer change'}`],
39
- });
40
- if (!last || last.kind === 'proposal')
41
- return { state: 'awaiting-challenge' };
42
- if (last.kind === 'challenge') {
43
- if (last.challenge.verdict === 'accept')
44
- return { state: 'resolved', how: 'accepted-by-reviewer' };
45
- if (rounds >= d.maxRounds)
46
- return escalate();
47
- return { state: 'awaiting-revision', required: last.challenge.requiredChange };
48
- }
49
- // last is a revision
50
- if (last.revision.accepted)
51
- return { state: 'resolved', how: 'lead-accepted' };
52
- if (rounds >= d.maxRounds)
53
- return escalate();
54
- return { state: 'awaiting-challenge' };
55
- }
56
- export function renderDebate(d, ui = PLAIN) {
57
- const lines = [`Debate: ${d.topic}`, ''];
58
- for (const e of d.entries) {
59
- if (e.kind === 'proposal')
60
- lines.push(`proposal${e.proposal.by ? ` (${e.proposal.by})` : ''}: ${e.proposal.text}`);
61
- else if (e.kind === 'challenge') {
62
- const c = e.challenge;
63
- lines.push(c.verdict === 'accept'
64
- ? `challenge${c.by ? ` (${c.by})` : ''}: accept`
65
- : `challenge${c.by ? ` (${c.by})` : ''}: ${c.claim ?? ''}${c.requiredChange ? ` → require: ${c.requiredChange}` : ''}`);
66
- }
67
- else {
68
- lines.push(`revision: ${e.revision.accepted ? 'accepted' : 'rejected'}${e.revision.text ? ` — ${e.revision.text}` : ''}`);
69
- }
70
- }
71
- lines.push('');
72
- const st = debateStatus(d);
73
- if (st.state === 'resolved')
74
- lines.push(ui.tone(`Resolved (${st.how}).`, 'success'));
75
- else if (st.state === 'escalate') {
76
- lines.push(ui.tone(`Escalate to human — ${st.reason}:`, 'warning'));
77
- for (const o of st.options)
78
- lines.push(` ${o}`);
79
- }
80
- else if (st.state === 'awaiting-revision')
81
- lines.push(ui.tone(`Awaiting lead revision${st.required ? ` (required: ${st.required})` : ''}.`, 'info'));
82
- else
83
- lines.push(ui.tone('Awaiting reviewer challenge.', 'info'));
84
- return lines.join('\n');
85
- }
1
+ // Disagreement Protocol (F-LOOP-5, ADR-0008): bound model-vs-model debate so it converges instead
2
+ // of looping. A proposal is met with at most `maxRounds` blocking challenges; the reviewer returns
3
+ // CLAIMS + a required change (never edits — the lead keeps control); no agreement after the cap
4
+ // escalates to the human with exactly two options. Pure state machine; the model-authored content
5
+ // of each turn is the deferred live path.
6
+ import { PLAIN } from './ui/theme.js';
7
+ export const MAX_ROUNDS = 2;
8
+ export function newDebate(topic, proposal, maxRounds = MAX_ROUNDS) {
9
+ return { topic, entries: [{ kind: 'proposal', proposal }], maxRounds };
10
+ }
11
+ export function challengeCount(d) {
12
+ return d.entries.filter((e) => e.kind === 'challenge').length;
13
+ }
14
+ function leadPosition(d) {
15
+ for (let i = d.entries.length - 1; i >= 0; i--) {
16
+ const e = d.entries[i];
17
+ if (e.kind === 'revision')
18
+ return e.revision.text || d.topic;
19
+ if (e.kind === 'response')
20
+ return e.response.proposedRevision ?? e.response.text ?? d.topic;
21
+ if (e.kind === 'proposal')
22
+ return e.proposal.text;
23
+ }
24
+ return d.topic;
25
+ }
26
+ function reviewerRequirement(d) {
27
+ for (let i = d.entries.length - 1; i >= 0; i--) {
28
+ const e = d.entries[i];
29
+ if (e.kind === 'challenge' && e.challenge.verdict === 'challenge')
30
+ return e.challenge.requiredChange ?? e.challenge.claim;
31
+ }
32
+ return undefined;
33
+ }
34
+ export function debateStatus(d) {
35
+ const last = d.entries[d.entries.length - 1];
36
+ const rounds = challengeCount(d);
37
+ const escalate = () => ({
38
+ state: 'escalate',
39
+ reason: `no agreement after ${rounds} round${rounds === 1 ? '' : 's'} (max ${d.maxRounds})`,
40
+ options: [`A: ${leadPosition(d)}`, `B: ${reviewerRequirement(d) ?? 'reviewer change'}`],
41
+ });
42
+ if (!last || last.kind === 'proposal')
43
+ return { state: 'awaiting-challenge' };
44
+ if (last.kind === 'challenge') {
45
+ if (last.challenge.verdict === 'accept')
46
+ return { state: 'resolved', how: 'accepted-by-reviewer' };
47
+ if (rounds >= d.maxRounds)
48
+ return escalate();
49
+ return { state: 'awaiting-revision', required: last.challenge.requiredChange };
50
+ }
51
+ if (last.kind === 'response') {
52
+ if (rounds >= d.maxRounds)
53
+ return escalate();
54
+ return { state: 'awaiting-decision', required: reviewerRequirement(d) };
55
+ }
56
+ // last is a revision
57
+ if (last.revision.accepted)
58
+ return { state: 'resolved', how: 'lead-accepted' };
59
+ if (rounds >= d.maxRounds)
60
+ return escalate();
61
+ return { state: 'awaiting-challenge' };
62
+ }
63
+ export function renderDebate(d, ui = PLAIN) {
64
+ const lines = [`Debate: ${d.topic}`, ''];
65
+ for (const e of d.entries) {
66
+ if (e.kind === 'proposal')
67
+ lines.push(`proposal${e.proposal.by ? ` (${e.proposal.by})` : ''}: ${e.proposal.text}`);
68
+ else if (e.kind === 'challenge') {
69
+ const c = e.challenge;
70
+ lines.push(c.verdict === 'accept'
71
+ ? `challenge${c.by ? ` (${c.by})` : ''}: accept`
72
+ : `challenge${c.by ? ` (${c.by})` : ''}: ${c.claim ?? ''}${c.requiredChange ? ` → require: ${c.requiredChange}` : ''}`);
73
+ if (c.basis?.length)
74
+ lines.push(` basis: ${c.basis.join(', ')}`);
75
+ if (c.missingEvidence?.length)
76
+ lines.push(` missing_evidence: ${c.missingEvidence.join('; ')}`);
77
+ }
78
+ else if (e.kind === 'response') {
79
+ const r = e.response;
80
+ lines.push(`response${r.by ? ` (${r.by})` : ''}: ${r.text}`);
81
+ if (r.proposedRevision)
82
+ lines.push(` proposed_revision: ${r.proposedRevision}`);
83
+ if (r.acceptsRequiredChange !== undefined)
84
+ lines.push(` accepts_required_change: ${r.acceptsRequiredChange ? 'yes' : 'no'}`);
85
+ }
86
+ else {
87
+ lines.push(`revision: ${e.revision.accepted ? 'accepted' : 'rejected'}${e.revision.text ? ` — ${e.revision.text}` : ''}`);
88
+ }
89
+ }
90
+ lines.push('');
91
+ const st = debateStatus(d);
92
+ if (st.state === 'resolved')
93
+ lines.push(ui.tone(`Resolved (${st.how}).`, 'success'));
94
+ else if (st.state === 'escalate') {
95
+ lines.push(ui.tone(`Escalate to human — ${st.reason}:`, 'warning'));
96
+ for (const o of st.options)
97
+ lines.push(` ${o}`);
98
+ }
99
+ else if (st.state === 'awaiting-decision')
100
+ lines.push(ui.tone(`Awaiting lead decision${st.required ? ` (reviewer requires: ${st.required})` : ''}.`, 'info'));
101
+ else if (st.state === 'awaiting-revision')
102
+ lines.push(ui.tone(`Awaiting lead revision${st.required ? ` (required: ${st.required})` : ''}.`, 'info'));
103
+ else
104
+ lines.push(ui.tone('Awaiting reviewer challenge.', 'info'));
105
+ return lines.join('\n');
106
+ }
package/dist/evidence.js CHANGED
@@ -1,72 +1,72 @@
1
- // Validation Gate (F-LOOP-2, ADR-0008): "done" is a verified check bundle, not a natural-language
2
- // claim. Pure logic — parse a test runner's output, gate the bundle against the Task Contract, and
3
- // render the ship summary. Actually RUNNING build/test (local, deterministic) and the reviewer's
4
- // model call live in cli.ts; only the latter is the deferred live path.
5
- import { contractIssues } from './task.js';
6
- import { PLAIN, statusTone } from './ui/theme.js';
7
- /** Parse pass/fail counts from common runners (node:test "pass N", jest/vitest "N passed"). */
8
- export function parseTestSummary(output) {
9
- const t = output ?? '';
10
- const num = (re) => { const m = t.match(re); return m ? Number(m[1]) : null; };
11
- const passed = num(/\bpass(?:ed|ing)?\s+(\d+)\b/i) ?? num(/\b(\d+)\s+pass(?:ed|ing)?\b/i);
12
- const failed = num(/\bfail(?:ed|ing|ures)?\s+(\d+)\b/i) ?? num(/\b(\d+)\s+fail(?:ed|ing|ures)?\b/i);
13
- if (passed === null && failed === null)
14
- return null;
15
- return { passed: passed ?? 0, failed: failed ?? 0 };
16
- }
17
- /**
18
- * Gate the evidence against the contract. Hard checks (build, tests) decide `ready`; the contract's
19
- * acceptance criteria and unresolved items surface as warnings (they need a reviewer/human, which
20
- * the gate never auto-claims as verified).
21
- */
22
- export function gate(contract, bundle) {
23
- const checks = [];
24
- const warnings = [];
25
- if (bundle.build)
26
- checks.push({ label: 'Build', ok: bundle.build.exitCode === 0, detail: bundle.build.command });
27
- if (bundle.tests) {
28
- const s = bundle.tests.summary;
29
- const ok = bundle.tests.exitCode === 0 && (!s || s.failed === 0);
30
- checks.push({ label: 'Tests', ok, detail: s ? `${s.passed} passed, ${s.failed} failed` : `exit ${bundle.tests.exitCode}` });
31
- }
32
- if (checks.length === 0)
33
- warnings.push('no build/test commands found — nothing was actually verified');
34
- if (contract) {
35
- for (const issue of contractIssues(contract))
36
- warnings.push(`contract: ${issue}`);
37
- if (contract.acceptance.length)
38
- warnings.push(`${contract.acceptance.length} acceptance criteria need verification (reviewer/human)`);
39
- }
40
- else {
41
- warnings.push('no task contract — run `frame start <goal>` to define "done"');
42
- }
43
- for (const u of bundle.unresolved ?? [])
44
- warnings.push(`unresolved: ${u}`);
45
- return { ready: checks.length > 0 && checks.every((c) => c.ok), checks, warnings };
46
- }
47
- function header(r) {
48
- if (!r.ready)
49
- return 'NOT READY';
50
- return r.warnings.length ? `READY WITH ${r.warnings.length} WARNING${r.warnings.length > 1 ? 'S' : ''}` : 'READY';
51
- }
52
- /** Shared gate body: header + checks + warnings (used by `frame verify`). */
53
- export function renderGate(r, ui = PLAIN) {
54
- const h = header(r);
55
- const lines = [ui.tone(h, statusTone(h)), ''];
56
- for (const c of r.checks) {
57
- const mark = c.ok ? ui.tone(ui.sym.pass, 'success') : ui.tone(ui.sym.fail, 'danger');
58
- lines.push(`${mark} ${c.label}${c.detail ? ': ' + c.detail : ''}`);
59
- }
60
- for (const w of r.warnings)
61
- lines.push(`${ui.tone(ui.sym.warn, 'warning')} ${w}`);
62
- return lines.join('\n');
63
- }
64
- /** Gate body + commit/deploy guidance (used by `frame ship`). */
65
- export function renderShip(r, ui = PLAIN) {
66
- return [
67
- renderGate(r, ui),
68
- '',
69
- `Safe to commit: ${r.ready ? 'yes' : 'no'}`,
70
- `Safe to deploy: ${r.ready ? 'requires human confirmation' : 'no'}`,
71
- ].join('\n');
72
- }
1
+ // Validation Gate (F-LOOP-2, ADR-0008): "done" is a verified check bundle, not a natural-language
2
+ // claim. Pure logic — parse a test runner's output, gate the bundle against the Task Contract, and
3
+ // render the ship summary. Actually RUNNING build/test (local, deterministic) and the reviewer's
4
+ // model call live in cli.ts; only the latter is the deferred live path.
5
+ import { contractIssues } from './task.js';
6
+ import { PLAIN, statusTone } from './ui/theme.js';
7
+ /** Parse pass/fail counts from common runners (node:test "pass N", jest/vitest "N passed"). */
8
+ export function parseTestSummary(output) {
9
+ const t = output ?? '';
10
+ const num = (re) => { const m = t.match(re); return m ? Number(m[1]) : null; };
11
+ const passed = num(/\bpass(?:ed|ing)?\s+(\d+)\b/i) ?? num(/\b(\d+)\s+pass(?:ed|ing)?\b/i);
12
+ const failed = num(/\bfail(?:ed|ing|ures)?\s+(\d+)\b/i) ?? num(/\b(\d+)\s+fail(?:ed|ing|ures)?\b/i);
13
+ if (passed === null && failed === null)
14
+ return null;
15
+ return { passed: passed ?? 0, failed: failed ?? 0 };
16
+ }
17
+ /**
18
+ * Gate the evidence against the contract. Hard checks (build, tests) decide `ready`; the contract's
19
+ * acceptance criteria and unresolved items surface as warnings (they need a reviewer/human, which
20
+ * the gate never auto-claims as verified).
21
+ */
22
+ export function gate(contract, bundle) {
23
+ const checks = [];
24
+ const warnings = [];
25
+ if (bundle.build)
26
+ checks.push({ label: 'Build', ok: bundle.build.exitCode === 0, detail: bundle.build.command });
27
+ if (bundle.tests) {
28
+ const s = bundle.tests.summary;
29
+ const ok = bundle.tests.exitCode === 0 && (!s || s.failed === 0);
30
+ checks.push({ label: 'Tests', ok, detail: s ? `${s.passed} passed, ${s.failed} failed` : `exit ${bundle.tests.exitCode}` });
31
+ }
32
+ if (checks.length === 0)
33
+ warnings.push('no build/test commands found — nothing was actually verified');
34
+ if (contract) {
35
+ for (const issue of contractIssues(contract))
36
+ warnings.push(`contract: ${issue}`);
37
+ if (contract.acceptance.length)
38
+ warnings.push(`${contract.acceptance.length} acceptance criteria need verification (reviewer/human)`);
39
+ }
40
+ else {
41
+ warnings.push('no task contract — run `frame start <goal>` to define "done"');
42
+ }
43
+ for (const u of bundle.unresolved ?? [])
44
+ warnings.push(`unresolved: ${u}`);
45
+ return { ready: checks.length > 0 && checks.every((c) => c.ok), checks, warnings };
46
+ }
47
+ function header(r) {
48
+ if (!r.ready)
49
+ return 'NOT READY';
50
+ return r.warnings.length ? `READY WITH ${r.warnings.length} WARNING${r.warnings.length > 1 ? 'S' : ''}` : 'READY';
51
+ }
52
+ /** Shared gate body: header + checks + warnings (used by `frame verify`). */
53
+ export function renderGate(r, ui = PLAIN) {
54
+ const h = header(r);
55
+ const lines = [ui.tone(h, statusTone(h)), ''];
56
+ for (const c of r.checks) {
57
+ const mark = c.ok ? ui.tone(ui.sym.pass, 'success') : ui.tone(ui.sym.fail, 'danger');
58
+ lines.push(`${mark} ${c.label}${c.detail ? ': ' + c.detail : ''}`);
59
+ }
60
+ for (const w of r.warnings)
61
+ lines.push(`${ui.tone(ui.sym.warn, 'warning')} ${w}`);
62
+ return lines.join('\n');
63
+ }
64
+ /** Gate body + commit/deploy guidance (used by `frame ship`). */
65
+ export function renderShip(r, ui = PLAIN) {
66
+ return [
67
+ renderGate(r, ui),
68
+ '',
69
+ `Safe to commit: ${r.ready ? 'yes' : 'no'}`,
70
+ `Safe to deploy: ${r.ready ? 'requires human confirmation' : 'no'}`,
71
+ ].join('\n');
72
+ }
@@ -1,35 +1,35 @@
1
- import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { renderManagedBlock } from './projector.js';
4
- import { upsertManagedBlock } from './managedBlock.js';
5
- const NATIVE_FILES = [
6
- ['CLAUDE.md', 'CLAUDE.md'],
7
- ['AGENTS.md', 'AGENTS.md'],
8
- ['GEMINI.md', 'GEMINI.md'],
9
- ];
10
- /** Compute what each native file WOULD become (managed-block upsert), without writing. */
11
- export function planNativeFiles(dir, state) {
12
- const managed = renderManagedBlock(state);
13
- return NATIVE_FILES.map(([name, title]) => {
14
- const path = join(dir, name);
15
- const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
16
- const content = upsertManagedBlock(existing, title, managed);
17
- return { path, content, existed: existing != null, changed: existing !== content };
18
- });
19
- }
20
- /**
21
- * Upsert the managed block into the three native files, preserving any user content
22
- * outside the framein markers. Only files whose content actually changes are written
23
- * (no mtime churn / spurious file-watcher events). Returns the written paths.
24
- */
25
- export function writeNativeFiles(dir, state) {
26
- mkdirSync(dir, { recursive: true });
27
- const written = [];
28
- for (const p of planNativeFiles(dir, state)) {
29
- if (p.changed) {
30
- writeFileSync(p.path, p.content, 'utf8');
31
- written.push(p.path);
32
- }
33
- }
34
- return written;
35
- }
1
+ import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { renderManagedBlock } from './projector.js';
4
+ import { upsertManagedBlock } from './managedBlock.js';
5
+ const NATIVE_FILES = [
6
+ ['CLAUDE.md', 'CLAUDE.md'],
7
+ ['AGENTS.md', 'AGENTS.md'],
8
+ ['GEMINI.md', 'GEMINI.md'],
9
+ ];
10
+ /** Compute what each native file WOULD become (managed-block upsert), without writing. */
11
+ export function planNativeFiles(dir, state) {
12
+ const managed = renderManagedBlock(state);
13
+ return NATIVE_FILES.map(([name, title]) => {
14
+ const path = join(dir, name);
15
+ const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
16
+ const content = upsertManagedBlock(existing, title, managed);
17
+ return { path, content, existed: existing != null, changed: existing !== content };
18
+ });
19
+ }
20
+ /**
21
+ * Upsert the managed block into the three native files, preserving any user content
22
+ * outside the framein markers. Only files whose content actually changes are written
23
+ * (no mtime churn / spurious file-watcher events). Returns the written paths.
24
+ */
25
+ export function writeNativeFiles(dir, state) {
26
+ mkdirSync(dir, { recursive: true });
27
+ const written = [];
28
+ for (const p of planNativeFiles(dir, state)) {
29
+ if (p.changed) {
30
+ writeFileSync(p.path, p.content, 'utf8');
31
+ written.push(p.path);
32
+ }
33
+ }
34
+ return written;
35
+ }
package/dist/ingest.js CHANGED
@@ -1,38 +1,38 @@
1
- // Structured ingest (F-LOOP-5, live): pull a JSON object out of a model's free-form reply so its
2
- // answer becomes structured framein state (e.g. a reviewer verdict -> a Challenge). Models wrap
3
- // JSON in prose / ```json fences, so scan for the first balanced, string-aware {...} that parses.
4
- // Pure; the live model call that produces the text lives in cli.ts.
5
- export function extractJson(text) {
6
- const s = text ?? '';
7
- for (let i = s.indexOf('{'); i !== -1; i = s.indexOf('{', i + 1)) {
8
- let depth = 0, inStr = false, esc = false;
9
- for (let j = i; j < s.length; j++) {
10
- const c = s[j];
11
- if (inStr) {
12
- if (esc)
13
- esc = false;
14
- else if (c === '\\')
15
- esc = true;
16
- else if (c === '"')
17
- inStr = false;
18
- continue;
19
- }
20
- if (c === '"')
21
- inStr = true;
22
- else if (c === '{')
23
- depth++;
24
- else if (c === '}') {
25
- if (--depth === 0) {
26
- try {
27
- const o = JSON.parse(s.slice(i, j + 1));
28
- if (o && typeof o === 'object' && !Array.isArray(o))
29
- return o;
30
- }
31
- catch { /* not valid JSON — fall through and try the next `{` */ }
32
- break;
33
- }
34
- }
35
- }
36
- }
37
- return null;
38
- }
1
+ // Structured ingest (F-LOOP-5, live): pull a JSON object out of a model's free-form reply so its
2
+ // answer becomes structured framein state (e.g. a reviewer verdict -> a Challenge). Models wrap
3
+ // JSON in prose / ```json fences, so scan for the first balanced, string-aware {...} that parses.
4
+ // Pure; the live model call that produces the text lives in cli.ts.
5
+ export function extractJson(text) {
6
+ const s = text ?? '';
7
+ for (let i = s.indexOf('{'); i !== -1; i = s.indexOf('{', i + 1)) {
8
+ let depth = 0, inStr = false, esc = false;
9
+ for (let j = i; j < s.length; j++) {
10
+ const c = s[j];
11
+ if (inStr) {
12
+ if (esc)
13
+ esc = false;
14
+ else if (c === '\\')
15
+ esc = true;
16
+ else if (c === '"')
17
+ inStr = false;
18
+ continue;
19
+ }
20
+ if (c === '"')
21
+ inStr = true;
22
+ else if (c === '{')
23
+ depth++;
24
+ else if (c === '}') {
25
+ if (--depth === 0) {
26
+ try {
27
+ const o = JSON.parse(s.slice(i, j + 1));
28
+ if (o && typeof o === 'object' && !Array.isArray(o))
29
+ return o;
30
+ }
31
+ catch { /* not valid JSON — fall through and try the next `{` */ }
32
+ break;
33
+ }
34
+ }
35
+ }
36
+ }
37
+ return null;
38
+ }