@caiqueoak/flow 0.4.0 → 0.5.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 (46) hide show
  1. package/README.md +44 -43
  2. package/package.json +11 -2
  3. package/skills/flow/SKILL.md +10 -137
  4. package/skills/flow/build/step-01-execute-task.md +9 -0
  5. package/skills/flow/discovery/step-01-project.md +5 -0
  6. package/skills/flow/discovery/step-02-await-approval.md +3 -0
  7. package/skills/flow/engineering/profiles/readability-first.md +20 -0
  8. package/skills/flow/engineering/step-02-synthesize.md +37 -0
  9. package/skills/flow/engineering/step-05-present.md +3 -0
  10. package/skills/flow/engineering/technology-defaults.md +44 -0
  11. package/skills/flow/invariants.md +16 -0
  12. package/skills/flow/migration/step-01-reconcile.md +7 -0
  13. package/skills/flow/planning/step-01-plan-work-item.md +9 -0
  14. package/skills/flow/planning/step-02-prepare-plan.md +33 -0
  15. package/skills/flow/planning/step-03-await-approval.md +3 -0
  16. package/skills/flow/reconcile/step-01-reconcile.md +7 -0
  17. package/skills/flow/review/step-01-review-work-item.md +5 -0
  18. package/src/artifacts/backlog.mjs +162 -0
  19. package/src/artifacts/document.mjs +30 -0
  20. package/src/artifacts/engineering.mjs +26 -0
  21. package/src/artifacts/gates.mjs +28 -0
  22. package/src/artifacts/implementation-plan.mjs +27 -0
  23. package/src/artifacts/prd.mjs +12 -0
  24. package/src/artifacts/state.mjs +63 -0
  25. package/src/artifacts/tasks.mjs +90 -0
  26. package/src/cli.mjs +34 -316
  27. package/src/commands/gates.mjs +68 -0
  28. package/src/commands/graph.mjs +83 -0
  29. package/src/commands/init.mjs +111 -0
  30. package/src/commands/migrate.mjs +189 -0
  31. package/src/commands/route.mjs +135 -0
  32. package/src/commands/status.mjs +32 -0
  33. package/src/commands/trace.mjs +44 -0
  34. package/src/commands/validate.mjs +174 -0
  35. package/src/shared/cli-io.mjs +86 -0
  36. package/src/shared/profiles.mjs +22 -0
  37. package/src/shared/project-config.mjs +39 -0
  38. package/src/shared/project-path.mjs +10 -0
  39. package/src/shared/skill-installer.mjs +34 -0
  40. package/skills/flow/references/build.md +0 -7
  41. package/skills/flow/references/discovery.md +0 -15
  42. package/skills/flow/references/graph.md +0 -74
  43. package/skills/flow/references/planning.md +0 -7
  44. package/skills/flow/references/reconcile.md +0 -5
  45. package/skills/flow/references/review.md +0 -5
  46. package/src/graph.mjs +0 -161
