File size: 35,731 Bytes
f8b48da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
"""Generate the AskMyDocs enterprise documentation corpus.



Produces a mixed-format tree under ``data/corpus/``:



    data/corpus/

    ├── markdown/    ~30 docs

    ├── pdf/         5 docs (authentication-guide, api-reference, security-guide,

                     deployment-guide, troubleshooting-guide)

    ├── docx/        ~5 docs

    └── html/        ~6 docs



Run:

    python scripts/generate_corpus.py [--out data/corpus] [--seed 42]

"""

from __future__ import annotations

import argparse
import sys
from collections.abc import Callable
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))

from corpus_content import FACTS, create_auth_context, doc  # noqa: E402
from corpus_shims import (  # noqa: E402
    code_block,
    h2,
    note_box,
    para,
    table,
    write_doc,
)
from pdf_docx_render import render_docx, render_html, render_pdf  # noqa: E402

DATA_DIR = Path(__file__).resolve().parent.parent / "data" / "corpus"


# --------------------------------------------------------------------------
# Section builders (return markdown fragments)
# --------------------------------------------------------------------------


def sec_auth_intro() -> str:
    return (
        para("This guide describes how authentication works for the AskMyDocs platform.")
        + para(
            "Every API request must present credentials. AskMyDocs supports "
            "OAuth 2.0 bearer tokens as the primary mechanism for service "
            "accounts and API keys for long-running integrations."
        )
        + para(
            "The platform issues tokens through a dedicated Identity Service. "
            "The Identity Service is the only component allowed to mint or "
            "refresh tokens. Other services validate tokens against the "
            "Identity Service's public key endpoint."
        )
    )


def sec_auth_tokens() -> str:
    ctx = create_auth_context()
    return (
        h2("Token Lifecycle")
        + para(
            f"Access tokens are short-lived. By default, an access token expires after "
            f"{ctx['token_expiry']}. Refresh tokens are long-lived and expire after "
            f"{ctx['refresh_token_expiry']}."
        )
        + table(
            ["Token type", "Lifetime", "Transport"],
            [
                ["Access token (JWT)", ctx["token_expiry"], "Authorization header"],
                ["Refresh token (opaque)", ctx["refresh_token_expiry"], "Secure cookie or POST body"],
            ],
        )
        + para(
            "When an access token expires, the client must use the refresh token "
            "to obtain a new one. The refresh flow is described in the OAuth "
            "guide."
        )
        + note_box(
            "warning",
            "Never store refresh tokens in browser local storage. Use a secure, "
            "HttpOnly cookie.",
        )
    )


def sec_auth_api_keys() -> str:
    return (
        h2("API Keys")
        + para(
            "API keys are the recommended credential for server-to-server "
            "integrations. Keys are prefixed with "
            f"``{FACTS['api_key_prefix']}`` and are shown only once at creation time."
        )
        + para(
            "An API key can be scoped to a set of permissions using the "
            "permissions claim. Scoping keys is strongly recommended."
        )
        + para(
            f"Keys expire after {FACTS['api_key_lifetime']} and can be revoked at any time from the Admin Console."
        )
        + code_block(
            "bash",
            "# Example: authenticate with an API key\n"
            'curl -H "Authorization: Bearer amsk_live_abc123" \\\n'
            "  https://api.askmydocs.example/v1/projects",
        )
    )


def sec_auth_basic() -> str:
    return (
        h2("Basic Authentication for Legacy Integrations")
        + para(
            "Basic authentication is supported only for legacy integrations and "
            "is disabled by default. Use OAuth 2.0 or API keys for new "
            "integrations."
        )
        + note_box(
            "note",
            "Basic auth is not available for accounts enrolled in SSO.",
        )
    )


