hvp-shared 7.6.0 → 7.7.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.
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Document Enums — new document management system (GH#21).
3
+ *
4
+ * Replaces the legacy `documentation.enums.ts` (which keeps serving the legacy
5
+ * `documentations` collection during the 60-day retention window). Do not mix
6
+ * the two — `documentation.*` = legacy, `document.*` = new system.
7
+ *
8
+ * Conventions:
9
+ * - snake_case values per `.claude/rules/enums.md`
10
+ * - `*_LABELS: Record<Enum, string>` Spanish labels alongside each enum
11
+ *
12
+ * @see .claude/plans/active/20260427-GH21-documentation-system-redesign.md
13
+ */
14
+ /**
15
+ * Sentinel value for `audience: WebAppRole[]` meaning "visible to everyone".
16
+ * Stored as a literal string in the audience array; resolved at query time to
17
+ * skip the role filter.
18
+ */
19
+ export declare const AUDIENCE_ALL = "all";
20
+ /**
21
+ * High-level reason a document exists. The first axis of the 3-tier
22
+ * classification (purpose → type → tags).
23
+ */
24
+ export declare enum Purpose {
25
+ operational = "operational",
26
+ resource = "resource",
27
+ archive = "archive"
28
+ }
29
+ export declare const PURPOSE_LABELS: Record<Purpose, string>;
30
+ /**
31
+ * Specific kind of document within a purpose. Second axis of classification.
32
+ *
33
+ * Mapping of legacy `DocumentationType` is documented in the migration script
34
+ * (`migrate-documentations-to-documents.ts`). Some values exist purely so
35
+ * legacy docs map cleanly (e.g. `meeting_recording` for `admon` videos).
36
+ */
37
+ export declare enum DocumentType {
38
+ protocol = "protocol",
39
+ guidance = "guidance",
40
+ tutorial = "tutorial",
41
+ format = "format",
42
+ tool = "tool",
43
+ policy = "policy",
44
+ reference_data = "reference_data",
45
+ training_material = "training_material",
46
+ legal_document = "legal_document",
47
+ brand_asset = "brand_asset",
48
+ meeting_recording = "meeting_recording",
49
+ historical = "historical"
50
+ }
51
+ export declare const DOCUMENT_TYPE_LABELS: Record<DocumentType, string>;
52
+ /**
53
+ * Lifecycle state of the document.
54
+ *
55
+ * Per workflow decision (master plan §Decision 3): there are NO automatic
56
+ * direct-publish rules per criticality — author chooses to publish directly
57
+ * or send to in_review. Reviewer (different person from author) approves.
58
+ */
59
+ export declare enum DocumentStatus {
60
+ draft = "draft",
61
+ in_review = "in_review",
62
+ published = "published",
63
+ archived = "archived"
64
+ }
65
+ export declare const DOCUMENT_STATUS_LABELS: Record<DocumentStatus, string>;
66
+ /**
67
+ * How critical the document is. Used for ranking, badge color, and (in SP4)
68
+ * acknowledgment urgency.
69
+ */
70
+ export declare enum Criticality {
71
+ informational = "informational",
72
+ standard = "standard",
73
+ important = "important",
74
+ critical = "critical"
75
+ }
76
+ export declare const CRITICALITY_LABELS: Record<Criticality, string>;
77
+ /**
78
+ * Semver-style classification of a new version's changes.
79
+ *
80
+ * Drives ack invalidation logic in SP4: `major`/`minor` invalidate prior acks;
81
+ * `patch` does not (per master plan §4.3).
82
+ */
83
+ export declare enum ChangeType {
84
+ patch = "patch",
85
+ minor = "minor",
86
+ major = "major"
87
+ }
88
+ export declare const CHANGE_TYPE_LABELS: Record<ChangeType, string>;
89
+ /**
90
+ * How the document body is stored.
91
+ *
92
+ * - `markdown`: body is in `DocumentVersion.markdown`
93
+ * - `external_link`: body lives outside the system (Dropbox/YouTube/etc.)
94
+ * - `mixed`: both — markdown wraps an embedded link
95
+ */
96
+ export declare enum ContentType {
97
+ markdown = "markdown",
98
+ external_link = "external_link",
99
+ mixed = "mixed"
100
+ }
101
+ export declare const CONTENT_TYPE_LABELS: Record<ContentType, string>;
102
+ /**
103
+ * Known providers for `external_link` content. Drives icon + preview card +
104
+ * disclaimer in the doc viewer.
105
+ */
106
+ export declare enum ExternalLinkProvider {
107
+ youtube = "youtube",
108
+ dropbox = "dropbox",
109
+ google_drive = "google_drive",
110
+ dailymotion = "dailymotion",
111
+ other = "other"
112
+ }
113
+ export declare const EXTERNAL_LINK_PROVIDER_LABELS: Record<ExternalLinkProvider, string>;
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ /**
3
+ * Document Enums — new document management system (GH#21).
4
+ *
5
+ * Replaces the legacy `documentation.enums.ts` (which keeps serving the legacy
6
+ * `documentations` collection during the 60-day retention window). Do not mix
7
+ * the two — `documentation.*` = legacy, `document.*` = new system.
8
+ *
9
+ * Conventions:
10
+ * - snake_case values per `.claude/rules/enums.md`
11
+ * - `*_LABELS: Record<Enum, string>` Spanish labels alongside each enum
12
+ *
13
+ * @see .claude/plans/active/20260427-GH21-documentation-system-redesign.md
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.EXTERNAL_LINK_PROVIDER_LABELS = exports.ExternalLinkProvider = exports.CONTENT_TYPE_LABELS = exports.ContentType = exports.CHANGE_TYPE_LABELS = exports.ChangeType = exports.CRITICALITY_LABELS = exports.Criticality = exports.DOCUMENT_STATUS_LABELS = exports.DocumentStatus = exports.DOCUMENT_TYPE_LABELS = exports.DocumentType = exports.PURPOSE_LABELS = exports.Purpose = exports.AUDIENCE_ALL = void 0;
17
+ // ─── Audience sentinel ──────────────────────────────────────────────────────
18
+ /**
19
+ * Sentinel value for `audience: WebAppRole[]` meaning "visible to everyone".
20
+ * Stored as a literal string in the audience array; resolved at query time to
21
+ * skip the role filter.
22
+ */
23
+ exports.AUDIENCE_ALL = "all";
24
+ // ─── Purpose ────────────────────────────────────────────────────────────────
25
+ /**
26
+ * High-level reason a document exists. The first axis of the 3-tier
27
+ * classification (purpose → type → tags).
28
+ */
29
+ var Purpose;
30
+ (function (Purpose) {
31
+ Purpose["operational"] = "operational";
32
+ Purpose["resource"] = "resource";
33
+ Purpose["archive"] = "archive";
34
+ })(Purpose || (exports.Purpose = Purpose = {}));
35
+ exports.PURPOSE_LABELS = {
36
+ [Purpose.operational]: "Operativo",
37
+ [Purpose.resource]: "Recurso",
38
+ [Purpose.archive]: "Archivo",
39
+ };
40
+ // ─── DocumentType ───────────────────────────────────────────────────────────
41
+ /**
42
+ * Specific kind of document within a purpose. Second axis of classification.
43
+ *
44
+ * Mapping of legacy `DocumentationType` is documented in the migration script
45
+ * (`migrate-documentations-to-documents.ts`). Some values exist purely so
46
+ * legacy docs map cleanly (e.g. `meeting_recording` for `admon` videos).
47
+ */
48
+ var DocumentType;
49
+ (function (DocumentType) {
50
+ // Operational
51
+ DocumentType["protocol"] = "protocol";
52
+ DocumentType["guidance"] = "guidance";
53
+ DocumentType["tutorial"] = "tutorial";
54
+ DocumentType["format"] = "format";
55
+ DocumentType["tool"] = "tool";
56
+ DocumentType["policy"] = "policy";
57
+ // Resource
58
+ DocumentType["reference_data"] = "reference_data";
59
+ DocumentType["training_material"] = "training_material";
60
+ DocumentType["legal_document"] = "legal_document";
61
+ DocumentType["brand_asset"] = "brand_asset";
62
+ // Archive
63
+ DocumentType["meeting_recording"] = "meeting_recording";
64
+ DocumentType["historical"] = "historical";
65
+ })(DocumentType || (exports.DocumentType = DocumentType = {}));
66
+ exports.DOCUMENT_TYPE_LABELS = {
67
+ [DocumentType.protocol]: "Protocolo",
68
+ [DocumentType.guidance]: "Lineamiento",
69
+ [DocumentType.tutorial]: "Tutorial",
70
+ [DocumentType.format]: "Formato",
71
+ [DocumentType.tool]: "Herramienta",
72
+ [DocumentType.policy]: "Política",
73
+ [DocumentType.reference_data]: "Datos de referencia",
74
+ [DocumentType.training_material]: "Material de capacitación",
75
+ [DocumentType.legal_document]: "Documento legal",
76
+ [DocumentType.brand_asset]: "Activo de marca",
77
+ [DocumentType.meeting_recording]: "Grabación de reunión",
78
+ [DocumentType.historical]: "Histórico",
79
+ };
80
+ // ─── DocumentStatus ─────────────────────────────────────────────────────────
81
+ /**
82
+ * Lifecycle state of the document.
83
+ *
84
+ * Per workflow decision (master plan §Decision 3): there are NO automatic
85
+ * direct-publish rules per criticality — author chooses to publish directly
86
+ * or send to in_review. Reviewer (different person from author) approves.
87
+ */
88
+ var DocumentStatus;
89
+ (function (DocumentStatus) {
90
+ DocumentStatus["draft"] = "draft";
91
+ DocumentStatus["in_review"] = "in_review";
92
+ DocumentStatus["published"] = "published";
93
+ DocumentStatus["archived"] = "archived";
94
+ })(DocumentStatus || (exports.DocumentStatus = DocumentStatus = {}));
95
+ exports.DOCUMENT_STATUS_LABELS = {
96
+ [DocumentStatus.draft]: "Borrador",
97
+ [DocumentStatus.in_review]: "En revisión",
98
+ [DocumentStatus.published]: "Publicado",
99
+ [DocumentStatus.archived]: "Archivado",
100
+ };
101
+ // ─── Criticality ────────────────────────────────────────────────────────────
102
+ /**
103
+ * How critical the document is. Used for ranking, badge color, and (in SP4)
104
+ * acknowledgment urgency.
105
+ */
106
+ var Criticality;
107
+ (function (Criticality) {
108
+ Criticality["informational"] = "informational";
109
+ Criticality["standard"] = "standard";
110
+ Criticality["important"] = "important";
111
+ Criticality["critical"] = "critical";
112
+ })(Criticality || (exports.Criticality = Criticality = {}));
113
+ exports.CRITICALITY_LABELS = {
114
+ [Criticality.informational]: "Informativo",
115
+ [Criticality.standard]: "Estándar",
116
+ [Criticality.important]: "Importante",
117
+ [Criticality.critical]: "Crítico",
118
+ };
119
+ // ─── ChangeType ─────────────────────────────────────────────────────────────
120
+ /**
121
+ * Semver-style classification of a new version's changes.
122
+ *
123
+ * Drives ack invalidation logic in SP4: `major`/`minor` invalidate prior acks;
124
+ * `patch` does not (per master plan §4.3).
125
+ */
126
+ var ChangeType;
127
+ (function (ChangeType) {
128
+ ChangeType["patch"] = "patch";
129
+ ChangeType["minor"] = "minor";
130
+ ChangeType["major"] = "major";
131
+ })(ChangeType || (exports.ChangeType = ChangeType = {}));
132
+ exports.CHANGE_TYPE_LABELS = {
133
+ [ChangeType.patch]: "Corrección menor",
134
+ [ChangeType.minor]: "Cambio menor",
135
+ [ChangeType.major]: "Cambio mayor",
136
+ };
137
+ // ─── ContentType ────────────────────────────────────────────────────────────
138
+ /**
139
+ * How the document body is stored.
140
+ *
141
+ * - `markdown`: body is in `DocumentVersion.markdown`
142
+ * - `external_link`: body lives outside the system (Dropbox/YouTube/etc.)
143
+ * - `mixed`: both — markdown wraps an embedded link
144
+ */
145
+ var ContentType;
146
+ (function (ContentType) {
147
+ ContentType["markdown"] = "markdown";
148
+ ContentType["external_link"] = "external_link";
149
+ ContentType["mixed"] = "mixed";
150
+ })(ContentType || (exports.ContentType = ContentType = {}));
151
+ exports.CONTENT_TYPE_LABELS = {
152
+ [ContentType.markdown]: "Markdown",
153
+ [ContentType.external_link]: "Enlace externo",
154
+ [ContentType.mixed]: "Mixto",
155
+ };
156
+ // ─── ExternalLinkProvider ───────────────────────────────────────────────────
157
+ /**
158
+ * Known providers for `external_link` content. Drives icon + preview card +
159
+ * disclaimer in the doc viewer.
160
+ */
161
+ var ExternalLinkProvider;
162
+ (function (ExternalLinkProvider) {
163
+ ExternalLinkProvider["youtube"] = "youtube";
164
+ ExternalLinkProvider["dropbox"] = "dropbox";
165
+ ExternalLinkProvider["google_drive"] = "google_drive";
166
+ ExternalLinkProvider["dailymotion"] = "dailymotion";
167
+ ExternalLinkProvider["other"] = "other";
168
+ })(ExternalLinkProvider || (exports.ExternalLinkProvider = ExternalLinkProvider = {}));
169
+ exports.EXTERNAL_LINK_PROVIDER_LABELS = {
170
+ [ExternalLinkProvider.youtube]: "YouTube",
171
+ [ExternalLinkProvider.dropbox]: "Dropbox",
172
+ [ExternalLinkProvider.google_drive]: "Google Drive",
173
+ [ExternalLinkProvider.dailymotion]: "Dailymotion",
174
+ [ExternalLinkProvider.other]: "Otro",
175
+ };
@@ -23,6 +23,7 @@ export * from './hris.constants';
23
23
  export * from './payroll-features.constants';
24
24
  export * from './inventory-session.enums';
25
25
  export * from './documentation.enums';
26
+ export * from './document.enums';
26
27
  export * from './settlement.enums';
27
28
  export * from './client-billing.enums';
28
29
  export * from './sat-income-invoice';
@@ -39,6 +39,7 @@ __exportStar(require("./hris.constants"), exports);
39
39
  __exportStar(require("./payroll-features.constants"), exports);
40
40
  __exportStar(require("./inventory-session.enums"), exports);
41
41
  __exportStar(require("./documentation.enums"), exports);
42
+ __exportStar(require("./document.enums"), exports);
42
43
  __exportStar(require("./settlement.enums"), exports);
43
44
  __exportStar(require("./client-billing.enums"), exports);
44
45
  __exportStar(require("./sat-income-invoice"), exports);
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Document API Contracts (GH#21)
3
+ * Request and Response types for the new document management system.
4
+ */
5
+ export * from "./requests";
6
+ export * from "./responses";
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ /**
3
+ * Document API Contracts (GH#21)
4
+ * Request and Response types for the new document management system.
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
18
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ __exportStar(require("./requests"), exports);
22
+ __exportStar(require("./responses"), exports);
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Document API Request Contracts (GH#21)
3
+ *
4
+ * Shapes the frontend (and migration script) sends to the backend.
5
+ * Per `api-contracts.md`:
6
+ * - Date fields are ISO 8601 strings, never `Date` objects.
7
+ * - Update requests are partial; Create requests are complete.
8
+ */
9
+ import { ChangeType, ContentType, Criticality, DocumentStatus, DocumentType, ExternalLinkProvider, Purpose } from "../../constants/document.enums";
10
+ import { WebAppRole } from "../../constants/collaborator.constants";
11
+ /**
12
+ * One element of `audience[]`. Either a role or the `"all"` sentinel.
13
+ *
14
+ * Stored as plain strings in DB (e.g. `["Administrador", "Gerente"]` or
15
+ * `["all"]`); resolved at query time.
16
+ */
17
+ export type DocumentAudienceEntry = WebAppRole | "all";
18
+ /**
19
+ * Body for a document version's content. Exactly one of `markdown` /
20
+ * `externalLink` is required for `markdown` / `external_link` contentType;
21
+ * BOTH are required for `mixed`.
22
+ */
23
+ export interface DocumentVersionContent {
24
+ contentType: ContentType;
25
+ markdown?: string;
26
+ externalLink?: {
27
+ url: string;
28
+ provider: ExternalLinkProvider;
29
+ };
30
+ }
31
+ /**
32
+ * Create Document Request
33
+ *
34
+ * Creates a new document AND its initial version (always status=draft,
35
+ * versionNumber="1.0.0"). Author becomes `createdBy`.
36
+ *
37
+ * @example POST /api/documents
38
+ */
39
+ export interface CreateDocumentRequest {
40
+ title: string;
41
+ /** Optional — auto-generated from title via slugify if omitted. */
42
+ slug?: string;
43
+ /** Optional human-readable code (e.g. "PROT-EGO-01"). */
44
+ code?: string;
45
+ purpose: Purpose;
46
+ type: DocumentType;
47
+ tags?: string[];
48
+ audience: DocumentAudienceEntry[];
49
+ criticality: Criticality;
50
+ requiredForOnboarding?: boolean;
51
+ requiresAcknowledgment?: boolean;
52
+ /** ObjectIds (as strings) of related documents. */
53
+ references?: string[];
54
+ /** Initial version content. */
55
+ initialVersion: DocumentVersionContent;
56
+ }
57
+ /**
58
+ * Update Document Metadata Request
59
+ *
60
+ * Updates metadata only (no version change). For content edits use
61
+ * SaveDraftVersionRequest or CreateNewVersionRequest.
62
+ *
63
+ * @example PATCH /api/documents/:id
64
+ */
65
+ export interface UpdateDocumentMetadataRequest {
66
+ title?: string;
67
+ slug?: string;
68
+ code?: string | null;
69
+ purpose?: Purpose;
70
+ type?: DocumentType;
71
+ tags?: string[];
72
+ audience?: DocumentAudienceEntry[];
73
+ criticality?: Criticality;
74
+ requiredForOnboarding?: boolean;
75
+ requiresAcknowledgment?: boolean;
76
+ references?: string[];
77
+ }
78
+ /**
79
+ * Save Draft Version Request
80
+ *
81
+ * Updates the existing draft version row in place (no new row created).
82
+ * Only valid while a draft version exists.
83
+ *
84
+ * @example PATCH /api/documents/:id/versions/draft
85
+ */
86
+ export interface SaveDraftVersionRequest {
87
+ content: DocumentVersionContent;
88
+ }
89
+ /**
90
+ * Create New Version Request
91
+ *
92
+ * Branches a new draft version from the currently published version.
93
+ * Used when editing an already-published document.
94
+ *
95
+ * @example POST /api/documents/:id/versions
96
+ */
97
+ export interface CreateNewVersionRequest {
98
+ changeType: ChangeType;
99
+ changeNotes: string;
100
+ content: DocumentVersionContent;
101
+ }
102
+ /**
103
+ * Submit For Review Request
104
+ *
105
+ * Transitions a draft to `in_review`. Empty body — context comes from URL.
106
+ *
107
+ * @example POST /api/documents/:id/submit-for-review
108
+ */
109
+ export type SubmitForReviewRequest = Record<string, never>;
110
+ /**
111
+ * Publish Document Request
112
+ *
113
+ * Transitions a draft or in_review version to `published`. The current
114
+ * published version (if any) becomes superseded.
115
+ *
116
+ * Per master plan §Decision 3: no automatic enforcement of review-required
117
+ * rules per criticality — backend trusts the caller's authorization.
118
+ *
119
+ * @example POST /api/documents/:id/publish
120
+ */
121
+ export interface PublishDocumentRequest {
122
+ /** Required if publishing without prior in_review (audit trail). */
123
+ changeNotes?: string;
124
+ }
125
+ /**
126
+ * Archive Document Request
127
+ *
128
+ * Marks the document as `archived`. Archived docs are hidden from default
129
+ * lists but data is preserved. Restricted to admin for `legal_document` and
130
+ * `brand_asset` types.
131
+ *
132
+ * @example POST /api/documents/:id/archive
133
+ */
134
+ export interface ArchiveDocumentRequest {
135
+ reason?: string;
136
+ }
137
+ /**
138
+ * Unarchive Document Request
139
+ *
140
+ * Returns an archived doc to its prior `published` state. Admin only.
141
+ *
142
+ * @example POST /api/documents/:id/unarchive
143
+ */
144
+ export type UnarchiveDocumentRequest = Record<string, never>;
145
+ /**
146
+ * Acknowledge Version Request
147
+ *
148
+ * Records that the calling user has read the current version of the document.
149
+ * Idempotent — second call is a no-op.
150
+ *
151
+ * @example POST /api/documents/:id/acknowledge
152
+ */
153
+ export type AcknowledgeVersionRequest = Record<string, never>;
154
+ /**
155
+ * List Documents Query Filters
156
+ *
157
+ * Query string parameters for `GET /api/documents`. All optional. Audience
158
+ * filtering by current user role is implicit (handled server-side).
159
+ *
160
+ * @example GET /api/documents?purpose=operational&status=published
161
+ */
162
+ export interface ListDocumentsQuery {
163
+ purpose?: Purpose;
164
+ type?: DocumentType;
165
+ status?: DocumentStatus;
166
+ criticality?: Criticality;
167
+ /** Comma-separated tag list, OR semantics. */
168
+ tags?: string;
169
+ /** Substring match on title (server may also $text-search). */
170
+ search?: string;
171
+ requiredForOnboarding?: boolean;
172
+ requiresAcknowledgment?: boolean;
173
+ limit?: number;
174
+ skip?: number;
175
+ }
176
+ /**
177
+ * Search Documents Query
178
+ *
179
+ * @example GET /api/documents/search?q=comisiones
180
+ */
181
+ export interface SearchDocumentsQuery {
182
+ q: string;
183
+ limit?: number;
184
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ /**
3
+ * Document API Request Contracts (GH#21)
4
+ *
5
+ * Shapes the frontend (and migration script) sends to the backend.
6
+ * Per `api-contracts.md`:
7
+ * - Date fields are ISO 8601 strings, never `Date` objects.
8
+ * - Update requests are partial; Create requests are complete.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Document API Response Contracts (GH#21)
3
+ *
4
+ * Shapes the backend returns. Per `api-contracts.md`:
5
+ * - Date fields are ISO 8601 strings (server converts Date → ISO at the mapper layer).
6
+ * - Use Public → View → Admin inheritance where it adds value.
7
+ */
8
+ import { ChangeType, ContentType, Criticality, DocumentStatus, DocumentType, ExternalLinkProvider, Purpose } from "../../constants/document.enums";
9
+ import { DocumentAudienceEntry } from "./requests";
10
+ /**
11
+ * Public Document Response
12
+ *
13
+ * Minimal fields. Used for related-doc previews in the viewer sidebar.
14
+ */
15
+ export interface PublicDocumentResponse {
16
+ id: string;
17
+ slug: string;
18
+ title: string;
19
+ code: string | null;
20
+ purpose: Purpose;
21
+ type: DocumentType;
22
+ status: DocumentStatus;
23
+ criticality: Criticality;
24
+ }
25
+ /**
26
+ * Document View Response
27
+ *
28
+ * Standard fields shown in lists and the viewer header. Used by all
29
+ * authenticated endpoints unless admin-only fields are needed.
30
+ */
31
+ export interface DocumentViewResponse extends PublicDocumentResponse {
32
+ tags: string[];
33
+ audience: DocumentAudienceEntry[];
34
+ requiredForOnboarding: boolean;
35
+ requiresAcknowledgment: boolean;
36
+ references: string[];
37
+ /** ObjectId of the current published version (or null for draft-only docs). */
38
+ currentVersionId: string | null;
39
+ createdAt: string;
40
+ updatedAt: string;
41
+ createdBy: string;
42
+ updatedBy: string;
43
+ }
44
+ /**
45
+ * Admin Document Response
46
+ *
47
+ * Adds audit metadata. Used in admin tooling.
48
+ */
49
+ export interface AdminDocumentResponse extends DocumentViewResponse {
50
+ /** Total versions ever created (current + superseded + drafts). */
51
+ versionCount: number;
52
+ /** Count of users who have acked the current version (null if requiresAck=false). */
53
+ currentVersionAckCount: number | null;
54
+ }
55
+ /**
56
+ * Document Summary Response
57
+ *
58
+ * Compact card used in list views and Cmd+K search results. Excludes heavy
59
+ * fields like `references`, `audience`, audit metadata.
60
+ */
61
+ export interface DocumentSummaryResponse {
62
+ id: string;
63
+ slug: string;
64
+ title: string;
65
+ code: string | null;
66
+ purpose: Purpose;
67
+ type: DocumentType;
68
+ status: DocumentStatus;
69
+ criticality: Criticality;
70
+ tags: string[];
71
+ updatedAt: string;
72
+ /** Match snippet — only present in search responses. */
73
+ snippet?: string;
74
+ }
75
+ /**
76
+ * Document Version Response
77
+ *
78
+ * Full version row. Returned by the viewer, version history, and
79
+ * acknowledgment lookups.
80
+ */
81
+ export interface DocumentVersionResponse {
82
+ id: string;
83
+ documentId: string;
84
+ versionNumber: string;
85
+ status: "draft" | "in_review" | "current" | "superseded";
86
+ contentType: ContentType;
87
+ markdown: string | null;
88
+ externalLink: {
89
+ url: string;
90
+ provider: ExternalLinkProvider;
91
+ } | null;
92
+ changeType: ChangeType;
93
+ changeNotes: string | null;
94
+ createdAt: string;
95
+ createdBy: string;
96
+ publishedAt: string | null;
97
+ publishedBy: string | null;
98
+ }
99
+ /**
100
+ * Document Acknowledgment Response
101
+ *
102
+ * One ack record. Used in the admin "who acknowledged" view.
103
+ */
104
+ export interface DocumentAcknowledgmentResponse {
105
+ id: string;
106
+ documentId: string;
107
+ documentVersionId: string;
108
+ userId: string;
109
+ acknowledgedAt: string;
110
+ /** Distinguishes onboarding acks from voluntary ones. */
111
+ source: "voluntary" | "onboarding";
112
+ }
113
+ /**
114
+ * Pending Acknowledgment Response
115
+ *
116
+ * One row in the user-facing "pendientes de acuse" widget. Combines doc + version
117
+ * snapshot needed to render the card.
118
+ */
119
+ export interface PendingAcknowledgmentResponse {
120
+ documentId: string;
121
+ documentSlug: string;
122
+ documentTitle: string;
123
+ documentVersionId: string;
124
+ versionNumber: string;
125
+ criticality: Criticality;
126
+ /** Days since the version was published (used for the 7-day red badge). */
127
+ pendingDays: number;
128
+ isFromOnboarding: boolean;
129
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ /**
3
+ * Document API Response Contracts (GH#21)
4
+ *
5
+ * Shapes the backend returns. Per `api-contracts.md`:
6
+ * - Date fields are ISO 8601 strings (server converts Date → ISO at the mapper layer).
7
+ * - Use Public → View → Admin inheritance where it adds value.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -20,3 +20,4 @@ export * from './study-type-catalog';
20
20
  export * from './supplier-overlay';
21
21
  export * from './external-study';
22
22
  export * from './pending-dashboard';
23
+ export * from './document';
@@ -36,3 +36,4 @@ __exportStar(require("./study-type-catalog"), exports);
36
36
  __exportStar(require("./supplier-overlay"), exports);
37
37
  __exportStar(require("./external-study"), exports);
38
38
  __exportStar(require("./pending-dashboard"), exports);
39
+ __exportStar(require("./document"), exports);
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Document Validation (GH#21)
3
+ *
4
+ * Pure validation functions for document fields. Mirrors the pattern of
5
+ * `rfc.validation.ts`, `email.validation.ts`, etc. — returns
6
+ * `ValidationResult` so callers (Value Objects in backend, frontend Zod
7
+ * schemas, migration scripts) all share one source of truth.
8
+ */
9
+ import { ValidationResult } from "./rfc.validation";
10
+ /**
11
+ * Validates a document slug.
12
+ *
13
+ * Rules:
14
+ * - 3–80 chars
15
+ * - lowercase alphanumeric + hyphens only
16
+ * - no leading/trailing/consecutive hyphens
17
+ *
18
+ * @example
19
+ * validateDocumentSlug("protocolo-ego") // { isValid: true }
20
+ * validateDocumentSlug("Protocolo EGO") // { isValid: false, error: ... }
21
+ * validateDocumentSlug("ab") // { isValid: false, error: ... }
22
+ * validateDocumentSlug("foo--bar") // { isValid: false, error: ... }
23
+ */
24
+ export declare function validateDocumentSlug(value: string | null | undefined): ValidationResult;
25
+ /**
26
+ * Generates a slug candidate from arbitrary text. Caller is responsible for
27
+ * resolving collisions (typically by appending `-2`, `-3`, ...).
28
+ *
29
+ * NOTE: this is a best-effort transliteration. Spanish accents are mapped to
30
+ * their unaccented form. Other unicode is dropped.
31
+ */
32
+ export declare function slugifyTitle(title: string): string;
33
+ /**
34
+ * Validates a semver-style version number `major.minor.patch`.
35
+ *
36
+ * @example
37
+ * validateVersionNumber("1.0.0") // { isValid: true }
38
+ * validateVersionNumber("1.0") // { isValid: false }
39
+ * validateVersionNumber("v1.0.0") // { isValid: false }
40
+ */
41
+ export declare function validateVersionNumber(value: string | null | undefined): ValidationResult;
42
+ /**
43
+ * Parses a validated version string into its three numeric components.
44
+ * Throws if the input is invalid — call `validateVersionNumber` first.
45
+ */
46
+ export declare function parseVersionNumber(value: string): {
47
+ major: number;
48
+ minor: number;
49
+ patch: number;
50
+ };
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ /**
3
+ * Document Validation (GH#21)
4
+ *
5
+ * Pure validation functions for document fields. Mirrors the pattern of
6
+ * `rfc.validation.ts`, `email.validation.ts`, etc. — returns
7
+ * `ValidationResult` so callers (Value Objects in backend, frontend Zod
8
+ * schemas, migration scripts) all share one source of truth.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.validateDocumentSlug = validateDocumentSlug;
12
+ exports.slugifyTitle = slugifyTitle;
13
+ exports.validateVersionNumber = validateVersionNumber;
14
+ exports.parseVersionNumber = parseVersionNumber;
15
+ // ─── Slug ───────────────────────────────────────────────────────────────────
16
+ const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
17
+ const SLUG_MIN_LENGTH = 3;
18
+ const SLUG_MAX_LENGTH = 80;
19
+ /**
20
+ * Validates a document slug.
21
+ *
22
+ * Rules:
23
+ * - 3–80 chars
24
+ * - lowercase alphanumeric + hyphens only
25
+ * - no leading/trailing/consecutive hyphens
26
+ *
27
+ * @example
28
+ * validateDocumentSlug("protocolo-ego") // { isValid: true }
29
+ * validateDocumentSlug("Protocolo EGO") // { isValid: false, error: ... }
30
+ * validateDocumentSlug("ab") // { isValid: false, error: ... }
31
+ * validateDocumentSlug("foo--bar") // { isValid: false, error: ... }
32
+ */
33
+ function validateDocumentSlug(value) {
34
+ if (!value || !value.trim()) {
35
+ return { isValid: false, error: "El slug es requerido" };
36
+ }
37
+ const trimmed = value.trim();
38
+ if (trimmed.length < SLUG_MIN_LENGTH) {
39
+ return {
40
+ isValid: false,
41
+ error: `El slug debe tener al menos ${SLUG_MIN_LENGTH} caracteres`,
42
+ };
43
+ }
44
+ if (trimmed.length > SLUG_MAX_LENGTH) {
45
+ return {
46
+ isValid: false,
47
+ error: `El slug no puede exceder ${SLUG_MAX_LENGTH} caracteres`,
48
+ };
49
+ }
50
+ if (!SLUG_PATTERN.test(trimmed)) {
51
+ return {
52
+ isValid: false,
53
+ error: "El slug solo puede contener minúsculas, números y guiones (sin espacios ni guiones consecutivos)",
54
+ };
55
+ }
56
+ return { isValid: true };
57
+ }
58
+ /**
59
+ * Generates a slug candidate from arbitrary text. Caller is responsible for
60
+ * resolving collisions (typically by appending `-2`, `-3`, ...).
61
+ *
62
+ * NOTE: this is a best-effort transliteration. Spanish accents are mapped to
63
+ * their unaccented form. Other unicode is dropped.
64
+ */
65
+ function slugifyTitle(title) {
66
+ return title
67
+ .toLowerCase()
68
+ .normalize("NFD")
69
+ .replace(/[̀-ͯ]/g, "") // strip accents
70
+ .replace(/ñ/g, "n")
71
+ .replace(/[^a-z0-9]+/g, "-")
72
+ .replace(/^-+|-+$/g, "")
73
+ .slice(0, SLUG_MAX_LENGTH)
74
+ .replace(/-+$/g, ""); // re-trim if slice landed on a hyphen
75
+ }
76
+ // ─── Version Number ─────────────────────────────────────────────────────────
77
+ const VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
78
+ /**
79
+ * Validates a semver-style version number `major.minor.patch`.
80
+ *
81
+ * @example
82
+ * validateVersionNumber("1.0.0") // { isValid: true }
83
+ * validateVersionNumber("1.0") // { isValid: false }
84
+ * validateVersionNumber("v1.0.0") // { isValid: false }
85
+ */
86
+ function validateVersionNumber(value) {
87
+ if (!value || !value.trim()) {
88
+ return { isValid: false, error: "El número de versión es requerido" };
89
+ }
90
+ if (!VERSION_PATTERN.test(value.trim())) {
91
+ return {
92
+ isValid: false,
93
+ error: "Versión inválida. Formato esperado: MAJOR.MINOR.PATCH (ej. 1.0.0)",
94
+ };
95
+ }
96
+ return { isValid: true };
97
+ }
98
+ /**
99
+ * Parses a validated version string into its three numeric components.
100
+ * Throws if the input is invalid — call `validateVersionNumber` first.
101
+ */
102
+ function parseVersionNumber(value) {
103
+ const result = validateVersionNumber(value);
104
+ if (!result.isValid) {
105
+ throw new Error(result.error ?? "Versión inválida");
106
+ }
107
+ const [major, minor, patch] = value.trim().split(".").map(Number);
108
+ return { major, minor, patch };
109
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const document_validation_1 = require("./document.validation");
4
+ describe("validateDocumentSlug", () => {
5
+ describe("valid", () => {
6
+ it("accepts a kebab-case word", () => {
7
+ expect((0, document_validation_1.validateDocumentSlug)("protocolo")).toEqual({ isValid: true });
8
+ });
9
+ it("accepts hyphens between segments", () => {
10
+ expect((0, document_validation_1.validateDocumentSlug)("protocolo-ego")).toEqual({ isValid: true });
11
+ });
12
+ it("accepts numbers", () => {
13
+ expect((0, document_validation_1.validateDocumentSlug)("manual-mpp-v20")).toEqual({ isValid: true });
14
+ });
15
+ it("accepts the minimum length", () => {
16
+ expect((0, document_validation_1.validateDocumentSlug)("abc")).toEqual({ isValid: true });
17
+ });
18
+ });
19
+ describe("invalid", () => {
20
+ it("rejects empty", () => {
21
+ expect((0, document_validation_1.validateDocumentSlug)("")).toMatchObject({ isValid: false });
22
+ expect((0, document_validation_1.validateDocumentSlug)(" ")).toMatchObject({ isValid: false });
23
+ expect((0, document_validation_1.validateDocumentSlug)(null)).toMatchObject({ isValid: false });
24
+ expect((0, document_validation_1.validateDocumentSlug)(undefined)).toMatchObject({ isValid: false });
25
+ });
26
+ it("rejects too short", () => {
27
+ expect((0, document_validation_1.validateDocumentSlug)("ab")).toMatchObject({ isValid: false });
28
+ });
29
+ it("rejects too long", () => {
30
+ expect((0, document_validation_1.validateDocumentSlug)("a".repeat(81))).toMatchObject({
31
+ isValid: false,
32
+ });
33
+ });
34
+ it("rejects uppercase", () => {
35
+ expect((0, document_validation_1.validateDocumentSlug)("Protocolo")).toMatchObject({
36
+ isValid: false,
37
+ });
38
+ });
39
+ it("rejects spaces", () => {
40
+ expect((0, document_validation_1.validateDocumentSlug)("protocolo ego")).toMatchObject({
41
+ isValid: false,
42
+ });
43
+ });
44
+ it("rejects underscores", () => {
45
+ expect((0, document_validation_1.validateDocumentSlug)("protocolo_ego")).toMatchObject({
46
+ isValid: false,
47
+ });
48
+ });
49
+ it("rejects leading/trailing hyphens", () => {
50
+ expect((0, document_validation_1.validateDocumentSlug)("-protocolo")).toMatchObject({
51
+ isValid: false,
52
+ });
53
+ expect((0, document_validation_1.validateDocumentSlug)("protocolo-")).toMatchObject({
54
+ isValid: false,
55
+ });
56
+ });
57
+ it("rejects consecutive hyphens", () => {
58
+ expect((0, document_validation_1.validateDocumentSlug)("foo--bar")).toMatchObject({
59
+ isValid: false,
60
+ });
61
+ });
62
+ it("rejects accented characters", () => {
63
+ expect((0, document_validation_1.validateDocumentSlug)("protocolo-ñ")).toMatchObject({
64
+ isValid: false,
65
+ });
66
+ });
67
+ });
68
+ });
69
+ describe("slugifyTitle", () => {
70
+ it("converts spaces to hyphens", () => {
71
+ expect((0, document_validation_1.slugifyTitle)("Protocolo EGO")).toBe("protocolo-ego");
72
+ });
73
+ it("strips Spanish accents", () => {
74
+ expect((0, document_validation_1.slugifyTitle)("Anestésica común")).toBe("anestesica-comun");
75
+ });
76
+ it("converts ñ to n", () => {
77
+ expect((0, document_validation_1.slugifyTitle)("Compañeros")).toBe("companeros");
78
+ });
79
+ it("collapses repeated hyphens", () => {
80
+ expect((0, document_validation_1.slugifyTitle)("foo bar — baz")).toBe("foo-bar-baz");
81
+ });
82
+ it("trims leading/trailing hyphens", () => {
83
+ expect((0, document_validation_1.slugifyTitle)("--foo--")).toBe("foo");
84
+ });
85
+ it("preserves digits", () => {
86
+ expect((0, document_validation_1.slugifyTitle)("Manual MPP v20")).toBe("manual-mpp-v20");
87
+ });
88
+ it("returns a slug that passes validateDocumentSlug for typical titles", () => {
89
+ const cases = [
90
+ "Protocolo para Examen General de Orina (EGO)",
91
+ "Reunión enero 2020",
92
+ "Política interna de venta de medicamentos",
93
+ "HVP Web - Módulo documentación",
94
+ ];
95
+ for (const title of cases) {
96
+ const slug = (0, document_validation_1.slugifyTitle)(title);
97
+ expect((0, document_validation_1.validateDocumentSlug)(slug).isValid).toBe(true);
98
+ }
99
+ });
100
+ });
101
+ describe("validateVersionNumber", () => {
102
+ it("accepts valid semver", () => {
103
+ expect((0, document_validation_1.validateVersionNumber)("1.0.0")).toEqual({ isValid: true });
104
+ expect((0, document_validation_1.validateVersionNumber)("12.34.56")).toEqual({ isValid: true });
105
+ });
106
+ it("rejects empty", () => {
107
+ expect((0, document_validation_1.validateVersionNumber)("")).toMatchObject({ isValid: false });
108
+ expect((0, document_validation_1.validateVersionNumber)(null)).toMatchObject({ isValid: false });
109
+ expect((0, document_validation_1.validateVersionNumber)(undefined)).toMatchObject({ isValid: false });
110
+ });
111
+ it("rejects 2-part versions", () => {
112
+ expect((0, document_validation_1.validateVersionNumber)("1.0")).toMatchObject({ isValid: false });
113
+ });
114
+ it("rejects v-prefix", () => {
115
+ expect((0, document_validation_1.validateVersionNumber)("v1.0.0")).toMatchObject({ isValid: false });
116
+ });
117
+ it("rejects non-numeric segments", () => {
118
+ expect((0, document_validation_1.validateVersionNumber)("1.0.x")).toMatchObject({ isValid: false });
119
+ });
120
+ });
121
+ describe("parseVersionNumber", () => {
122
+ it("parses each segment", () => {
123
+ expect((0, document_validation_1.parseVersionNumber)("1.2.3")).toEqual({
124
+ major: 1,
125
+ minor: 2,
126
+ patch: 3,
127
+ });
128
+ });
129
+ it("throws on invalid input", () => {
130
+ expect(() => (0, document_validation_1.parseVersionNumber)("1.0")).toThrow();
131
+ });
132
+ });
@@ -8,3 +8,4 @@ export * from './nss.validation';
8
8
  export * from './email.validation';
9
9
  export * from './phone.validation';
10
10
  export * from './address.validation';
11
+ export * from './document.validation';
@@ -24,3 +24,4 @@ __exportStar(require("./nss.validation"), exports);
24
24
  __exportStar(require("./email.validation"), exports);
25
25
  __exportStar(require("./phone.validation"), exports);
26
26
  __exportStar(require("./address.validation"), exports);
27
+ __exportStar(require("./document.validation"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hvp-shared",
3
- "version": "7.6.0",
3
+ "version": "7.7.0",
4
4
  "description": "Shared types and utilities for HVP backend and frontend",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",