diff --git a/docs/guides/json-resume-schema.mdx b/docs/guides/json-resume-schema.mdx index 06688e6ae..4a4de25a3 100644 --- a/docs/guides/json-resume-schema.mdx +++ b/docs/guides/json-resume-schema.mdx @@ -434,8 +434,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "network": { "type": "string", - "minLength": 1, - "description": "The name of the network or platform." + "description": "The name of the network or platform. Empty while the entry is a draft, which isn't printed." }, "username": { "type": "string", @@ -548,8 +547,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "company": { "type": "string", - "minLength": 1, - "description": "The name of the company or organization." + "description": "The name of the company or organization. Empty while the entry is a draft, which isn't printed." }, "position": { "type": "string", @@ -561,7 +559,55 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "period": { "type": "string", - "description": "The overall period of time at the company. When multiple roles are used, this should reflect the total tenure." + "description": "The overall period of time at the company, as text. When multiple roles are used, this should reflect the total tenure. Kept in sync from `dates`; prefer writing `dates`." + }, + "dates": { + "type": "object", + "properties": { + "start": { + "default": null, + "description": "When it started, or the date itself for single-date entries (awards, certifications, publications).", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "end": { + "default": null, + "description": "When it ended. Null while ongoing or for single dates.", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "present": { + "default": false, + "description": "Whether it's ongoing, printed as 'Present'.", + "type": "boolean" + }, + "raw": { + "description": "The original text when it couldn't be read exactly, such as 'Summer 2016'. It's printed as written until the dates are edited, which removes it.", + "type": "string" + } + }, + "required": [ + "start", + "end", + "present" + ], + "description": "Structured dates. Write these rather than the legacy text field (`period` or `date`), which is kept in sync from them." }, "website": { "default": { @@ -613,7 +659,55 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "period": { "type": "string", - "description": "The period of time this role was held." + "description": "The period of time this role was held, as text. Kept in sync from `dates`; prefer writing `dates`." + }, + "dates": { + "type": "object", + "properties": { + "start": { + "default": null, + "description": "When it started, or the date itself for single-date entries (awards, certifications, publications).", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "end": { + "default": null, + "description": "When it ended. Null while ongoing or for single dates.", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "present": { + "default": false, + "description": "Whether it's ongoing, printed as 'Present'.", + "type": "boolean" + }, + "raw": { + "description": "The original text when it couldn't be read exactly, such as 'Summer 2016'. It's printed as written until the dates are edited, which removes it.", + "type": "string" + } + }, + "required": [ + "start", + "end", + "present" + ], + "description": "Structured dates. Write these rather than the legacy text field (`period` or `date`), which is kept in sync from them." }, "description": { "type": "string", @@ -708,8 +802,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "school": { "type": "string", - "minLength": 1, - "description": "The name of the school or institution." + "description": "The name of the school or institution. Empty while the entry is a draft, which isn't printed." }, "degree": { "type": "string", @@ -729,7 +822,55 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "period": { "type": "string", - "description": "The period of time the education was obtained over." + "description": "The period of time the education was obtained over, as text. Kept in sync from `dates`; prefer writing `dates`." + }, + "dates": { + "type": "object", + "properties": { + "start": { + "default": null, + "description": "When it started, or the date itself for single-date entries (awards, certifications, publications).", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "end": { + "default": null, + "description": "When it ended. Null while ongoing or for single dates.", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "present": { + "default": false, + "description": "Whether it's ongoing, printed as 'Present'.", + "type": "boolean" + }, + "raw": { + "description": "The original text when it couldn't be read exactly, such as 'Summer 2016'. It's printed as written until the dates are edited, which removes it.", + "type": "string" + } + }, + "required": [ + "start", + "end", + "present" + ], + "description": "Structured dates. Write these rather than the legacy text field (`period` or `date`), which is kept in sync from them." }, "website": { "default": { @@ -845,12 +986,59 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "name": { "type": "string", - "minLength": 1, - "description": "The name of the project." + "description": "The name of the project. Empty while the entry is a draft, which isn't printed." }, "period": { "type": "string", - "description": "The period of time the project was worked on." + "description": "The period of time the project was worked on, as text. Kept in sync from `dates`; prefer writing `dates`." + }, + "dates": { + "type": "object", + "properties": { + "start": { + "default": null, + "description": "When it started, or the date itself for single-date entries (awards, certifications, publications).", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "end": { + "default": null, + "description": "When it ended. Null while ongoing or for single dates.", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "present": { + "default": false, + "description": "Whether it's ongoing, printed as 'Present'.", + "type": "boolean" + }, + "raw": { + "description": "The original text when it couldn't be read exactly, such as 'Summer 2016'. It's printed as written until the dates are edited, which removes it.", + "type": "string" + } + }, + "required": [ + "start", + "end", + "present" + ], + "description": "Structured dates. Write these rather than the legacy text field (`period` or `date`), which is kept in sync from them." }, "website": { "default": { @@ -972,8 +1160,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "name": { "type": "string", - "minLength": 1, - "description": "The name of the skill." + "description": "The name of the skill. Empty while the entry is a draft, which isn't printed." }, "proficiency": { "type": "string", @@ -1090,8 +1277,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "language": { "type": "string", - "minLength": 1, - "description": "The name of the language the author knows." + "description": "The name of the language the author knows. Empty while the entry is a draft, which isn't printed." }, "fluency": { "type": "string", @@ -1189,8 +1375,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "name": { "type": "string", - "minLength": 1, - "description": "The name of the interest/hobby." + "description": "The name of the interest/hobby. Empty while the entry is a draft, which isn't printed." }, "keywords": { "default": [], @@ -1277,8 +1462,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "title": { "type": "string", - "minLength": 1, - "description": "The title of the award." + "description": "The title of the award. Empty while the entry is a draft, which isn't printed." }, "awarder": { "type": "string", @@ -1286,7 +1470,55 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "date": { "type": "string", - "description": "The date when the award was received." + "description": "The date when the award was received, as text. Kept in sync from `dates`; prefer writing `dates`." + }, + "dates": { + "type": "object", + "properties": { + "start": { + "default": null, + "description": "When it started, or the date itself for single-date entries (awards, certifications, publications).", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "end": { + "default": null, + "description": "When it ended. Null while ongoing or for single dates.", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "present": { + "default": false, + "description": "Whether it's ongoing, printed as 'Present'.", + "type": "boolean" + }, + "raw": { + "description": "The original text when it couldn't be read exactly, such as 'Summer 2016'. It's printed as written until the dates are edited, which removes it.", + "type": "string" + } + }, + "required": [ + "start", + "end", + "present" + ], + "description": "Structured dates. Write these rather than the legacy text field (`period` or `date`), which is kept in sync from them." }, "website": { "default": { @@ -1399,8 +1631,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "title": { "type": "string", - "minLength": 1, - "description": "The title of the certification." + "description": "The title of the certification. Empty while the entry is a draft, which isn't printed." }, "issuer": { "type": "string", @@ -1408,7 +1639,55 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "date": { "type": "string", - "description": "The date when the certification was received." + "description": "The date when the certification was received, as text. Kept in sync from `dates`; prefer writing `dates`." + }, + "dates": { + "type": "object", + "properties": { + "start": { + "default": null, + "description": "When it started, or the date itself for single-date entries (awards, certifications, publications).", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "end": { + "default": null, + "description": "When it ended. Null while ongoing or for single dates.", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "present": { + "default": false, + "description": "Whether it's ongoing, printed as 'Present'.", + "type": "boolean" + }, + "raw": { + "description": "The original text when it couldn't be read exactly, such as 'Summer 2016'. It's printed as written until the dates are edited, which removes it.", + "type": "string" + } + }, + "required": [ + "start", + "end", + "present" + ], + "description": "Structured dates. Write these rather than the legacy text field (`period` or `date`), which is kept in sync from them." }, "website": { "default": { @@ -1521,8 +1800,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "title": { "type": "string", - "minLength": 1, - "description": "The title of the publication." + "description": "The title of the publication. Empty while the entry is a draft, which isn't printed." }, "publisher": { "type": "string", @@ -1530,7 +1808,55 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "date": { "type": "string", - "description": "The date when the publication was published." + "description": "The date when the publication was published, as text. Kept in sync from `dates`; prefer writing `dates`." + }, + "dates": { + "type": "object", + "properties": { + "start": { + "default": null, + "description": "When it started, or the date itself for single-date entries (awards, certifications, publications).", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "end": { + "default": null, + "description": "When it ended. Null while ongoing or for single dates.", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "present": { + "default": false, + "description": "Whether it's ongoing, printed as 'Present'.", + "type": "boolean" + }, + "raw": { + "description": "The original text when it couldn't be read exactly, such as 'Summer 2016'. It's printed as written until the dates are edited, which removes it.", + "type": "string" + } + }, + "required": [ + "start", + "end", + "present" + ], + "description": "Structured dates. Write these rather than the legacy text field (`period` or `date`), which is kept in sync from them." }, "website": { "default": { @@ -1643,8 +1969,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "organization": { "type": "string", - "minLength": 1, - "description": "The name of the organization or company." + "description": "The name of the organization or company. Empty while the entry is a draft, which isn't printed." }, "location": { "type": "string", @@ -1652,7 +1977,55 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "period": { "type": "string", - "description": "The period of time the author was volunteered at the organization or company." + "description": "The period of time the author was volunteered at the organization or company, as text. Kept in sync from `dates`; prefer writing `dates`." + }, + "dates": { + "type": "object", + "properties": { + "start": { + "default": null, + "description": "When it started, or the date itself for single-date entries (awards, certifications, publications).", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "end": { + "default": null, + "description": "When it ended. Null while ongoing or for single dates.", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "present": { + "default": false, + "description": "Whether it's ongoing, printed as 'Present'.", + "type": "boolean" + }, + "raw": { + "description": "The original text when it couldn't be read exactly, such as 'Summer 2016'. It's printed as written until the dates are edited, which removes it.", + "type": "string" + } + }, + "required": [ + "start", + "end", + "present" + ], + "description": "Structured dates. Write these rather than the legacy text field (`period` or `date`), which is kept in sync from them." }, "website": { "default": { @@ -1765,8 +2138,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "name": { "type": "string", - "minLength": 1, - "description": "The name of the reference, or a note such as 'Available upon request'." + "description": "The name of the reference, or a note such as 'Available upon request'. Empty while the entry is a draft, which isn't printed." }, "position": { "type": "string", @@ -2108,8 +2480,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "network": { "type": "string", - "minLength": 1, - "description": "The name of the network or platform." + "description": "The name of the network or platform. Empty while the entry is a draft, which isn't printed." }, "username": { "type": "string", @@ -2234,8 +2605,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "company": { "type": "string", - "minLength": 1, - "description": "The name of the company or organization." + "description": "The name of the company or organization. Empty while the entry is a draft, which isn't printed." }, "position": { "type": "string", @@ -2247,7 +2617,55 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "period": { "type": "string", - "description": "The overall period of time at the company. When multiple roles are used, this should reflect the total tenure." + "description": "The overall period of time at the company, as text. When multiple roles are used, this should reflect the total tenure. Kept in sync from `dates`; prefer writing `dates`." + }, + "dates": { + "type": "object", + "properties": { + "start": { + "default": null, + "description": "When it started, or the date itself for single-date entries (awards, certifications, publications).", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "end": { + "default": null, + "description": "When it ended. Null while ongoing or for single dates.", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "present": { + "default": false, + "description": "Whether it's ongoing, printed as 'Present'.", + "type": "boolean" + }, + "raw": { + "description": "The original text when it couldn't be read exactly, such as 'Summer 2016'. It's printed as written until the dates are edited, which removes it.", + "type": "string" + } + }, + "required": [ + "start", + "end", + "present" + ], + "description": "Structured dates. Write these rather than the legacy text field (`period` or `date`), which is kept in sync from them." }, "website": { "default": { @@ -2299,7 +2717,55 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "period": { "type": "string", - "description": "The period of time this role was held." + "description": "The period of time this role was held, as text. Kept in sync from `dates`; prefer writing `dates`." + }, + "dates": { + "type": "object", + "properties": { + "start": { + "default": null, + "description": "When it started, or the date itself for single-date entries (awards, certifications, publications).", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "end": { + "default": null, + "description": "When it ended. Null while ongoing or for single dates.", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "present": { + "default": false, + "description": "Whether it's ongoing, printed as 'Present'.", + "type": "boolean" + }, + "raw": { + "description": "The original text when it couldn't be read exactly, such as 'Summer 2016'. It's printed as written until the dates are edited, which removes it.", + "type": "string" + } + }, + "required": [ + "start", + "end", + "present" + ], + "description": "Structured dates. Write these rather than the legacy text field (`period` or `date`), which is kept in sync from them." }, "description": { "type": "string", @@ -2406,8 +2872,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "school": { "type": "string", - "minLength": 1, - "description": "The name of the school or institution." + "description": "The name of the school or institution. Empty while the entry is a draft, which isn't printed." }, "degree": { "type": "string", @@ -2427,7 +2892,55 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "period": { "type": "string", - "description": "The period of time the education was obtained over." + "description": "The period of time the education was obtained over, as text. Kept in sync from `dates`; prefer writing `dates`." + }, + "dates": { + "type": "object", + "properties": { + "start": { + "default": null, + "description": "When it started, or the date itself for single-date entries (awards, certifications, publications).", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "end": { + "default": null, + "description": "When it ended. Null while ongoing or for single dates.", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "present": { + "default": false, + "description": "Whether it's ongoing, printed as 'Present'.", + "type": "boolean" + }, + "raw": { + "description": "The original text when it couldn't be read exactly, such as 'Summer 2016'. It's printed as written until the dates are edited, which removes it.", + "type": "string" + } + }, + "required": [ + "start", + "end", + "present" + ], + "description": "Structured dates. Write these rather than the legacy text field (`period` or `date`), which is kept in sync from them." }, "website": { "default": { @@ -2555,12 +3068,59 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "name": { "type": "string", - "minLength": 1, - "description": "The name of the project." + "description": "The name of the project. Empty while the entry is a draft, which isn't printed." }, "period": { "type": "string", - "description": "The period of time the project was worked on." + "description": "The period of time the project was worked on, as text. Kept in sync from `dates`; prefer writing `dates`." + }, + "dates": { + "type": "object", + "properties": { + "start": { + "default": null, + "description": "When it started, or the date itself for single-date entries (awards, certifications, publications).", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "end": { + "default": null, + "description": "When it ended. Null while ongoing or for single dates.", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "present": { + "default": false, + "description": "Whether it's ongoing, printed as 'Present'.", + "type": "boolean" + }, + "raw": { + "description": "The original text when it couldn't be read exactly, such as 'Summer 2016'. It's printed as written until the dates are edited, which removes it.", + "type": "string" + } + }, + "required": [ + "start", + "end", + "present" + ], + "description": "Structured dates. Write these rather than the legacy text field (`period` or `date`), which is kept in sync from them." }, "website": { "default": { @@ -2701,8 +3261,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "name": { "type": "string", - "minLength": 1, - "description": "The name of the skill." + "description": "The name of the skill. Empty while the entry is a draft, which isn't printed." }, "proficiency": { "type": "string", @@ -2814,8 +3373,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "language": { "type": "string", - "minLength": 1, - "description": "The name of the language the author knows." + "description": "The name of the language the author knows. Empty while the entry is a draft, which isn't printed." }, "fluency": { "type": "string", @@ -2925,8 +3483,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "name": { "type": "string", - "minLength": 1, - "description": "The name of the interest/hobby." + "description": "The name of the interest/hobby. Empty while the entry is a draft, which isn't printed." }, "keywords": { "default": [], @@ -3025,8 +3582,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "title": { "type": "string", - "minLength": 1, - "description": "The title of the award." + "description": "The title of the award. Empty while the entry is a draft, which isn't printed." }, "awarder": { "type": "string", @@ -3034,7 +3590,55 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "date": { "type": "string", - "description": "The date when the award was received." + "description": "The date when the award was received, as text. Kept in sync from `dates`; prefer writing `dates`." + }, + "dates": { + "type": "object", + "properties": { + "start": { + "default": null, + "description": "When it started, or the date itself for single-date entries (awards, certifications, publications).", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "end": { + "default": null, + "description": "When it ended. Null while ongoing or for single dates.", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "present": { + "default": false, + "description": "Whether it's ongoing, printed as 'Present'.", + "type": "boolean" + }, + "raw": { + "description": "The original text when it couldn't be read exactly, such as 'Summer 2016'. It's printed as written until the dates are edited, which removes it.", + "type": "string" + } + }, + "required": [ + "start", + "end", + "present" + ], + "description": "Structured dates. Write these rather than the legacy text field (`period` or `date`), which is kept in sync from them." }, "website": { "default": { @@ -3159,8 +3763,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "title": { "type": "string", - "minLength": 1, - "description": "The title of the certification." + "description": "The title of the certification. Empty while the entry is a draft, which isn't printed." }, "issuer": { "type": "string", @@ -3168,7 +3771,55 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "date": { "type": "string", - "description": "The date when the certification was received." + "description": "The date when the certification was received, as text. Kept in sync from `dates`; prefer writing `dates`." + }, + "dates": { + "type": "object", + "properties": { + "start": { + "default": null, + "description": "When it started, or the date itself for single-date entries (awards, certifications, publications).", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "end": { + "default": null, + "description": "When it ended. Null while ongoing or for single dates.", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "present": { + "default": false, + "description": "Whether it's ongoing, printed as 'Present'.", + "type": "boolean" + }, + "raw": { + "description": "The original text when it couldn't be read exactly, such as 'Summer 2016'. It's printed as written until the dates are edited, which removes it.", + "type": "string" + } + }, + "required": [ + "start", + "end", + "present" + ], + "description": "Structured dates. Write these rather than the legacy text field (`period` or `date`), which is kept in sync from them." }, "website": { "default": { @@ -3293,8 +3944,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "title": { "type": "string", - "minLength": 1, - "description": "The title of the publication." + "description": "The title of the publication. Empty while the entry is a draft, which isn't printed." }, "publisher": { "type": "string", @@ -3302,7 +3952,55 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "date": { "type": "string", - "description": "The date when the publication was published." + "description": "The date when the publication was published, as text. Kept in sync from `dates`; prefer writing `dates`." + }, + "dates": { + "type": "object", + "properties": { + "start": { + "default": null, + "description": "When it started, or the date itself for single-date entries (awards, certifications, publications).", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "end": { + "default": null, + "description": "When it ended. Null while ongoing or for single dates.", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "present": { + "default": false, + "description": "Whether it's ongoing, printed as 'Present'.", + "type": "boolean" + }, + "raw": { + "description": "The original text when it couldn't be read exactly, such as 'Summer 2016'. It's printed as written until the dates are edited, which removes it.", + "type": "string" + } + }, + "required": [ + "start", + "end", + "present" + ], + "description": "Structured dates. Write these rather than the legacy text field (`period` or `date`), which is kept in sync from them." }, "website": { "default": { @@ -3427,8 +4125,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "organization": { "type": "string", - "minLength": 1, - "description": "The name of the organization or company." + "description": "The name of the organization or company. Empty while the entry is a draft, which isn't printed." }, "location": { "type": "string", @@ -3436,7 +4133,55 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "period": { "type": "string", - "description": "The period of time the author was volunteered at the organization or company." + "description": "The period of time the author was volunteered at the organization or company, as text. Kept in sync from `dates`; prefer writing `dates`." + }, + "dates": { + "type": "object", + "properties": { + "start": { + "default": null, + "description": "When it started, or the date itself for single-date entries (awards, certifications, publications).", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "end": { + "default": null, + "description": "When it ended. Null while ongoing or for single dates.", + "anyOf": [ + { + "type": "string", + "pattern": "^\\d{4}(?:-(?:0[1-9]|1[0-2]))?$", + "description": "A year (\"2022\") or a year and month (\"2022-03\")." + }, + { + "type": "null" + } + ] + }, + "present": { + "default": false, + "description": "Whether it's ongoing, printed as 'Present'.", + "type": "boolean" + }, + "raw": { + "description": "The original text when it couldn't be read exactly, such as 'Summer 2016'. It's printed as written until the dates are edited, which removes it.", + "type": "string" + } + }, + "required": [ + "start", + "end", + "present" + ], + "description": "Structured dates. Write these rather than the legacy text field (`period` or `date`), which is kept in sync from them." }, "website": { "default": { @@ -3561,8 +4306,7 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest }, "name": { "type": "string", - "minLength": 1, - "description": "The name of the reference, or a note such as 'Available upon request'." + "description": "The name of the reference, or a note such as 'Available upon request'. Empty while the entry is a draft, which isn't printed." }, "position": { "type": "string", @@ -3752,6 +4496,16 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "string", "description": "The locale of the page. Used for displaying pre-translated section headings, if not overridden." }, + "dateFormat": { + "description": "How dates print: 'short' (Mar 2022), 'long' (March 2022), 'numeric' (03/2022) or 'iso' (2022-03). When missing, it's read from how the dates were typed.", + "type": "string", + "enum": [ + "short", + "long", + "numeric", + "iso" + ] + }, "hideLinkUnderline": { "default": false, "type": "boolean", diff --git a/packages/api/src/features/resume/resume-data-validation.ts b/packages/api/src/features/resume/resume-data-validation.ts index defa7d890..e5ea476f0 100644 --- a/packages/api/src/features/resume/resume-data-validation.ts +++ b/packages/api/src/features/resume/resume-data-validation.ts @@ -2,6 +2,7 @@ import type { ResumeData } from "@reactive-resume/schema/resume/data"; import { ORPCError } from "@orpc/client"; import { SEMANTIC_CSS_LIMITS_V1 } from "@reactive-resume/resume/stylesheet"; import { parseResumeData } from "@reactive-resume/schema/resume/data"; +import { syncResumeDates, upgradeResumeDates } from "@reactive-resume/schema/resume/dates"; import { parseResumeDataForWrite } from "@reactive-resume/schema/resume/write"; function parseApiResumeData(data: unknown, code: "BAD_REQUEST" | "INTERNAL_SERVER_ERROR", message: string): ResumeData { @@ -21,8 +22,17 @@ function parseApiResumeData(data: unknown, code: "BAD_REQUEST" | "INTERNAL_SERVE } } -export const parseWritableResumeData = (data: unknown) => - parseApiResumeData(data, "BAD_REQUEST", "Resume data does not match the canonical schema."); +/** + * Validates data before it's saved, then keeps structured dates and their legacy text in step. `previous` is + * the stored data being replaced, which tells an edit to the text alone (from a client that doesn't know + * structured dates) from an edit to the dates. + */ +export const parseWritableResumeData = (data: unknown, previous?: ResumeData) => { + const parsed = parseApiResumeData(data, "BAD_REQUEST", "Resume data does not match the canonical schema."); + upgradeResumeDates(parsed); + syncResumeDates(parsed, previous); + return parsed; +}; export const parseStoredResumeData = (data: unknown) => parseApiResumeData(data, "INTERNAL_SERVER_ERROR", "Stored resume data does not match the canonical schema."); diff --git a/packages/api/src/features/resume/service.ts b/packages/api/src/features/resume/service.ts index 18288bb87..6a2058a0b 100644 --- a/packages/api/src/features/resume/service.ts +++ b/packages/api/src/features/resume/service.ts @@ -140,9 +140,10 @@ async function applyResumePatchTx( input.operations.forEach(assertValidPatchPointers); let patchedData: ResumeData; + const storedData = parseStoredResumeData(existing.data); try { - patchedData = applyResumePatches(parseStoredResumeData(existing.data), input.operations); + patchedData = applyResumePatches(storedData, input.operations); } catch (error) { if (error instanceof ResumePatchError) { throw new ORPCError("INVALID_PATCH_OPERATIONS", { @@ -158,7 +159,7 @@ async function applyResumePatchTx( }); } - patchedData = parseWritableResumeData(patchedData); + patchedData = parseWritableResumeData(patchedData, storedData); // The version guard is the ms-precision JS check above, under the SELECT ... FOR UPDATE lock. // Never compare expectedUpdatedAt in SQL: rows stamped by Postgres now() (defaultNow() on // insert) carry microseconds, while JS Dates are ms-truncated — SQL equality then matches @@ -509,7 +510,8 @@ export const resumeService = { if (!resume) throw new ORPCError("NOT_FOUND"); - return resume; + // Clients get the current data shape (structured dates and their text in step), not the stored one. + return { ...resume, data: parseStoredResumeData(resume.data) }; }, getBySlug: async (input: { @@ -562,7 +564,8 @@ export const resumeService = { } } - return toSharedResumeResponse(redactResumeForViewer(resume, isOwner(resume, viewer)), resume.hasPassword); + const current = { ...resume, data: parseStoredResumeData(resume.data) }; + return toSharedResumeResponse(redactResumeForViewer(current, isOwner(current, viewer)), resume.hasPassword); }, create: async (input: { @@ -633,7 +636,9 @@ export const resumeService = { if (!existing) throw new ORPCError("NOT_FOUND"); if (existing.isLocked) throw new ORPCError("RESUME_LOCKED"); - const normalizedData = input.data ? parseWritableResumeData(input.data) : undefined; + const normalizedData = input.data + ? parseWritableResumeData(input.data, parseStoredResumeData(existing.data)) + : undefined; const updateData: Partial = { ...(input.name !== undefined ? { name: input.name } : {}), ...(input.slug !== undefined ? { slug: input.slug } : {}), diff --git a/packages/import/src/date.test.ts b/packages/import/src/date.test.ts index fa869c6d8..00ebb9129 100644 --- a/packages/import/src/date.test.ts +++ b/packages/import/src/date.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { formatDate, formatPeriod, formatSingleDate } from "./date"; +import { formatDate, formatPeriod, formatSingleDate, toRangeDates, toSingleDates, toYearMonth } from "./date"; describe("formatDate", () => { it("formats YYYY-MM as 'Month Year'", () => { @@ -87,3 +87,17 @@ describe("formatSingleDate", () => { expect(formatSingleDate("2024")).toBe("2024"); }); }); + +describe("structured dates", () => { + it("keeps the year and month of ISO dates", () => { + expect(toYearMonth("2024-01-15")).toBe("2024-01"); + expect(toYearMonth("2024")).toBe("2024"); + expect(toYearMonth("soon")).toBeNull(); + }); + + it("marks a start without an end as ongoing", () => { + expect(toRangeDates("2020-03", undefined)).toEqual({ start: "2020-03", end: null, present: true }); + expect(toRangeDates("2020-03", "2021")).toEqual({ start: "2020-03", end: "2021", present: false }); + expect(toSingleDates("2019-11-02")).toEqual({ start: "2019-11", end: null, present: false }); + }); +}); diff --git a/packages/import/src/date.ts b/packages/import/src/date.ts index 2a4ce559c..6f6d0d53e 100644 --- a/packages/import/src/date.ts +++ b/packages/import/src/date.ts @@ -1,3 +1,5 @@ +import type { ResumeDates, YearMonth } from "@reactive-resume/schema/resume/dates"; + // ponytail: Intl replaces the 12-line MONTH_NAMES array; pinned to en-US so output is stable const fmt = new Intl.DateTimeFormat("en-US", { month: "long" }); @@ -49,3 +51,20 @@ export function formatSingleDate(date?: string): string { if (!date) return ""; return formatDate(date, true); } + +/** The year and month of an ISO 8601 date ("2024-01-15" → "2024-01"); null when it isn't one. */ +export function toYearMonth(date?: string): YearMonth | null { + const match = /^(\d{4})(?:-(0[1-9]|1[0-2]))?/.exec(date?.trim() ?? ""); + if (!match?.[1]) return null; + return match[2] ? `${match[1]}-${match[2]}` : match[1]; +} + +/** Structured dates for a range of ISO dates; a start with no end is ongoing, as `formatPeriod` prints it. */ +export function toRangeDates(startDate?: string, endDate?: string): ResumeDates { + const start = toYearMonth(startDate); + const end = toYearMonth(endDate); + return { start, end, present: Boolean(start) && !endDate }; +} + +/** Structured dates for a single ISO date. The day, if any, doesn't print. */ +export const toSingleDates = (date?: string): ResumeDates => ({ start: toYearMonth(date), end: null, present: false }); diff --git a/packages/import/src/json-resume.tsx b/packages/import/src/json-resume.tsx index 6d1e260c2..a4a233cb0 100644 --- a/packages/import/src/json-resume.tsx +++ b/packages/import/src/json-resume.tsx @@ -4,7 +4,7 @@ import { getNetworkIcon } from "@reactive-resume/resume/icons"; import { resumeDataSchema } from "@reactive-resume/schema/resume/data"; import { defaultResumeData } from "@reactive-resume/schema/resume/default"; import { generateId } from "@reactive-resume/utils/string"; -import { formatPeriod, formatSingleDate } from "./date"; +import { formatPeriod, formatSingleDate, toRangeDates, toSingleDates } from "./date"; import { rethrowAsImportError } from "./error"; import { arrayToHtmlList, toHtmlDescription } from "./html"; import { parseLevel } from "./level"; @@ -221,6 +221,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData { position: work.position || "", location: work.location || "", period: formatPeriod(work.startDate, work.endDate), + dates: toRangeDates(work.startDate, work.endDate), website: createItemWebsite(work.url), roles: [], description: toHtmlDescription(work.summary, work.highlights), @@ -243,6 +244,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData { grade: edu.score || "", location: "", period: formatPeriod(edu.startDate, edu.endDate), + dates: toRangeDates(edu.startDate, edu.endDate), website: createItemWebsite(edu.url), description: edu.courses && edu.courses.length > 0 ? arrayToHtmlList(edu.courses) : "", })), @@ -260,6 +262,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData { hidden: false, name: project.name || "", period: formatPeriod(project.startDate, project.endDate), + dates: toRangeDates(project.startDate, project.endDate), website: createItemWebsite(project.url), description: toHtmlDescription(project.description, project.highlights), })), @@ -330,6 +333,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData { title: award.title || "", awarder: award.awarder || "", date: formatSingleDate(award.date), + dates: toSingleDates(award.date), website: createItemWebsite(), description: award.summary ? `

${award.summary}

` : "", })), @@ -348,6 +352,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData { title: cert.name || "", issuer: cert.issuer || "", date: formatSingleDate(cert.date), + dates: toSingleDates(cert.date), website: createItemWebsite(cert.url), description: "", })), @@ -366,6 +371,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData { title: pub.name || "", publisher: pub.publisher || "", date: formatSingleDate(pub.releaseDate), + dates: toSingleDates(pub.releaseDate), website: createItemWebsite(pub.url), description: pub.summary ? `

${pub.summary}

` : "", })), @@ -384,6 +390,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData { organization: vol.organization || "", location: "", period: formatPeriod(vol.startDate, vol.endDate), + dates: toRangeDates(vol.startDate, vol.endDate), website: createItemWebsite(vol.url), description: toHtmlDescription(vol.summary, vol.highlights), })), diff --git a/packages/import/src/linkedin.test.ts b/packages/import/src/linkedin.test.ts index 1163333b6..37870375e 100644 --- a/packages/import/src/linkedin.test.ts +++ b/packages/import/src/linkedin.test.ts @@ -45,6 +45,7 @@ describe("parseLinkedInExport", () => { expect(item.company).toBe("Acme"); expect(item.position).toBe("Engineer"); expect(item.period).toBe("January 2020 - December 2022"); + expect(item.dates).toEqual({ start: "2020-01", end: "2022-12", present: false }); expect(item.description).toBe("

Built stuff

"); }); @@ -56,6 +57,7 @@ describe("parseLinkedInExport", () => { const result = parseLinkedInExport(zip); expect(result.sections.experience.items).toHaveLength(1); expect(result.sections.experience.items[0]!.period).toBe("January 2020 - Present"); + expect(result.sections.experience.items[0]!.dates).toEqual({ start: "2020-01", end: null, present: true }); }); it("skips positions without a company name", () => { @@ -123,7 +125,10 @@ describe("parseLinkedInExport", () => { "Positions.csv": "Company Name,Started On,Finished On\nAcme,Jan 2020,2021-06-30\n", }); - expect(parseLinkedInExport(zip).sections.experience.items[0]!.period).toBe("January 2020 - 2021-06-30"); + const [item] = parseLinkedInExport(zip).sections.experience.items; + expect(item!.period).toBe("January 2020 - 2021-06-30"); + // An end cell it couldn't read leaves the text to be read, and flagged, when saved. + expect(item!.dates).toBeUndefined(); }); it("reads headers behind a UTF-8 byte order mark", () => { diff --git a/packages/import/src/linkedin.ts b/packages/import/src/linkedin.ts index efa0124d0..e8fdcb2c5 100644 --- a/packages/import/src/linkedin.ts +++ b/packages/import/src/linkedin.ts @@ -1,9 +1,10 @@ import type { ResumeData } from "@reactive-resume/schema/resume/data"; +import type { ResumeDates } from "@reactive-resume/schema/resume/dates"; import { unzipSync } from "fflate"; import { resumeDataSchema } from "@reactive-resume/schema/resume/data"; import { defaultResumeData } from "@reactive-resume/schema/resume/default"; import { generateId } from "@reactive-resume/utils/string"; -import { formatDate } from "./date"; +import { formatDate, toRangeDates, toSingleDates } from "./date"; import { rethrowAsImportError } from "./error"; import { toHtml } from "./html"; import { parseLevel } from "./level"; @@ -46,11 +47,18 @@ const MONTHS: Record = { // LinkedIn's "Started On" / "Finished On" cells are "Mon YYYY" (e.g. "Jan 2020") or a bare year. // Anything else is kept verbatim rather than dropped. -function formatLinkedInDate(value = ""): string { +// LinkedIn writes "Jan 2020" (or a bare year); anything else stays as written and asks for a review. +function toLinkedInIsoDate(value = ""): string | null { const trimmed = value.trim(); + if (/^\d{4}$/.test(trimmed)) return trimmed; const monthYear = /^([A-Za-z]{3})[a-z]*\s+(\d{4})$/.exec(trimmed); const month = MONTHS[monthYear?.[1]?.toLowerCase() ?? ""]; - return monthYear && month ? formatDate(`${monthYear[2]}-${month}`) : trimmed; + return monthYear && month ? `${monthYear[2]}-${month}` : null; +} + +function formatLinkedInDate(value = ""): string { + const iso = toLinkedInIsoDate(value); + return iso ? formatDate(iso) : value.trim(); } // Only an empty end cell means the entry is ongoing; an unrecognised one must not read as "Present". @@ -61,6 +69,19 @@ function formatLinkedInPeriod(start?: string, end?: string): string { return `${from} - ${to || "Present"}`; } +// Dates only when every cell was read; otherwise the text is read (and flagged if need be) when saved. +function linkedInPeriodDates(start?: string, end?: string): ResumeDates | undefined { + const from = toLinkedInIsoDate(start); + const to = toLinkedInIsoDate(end); + if (!from || (end?.trim() && !to)) return undefined; + return toRangeDates(from, to ?? undefined); +} + +function linkedInSingleDates(value?: string): ResumeDates | undefined { + const iso = toLinkedInIsoDate(value); + return iso ? toSingleDates(iso) : undefined; +} + const textToHtml = (text = "") => toHtml(text.split(/\r\n?|\n/)); const languageLevel = (proficiency = "") => @@ -203,6 +224,7 @@ export function parseLinkedInExport(zipBytes: Uint8Array): ResumeData { position: position.Title || "", location: position.Location || "", period: formatLinkedInPeriod(position["Started On"], position["Finished On"]), + dates: linkedInPeriodDates(position["Started On"], position["Finished On"]), website: emptyWebsite, roles: [], description: textToHtml(position.Description), @@ -223,6 +245,7 @@ export function parseLinkedInExport(zipBytes: Uint8Array): ResumeData { grade: "", location: "", period: formatLinkedInPeriod(edu["Start Date"], edu["End Date"]), + dates: linkedInPeriodDates(edu["Start Date"], edu["End Date"]), website: emptyWebsite, description: textToHtml(edu.Notes), })), @@ -270,6 +293,7 @@ export function parseLinkedInExport(zipBytes: Uint8Array): ResumeData { title: cert.Name ?? "", issuer: cert.Authority || "", date: formatLinkedInDate(cert["Started On"]), + dates: linkedInSingleDates(cert["Started On"]), website: linkWebsite(cert.Url), description: "", })), diff --git a/packages/import/src/plain-text.test.ts b/packages/import/src/plain-text.test.ts index bd081b845..84e9c7377 100644 --- a/packages/import/src/plain-text.test.ts +++ b/packages/import/src/plain-text.test.ts @@ -61,12 +61,12 @@ describe("parseResumeText", () => { company: "Analytical Engines", position: "Senior Engineer", location: "Berlin", - period: "Jan 2020 - Present", + period: "Jan 2020 – Present", }); expect(data.sections.experience.items[1]).toMatchObject({ company: "Babbage Ltd", position: "Engineer", - period: "Mar 2016 - Dec 2019", + period: "Mar 2016 – Dec 2019", }); }); @@ -80,7 +80,7 @@ describe("parseResumeText", () => { expect(data.sections.education.items[0]).toMatchObject({ school: "University of London", degree: "BSc Mathematics", - period: "2012 - 2016", + period: "2012 – 2016", }); }); @@ -173,7 +173,7 @@ describe("parseResumeText review findings", () => { expect(data.sections.experience.items[0]).toMatchObject({ company: "Analytical Engines", position: "Senior Engineer", - period: "Jan 2020 - Present", + period: "Jan 2020 – Present", }); }); @@ -201,7 +201,7 @@ describe("parseResumeText multi-line entry preambles", () => { expect(data.sections.experience.items[0]).toMatchObject({ company: "ACME CORPORATION", position: "Senior Engineer", - period: "Jan 2020 - Present", + period: "Jan 2020 – Present", }); }); @@ -213,7 +213,7 @@ describe("parseResumeText multi-line entry preambles", () => { expect(data.sections.education.items[0]).toMatchObject({ school: "UNIVERSITY OF LONDON", degree: "BSc Mathematics", - period: "2012 - 2016", + period: "2012 – 2016", }); }); @@ -237,7 +237,7 @@ describe("parseResumeText four-line entry preambles", () => { company: "ACME CORPORATION", position: "Senior Engineer", location: "Berlin, Germany", - period: "Jan 2020 - Present", + period: "Jan 2020 – Present", }); }); @@ -250,7 +250,7 @@ describe("parseResumeText four-line entry preambles", () => { school: "UNIVERSITY OF LONDON", degree: "BSc Mathematics", location: "London, UK", - period: "2012 - 2016", + period: "2012 – 2016", }); }); }); @@ -314,7 +314,7 @@ describe("parseResumeText keeps content the layout hides", () => { ); expect(() => resumeDataSchema.parse(data)).not.toThrow(); - expect(data.sections.experience.items[0]).toMatchObject({ company: "Acme Corp", period: "Jan 2020 - Present" }); + expect(data.sections.experience.items[0]).toMatchObject({ company: "Acme Corp", period: "Jan 2020 – Present" }); expect(data.sections.education.items[0]).toMatchObject({ school: "University of London" }); expect(data.sections.certifications.items).toHaveLength(0); }); diff --git a/packages/import/src/plain-text.ts b/packages/import/src/plain-text.ts index bfcf76424..a75ef9460 100644 --- a/packages/import/src/plain-text.ts +++ b/packages/import/src/plain-text.ts @@ -1,7 +1,7 @@ import type { ResumeData, SectionType } from "@reactive-resume/schema/resume/data"; -import { parsePeriod, parseSingleDate } from "@reactive-resume/resume/ats"; import { parseResumeData } from "@reactive-resume/schema/resume/data"; import { defaultResumeData } from "@reactive-resume/schema/resume/default"; +import { parsePeriod, parseSingleDate } from "@reactive-resume/schema/resume/period"; import { generateId } from "@reactive-resume/utils/string"; import { BULLET_PATTERN, toHtml } from "./html"; diff --git a/packages/mcp/src/resources.ts b/packages/mcp/src/resources.ts index ab350d256..be8ee1440 100644 --- a/packages/mcp/src/resources.ts +++ b/packages/mcp/src/resources.ts @@ -2,7 +2,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { RouterClient } from "@orpc/server"; import type router from "@reactive-resume/api/routers"; import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; -import schemaJSON from "@reactive-resume/schema/schema.json"; +import { createResumeDataJsonSchema } from "@reactive-resume/schema/resume/json-schema"; import { MCP_TOOL_NAME as T } from "./mcp-tool-names"; export function registerResources(server: McpServer, client: RouterClient) { @@ -63,7 +63,8 @@ export function registerResources(server: McpServer, client: RouterClient