def sec_auth_errors() -> str:
    return (
        h2("Authentication Errors")
        + para("The Identity Service returns the following error codes:")
        + table(
            ["Error code", "HTTP status", "Meaning"],
            [
                ["invalid_client", "401", "Client id or secret is invalid"],
                ["invalid_grant", "400", "Refresh token is invalid or expired"],
                ["invalid_token", "401", "Access token is invalid or expired"],
                ["insufficient_scope", "403", "Token lacks required scope"],
                ["temporarily_unavailable", "503", "Identity Service is unavailable"],
            ],
        )
        + note_box(
            "tip",
            "Clients should treat 401 responses as a signal to re-authenticate, "
            "not to retry the same request.",
        )
    )


def sec_mfa() -> str:
    return (
        h2("Multi-Factor Authentication (MFA)")
        + para(
            "MFA adds a second verification factor on top of a password. "
            "Supported factors: TOTP authenticator apps, SMS, and WebAuthn "
            "security keys."
        )
        + para(
            "MFA is required for administrators by default. Users can enroll "
            "multiple devices, but only the most recently enrolled device is "
            "active at any time."
        )
    )


def sec_security_encryption() -> str:
    return (
        h2("Encryption")
        + para(
            f"All data at rest is encrypted with {FACTS['pii_encryption']}. "
            "Data in transit uses TLS 1.2 or later."
        )
        + para(
            "Customer encryption keys are supported for enterprise plans. "
            "If you provision a customer key, AskMyDocs cannot recover data if "
            "the key is lost."
        )
    )


def sec_security_audit() -> str:
    return (
        h2("Audit Logging")
        + para(
            "Security-relevant events are recorded in the audit log: sign-in, "
            "sign-out, permission changes, API key creation/revocation, and "
            "webhook secret rotation."
        )
        + para(
            "Audit logs are retained for 180 days by default and can be "
            "exported to an external SIEM through the Admin Console."
        )
    )


def sec_oauth_intro() -> str:
    return (
        h2("Overview")
        + para(
            f"The AskMyDocs platform implements {FACTS['oauth_provider']} to let "
            "third-party applications access customer resources on the user's "
            "behalf."
        )
        + para(
            "The authorization server is the Identity Service. It supports the "
            "Authorization Code grant flow with Proof Key for Code Exchange "
            "(PKCE), the Client Credentials grant, and the Refresh Token grant."
        )
    )


def sec_oauth_flows() -> str:
    return (
        h2("Supported Flows")
        + table(
            ["Flow", "Use case", "Actor"],
            [
                ["Authorization Code + PKCE", "Browser, mobile, SPAs", "End user"],
                ["Client Credentials", "Server-to-server", "Service account"],
                ["Refresh Token", "Rotate access token", "End user or service"],
            ],
        )
        + para(
            "For browser-based applications, the Authorization Code flow with "
            "PKCE is recommended. The implicit flow is not supported."
        )
    )


def sec_oauth_scopes() -> str:
    return (
        h2("Scopes")
        + para(
            "Scopes are space-delimited strings. The platform defines the "
            "following standard scopes:"
        )
        + table(
            ["Scope", "Grants"],
            [
                ["openid", "Identity information"],
                ["profile", "Read user profile"],
                ["documents:read", "Read documents"],
                ["documents:write", "Create, edit, delete documents"],
                ["admin", "Admin Console access (superuser)"],
            ],
        )
        + para(
            "Scopes are enforced by the API gateway. A token without the "
            "``documents:write`` scope receives 403 for write operations."
        )
    )


def sec_oauth_claims() -> str:
    return (
        h2("Token Claims")
        + para(
            "Access tokens are JWTs signed with RS256. Standard claims: "
            "``iss``, ``sub``, ``aud``, ``exp``, ``iat``, ``scope``, and "
            "``permissions``."
        )
        + para(
            "The ``permissions`` claim is an array of permission strings such "
            "as ``documents:read``. Services must validate the signature using "
            "the Identity Service JWKS endpoint before trusting any claim."
        )
    )