@@ -0,0 +1,22 @@
1
+ import fs from 'node:fs';
2
+ import { parse } from 'yaml';
3
+ const text = fs.readFileSync(
4
+ new URL('../../skills/flow/engineering/profiles/readability-first.md', import.meta.url),
5
+ 'utf8'
6
+ );
7
+ export const READABILITY_FIRST_PROFILE = parse(text.match(/^---\n([\s\S]*?)\n---/)[1]);
8
+ export const ENGINEERING_PROFILES = {
9
+ 'readability-first': READABILITY_FIRST_PROFILE,
10
+ [READABILITY_FIRST_PROFILE.id]: READABILITY_FIRST_PROFILE
11
+ };
12
+ export const BROWNFIELD_POLICIES = {
13
+ improve: {
14
+ label: 'Improve existing structure — Recommended',
15
+ description:
16
+ 'Preserve behavior and external contracts; recommend clearer structure where justified. Does not authorize refactoring.'
17
+ },
18
+ preserve: {
19
+ label: 'Keep existing structure',
20
+ description: 'Retain consistent conventions unless a concrete problem warrants an approved change.'
21
+ }
22
+ };
@@ -0,0 +1,39 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parse, stringify } from 'yaml';
4
+
5
+ export function configPath(root) {
6
+ return path.join(root, '.flow', 'config.yaml');
7
+ }
8
+
9
+ export function defaultConfig() {
10
+ return {
11
+ schema_version: 2,
12
+ runtimes: [],
13
+ engineering: { profile: 'flow/readability-first@1', existing_code_policy: 'not_applicable' }
14
+ };
15
+ }
16
+
17
+ export function readConfig(root) {
18
+ const file = configPath(root);
19
+ if (!fs.existsSync(file)) return null;
20
+ const raw = parse(fs.readFileSync(file, 'utf8')) ?? {};
21
+ const config = defaultConfig();
22
+ config.schema_version = raw.schema_version ?? 1;
23
+ config.runtimes = Array.isArray(raw.runtimes) ? raw.runtimes : [];
24
+ config.engineering = { ...config.engineering, ...(raw.engineering ?? {}) };
25
+ return config;
26
+ }
27
+
28
+ export function writeConfig(root, config) {
29
+ const normalized = {
30
+ schema_version: 2,
31
+ runtimes: config.runtimes ?? [],
32
+ engineering: {
33
+ profile: config.engineering?.profile ?? defaultConfig().engineering.profile,
34
+ existing_code_policy: config.engineering?.existing_code_policy ?? 'not_applicable'
35
+ }
36
+ };
37
+ fs.mkdirSync(path.join(root, '.flow'), { recursive: true });
38
+ fs.writeFileSync(configPath(root), stringify(normalized, { lineWidth: 0 }), 'utf8');
39
+ }
@@ -0,0 +1,10 @@
1
+ import path from 'node:path';
2
+
3
+ export function valueAfter(args, name) {
4
+ const index = args.indexOf(name);
5
+ return index >= 0 ? args[index + 1] : undefined;
6
+ }
7
+
8
+ export function projectRoot(args) {
9
+ return path.resolve(valueAfter(args, '--path') || process.cwd());
10
+ }
@@ -0,0 +1,34 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ function copyDirectory(source, target) {
5
+ fs.mkdirSync(target, { recursive: true });
6
+ for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
7
+ const sourcePath = path.join(source, entry.name);
8
+ const targetPath = path.join(target, entry.name);
9
+ if (entry.isDirectory()) copyDirectory(sourcePath, targetPath);
10
+ else fs.copyFileSync(sourcePath, targetPath);
11
+ }
12
+ }
13
+
14
+ export function installRuntimeSkill(root, runtime, packageRoot) {
15
+ if (
16
+ typeof runtime.skills_path !== 'string' ||
17
+ !runtime.skills_path ||
18
+ path.isAbsolute(runtime.skills_path) ||
19
+ runtime.skills_path.split(/[\\/]/).includes('..')
20
+ )
21
+ throw new Error('Runtime skills_path must remain project-local.');
22
+ const target = path.join(root, runtime.skills_path, 'flow');
23
+ let ancestor = root;
24
+ for (const segment of path.relative(root, target).split(path.sep)) {
25
+ ancestor = path.join(ancestor, segment);
26
+ if (fs.existsSync(ancestor) && fs.lstatSync(ancestor).isSymbolicLink())
27
+ throw new Error('Refusing a symlinked runtime path.');
28
+ }
29
+ if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink())
30
+ throw new Error('Refusing to overwrite a symlinked skill.');
31
+ fs.rmSync(target, { recursive: true, force: true });
32
+ copyDirectory(path.join(packageRoot, 'skills', 'flow'), target);
33
+ return target;
34
+ }
@@ -1,7 +0,0 @@
1
- # Build
2
-
3
- Compute ready tasks, exclude claims by another execution ID, select the largest safe set, and mark it `in_progress` before editing or delegating. Load only relevant specs, decisions, code, and gates. Follow approved conventions, avoid unrelated refactors, run the cheapest relevant deterministic checks, make atomic commits, and mark tasks complete only after acceptance checks pass.
4
-
5
- Synchronize `TASKS.yaml`, `BACKLOG.yaml`, `STATE.md`, `GRAPH.md`, and other affected approved canonical artifacts at each meaningful transition. Never create a new document or artifact unless the developer explicitly requested or authorized it. If a consequential new choice emerges, stop only the affected path and present the decision while independent paths continue when safe.
6
-
7
- Do not pause or return control merely to report that a task or work item completed, to announce progress, or to state the next step. Recompute readiness and continue automatically while useful ready work exists. Surface execution status only when developer input is required or execution has actually stopped.
@@ -1,15 +0,0 @@
1
- # Global Discovery
2
-
3
- ## Goal
4
-
5
- Define the smallest coherent, production-capable MVP and the global product/engineering rules required to plan it. Do not specify work-item detail prematurely.
6
-
7
- ## Scope
8
-
9
- Resolve only consequential global decisions affecting the problem, users, MVP, business constraints, production environment, deployment, architecture, persistence, integrations, security, observability, testing, conventions, documentation, Git strategy, and reusable gates. Infer trivial conventions from the existing codebase or chosen ecosystem.
10
-
11
- ## Outputs
12
-
13
- Update valid canonical artifacts when their truth changes. Create `PRD.md`, `ENGINEERING.md`, `DECISIONS.md`, `BACKLOG.yaml`, `STATE.md`, `GRAPH.md`, work-item artifacts, or gate definitions only when the developer explicitly requested or authorized creation of that artifact class. Never create substitute, progress, summary, handoff, or ad-hoc documents to capture information that belongs in an existing approved artifact.
14
-
15
- `GRAPH.md` is a derived human-readable projection of `BACKLOG.yaml`, not an independent source of truth. Discovery ends when no unresolved global decision is needed for a coherent production-capable MVP and initial work-item DAG.
@@ -1,74 +0,0 @@
1
- # `GRAPH.md` Rules
2
-
3
- Every Flow `GRAPH.md` must be a derived, human-readable projection of
4
- `.flow/BACKLOG.yaml`. It never becomes an independent source of truth.
5
-
6
- ## Generation
7
-
8
- After every change to work-item existence, title, status, or dependencies, run:
9
-
10
- ```text
11
- flow graph --path .
12
- ```
13
-
14
- The command validates the backlog DAG and regenerates the complete file. Do
15
- not edit `GRAPH.md` directly. It always emits Mermaid with `curve: 'linear'`;
16
- do not replace that setting or introduce curved arrows.
17
-
18
- ## Card content
19
-
20
- Each Mermaid card contains exactly two lines:
21
-
22
- ```text
23
- <work-item ID>
24
- <work-item title>
25
- ```
26
-
27
- Do not include status, priority, folder, dependency lists, or explanatory
28
- prose inside a card. The title is the canonical `title` value from
29
- `BACKLOG.yaml`.
30
-
31
- ## Status derivation and colors
32
-
33
- Derive status from the backlog and dependencies after every meaningful state
34
- transition.
35
-
36
- | Status | Meaning | Card color | Outgoing arrow style |
37
- | --- | --- | --- | --- |
38
- | Complete | Work item is accepted and complete. | Green (`#16a34a`) | Solid green |
39
- | In progress | Work item has active execution. | Blue (`#2563eb`) | Solid blue, thicker |
40
- | Pending | Every dependency is complete and the item is ready to start. | Yellow (`#d97706`) | Dashed yellow |
41
- | Blocked | The item is neither complete nor in progress and one or more dependencies are incomplete. | Red (`#dc2626`) | Dotted red |
42
-
43
- Use matching fill, stroke, and readable text colors for cards. Every graph
44
- must include a titled status legend using these colors.
45
-
46
- ## Dependencies and arrows
47
-
48
- - Draw one arrow for every `depends_on` relationship in `BACKLOG.yaml`.
49
- - The arrow goes from dependency to dependent item.
50
- - Every outgoing arrow inherits the color and line style of its source card.
51
- - Recalculate Mermaid `linkStyle` indexes whenever edges change; do not leave
52
- a stale style assignment behind.
53
- - Do not add visual-only dependency edges to force layout.
54
-
55
- ## Parallelism and layout
56
-
57
- - Items on the same vertical rank must have no dependency on one another and
58
- are candidates for parallel work.
59
- - Keep independent, ready items on the same vertical rank where Mermaid can
60
- represent the real dependency graph without artificial edges.
61
- - Do not represent blocked items as pending merely because they are planned;
62
- dependency readiness determines their graph status.
63
-
64
- ## Synchronization checklist
65
-
66
- When work-item state, existence, title, or dependencies change:
67
-
68
- 1. Update `BACKLOG.yaml` first.
69
- 2. Recompute derived statuses using the definitions above.
70
- 3. Update `GRAPH.md`, including cards, arrows, styles, legend, and parallel
71
- layout.
72
- 4. Update `STATE.md` when the active path or next ready work changes.
73
- 5. Verify that every backlog item appears exactly once and every dependency
74
- appears exactly once in the graph.
@@ -1,7 +0,0 @@
1
- # Work-item Planning
2
-
3
- Read only relevant global decisions, backlog context, existing spec/tasks, and source. Ask consequential work-item decisions only when viable answers materially change behavior, contracts, data semantics, technical boundaries, security, UX, or gates.
4
-
5
- For an already authorized work-item artifact class, create or update `.flow/work-items/<id>/SPEC.md` with goal, scope/non-goals, relevant decisions, approach, requirements, acceptance criteria, gates, dependencies/impacts, and validation. Create or update `TASKS.yaml` with bounded, testable, independently executable tasks, real blocking dependencies only, and concise acceptance/validation expectations.
6
-
7
- If the required `SPEC.md` or `TASKS.yaml` does not yet exist and the developer has not explicitly requested or authorized creation of that artifact class, request authorization before creating it. Do not create substitute planning documents. After planning, continue automatically into ready execution instead of stopping to report the plan unless a consequential developer decision is required.
@@ -1,5 +0,0 @@
1
- # Reconciliation
2
-
3
- Keep canonical truth and active plans aligned when new intent or accepted decisions affect work. Reconcile pending/planned work before execution, preserve unaffected in-progress work, and create maintenance work for completed work that must change. Preserve superseded decisions with links to their replacements. Update only approved artifacts whose current truth changed, including the derived `GRAPH.md` whenever work-item existence, dependencies, or status change.
4
-
5
- Never create a new document or artifact during reconciliation unless the developer explicitly requested or authorized it. After reconciliation, recompute readiness and continue execution automatically when safe rather than pausing to report status.
@@ -1,5 +0,0 @@
1
- # Review
2
-
3
- Verify completed work against its spec and applicable gates: inspect task completion and diff, validate requirements and acceptance criteria, run deterministic blocking gates, then applicable agentic gates, and check integration/regressions. Create targeted fix tasks within approved scope; request a decision only for consequential new choices. On success, update the spec Overview and Validation, then synchronize `BACKLOG.yaml`, `STATE.md`, `GRAPH.md`, and other affected approved canonical artifacts.
4
-
5
- Never create a new document or artifact during review unless the developer explicitly requested or authorized it. Do not stop merely to announce successful validation or the next ready work item; return to orchestration and continue while useful ready work exists.
package/src/graph.mjs DELETED
@@ -1,161 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { parseDocument } from 'yaml';
4
-
5
- const KINDS = new Set(['feature', 'technical', 'maintenance']);
6
- const STATUSES = new Set(['completed', 'in_progress', 'pending', 'blocked']);
7
-
8
- export class GraphValidationError extends Error {
9
- constructor(message) {
10
- super(message);
11
- this.name = 'GraphValidationError';
12
- }
13
- }
14
-
15
- function compareIds(left, right) { return left < right ? -1 : left > right ? 1 : 0; }
16
- function fail(message) { throw new GraphValidationError(message); }
17
- function requireString(value, label) {
18
- if (typeof value !== 'string' || !value.trim()) fail(`${label} must be a non-empty string.`);
19
- return value;
20
- }
21
- function requireObject(value, label) {
22
- if (!value || typeof value !== 'object' || Array.isArray(value)) fail(`${label} must be a mapping.`);
23
- return value;
24
- }
25
-
26
- function parseBacklog(text) {
27
- const document = parseDocument(text, { prettyErrors: false, uniqueKeys: true });
28
- if (document.errors.length) fail(`BACKLOG.yaml is invalid: ${document.errors[0].message}`);
29
- const backlog = requireObject(document.toJS(), 'BACKLOG.yaml');
30
- if (backlog.schema_version !== 1) fail('BACKLOG.yaml schema_version must be 1.');
31
- requireString(backlog.source_of_truth, 'BACKLOG.yaml source_of_truth');
32
- if (!Array.isArray(backlog.work_items)) fail('BACKLOG.yaml work_items must be a list.');
33
-
34
- const ids = new Set();
35
- const items = backlog.work_items.map((raw, index) => {
36
- const label = `work_items[${index}]`;
37
- const item = requireObject(raw, label);
38
- const id = requireString(item.id, `${label}.id`);
39
- if (ids.has(id)) fail(`BACKLOG.yaml contains duplicate work-item ID '${id}'.`);
40
- ids.add(id);
41
- const folder = requireString(item.folder, `${label}.folder`);
42
- if (!KINDS.has(item.kind)) fail(`${label}.kind must be feature, technical, or maintenance.`);
43
- const title = requireString(item.title, `${label}.title`);
44
- if (!STATUSES.has(item.status)) fail(`${label}.status must be completed, in_progress, pending, or blocked.`);
45
- if (!Number.isInteger(item.priority) || item.priority < 1) fail(`${label}.priority must be a positive integer.`);
46
- if (!Array.isArray(item.depends_on)) fail(`${label}.depends_on must be a list.`);
47
- const dependencySet = new Set();
48
- for (const dependency of item.depends_on) {
49
- requireString(dependency, `${label}.depends_on entry`);
50
- if (dependency === id) fail(`Work item '${id}' cannot depend on itself.`);
51
- if (dependencySet.has(dependency)) fail(`Work item '${id}' lists dependency '${dependency}' more than once.`);
52
- dependencySet.add(dependency);
53
- }
54
- return { id, folder, kind: item.kind, title, status: item.status, priority: item.priority, depends_on: [...dependencySet] };
55
- });
56
- const byId = new Map(items.map((item) => [item.id, item]));
57
- for (const item of items) for (const dependency of item.depends_on) {
58
- if (!byId.has(dependency)) fail(`Work item '${item.id}' depends on unknown work item '${dependency}'.`);
59
- }
60
- return items;
61
- }
62
-
63
- function topology(items) {
64
- const byId = new Map(items.map((item) => [item.id, item]));
65
- const remaining = new Map(items.map((item) => [item.id, item.depends_on.length]));
66
- const dependents = new Map(items.map((item) => [item.id, []]));
67
- for (const item of items) for (const dependency of item.depends_on) dependents.get(dependency).push(item.id);
68
- for (const ids of dependents.values()) ids.sort(compareIds);
69
- const ready = items.filter((item) => item.depends_on.length === 0).map((item) => item.id).sort(compareIds);
70
- const levels = new Map(ready.map((id) => [id, 0]));
71
- const orderedIds = [];
72
- while (ready.length) {
73
- const id = ready.shift();
74
- orderedIds.push(id);
75
- for (const dependent of dependents.get(id)) {
76
- levels.set(dependent, Math.max(levels.get(dependent) ?? 0, levels.get(id) + 1));
77
- const count = remaining.get(dependent) - 1;
78
- remaining.set(dependent, count);
79
- if (count === 0) { ready.push(dependent); ready.sort(compareIds); }
80
- }
81
- }
82
- if (orderedIds.length !== items.length) fail('BACKLOG.yaml work-item dependencies contain a cycle.');
83
- const ordered = orderedIds.map((id) => byId.get(id)).sort((left, right) => (levels.get(left.id) - levels.get(right.id)) || compareIds(left.id, right.id));
84
- return { ordered, byId };
85
- }
86
-
87
- function displayStatus(item, byId) {
88
- if (item.status === 'completed') return 'complete';
89
- if (item.status === 'in_progress') return 'active';
90
- const derived = item.depends_on.every((id) => byId.get(id).status === 'completed') ? 'pending' : 'blocked';
91
- if (item.status !== derived) fail(`Work item '${item.id}' is declared ${item.status} but derives as ${derived}.`);
92
- return derived;
93
- }
94
-
95
- function escapeMermaid(text) {
96
- return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
97
- }
98
-
99
- const CLASS_DEFINITIONS = [
100
- ' classDef complete fill:#dcfce7,stroke:#16a34a,color:#14532d;',
101
- ' classDef active fill:#dbeafe,stroke:#2563eb,color:#1e3a8a;',
102
- ' classDef pending fill:#fef3c7,stroke:#d97706,color:#78350f;',
103
- ' classDef blocked fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;'
104
- ];
105
- const LINK_STYLES = {
106
- complete: 'stroke:#16a34a,stroke-width:2px',
107
- active: 'stroke:#2563eb,stroke-width:3px',
108
- pending: 'stroke:#d97706,stroke-width:2px,stroke-dasharray:6 3',
109
- blocked: 'stroke:#dc2626,stroke-width:2px,stroke-dasharray:2 3'
110
- };
111
-
112
- export function generateGraphMarkdown(backlogText) {
113
- const items = parseBacklog(backlogText);
114
- const { ordered, byId } = topology(items);
115
- const statuses = new Map(ordered.map((item) => [item.id, displayStatus(item, byId)]));
116
- const edges = ordered.flatMap((source) => ordered
117
- .filter((target) => target.depends_on.includes(source.id))
118
- .map((target) => ({ source: source.id, target: target.id, status: statuses.get(source.id) })));
119
- const lines = [
120
- '# Work-item dependency graph', '', '## Status', '',
121
- '- <span style="color:#16a34a">Complete</span> — all work accepted.',
122
- '- <span style="color:#2563eb">In progress</span> — active delivery path.',
123
- '- <span style="color:#d97706">Pending</span> — all dependencies complete; ready to start.',
124
- '- <span style="color:#dc2626">Blocked</span> — one or more dependencies remain incomplete.',
125
- '', '```mermaid',
126
- "%%{init: {'flowchart': {'curve': 'linear', 'nodeSpacing': 32, 'rankSpacing': 54}} }%%",
127
- 'flowchart TD'
128
- ];
129
- for (const item of ordered) lines.push(` ${item.id}["${escapeMermaid(item.id)}<br/>${escapeMermaid(item.title)}"]`);
130
- lines.push('');
131
- for (const edge of edges) lines.push(` ${edge.source} --> ${edge.target}`);
132
- lines.push('', ...CLASS_DEFINITIONS);
133
- for (const status of ['complete', 'active', 'pending', 'blocked']) {
134
- const ids = ordered.filter((item) => statuses.get(item.id) === status).map((item) => item.id);
135
- if (ids.length) lines.push(` class ${ids.join(',')} ${status};`);
136
- }
137
- for (const status of ['complete', 'active', 'pending', 'blocked']) {
138
- const indices = edges.map((edge, index) => edge.status === status ? index : null).filter((index) => index !== null);
139
- if (indices.length) lines.push(` linkStyle ${indices.join(',')} ${LINK_STYLES[status]};`);
140
- }
141
- lines.push(
142
- '```', '',
143
- 'Cards in the same vertical rank have no dependency between them and can be',
144
- 'worked in parallel. Arrow color and line style are inherited from the source',
145
- 'card: solid green for complete, solid blue for active, dashed yellow for',
146
- 'pending, and dotted red for blocked.', ''
147
- );
148
- return lines.join('\n');
149
- }
150
-
151
- export function writeGraph(projectRoot) {
152
- const flowDir = path.join(projectRoot, '.flow');
153
- const backlogPath = path.join(flowDir, 'BACKLOG.yaml');
154
- if (!fs.existsSync(backlogPath)) fail(`Backlog not found: ${backlogPath}`);
155
- const markdown = generateGraphMarkdown(fs.readFileSync(backlogPath, 'utf8'));
156
- const graphPath = path.join(flowDir, 'GRAPH.md');
157
- const temporaryPath = path.join(flowDir, `.GRAPH.md.${process.pid}.${Date.now()}.tmp`);
158
- fs.writeFileSync(temporaryPath, markdown, 'utf8');
159
- fs.renameSync(temporaryPath, graphPath);
160
- return { graphPath, markdown };
161
- }