taste-lint 0.2.0 → 0.3.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 (30) hide show
  1. package/README.md +5 -3
  2. package/data/config.schema.json +7 -0
  3. package/data/rules/copywriting/copywriting-anthropomorphism.yaml +4 -3
  4. package/data/rules/copywriting/copywriting-antithesis-frame.yaml +21 -0
  5. package/data/rules/copywriting/copywriting-click-here-links.yaml +4 -3
  6. package/data/rules/copywriting/copywriting-conditions-first.yaml +2 -2
  7. package/data/rules/copywriting/copywriting-copula-avoidance.yaml +21 -0
  8. package/data/rules/copywriting/copywriting-error-descriptions.yaml +2 -2
  9. package/data/rules/copywriting/copywriting-filler-words.yaml +4 -3
  10. package/data/rules/copywriting/copywriting-latinisms.yaml +6 -4
  11. package/data/rules/copywriting/copywriting-nominalisations.yaml +4 -3
  12. package/data/rules/copywriting/copywriting-participle-tail.yaml +21 -0
  13. package/data/rules/copywriting/copywriting-passive-voice.yaml +4 -3
  14. package/data/rules/copywriting/copywriting-promotional-docs-voice.yaml +4 -3
  15. package/data/rules/copywriting/copywriting-requirements-language.yaml +2 -2
  16. package/data/rules/copywriting/copywriting-searchable-headings.yaml +2 -2
  17. package/data/rules/copywriting/copywriting-tee-up.yaml +21 -0
  18. package/data/rules/copywriting/copywriting-temporal-words.yaml +4 -3
  19. package/data/rules/craft/craft-atmosphere-gradient.yaml +28 -0
  20. package/data/rules/craft/craft-default-indigo.yaml +28 -0
  21. package/data/rules/motion/motion-will-change-misuse.yaml +28 -0
  22. package/data/rules/product/product-nested-modals.yaml +1 -1
  23. package/data/rules/product/product-select-for-few-options.yaml +1 -1
  24. package/dist/cli.js +186 -37
  25. package/dist/cli.js.map +1 -1
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +185 -35
  29. package/dist/index.js.map +1 -1
  30. package/package.json +1 -1
package/README.md CHANGED
@@ -40,15 +40,17 @@ Checks bill your key and cache between runs. `--dry-run` previews the cost first
40
40
 
41
41
  ## What it checks
42
42
 