def sec_oauth_discovery() -> str:
    return (
        h2("Discovery")
        + para("OAuth configuration is published at the well-known endpoint:")
        + code_block(
            "text",
            "https://identity.askmydocs.example/.well-known/openid-configuration",
        )
        + para(
            "The discovery document lists authorization, token, revocation, and "
            "JWKS endpoints. Clients must fetch this document at startup and "
            "honor the ``expiration`` hints."
        )
    )


def sec_oauth_refresh() -> str:
    return (
        h2("Refresh Token Rotation")
        + para(
            f"Refresh tokens expire after {FACTS['refresh_token_expiry']}. "
            "AskMyDocs rotates refresh tokens on every use: the previous "
            "refresh token is invalidated immediately."
        )
        + para(
            "If a rotated refresh token is used again (token replay), the "
            "entire session is revoked and the user is forced to "
            "re-authenticate. Clients must therefore persist each new refresh "
            "token before using the access token it returns."
        )
    )


def sec_oauth_sso() -> str:
    return (
        h2("SSO and Federation")
        + para(
            "Enterprise SSO is available on the Enterprise plan. AskMyDocs "
            f"supports {FACTS['sso_saml']} and {FACTS['sso_oidc']} identity "
            "providers."
        )
        + para(
            "When SSO is enabled, password authentication is disabled for all "
            "users in that organization. SCIM is supported for automatic "
            "user provisioning and deprovisioning."
        )
    )


def sec_api_terminal() -> str:
    return (
        h2("API Terminal")
        + para("All API requests must use HTTPS.")
        + code_block(
            "text",
            "https://api.askmydocs.example/v1",
        )
        + para(
            "The API is versioned in the URL path. The current version is v1. "
            "Breaking changes are announced at least 6 months in advance."
        )
    )


def sec_api_auth_header() -> str:
    return (
        h2("Authentication")
        + para(
            "Every request must include an Authorization header with a bearer "
            "token:"
        )
        + code_block(
            "http",
            'GET /v1/projects HTTP/1.1\nHost: api.askmydocs.example\nAuthorization: Bearer eyJhbGciOiJSUzI1NiIs...',
        )
        + para(
            f"Tokens expire after {FACTS['token_expiry']}. Use the refresh "
            "flow before expiry to avoid 401 responses."
        )
    )


def sec_api_pagination() -> str:
    return (
        h2("Pagination")
        + para(
            "List endpoints paginate using cursor-based pagination. The response "
            "contains a ``next_cursor`` field; pass it as the ``cursor`` "
            "parameter to fetch the next page."
        )
        + table(
            ["Parameter", "Type", "Description"],
            [
                ["limit", "int", "Max items per page (1-100, default 20)"],
                ["cursor", "string", "Opaque pagination cursor"],
            ],
        )
    )


def sec_api_idempotency() -> str:
    return (
        h2("Idempotency")
        + para(
            "Write endpoints accept an ``Idempotency-Key`` header. If a "
            "request with the same key is retried, the server returns the "
            "original response without applying the change twice."
        )
        + para(
            "Idempotency keys are honored for 24 hours. Use UUID v4 values."
        )
    )


def sec_api_rate_limits() -> str:
    return (
        h2("Rate Limits")
        + para(
            f"The default rate limit is {FACTS['rate_limit_default']}."
            f"Short bursts up to {FACTS['rate_limit_burst']} are allowed."
        )
        + para(
            "Rate limits apply per API key, per endpoint. Responses include "
            "``X-RateLimit-Limit``, ``X-RateLimit-Remaining``, and "
            "``X-RateLimit-Reset`` headers."
        )
        + note_box(
            "warning",
            "Hitting the rate limit returns HTTP 429 with a ``Retry-After`` "
            "header. Respect the header; do not retry immediately.",
        )
    )


def sec_api_webhooks() -> str:
    return (
        h2("Webhooks")
        + para(
            "Webhooks notify your system about events such as document "
            "processed, document failed, and ingestion complete."
        )
        + para(
            f"Each delivery is signed with {FACTS['webhook_delivery']} using "
            "the webhook secret. The signature is sent in the "
            "``X-AskMyDocs-Signature`` header as ``t=<timestamp>,v1=<hex>``."
        )
        + para(
            f"Delivery is retried up to {FACTS['webhook_retry']}."
        )
        + code_block(
            "python",
            "# Verify a webhook signature\n"
            "import hashlib, hmac\n\n"
            "def verify(secret, body, header):\n"
            "    timestamp, signature = header.split(',')\n"
            "    digest = hmac.new(secret.encode(), f'{timestamp}.{body}'.encode(), hashlib.sha256).hexdigest()\n"
            "    return hmac.compare_digest(digest, signature.split('=')[1])",
        )
    )


def sec_api_headers() -> str:
    return (
        h2("Common Headers")
        + table(
            ["Header", "Required", "Description"],
            [
                ["Authorization", "Yes", "Bearer token"],
                ["Idempotency-Key", "Write ops", "Prevent duplicate writes"],
                ["X-Request-ID", "No", "Correlate logs; echoed in response"],
                ["Accept-Version", "No", "Pin API version (default v1)"],
            ],
        )
    )


def sec_errors_summary() -> str:
    return (
        h2("Error Response Format")
        + para("All errors use a consistent JSON envelope:")
        + code_block(
            "json",
            '{\n  "error": {\n    "code": "rate_limit_exceeded",\n'
            '    "message": "Rate limit exceeded. Retry after 5 seconds.",\n'
            '    "request_id": "req_9f1c",\n    "details": {}\n  }\n}',
        )
        + para(
            "The ``code`` field is a stable machine-readable string. The "
            "``message`` is human-readable and may change."
        )
    )


def sec_errors_codes() -> str:
    return (
        h2("Error Codes")
        + table(
            ["HTTP", "Code", "Common cause"],
            [
                ["400", "invalid_request", "Malformed request body"],
                ["401", "unauthenticated", "Missing or invalid token"],
                ["403", "permission_denied", "Token lacks required scope"],
                ["404", "not_found", "Resource does not exist"],
                ["409", "conflict", "Resource already exists or state conflict"],
                ["422", "validation_failed", "Request body failed validation"],
                ["429", "rate_limit_exceeded", "Too many requests"],
                ["5xx", "internal_error", "Server-side failure"],
            ],
        )
        + para(
            "Retryable errors are those in the 5xx range, 429, and 408. "
            "Client code should only retry those statuses, with exponential "
            "backoff and jitter."
        )
    )


def sec_errors_troubleshoot() -> str:
    return (
        h2("Troubleshooting Common Errors")
        + para("**401 unauthenticated**: verify the token is not expired and is sent in the Authorization header.")
        + para("**403 permission_denied**: the token lacks the required scope. Request the scope in the OAuth consent screen.")
        + para("**429 rate_limit_exceeded**: inspect the Retry-After header and back off.")
        + para("**5xx internal_error**: check the status page; retry with exponential backoff.")
    )


def sec_config_env() -> str:
    return (
        h2("Environment Variables")
        + para("The platform is configured through environment variables. Key variables:")
        + table(
            ["Variable", "Default", "Description"],
            [
                ["APP_ENV", "development", "Runtime environment"],
                ["DATABASE_URL", "", "PostgreSQL connection string"],
                ["QDRANT_URL", "http://localhost:6333", "Qdrant vector database"],
                ["OPENSEARCH_URL", "http://localhost:9200", "OpenSearch BM25 index"],
                ["EMBEDDING_MODEL", "BAAI/bge-m3", "Embedding model"],
                ["RERANKER_MODEL", "cross-encoder/mmarco-mMiniLMv2-L12-H384-v1", "Cross-encoder reranker"],
                ["OPENROUTER_API_KEY", "", "LLM provider key"],
                ["CHUNK_SIZE", "512", "Chunk size (tokens)"],
                ["CHUNK_OVERLAP", "64", "Chunk overlap"],
            ],
        )
        + note_box(
            "tip",
            "Never commit secrets to version control. Use a secret manager in "
            "production.",
        )
    )