43
- - **Motion:** `transition-all`, ease-in, linear easing, transitions over 300ms, entrances from scale zero.
44
- - **Copy:** “click here” links, “successfully” toasts, emoji in UI, Latinisms, nominalisations, anthropomorphism, sentences over 25 words.
43
+ - **Motion:** `transition-all` and `transition: all`, ease-in, linear easing, transitions over 300ms, entrances from scale zero, `framer-motion` imports.
44
+ - **Copy:** the [ghostwriter](https://github.com/mblode/agent-skills/tree/main/skills/ghostwriter) structure tells (participle tails, “it’s not X, it’s Y”, “Here’s the thing”), “click here” links, “successfully” toasts, emoji in UI, nominalisations, anthropomorphism, long sentences that chain clauses.
45
45
  - **Typography:** straight quotes, three-dot ellipses, hyphens for dashes, `x` for ×, missing space before a unit, light weights on small body text, uppercase without tracking.
46
46
  - **Under review:** vague errors, empty states with no action, bare “Confirm” labels, claims with nothing to check, missing error states. These go to [Jev by TypeSafe AI](https://docs.typesafe.ai/introduction), which answers with a probability.
47
47
 
48
- 22 checks block. 122 more report and never fail a run. They are pattern ports nobody has yet watched on a real codebase, and Jev questions no labelled corpus has yet promoted. See [usage](https://blode.co/taste-lint/docs/usage) for how promotion works.
48
+ 28 checks block. 125 more report and never fail a run. They are pattern ports nobody has yet watched on a real codebase, and Jev questions no labelled corpus has yet promoted. See [usage](https://blode.co/taste-lint/docs/usage) for how promotion works.
49
49
 
50
50
  Profiles scope a run: `product` (tsx, jsx, css), `writing` (md, mdx, README), `instructions` (AGENTS.md, skills, plans), `all`.
51
51
 
52
+ Add your own voice with a rule pack: `"rules": ["$GHOSTWRITER_HOME/taste-lint"]` in `taste-lint.config.json` checks drafts against your [ghostwriter](https://github.com/mblode/agent-skills/tree/main/skills/ghostwriter) profile.
53
+
52
54
  ## Docs
53
55
 
54
56
  <p>
@@ -14,6 +14,13 @@
14
14
  "smartQuotesAtBuild": {
15
15
  "type": "boolean"
16
16
  },
17
+ "rules": {
18
+ "description": "Extra rule directories, laid out like data/rules (domain folders of YAML rules). Paths resolve from the scan root; a leading ~ and $VARS expand, so a voice pack can live in $GHOSTWRITER_HOME.",
19
+ "type": "array",
20
+ "items": {
21
+ "type": "string"
22
+ }
23
+ },
17
24
  "components": {
18
25
  "type": "object",
19
26
  "additionalProperties": false,
@@ -1,12 +1,13 @@
1
- # Ported from mblode/agent-skills skills/docs-writing/rules/voice-no-anthropomorphism.md.
1
+ # Ported from mblode/agent-skills skills/docs-writing/rules/voice-no-anthropomorphism.md, now
2
+ # skills/ghostwriter/references/docs.md after docs-writing was retired.
2
3
  id: copywriting-anthropomorphism
3
4
  title: Software described as thinking, wanting or knowing
4
5
  categoryId: machine-prose
5
6
  source:
6
7
  {
7
8
  repo: mblode/agent-skills,
8
- path: skills/docs-writing/rules/voice-no-anthropomorphism.md,
9
- line: 7,
9
+ path: skills/ghostwriter/references/docs.md,
10
+ line: 13,
10
11
  }
11
12
  unit: [paragraph]
12
13
  mechanical:
@@ -0,0 +1,21 @@
1
+ # Hand-cut from mblode/agent-skills skills/ghostwriter/references/tells.md (structure tells).
2
+ id: copywriting-antithesis-frame
3
+ title: Sets up a claim by denying one nobody made
4
+ categoryId: machine-prose
5
+ source:
6
+ {
7
+ repo: mblode/agent-skills,
8
+ path: skills/ghostwriter/references/tells.md,
9
+ line: 12,
10
+ }
11
+ unit: [paragraph]
12
+ mechanical:
13
+ regex: "(?<![\"“‘'])\\bnot (?:just|only|merely) (?:a |an |the |about )?[\\w-]+(?: [\\w-]+){0,4}[,;]? but\\b|(?<![\"“‘'])\\b(?:it|this|that)(?:['’]s| is)(?: not|n['’]t) (?:just |only )?(?:about )?[^.!?,;]{1,40}[.,;] (?:it|this|that)(?:['’]s| is)\\b"
14
+ flags: giu
15
+ severity: minor
16
+ fix:
17
+ hint: >-
18
+ Say the positive thing. "It's not a tool, it's a workflow" becomes "It's a workflow."
19
+ preconditions: { notInCode: true }
20
+ status: active
21
+ handWritten: [mechanical, fix, preconditions]
@@ -1,4 +1,5 @@
1
- # Ported from mblode/agent-skills skills/docs-writing/rules/format-descriptive-links.md and taste-training
1
+ # Ported from mblode/agent-skills skills/docs-writing/rules/format-descriptive-links.md, now
2
+ # skills/ghostwriter/references/docs.md after docs-writing was retired. and taste-training
2
3
  # full-stack-audit.tsx (click-here).
3
4
  id: copywriting-click-here-links
4
5
  title: Link text that names nothing
@@ -6,8 +7,8 @@ categoryId: actionable-cta
6
7
  source:
7
8
  {
8
9
  repo: mblode/agent-skills,
9
- path: skills/docs-writing/rules/format-descriptive-links.md,
10
- line: 7,
10
+ path: skills/ghostwriter/references/docs.md,
11
+ line: 21,
11
12
  }
12
13
  unit: [jsx-text]
13
14
  mechanical:
@@ -3,8 +3,8 @@ title: An instruction hides its prerequisite after the action
3
3
  categoryId: page-structure
4
4
  source:
5
5
  repo: mblode/agent-skills
6
- path: skills/docs-writing/rules/structure-conditions-first.md
7
- line: 7
6
+ path: skills/ghostwriter/references/docs.md
7
+ line: 15
8
8
  ruleId: structure-conditions-first
9
9
  unit:
10
10
  - paragraph
@@ -0,0 +1,21 @@
1
+ # Hand-cut from mblode/agent-skills skills/ghostwriter/references/tells.md (structure tells).
2
+ id: copywriting-copula-avoidance
3
+ title: Dresses up "is" or "has"
4
+ categoryId: machine-prose
5
+ source:
6
+ {
7
+ repo: mblode/agent-skills,
8
+ path: skills/ghostwriter/references/tells.md,
9
+ line: 13,
10
+ }
11
+ unit: [paragraph]
12
+ mechanical:
13
+ regex: "(?<![\"“‘'])\\b(?:serves|stands) as (?:a|an|the)\\b|(?<![\"“‘'])\\bboasts\\b"
14
+ flags: giu
15
+ severity: minor
16
+ fix:
17
+ hint: >-
18
+ Write "is" or "has". "The index serves as a cache" becomes "The index is a cache."
19
+ preconditions: { notInCode: true }
20
+ status: review-only
21
+ handWritten: [mechanical, fix, preconditions]
@@ -3,8 +3,8 @@ title: An error explanation provides no recovery guidance
3
3
  categoryId: reference-reading
4
4
  source:
5
5
  repo: mblode/agent-skills
6
- path: skills/docs-writing/rules/code-error-descriptions.md
7
- line: 7
6
+ path: skills/ghostwriter/references/docs.md
7
+ line: 19
8
8
  ruleId: code-error-descriptions
9
9
  unit:
10
10
  - paragraph
@@ -1,4 +1,5 @@
1
- # Ported from mblode/agent-skills skills/docs-writing/rules/clarity-defaults.md (cut filler) and the lesson
1
+ # Ported from mblode/agent-skills skills/docs-writing/rules/clarity-defaults.md, now
2
+ # skills/ghostwriter/references/docs.md after docs-writing was retired. (cut filler) and the lesson
2
3
  # edit-one-lens-at-a-time.mdx.
3
4
  id: copywriting-filler-words
4
5
  title: Intensifiers and filler that add nothing
@@ -6,8 +7,8 @@ categoryId: machine-prose
6
7
  source:
7
8
  {
8
9
  repo: mblode/agent-skills,
9
- path: skills/docs-writing/rules/clarity-defaults.md,
10
- line: 12,
10
+ path: skills/ghostwriter/references/docs.md,
11
+ line: 17,
11
12
  }
12
13
  unit: [paragraph, jsx-text]
13
14
  mechanical:
@@ -1,12 +1,14 @@
1
- # Ported from mblode/agent-skills skills/docs-writing/rules/clarity-no-latin.md.
1
+ # Ported from mblode/agent-skills skills/docs-writing/rules/clarity-no-latin.md, now
2
+ # skills/ghostwriter/references/docs.md after docs-writing was retired. Review-only:
3
+ # ghostwriter keeps "plain words" but no longer bans Latin abbreviations outright.
2
4
  id: copywriting-latinisms
3
5
  title: Latin abbreviation in running text
4
6
  categoryId: reader-first-framing
5
7
  source:
6
8
  {
7
9
  repo: mblode/agent-skills,
8
- path: skills/docs-writing/rules/clarity-no-latin.md,
9
- line: 7,
10
+ path: skills/ghostwriter/references/docs.md,
11
+ line: 17,
10
12
  }
11
13
  unit: [paragraph, heading]
12
14
  mechanical:
@@ -21,5 +23,5 @@ preconditions:
21
23
  docType:
22
24
  [tutorial, howto, reference, explanation, agent-instructions, lesson],
23
25
  }
24
- status: active
26
+ status: review-only
25
27
  handWritten: [mechanical, fix, preconditions]
@@ -1,12 +1,13 @@
1
- # Ported from mblode/agent-skills skills/docs-writing/rules/clarity-no-nominalizations.md.
1
+ # Ported from mblode/agent-skills skills/docs-writing/rules/clarity-no-nominalizations.md, now
2
+ # skills/ghostwriter/references/docs.md after docs-writing was retired.
2
3
  id: copywriting-nominalisations
3
4
  title: Verb buried in a noun
4
5
  categoryId: reader-first-framing
5
6
  source:
6
7
  {
7
8
  repo: mblode/agent-skills,
8
- path: skills/docs-writing/rules/clarity-no-nominalizations.md,
9
- line: 7,
9
+ path: skills/ghostwriter/references/docs.md,
10
+ line: 17,
10
11
  }
11
12
  unit: [paragraph, jsx-text]
12
13
  mechanical:
@@ -0,0 +1,21 @@
1
+ # Hand-cut from mblode/agent-skills skills/ghostwriter/references/tells.md (structure tells).
2
+ id: copywriting-participle-tail
3
+ title: Sentence ends in a participle tail
4
+ categoryId: machine-prose
5
+ source:
6
+ {
7
+ repo: mblode/agent-skills,
8
+ path: skills/ghostwriter/references/tells.md,
9
+ line: 16,
10
+ }
11
+ unit: [paragraph]
12
+ mechanical:
13
+ regex: "(?<![\"“‘']),\\s+(?:ensuring|highlighting|underscoring|showcasing|emphasi[sz]ing|reinforcing|cementing|solidifying|fostering|signall?ing)\\b"
14
+ flags: giu
15
+ severity: minor
16
+ fix:
17
+ hint: >-
18
+ Keep the fact and cut at the comma. "Tests run on every push, ensuring quality" becomes "Tests run on every push."
19
+ preconditions: { notInCode: true }
20
+ status: active
21
+ handWritten: [mechanical, fix, preconditions]
@@ -1,12 +1,13 @@
1
- # Ported from mblode/agent-skills skills/docs-writing/rules/voice-defaults.md (active voice, present tense).
1
+ # Ported from mblode/agent-skills skills/docs-writing/rules/voice-defaults.md, now
2
+ # skills/ghostwriter/references/docs.md after docs-writing was retired. (active voice, present tense).
2
3
  id: copywriting-passive-voice
3
4
  title: Passive voice where the actor is known
4
5
  categoryId: machine-prose
5
6
  source:
6
7
  {
7
8
  repo: mblode/agent-skills,
8
- path: skills/docs-writing/rules/voice-defaults.md,
9
- line: 11,
9
+ path: skills/ghostwriter/references/docs.md,
10
+ line: 13,
10
11
  }
11
12
  unit: [paragraph, heading]
12
13
  mechanical:
@@ -1,12 +1,13 @@
1
- # Ported from mblode/agent-skills skills/docs-writing/rules/voice-defaults.md (professional, not promotional).
1
+ # Ported from mblode/agent-skills skills/docs-writing/rules/voice-defaults.md, now
2
+ # skills/ghostwriter/references/docs.md after docs-writing was retired. (professional, not promotional).
2
3
  id: copywriting-promotional-docs-voice
3
4
  title: Marketing voice inside documentation
4
5
  categoryId: evidence-over-claims
5
6
  source:
6
7
  {
7
8
  repo: mblode/agent-skills,
8
- path: skills/docs-writing/rules/voice-defaults.md,
9
- line: 14,
9
+ path: skills/ghostwriter/references/docs.md,
10
+ line: 13,
10
11
  }
11
12
  unit: [paragraph, heading]
12
13
  mechanical:
@@ -3,8 +3,8 @@ title: A requirement presented as a recommendation
3
3
  categoryId: reader-first-framing
4
4
  source:
5
5
  repo: mblode/agent-skills
6
- path: skills/docs-writing/rules/voice-requirements-language.md
7
- line: 7
6
+ path: skills/ghostwriter/references/docs.md
7
+ line: 13
8
8
  ruleId: voice-requirements-language
9
9
  unit:
10
10
  - paragraph
@@ -3,8 +3,8 @@ title: A documentation heading names no topic or action
3
3
  categoryId: page-structure
4
4
  source:
5
5
  repo: mblode/agent-skills
6
- path: skills/docs-writing/rules/nav-searchable-headings.md
7
- line: 7
6
+ path: skills/ghostwriter/references/docs.md
7
+ line: 21
8
8
  ruleId: nav-searchable-headings
9
9
  unit:
10
10
  - heading
@@ -0,0 +1,21 @@
1
+ # Hand-cut from mblode/agent-skills skills/ghostwriter/references/tells.md (structure tells).
2
+ id: copywriting-tee-up
3
+ title: Tees up a point instead of stating it
4
+ categoryId: machine-prose
5
+ source:
6
+ {
7
+ repo: mblode/agent-skills,
8
+ path: skills/ghostwriter/references/tells.md,
9
+ line: 15,
10
+ }
11
+ unit: [paragraph]
12
+ mechanical:
13
+ regex: "(?<![\"“‘'])\\bhere['’]s the (?:thing|kicker|catch|deal|twist)\\b|(?<![\"“‘'])\\bthe (?:kicker|catch|twist)\\?|(?<![\"“‘'])\\bwhat struck me (?:was|is)\\b|(?<![\"“‘'])\\blet that sink in\\b"
14
+ flags: giu
15
+ severity: minor
16
+ fix:
17
+ hint: >-
18
+ Delete the tee-up and state the thing.
19
+ preconditions: { notInCode: true }
20
+ status: active
21
+ handWritten: [mechanical, fix, preconditions]
@@ -1,4 +1,5 @@
1
- # Ported from mblode/agent-skills skills/docs-writing/rules/hygiene-no-temporal.md and
1
+ # Ported from mblode/agent-skills skills/docs-writing/rules/hygiene-no-temporal.md, now
2
+ # skills/ghostwriter/references/docs.md after docs-writing was retired. and
2
3
  # rules/hygiene-update-metadata.md.
3
4
  id: copywriting-temporal-words
4
5
  title: Wording anchored to the time of writing
@@ -6,8 +7,8 @@ categoryId: reference-reading
6
7
  source:
7
8
  {
8
9
  repo: mblode/agent-skills,
9
- path: skills/docs-writing/rules/hygiene-no-temporal.md,
10
- line: 7,
10
+ path: skills/ghostwriter/references/docs.md,
11
+ line: 23,
11
12
  }
12
13
  unit: [paragraph, heading]
13
14
  mechanical:
@@ -0,0 +1,28 @@
1
+ # Hand-cut from mblode/agent-skills skills/ui-design/direction/aesthetic-direction.md (slop signals).
2
+ id: craft-atmosphere-gradient
3
+ title: Purple or cyan gradient used as atmosphere
4
+ categoryId: generic-decoration
5
+ source:
6
+ {
7
+ repo: mblode/agent-skills,
8
+ path: skills/ui-design/direction/aesthetic-direction.md,
9
+ line: 12,
10
+ }
11
+ scope:
12
+ include: ["**/*.tsx", "**/*.jsx"]
13
+ unit: [source]
14
+ mechanical:
15
+ regex: "\\bbg-(?:gradient-to|linear-to|radial)-?[\\w-]*[^\"'`\\n]*?\\b(?:from|via|to)-(?:purple|violet|fuchsia|indigo|cyan)-\\d{2,3}\\b|\\b(?:from|via)-(?:purple|violet|fuchsia|indigo|cyan)-\\d{2,3}\\b[^\"'`\\n]*?\\bbg-(?:gradient-to|linear-to|radial)"
16
+ flags: gu
17
+ severity: minor
18
+ fix:
19
+ hint: >-
20
+ Give the surface a content or brand role, or remove the gradient. Purple and cyan washes are the most common AI-generated backdrop.
21
+ status: review-only
22
+ review:
23
+ applicability: A class list pairs a gradient background with purple, violet, fuchsia, indigo or cyan stops.
24
+ exceptions: Brand colours, data visualisation ramps, and illustrations where the gradient carries meaning.
25
+ evidence: The brand palette and what the gradient surface contains.
26
+ verification: Confirm the gradient carries a brand or content role, or replace it with a flat surface from the theme.
27
+ sourceHash: 696321dfa9cb90a6d31bba628bba33aecf23ff4aaadd3516ca2a2eea4e6219e1
28
+ handWritten: [mechanical, fix, review, scope]
@@ -0,0 +1,28 @@
1
+ # Hand-cut from mblode/agent-skills skills/ui-design/guidelines/colors.md.
2
+ id: craft-default-indigo
3
+ title: Indigo accent that looks like a template default
4
+ categoryId: colour-system
5
+ source:
6
+ {
7
+ repo: mblode/agent-skills,
8
+ path: skills/ui-design/guidelines/colors.md,
9
+ line: 5,
10
+ }
11
+ scope:
12
+ include: ["**/*.tsx", "**/*.jsx", "**/*.css"]
13
+ unit: [source]
14
+ mechanical:
15
+ regex: "\\b(?:bg|text|border|ring|from|via|to|fill|stroke|outline|accent|shadow|decoration)-indigo-\\d{2,3}\\b"
16
+ flags: gu
17
+ severity: minor
18
+ fix:
19
+ hint: >-
20
+ Pick an accent from the brand, or use the theme's primary token. Indigo is the colour every starter template ships with.
21
+ status: review-only
22
+ review:
23
+ applicability: Components use Tailwind indigo classes as the accent colour.
24
+ exceptions: A brand that is indigo, or a project whose theme already standardises on indigo.
25
+ evidence: The brand colour in the theme or design tokens, and how many components use indigo.
26
+ verification: Confirm indigo is the brand choice, or replace it with the theme's primary token.
27
+ sourceHash: cdd85836ef30c0827d41e0a07ed549a3b5a3d120b6f1aa39cf259aff87a8d0e8
28
+ handWritten: [mechanical, fix, review, scope]
@@ -0,0 +1,28 @@
1
+ # Hand-cut from mblode/agent-skills skills/ui-animation/references/performance-deep-dive.md.
2
+ id: motion-will-change-misuse
3
+ title: will-change promotes a property the compositor cannot animate
4
+ categoryId: motion-restraint
5
+ source:
6
+ {
7
+ repo: mblode/agent-skills,
8
+ path: skills/ui-animation/references/performance-deep-dive.md,
9
+ line: 161,
10
+ }
11
+ scope:
12
+ include: ["**/*.css", "**/*.scss", "**/*.tsx", "**/*.jsx"]
13
+ unit: [source]
14
+ mechanical:
15
+ regex: "will-change\\s*:\\s*[\"'`]?(?!\\s*(?:transform|opacity|auto)\\s*[\"'`]?\\s*[;},!])[a-z-]+|\\bwill-change-(?:scroll|contents|\\[(?!transform|opacity)[^\\]]+\\])"
16
+ flags: giu
17
+ severity: minor
18
+ fix:
19
+ hint: >-
20
+ Promote only transform or opacity, and only while the motion runs. Each promoted layer costs compositor memory.
21
+ status: review-only
22
+ review:
23
+ applicability: A stylesheet or class list sets will-change to a property other than transform or opacity.
24
+ exceptions: Measured scroll-performance fixes with a comment explaining the choice.
25
+ evidence: The declaration, the element it applies to, and whether it is toggled around the animation.
26
+ verification: Restrict will-change to transform or opacity and set it only during the animation.
27
+ sourceHash: f3f43c733bac58c4e64f13c769f7a360c39c0d446c886f79d929e76ae8b5fdfc
28
+ handWritten: [mechanical, fix, review, scope]
@@ -29,4 +29,4 @@ review:
29
29
  exceptions: JSX nesting alone does not prove simultaneous visibility; mutually exclusive steps and nonmodal surfaces are exceptions.
30
30
  evidence: Both open-state transitions, modal primitive implementations, and focus and Escape behavior.
31
31
  verification: Open the second step using the keyboard; confirm one modal owns focus and Escape closes the intended surface.
32
- sourceHash: e575cc8c2c06ae38542bd068a9f4d1871714391c543b5caf0dbb8ae1847a2f77
32
+ sourceHash: 9aab069ba1b93ab405a9741cd6d1e026eb08442ff881587b4f84a612653a2fbb
@@ -27,4 +27,4 @@ review:
27
27
  exceptions: Dynamically loaded options, dependent choices, or a documented platform constraint can require a select.
28
28
  evidence: The complete option source, dynamic branches, and the task requiring comparison.
29
29
  verification: Confirm all choices remain available and selection, keyboard input, and form submission work.
30
- sourceHash: e575cc8c2c06ae38542bd068a9f4d1871714391c543b5caf0dbb8ae1847a2f77
30
+ sourceHash: 9aab069ba1b93ab405a9741cd6d1e026eb08442ff881587b4f84a612653a2fbb