def sec_config_flags() -> str:
    return (
        h2("Feature Flags")
        + para(
            "Flags are evaluated server-side. Flags are boolean by default; "
            "some flags accept a percentage rollout or a targeting rule."
        )
        + table(
            ["Flag", "Default", "Effect"],
            [
                ["rag.hybrid_search", "true", "Enable hybrid BM25 + vector retrieval"],
                ["rag.reranker", "true", "Enable cross-encoder reranking"],
                ["citations.require", "true", "Require citations on answers"],
                ["experimental.new_parser", "false", "Use new document parser"],
            ],
        )
    )


def sec_deploy_overview() -> str:
    return (
        h2("Deployment Overview")
        + para(
            f"The AskMyDocs platform has a guaranteed uptime SLA of {FACTS['sla_uptime']} "
            "for the Enterprise plan."
        )
        + para(
            "The standard topology deploys the API service, the ingestion "
            "worker, and the Identity Service. The ingestion worker processes "
            "documents asynchronously from a queue."
        )
        + table(
            ["Component", "Purpose"],
            [
                ["api", "Serves the REST API"],
                ["ingestion-worker", "Parses and indexes documents"],
                ["identity", "OAuth tokens and user management"],
                ["postgres", "Application metadata"],
                ["qdrant", "Vector search"],
                ["opensearch", "BM25 full-text search"],
            ],
        )
    )


def sec_deploy_envs() -> str:
    return (
        h2("Environments")
        + para("The platform supports three deployment environments.")
        + table(
            ["Environment", "Purpose", "Data isolation"],
            [
                ["development", "Local iteration", "Full isolation"],
                ["staging", "Pre-production validation", "Shared test data"],
                ["production", "Live traffic", "Full isolation"],
            ],
        )
        + note_box(
            "warning",
            "Do not use production credentials in development or staging.",
        )
    )


def sec_deploy_migrations() -> str:
    return (
        h2("Database Migrations")
        + para(
            "Schema changes are applied through versioned migrations. "
            "Migrations run automatically at deploy time before new code is "
            "released."
        )
        + para(
            "Downgrades are not supported automatically. Always back up before "
            "deploying a migration."
        )
    )


def sec_monitoring_metrics() -> str:
    return (
        h2("Metrics")
        + para("The platform exposes Prometheus metrics at ``/metrics``.")
        + table(
            ["Metric", "Type", "Description"],
            [
                ["api_requests_total", "counter", "Total API requests by route and status"],
                ["api_request_duration_seconds", "histogram", "Request latency"],
                ["ingestion_documents_processed_total", "counter", "Documents ingested"],
                ["retrieval_latency_seconds", "histogram", "Retrieval pipeline latency"],
                ["verification_success_rate", "histogram", "Citation validation pass rate"],
            ],
        )
        + note_box(
            "note",
            "Metrics are retained for 30 days. Alerts should aggregate over 5-minute windows.",
        )
    )


def sec_monitoring_alerts() -> str:
    return (
        h2("Alerts")
        + para("Recommended alerting thresholds:")
        + table(
            ["Alert", "Threshold", "Severity"],
            [
                ["Error rate", "> 1% over 5 minutes", "critical"],
                ["p95 latency", "> 2s over 5 minutes", "warning"],
                ["Queue depth", "> 5000 messages", "critical"],
                ["Verification success rate", "< 95% over 1 hour", "warning"],
            ],
        )
    )


def sec_users_roles() -> str:
    return (
        h2("Roles")
        + para(
            "Permissions are assigned through roles. The platform ships with "
            "four default roles."
        )
        + table(
            ["Role", "Permissions"],
            [
                ["owner", "Full access including billing"],
                ["admin", "Manage users, projects, and settings"],
                ["editor", "Create and edit documents"],
                [FACTS["rbac_default_role"], "Read-only access"],
            ],
        )
        + para(
            "Roles can be scoped per project. Custom roles can be defined with "
            "an arbitrary combination of permissions."
        )
    )


def sec_users_scim() -> str:
    return (
        h2("SCIM Provisioning")
        + para(
            "SCIM 2.0 is supported on the Enterprise plan. When users are "
            "deprovisioned in the identity provider, their access is revoked "
            "within 5 minutes."
        )
    )


def sec_db_behavior() -> str:
    return (
        h2("Database Behavior")
        + para(
            f"The platform stores metadata in {FACTS['database_engine']}. "
            "The connection pool has a maximum of "
            f"{FACTS['db_connection_max']} connections per instance."
        )
        + para(
            "The database stores documents, chunks, ingestion jobs, queries, "
            "and evaluation runs. Vector embeddings are stored in Qdrant, not "
            "in PostgreSQL."
        )
        + para(
            "Backups are taken every 12 hours and retained for "
            f"{FACTS['backup_retention']}."
        )
    )


def sec_troubleshooting_common() -> str:
    return (
        h2("Common Issues")
        + para(
            "This section covers frequently reported issues and their "
            "resolutions."
        )
        + table(
            ["Symptom", "Likely cause", "Resolution"],
            [
                [
                    "401 from all endpoints",
                    "Expired access token",
                    "Refresh the token before expiry",
                ],
                [
                    "Documents not searchable after upload",
                    "Ingestion job failed or still running",
                    "Check ingestion status; retry the job",
                ],
                [
                    "Slow retrieval",
                    "Reranker enabled on large candidate sets",
                    "Reduce BM25/VECTOR_TOP_K",
                ],
                [
                    "429 responses",
                    "Rate limit exceeded",
                    "Back off using Retry-After",
                ],
                [
                    "Answers marked ungrounded",
                    "Evidence insufficient or citation mismatch",
                    "Re-ask with more specific wording",
                ],
            ],
        )
    )


def sec_troubleshooting_secrets() -> str:
    return (
        h2("Rotating Secrets")
        + para(
            "Webhook secrets can be rotated from the Admin Console. After "
            "rotation, signatures generated with the old secret fail "
            "verification immediately. Rotate during low-traffic windows."
        )
        + para(
            "API keys are single-secret; rotating an API key invalidates the "
            "previous key immediately."
        )
    )


def sec_limits_quotas() -> str:
    return (
        h2("Quotas by Plan")
        + para("Rate limits and quotas vary by plan.")
        + table(
            ["Plan", "Default rate", "Burst", "Max documents"],
            [
                ["Free", "30 req/min", "60 req/min", "100"],
                ["Pro", FACTS["rate_limit_default"], FACTS["rate_limit_burst"], "10,000"],
                ["Enterprise", "1,000 req/min", "2,000 req/min", "Unlimited"],
            ],
        )
    )


def sec_webhooks_events() -> str:
    return (
        h2("Webhook Events")
        + para("The platform emits the following events.")
        + table(
            ["Event", "Trigger"],
            [
                ["document.uploaded", "A document was uploaded"],
                ["document.ingested", "Ingestion completed successfully"],
                ["document.failed", "Ingestion failed"],
                ["query.completed", "A query completed with citations"],
            ],
        )
    )


def sec_webhooks_endpoints() -> str:
    return (
        h2("Registering Endpoints")
        + para(
            "Register up to 10 webhook endpoints per project from the Admin "
            "Console. Each endpoint can filter events by type."
        )
        + para(
            "Endpoints that fail 8 consecutive deliveries are automatically "
            "disabled and an alert is raised."
        )
    )


# --------------------------------------------------------------------------
# Document assembly
# --------------------------------------------------------------------------


def build_documents() -> dict[str, dict[str, object]]:
    """Return ``{doc_id: {"title", "module", "content"}}`` for all documents."""
    docs: dict[str, dict[str, object]] = {}

    def add(doc_id: str, title: str, module: str, *sections: Callable[[], str]) -> None:
        docs[doc_id] = {
            "title": title,
            "module": module,
            "content": doc(doc_id, title, "1.0", module, *sections),
        }

    # --- Authentication & security ---
    add(
        "authentication-guide",
        "Authentication Guide",
        "authentication",
        lambda: (sec_auth_intro() + sec_auth_tokens() + sec_auth_api_keys() + sec_auth_basic() + sec_auth_errors()),
    )
    add("mfa-guide", "Multi-Factor Authentication", "authentication", lambda: (sec_mfa() + sec_auth_errors()))
    add("security-guide", "Security Guide", "security", lambda: (sec_security_encryption() + sec_security_audit() + sec_auth_errors()))
    add("api-keys", "API Keys Management", "authentication", lambda: (sec_auth_api_keys() + sec_auth_basic()))
    add("token-refresh", "Token Refresh and Rotation", "authentication", lambda: (sec_oauth_refresh() + sec_auth_tokens()))

    # --- OAuth / SSO ---
    add(
        "oauth-guide",
        "OAuth 2.0 Integration Guide",
        "oauth",
        lambda: (sec_oauth_intro() + sec_oauth_flows() + sec_oauth_scopes() + sec_oauth_claims() + sec_oauth_discovery() + sec_oauth_refresh()),
    )
    add("sso-saml", "SAML 2.0 Enterprise SSO", "oauth", lambda: (sec_oauth_sso() + sec_oauth_discovery()))
    add("oauth-sso", "SSO and Federation", "oauth", lambda: (sec_oauth_sso() + sec_oauth_discovery()))
    add("scopes", "OAuth Scopes and Permissions", "oauth", lambda: (sec_oauth_scopes() + sec_oauth_claims()))

    # --- API ---
    add(
        "api-reference",
        "API Reference",
        "api",
        lambda: (sec_api_terminal() + sec_api_auth_header() + sec_api_pagination() + sec_api_idempotency() + sec_api_rate_limits() + sec_api_webhooks() + sec_api_headers()),
    )
    add(
        "api-errors",
        "API Error Reference",
        "api",
        lambda: (sec_errors_summary() + sec_errors_codes() + sec_errors_troubleshoot()),
    )
    add("api-pagination", "Pagination Guide", "api", lambda: (sec_api_pagination() + sec_api_headers()))
    add("api-idempotency", "Idempotency Guide", "api", lambda: (sec_api_idempotency() + sec_api_headers()))

    # --- Configuration ---
    add(
        "configuration-guide",
        "Configuration Guide",
        "configuration",
        lambda: (sec_config_env() + sec_config_flags()),
    )
    add("env-vars", "Environment Variables Reference", "configuration", lambda: (sec_config_env()))
    add("feature-flags", "Feature Flags", "configuration", lambda: (sec_config_flags()))

    # --- Deployment ---
    add(
        "deployment-guide",
        "Deployment Guide",
        "deployment",
        lambda: (sec_deploy_overview() + sec_deploy_envs() + sec_deploy_migrations()),
    )
    add("environments", "Environments", "deployment", lambda: (sec_deploy_envs() + sec_deploy_migrations()))

    # --- Monitoring ---
    add(
        "monitoring-guide",
        "Monitoring Guide",
        "monitoring",
        lambda: (sec_monitoring_metrics() + sec_monitoring_alerts()),
    )
    add("alerts-guide", "Alerting Guide", "monitoring", lambda: (sec_monitoring_alerts()))

    # --- User management ---
    add(
        "user-management",
        "User Management",
        "users",
        lambda: (sec_users_roles() + sec_users_scim()),
    )
    add("roles-permissions", "Roles and Permissions", "users", lambda: (sec_users_roles()))

    # --- Database ---
    add("database-behavior", "Database Behavior", "database", lambda: (sec_db_behavior()))
    add("backup-recovery", "Backup and Recovery", "database", lambda: (sec_db_behavior() + sec_deploy_migrations()))

    # --- Troubleshooting ---
    add(
        "troubleshooting-guide",
        "Troubleshooting Guide",
        "troubleshooting",
        lambda: (sec_troubleshooting_common() + sec_troubleshooting_secrets()),
    )
    add("rotating-secrets", "Rotating Secrets", "troubleshooting", lambda: (sec_troubleshooting_secrets()))

    # --- Rate limits ---
    add("rate-limits", "Rate Limits and Quotas", "rate-limits", lambda: (sec_api_rate_limits() + sec_limits_quotas()))

    # --- Webhooks ---
    add(
        "webhooks-guide",
        "Webhooks Guide",
        "webhooks",
        lambda: (sec_api_webhooks() + sec_webhooks_events() + sec_webhooks_endpoints()),
    )
    add("webhook-signing", "Webhook Signature Verification", "webhooks", lambda: (sec_api_webhooks()))

    return docs


# --------------------------------------------------------------------------
# Multi-format output
# --------------------------------------------------------------------------


def emit_files(docs: dict[str, dict[str, object]], out_root: Path) -> dict[str, Path]:
    """Write all documents to the four format directories.



    Returns a mapping of ``doc_id -> chosen primary path`` for the manifest.

    """
    markdown_dir = out_root / "markdown"
    pdf_dir = out_root / "pdf"
    docx_dir = out_root / "docx"
    html_dir = out_root / "html"
    for d in (markdown_dir, pdf_dir, docx_dir, html_dir):
        d.mkdir(parents=True, exist_ok=True)

    # Which documents become PDF/DOCX/HTML?
    pdf_set = {
        "authentication-guide",
        "api-reference",
        "security-guide",
        "deployment-guide",
        "troubleshooting-guide",
    }
    docx_set = {"oauth-guide", "configuration-guide", "monitoring-guide", "user-management", "webhooks-guide"}
    html_set = {"api-errors", "rate-limits", "environments", "roles-permissions", "sso-saml", "database-behavior"}

    manifest: dict[str, Path] = {}
    emitted: list[Path] = []
    for doc_id, meta in docs.items():
        content = str(meta["content"])
        md_path = markdown_dir / f"{doc_id}.md"
        write_doc(md_path, content)
        emitted.append(md_path)
        manifest[doc_id] = md_path

        if doc_id in pdf_set:
            emitted.append(render_pdf(content, md_path, output_dir=pdf_dir))
        if doc_id in docx_set:
            emitted.append(render_docx(content, md_path, output_dir=docx_dir))
        if doc_id in html_set:
            emitted.append(render_html(content, md_path, output_dir=html_dir))

    # Manifest for ingestion tests / reproducibility
    manifest_path = out_root / "manifest.json"
    manifest_json = {k: str(v.relative_to(out_root)) for k, v in sorted(manifest.items())}
    manifest_path.write_text(
        __import__("json").dumps(manifest_json, indent=2) + "\n", encoding="utf-8"
    )

    # Remove stale files from a previous run that are no longer produced.
    expected_names = {p.name for p in emitted}
    for d in (markdown_dir, pdf_dir, docx_dir, html_dir):
        for f in list(d.glob("*")):
            if f.is_file() and f.name not in expected_names:
                f.unlink()
    return manifest


def main() -> int:
    parser = argparse.ArgumentParser(description="Generate the AskMyDocs enterprise documentation corpus.")
    parser.add_argument("--out", default=str(DATA_DIR), help="Output directory (default: data/corpus)")
    args = parser.parse_args()
    out_root = Path(args.out).resolve()
    docs = build_documents()
    emit_files(docs, out_root)
    print(f"Generated {len(docs)} documents under {out_root}")
    for fmt in ("markdown", "pdf", "docx", "html"):
        d = out_root / fmt
        print(f"  {fmt}: {len(list(d.glob('*')))} file(s)")
    print(f"Manifest written to {out_root / 'manifest.json'}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())