diff --git a/.github/workflows/documents.yml b/.github/workflows/documents.yml new file mode 100644 index 0000000..23771b6 --- /dev/null +++ b/.github/workflows/documents.yml @@ -0,0 +1,63 @@ +name: Verify PDF publications + +on: + pull_request: + paths: + - '.github/workflows/documents.yml' + - 'source/chapters/**' + - 'source/pdf/**' + - 'docs/pdf/**' + push: + branches: + - main + paths: + - '.github/workflows/documents.yml' + - 'source/chapters/**' + - 'source/pdf/**' + - 'docs/pdf/**' + workflow_dispatch: + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Install document utilities and fonts + run: | + sudo apt-get update + sudo apt-get install --yes fonts-noto-mono fonts-texgyre poppler-utils + + - name: Install Pandoc 3.9.0.2 + run: | + curl --fail --location --silent --show-error \ + --output /tmp/pandoc.deb \ + https://github.com/jgm/pandoc/releases/download/3.9.0.2/pandoc-3.9.0.2-1-amd64.deb + echo 'ce4ac48f48aa7eadc1f5dbdf3449a1739f188ecb8c5421c5adc070fe7479e567 /tmp/pandoc.deb' \ + | sha256sum --check + sudo dpkg --install /tmp/pandoc.deb + + - uses: typst-community/setup-typst@v5 + with: + typst-version: 0.14.2 + + - name: Install Mermaid CLI 11.16.0 + run: npm install --global @mermaid-js/mermaid-cli@11.16.0 + + - name: Test Typst post-processing + run: python3 -m unittest discover -s source/pdf/tests -p 'test_*.py' + + - name: Rebuild and verify published PDFs + run: ./source/pdf/build-all.sh --check + + - name: Upload rebuilt PDFs on failure + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: rebuilt-pdfs + path: source/pdf/*.pdf + if-no-files-found: error + retention-days: 7 diff --git a/.gitignore b/.gitignore index eae8a84..1595bd8 100644 --- a/.gitignore +++ b/.gitignore @@ -20,8 +20,12 @@ source/pdf/* !source/pdf/metadata-suite-map.yaml !source/pdf/build-lite.sh !source/pdf/build-common.sh +!source/pdf/build-all.sh +!source/pdf/README.md !source/pdf/pandoc-typst-lite.typ !source/pdf/.gitignore +!source/pdf/tests/ +!source/pdf/tests/test_postprocess_typst.py .venv/ site/ __pycache__/ diff --git a/README.md b/README.md index 4b4ba03..d30bb37 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,16 @@ pip install mkdocs mkdocs-material mkdocs serve # http://127.0.0.1:8000 ``` +To rebuild and verify the public PDF suite (Pandoc 3.2+, Typst 0.14+, Mermaid +CLI 11+, and Poppler required): + +```bash +./source/pdf/build-all.sh --publish +``` + +See [the PDF pipeline guide](source/pdf/README.md) for profiles, accessibility +requirements, and reproducibility checks. + ## Licence See [About This Project](https://semanticdefects.foundryside.dev/reference/about/) for attribution and licence details. diff --git a/docs/pdf/document-suite-map.pdf b/docs/pdf/document-suite-map.pdf index 16c8b81..caebac9 100644 Binary files a/docs/pdf/document-suite-map.pdf and b/docs/pdf/document-suite-map.pdf differ diff --git a/docs/pdf/governing-ai-generated-code.pdf b/docs/pdf/governing-ai-generated-code.pdf index ce0c309..3dfed6e 100644 Binary files a/docs/pdf/governing-ai-generated-code.pdf and b/docs/pdf/governing-ai-generated-code.pdf differ diff --git a/docs/pdf/reviewing-ai-generated-code.pdf b/docs/pdf/reviewing-ai-generated-code.pdf index ef2cbeb..2954e84 100644 Binary files a/docs/pdf/reviewing-ai-generated-code.pdf and b/docs/pdf/reviewing-ai-generated-code.pdf differ diff --git a/docs/pdf/threat-model-discussion-paper-community.pdf b/docs/pdf/threat-model-discussion-paper-community.pdf index 2e706bd..8ab3f98 100644 Binary files a/docs/pdf/threat-model-discussion-paper-community.pdf and b/docs/pdf/threat-model-discussion-paper-community.pdf differ diff --git a/docs/pdf/wardline-companion-community.pdf b/docs/pdf/wardline-companion-community.pdf index d9a3593..54c446e 100644 Binary files a/docs/pdf/wardline-companion-community.pdf and b/docs/pdf/wardline-companion-community.pdf differ diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 7bb9df3..a30cb6d 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -2,6 +2,16 @@ Brand Theme for MkDocs Material ========================================================================== */ +/* -------------------------------------------------------------------------- + Texture tokens — grain on dark chrome, hairline blueprint grid on the hero + -------------------------------------------------------------------------- */ +:root { + --texture-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='0.16'/%3E%3C/svg%3E"); + --texture-grain-size: 120px 120px; + --texture-grid-dark: linear-gradient(rgba(255, 255, 255, 0.07) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, 0.07) 1px, transparent 1px); + --texture-grid-size: 32px 32px; +} + /* -------------------------------------------------------------------------- Scrollbar — wider for easier grab targets -------------------------------------------------------------------------- */ @@ -30,16 +40,18 @@ } /* -------------------------------------------------------------------------- - Brand Palette + Brand Palette — slate & cobalt recut -------------------------------------------------------------------------- - Signature green: #69EBA5 - Primary green: #29B480 - Dark green: #288264 - Primary navy: #1E3C50 - Light green: #C8EBD7 + Signature blue: #7FA8FF + Primary blue: #2E5BE3 + Dark blue: #2547B0 + Blue hover: #1F3D9C + Light blue: #C9D8F8 + Slate: #2A323D (light #3C4754, dark #1C2229) Body text: #414141 - Secondary gold: #FAD673 - Secondary salmon: #FFC2B0 + Secondary gold: #F0C75E + Secondary salmon: #FFB4A2 + Semantic green: #2F9E60 (reserved: low risk / good detection / core) Neutrals: #27292B, #70767B, #9FA4A7, #CACCCE, #F4F5F5 -------------------------------------------------------------------------- */ @@ -47,14 +59,14 @@ Light mode (default scheme) -------------------------------------------------------------------------- */ [data-md-color-scheme="default"] { - --md-primary-fg-color: #1E3C50; - --md-primary-fg-color--light: #2a5570; - --md-primary-fg-color--dark: #152d3d; + --md-primary-fg-color: #2A323D; + --md-primary-fg-color--light: #3C4754; + --md-primary-fg-color--dark: #1C2229; --md-primary-bg-color: #ffffff; --md-primary-bg-color--light: #F4F5F5; - --md-accent-fg-color: #29B480; - --md-accent-fg-color--transparent: rgba(41, 180, 128, 0.1); + --md-accent-fg-color: #2E5BE3; + --md-accent-fg-color--transparent: rgba(46, 91, 227, 0.1); --md-accent-bg-color: #ffffff; --md-default-fg-color: #414141; @@ -66,7 +78,7 @@ --md-default-bg-color--lighter: #F4F5F5; --md-default-bg-color--lightest: #ffffff; - --md-typeset-a-color: #1d5e49; + --md-typeset-a-color: #2444A8; --md-code-bg-color: #F4F5F5; --md-code-fg-color: #27292B; @@ -76,36 +88,47 @@ Dark mode (slate scheme) -------------------------------------------------------------------------- */ [data-md-color-scheme="slate"] { - --md-primary-fg-color: #1E3C50; - --md-primary-fg-color--light: #2a5570; - --md-primary-fg-color--dark: #152d3d; + --md-primary-fg-color: #2A323D; + --md-primary-fg-color--light: #3C4754; + --md-primary-fg-color--dark: #1C2229; --md-primary-bg-color: #ffffff; --md-primary-bg-color--light: #ffffffb3; - --md-accent-fg-color: #69EBA5; - --md-accent-fg-color--transparent: rgba(105, 235, 165, 0.1); + --md-accent-fg-color: #7FA8FF; + --md-accent-fg-color--transparent: rgba(127, 168, 255, 0.1); - --md-default-bg-color: #1a2631; - --md-default-bg-color--light: #1f3040; - --md-default-bg-color--lighter: #24384a; - --md-default-bg-color--lightest: #2a4155; + --md-default-bg-color: #161B22; + --md-default-bg-color--light: #1C232C; + --md-default-bg-color--lighter: #232C37; + --md-default-bg-color--lightest: #2A3542; --md-default-fg-color: #e0e4e7; --md-default-fg-color--light: #9FA4A7; --md-default-fg-color--lighter: #70767B; - --md-default-fg-color--lightest: #4a5258; + --md-default-fg-color--lightest: #454E58; - --md-typeset-a-color: #69EBA5; + --md-typeset-a-color: #7FA8FF; - --md-code-bg-color: #1f3040; - --md-code-fg-color: #C8EBD7; + --md-code-bg-color: #1C232C; + --md-code-fg-color: #C9D8F8; +} + +/* -------------------------------------------------------------------------- + Dark chrome texture — fine grain on header and tab bar + -------------------------------------------------------------------------- */ +.md-header, +.md-tabs { + background-image: var(--texture-grain); + background-size: var(--texture-grain-size); } /* -------------------------------------------------------------------------- Announcement bar — draft status banner -------------------------------------------------------------------------- */ .md-banner { - background-color: #29B480; + background-color: #2E5BE3; + background-image: var(--texture-grain); + background-size: var(--texture-grain-size); color: #ffffff; font-size: 0.7rem; font-weight: 600; @@ -119,14 +142,14 @@ } [data-md-color-scheme="slate"] .md-banner { - background-color: #237057; + background-color: #2547B0; } /* -------------------------------------------------------------------------- Classification bar — compact single-line banner -------------------------------------------------------------------------- */ .classification-bar { - background-color: #1E3C50; + background-color: #2A323D; color: #ffffff; text-align: center; padding: 0.3rem 1rem; @@ -142,17 +165,19 @@ } [data-md-color-scheme="slate"] .classification-bar { - background-color: #2a5570; + background-color: #3C4754; } /* -------------------------------------------------------------------------- - Hero section — landing page + Hero section — landing page (flat slate, grain + blueprint grid) -------------------------------------------------------------------------- */ .hero { text-align: center; padding: 2.5rem 1rem 2rem; margin: -0.6rem -0.6rem 2rem; - background: linear-gradient(135deg, #1E3C50 0%, #2a5570 100%); + background-color: #2A323D; + background-image: var(--texture-grain), var(--texture-grid-dark); + background-size: var(--texture-grain-size), var(--texture-grid-size); border-radius: 6px; color: #e0e4e7; } @@ -166,7 +191,7 @@ } .hero .hero-subtitle { - color: #C8EBD7; + color: #C9D8F8; font-size: 1rem; font-weight: 400; margin: 0 0 0.3rem; @@ -199,24 +224,24 @@ } .hero .hero-actions .md-button--primary { - background-color: #29B480; + background-color: #2E5BE3; color: #ffffff; - border: 2px solid #29B480; + border: 2px solid #2E5BE3; } .hero .hero-actions .md-button--primary:hover { - background-color: #237057; - border-color: #237057; + background-color: #1F3D9C; + border-color: #1F3D9C; } .hero .hero-actions .md-button--secondary { background-color: transparent; - color: #C8EBD7; - border: 2px solid #69EBA5; + color: #C9D8F8; + border: 2px solid #7FA8FF; } .hero .hero-actions .md-button--secondary:hover { - background-color: rgba(105, 235, 165, 0.12); + background-color: rgba(127, 168, 255, 0.12); color: #ffffff; } @@ -239,7 +264,7 @@ .grid.cards > ul > li:hover, .grid.cards > ol > li:hover { border-color: var(--md-accent-fg-color); - box-shadow: 0 2px 8px rgba(30, 60, 80, 0.08); + box-shadow: 0 2px 8px rgba(35, 45, 60, 0.08); } [data-md-color-scheme="slate"] .grid.cards > ul > li:hover, @@ -248,16 +273,16 @@ } /* -------------------------------------------------------------------------- - Navigation tabs — use navy background, green active indicator + Navigation tabs — slate background, signature blue active indicator -------------------------------------------------------------------------- */ .md-tabs { - background-color: #1E3C50; + background-color: #2A323D; } .md-tabs__link--active, .md-tabs__link:hover { - color: #69EBA5; - border-bottom: 2px solid #69EBA5; + color: #7FA8FF; + border-bottom: 2px solid #7FA8FF; } /* -------------------------------------------------------------------------- @@ -266,7 +291,7 @@ .md-header :focus-visible, .md-tabs :focus-visible, .md-footer :focus-visible { - outline: 3px solid #69EBA5; + outline: 3px solid #7FA8FF; outline-offset: 2px; } @@ -282,7 +307,7 @@ } /* -------------------------------------------------------------------------- - Content links — green with underline on hover (AAA contrast) + Content links — cobalt with underline (AAA contrast) -------------------------------------------------------------------------- */ .md-typeset a { color: var(--md-typeset-a-color); @@ -290,12 +315,12 @@ } .md-typeset a:hover { - color: #29B480; + color: #2E5BE3; text-decoration: underline; } [data-md-color-scheme="slate"] .md-typeset a:hover { - color: #C8EBD7; + color: #C9D8F8; } /* Don't underline card links or navigation links */ @@ -318,35 +343,35 @@ .md-typeset .admonition.tip, .md-typeset details.tip { - border-color: #29B480; + border-color: #2E5BE3; } .md-typeset .admonition.info, .md-typeset details.info { - border-color: #1E3C50; + border-color: #2A323D; } /* -------------------------------------------------------------------------- - Footer — navy background + Footer — slate background -------------------------------------------------------------------------- */ .md-footer { - background-color: #1E3C50; + background-color: #2A323D; } .md-footer-meta { - background-color: #152d3d; + background-color: #1C2229; } /* -------------------------------------------------------------------------- - Navigation sidebar — subtle green active state + Navigation sidebar — cobalt active state -------------------------------------------------------------------------- */ .md-nav__link--active { - color: #1d5e49; + color: #2444A8; font-weight: 600; } [data-md-color-scheme="slate"] .md-nav__link--active { - color: #69EBA5; + color: #7FA8FF; } /* -------------------------------------------------------------------------- @@ -360,12 +385,12 @@ Tables — brand-styled header -------------------------------------------------------------------------- */ .md-typeset table:not([class]) th { - background-color: #1E3C50; + background-color: #2A323D; color: #ffffff; } [data-md-color-scheme="slate"] .md-typeset table:not([class]) th { - background-color: #2a5570; + background-color: #3C4754; color: #e0e4e7; } diff --git a/reference-site/assets/_custom.scss b/reference-site/assets/_custom.scss index dbbc0ee..ebca8ea 100644 --- a/reference-site/assets/_custom.scss +++ b/reference-site/assets/_custom.scss @@ -4,14 +4,22 @@ ========================================================================== */ /* -------------------------------------------------------------------------- - Light mode variables (Hugo Book default) + Texture tokens — grain on dark chrome (sidebar) + -------------------------------------------------------------------------- */ +:root { + --texture-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='0.16'/%3E%3C/svg%3E"); + --texture-grain-size: 120px 120px; +} + +/* -------------------------------------------------------------------------- + Light mode variables (Hugo Book default) — slate & cobalt recut -------------------------------------------------------------------------- */ :root { --book-font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; --book-font-code: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; - /* Navy accent */ - --color-link: #29B480; + /* Cobalt accent */ + --color-link: #2444A8; --body-background: #ffffff; --body-font-color: #414141; @@ -19,7 +27,7 @@ --icon-filter: none; /* Table headers */ - --table-header-bg: #1E3C50; + --table-header-bg: #2A323D; --table-header-fg: #ffffff; /* Code blocks */ @@ -29,24 +37,24 @@ /* Infobox */ --infobox-bg: #F4F5F5; - --infobox-border: #1E3C50; - --infobox-header-bg: #1E3C50; + --infobox-border: #2A323D; + --infobox-header-bg: #2A323D; --infobox-header-fg: #ffffff; /* Risk level badges */ --risk-critical: #d32f2f; --risk-high: #e65100; - --risk-medium: #FAD673; - --risk-low: #29B480; + --risk-medium: #F0C75E; + --risk-low: #2F9E60; /* Detection badges */ --detection-none: #d32f2f; - --detection-partial: #FAD673; - --detection-good: #29B480; + --detection-partial: #F0C75E; + --detection-good: #2F9E60; --detection-na: #9FA4A7; /* Entry status */ - --status-core: #29B480; + --status-core: #2F9E60; --status-provisional: #9FA4A7; } @@ -56,20 +64,20 @@ We target both so manual dark mode toggle works correctly. -------------------------------------------------------------------------- */ @mixin dark-theme-vars { - --color-link: #69EBA5; - --body-background: #1a2631; + --color-link: #7FA8FF; + --body-background: #161B22; --body-font-color: #e0e4e7; - --gray-100: #1f3040; - --gray-200: #24384a; + --gray-100: #1C232C; + --gray-200: #232C37; --gray-500: #9FA4A7; - --infobox-bg: #1f3040; - --infobox-border: #2a5570; - --infobox-header-bg: #1E3C50; + --infobox-bg: #1C232C; + --infobox-border: #3C4754; + --infobox-header-bg: #2A323D; --infobox-header-fg: #e0e4e7; - --table-header-bg: #2a5570; + --table-header-bg: #3C4754; --table-header-fg: #e0e4e7; } @@ -86,10 +94,12 @@ body.dark { } /* -------------------------------------------------------------------------- - Sidebar — navy + Sidebar — slate with fine grain -------------------------------------------------------------------------- */ .book-menu { - background: #1E3C50; + background-color: #2A323D; + background-image: var(--texture-grain); + background-size: var(--texture-grain-size); nav { color: #e0e4e7; @@ -99,46 +109,51 @@ body.dark { color: #e0e4e7 !important; &:hover { - color: #69EBA5 !important; + color: #7FA8FF !important; } &.active { - color: #69EBA5 !important; + color: #7FA8FF !important; font-weight: 600; } } + + :focus-visible { + outline: 3px solid #7FA8FF; + outline-offset: 2px; + } } /* -------------------------------------------------------------------------- Header / brand area -------------------------------------------------------------------------- */ .book-brand { - background: #152d3d; + background: #1C2229; padding: 1rem; margin-bottom: 0.5rem; } /* -------------------------------------------------------------------------- - Links + Links — underlined in body content, cobalt -------------------------------------------------------------------------- */ .book-page a { color: var(--color-link); - text-decoration: none; + text-decoration: underline; &:hover { - color: #288264; + color: #2E5BE3; text-decoration: underline; } } @media (prefers-color-scheme: dark) { .book-page a:hover { - color: #C8EBD7; + color: #C9D8F8; } } body.dark .book-page a:hover { - color: #C8EBD7; + color: #C9D8F8; } /* -------------------------------------------------------------------------- diff --git a/source/chapters/sdag-06-stride.md b/source/chapters/sdag-06-stride.md index dd7edf6..21bad88 100644 --- a/source/chapters/sdag-06-stride.md +++ b/source/chapters/sdag-06-stride.md @@ -41,9 +41,9 @@ Two of the six categories — "fabricated default" (S) and the process-level DoS ### 3.2 Threat categories -#### S — Spoofing: Competence and identity spoofing[^stride-s-traditional] +#### S — Spoofing: Competence and identity spoofing -**Agentic variant:** Code *appears* to handle data correctly but operates on fabricated or default values, presenting a false picture of data integrity. +**Agentic variant:** Code *appears* to handle data correctly but operates on fabricated or default values, presenting a false picture of data integrity.[^stride-s-traditional] **Mechanism:** Agents default to defensive patterns that substitute values rather than failing. The code "works" — it produces output, it does not crash — but the output is based on fabricated data rather than actual data. The code spoofs the competence of correct data handling. @@ -69,9 +69,9 @@ if hasattr(obj, "security_clearance"): **Risk in government context:** Classification decisions, access control, evidentiary integrity — any domain where "I don't know" and "the default" are different answers with different consequences.[^stride-s-entries] -#### T — Tampering: Authority tier conflation[^stride-t-traditional] +#### T — Tampering: Authority tier conflation -**Agentic variant:** External (untrusted) data is treated as internal (trusted) data without validation, effectively tampering with the authority tier rather than the data itself. +**Agentic variant:** External (untrusted) data is treated as internal (trusted) data without validation, effectively tampering with the authority tier rather than the data itself.[^stride-t-traditional] **Mechanism:** Agents do not distinguish between data from different authority tiers because the programming language does not enforce it. A `dict` from a validated database query and a `dict` from an unvalidated API response are the same type. The agent treats them interchangeably. @@ -94,9 +94,9 @@ apply_system_settings(config) **Risk in government context:** Injection attacks through unvalidated external data, data corruption of authoritative records, and compliance failures when data provenance cannot be demonstrated.[^stride-t-entries] -#### R — Repudiation: Audit trail destruction through error handling[^stride-r-traditional] +#### R — Repudiation: Audit trail destruction through error handling -**Agentic variant:** Error handling patterns destroy the audit trail by catching, logging, and continuing rather than failing in a way that preserves the error as a first-class audit event. +**Agentic variant:** Error handling patterns destroy the audit trail by catching, logging, and continuing rather than failing in a way that preserves the error as a first-class audit event.[^stride-r-traditional] **Mechanism:** Agents generate broad exception handlers that prevent crashes but also prevent errors from being recorded in audit systems. The error is "handled" in the sense that the program continues, but the event that caused the error is lost to the audit trail. @@ -124,9 +124,9 @@ except NotificationError: **Risk in government context:** Regulatory compliance (failure to maintain complete audit trails), legal proceedings (gaps in evidence chains), and IRAP assessment failures (inability to demonstrate complete traceability).[^stride-r-entries] -#### I — Information Disclosure: Verbose error response and stack trace exposure[^stride-i-traditional] +#### I — Information Disclosure: Verbose error response and stack trace exposure -**Agentic variant:** Agent-generated error handling exposes internal system details in error responses, log messages, or API returns. +**Agentic variant:** Agent-generated error handling exposes internal system details in error responses, log messages, or API returns.[^stride-i-traditional] **Mechanism:** Agents produce "helpful" error messages that include internal state, query parameters, file paths, or stack traces (ACF-I1: Verbose Error Response). This is good practice for development but dangerous in production, and agents do not distinguish between the two contexts. Stack trace exposure — a related pattern well-covered by existing SAST tooling — is not catalogued as a separate ACF entry because existing tools provide adequate detection. @@ -153,9 +153,9 @@ except Exception as e: **Risk in government context:** Reconnaissance information for attackers, credential exposure, and violation of need-to-know principles.[^stride-i-entries] -#### D — Denial of Service: Finding flood and review capacity exhaustion (meta-threat)[^stride-d-traditional] +#### D — Denial of Service: Finding flood and review capacity exhaustion (meta-threat) -**Agentic variant:** The volume of agent-generated code overwhelms the review process, degrading review quality to the point where the review is no longer an effective security control. +**Agentic variant:** The volume of agent-generated code overwhelms the review process, degrading review quality to the point where the review is no longer an effective security control.[^stride-d-traditional] *Note: This extends STRIDE to the development lifecycle. The "service" being denied is the review process — a security control per ISM-2060/2061 — not a user-facing system.* @@ -175,9 +175,9 @@ A secondary mechanism: when automated analysis tools produce too many findings o **Risk in government context:** Security review as a compliance checkbox rather than an effective control, accreditation based on a process that no longer provides the assurance it claims to provide.[^stride-d-entries] -#### E — Elevation of Privilege: Implicit privilege grant[^stride-e-traditional] +#### E — Elevation of Privilege: Implicit privilege grant -**Agentic variant:** External system assertions are accepted without independent verification, granting privileges based on unvalidated claims — treating an external authority statement as if it were an internal trust decision. +**Agentic variant:** External system assertions are accepted without independent verification, granting privileges based on unvalidated claims — treating an external authority statement as if it were an internal trust decision.[^stride-e-traditional] **Mechanism:** Closely related to the Tampering category above (ACF-T1/T2), but focussed on the *consequence* rather than the *mechanism*. Where T1 describes the missing validation boundary, E1 describes what happens next: the external system's assertion is acted upon as though it carried internal authority. diff --git a/source/chapters/sdag-14-appendix-a-taxonomy.md b/source/chapters/sdag-14-appendix-a-taxonomy.md index 396372d..13f1c5c 100644 --- a/source/chapters/sdag-14-appendix-a-taxonomy.md +++ b/source/chapters/sdag-14-appendix-a-taxonomy.md @@ -12,17 +12,17 @@ A structured catalogue of failure modes observed in compliance-constrained agent ### Summary Table -| ID | Name | STRIDE | Failure Layer[^failure-layers] | Type | Relation[^relation-categories] | Risk | Detection | Portable Tool (Wardline)[^wardline-coverage] | +| ID | Name | STRIDE | Failure Layer [A] | Type | Relation [B] | Risk | Detection | Portable Tool (Wardline) [C] | |----------|----------------------|------------|----------------|----------------|---------------|----------|-------------------|----------------| | ACF-S1 | Fabricated Default | S | Training bias | Code Pattern | Agent-specific | High | Partial | Not covered (bespoke only) | | ACF-S2 | Spurious Field Access | S | Training bias | Code Pattern | Agent-specific | High | Partial | Not covered (bespoke only) | -| ACF-S3 | Structural Identity Spoofing | S[^s3-stride] | Training bias | Code Pattern | Agent-specific | High | Partial | Not covered (bespoke only) | -| ACF-T1 | Authority Tier Conflation | T | Training bias | Code Pattern | Agent-specific | Critical | None[^t1-detection] | Covered | +| ACF-S3 | Structural Identity Spoofing | S [D] | Training bias | Code Pattern | Agent-specific | High | Partial | Not covered (bespoke only) | +| ACF-T1 | Authority Tier Conflation | T | Training bias | Code Pattern | Agent-specific | Critical | None [E] | Covered | | ACF-T2 | Silent Coercion | T | Training bias | Code Pattern | Agent-specific | Medium | Partial | Not covered (bespoke only) | | ACF-T3 | Unstructured Signal Parsing | T | Training bias | Code Pattern | Agent-specific | High | Partial | Not covered | | ACF-R1 | Audit Trail Destruction | R | Training bias | Code Pattern | Known class, agent-amplified | High | Partial | Partial | | ACF-R2 | Partial Completion | R | Training bias | Code Pattern | Known class, agent-amplified | High | None | Partial | -| ACF-R3 | Verification Displacement | R | Context collapse | Code Pattern | Agent-specific | High | Partial (R3a) / None (R3b)[^r3b-detection] | Not covered | +| ACF-R3 | Verification Displacement | R | Context collapse | Code Pattern | Agent-specific | High | Partial (R3a) / None (R3b) [F] | Not covered | | | — R3a: Verification Substitution | | | | | | | | | | — R3b: Compensating Control Dependency | | | | | | | | | ACF-R5 | Remediation-Induced Violation | R | Training bias | Code Pattern | Agent-specific | High | None | Indirect only | @@ -41,17 +41,17 @@ A structured catalogue of failure modes observed in compliance-constrained agent !!! warning "Provisional candidates — not core taxonomy entries" ACF-S4, ACF-S5, ACF-R4, ACF-T4, and ACF-R6 are **provisional candidates** — failure modes observed in practice but not yet sufficiently validated for core classification. They are included in the detailed entries below for completeness and community feedback, but they are **not counted in the paper's "15 core failure modes" statistics** and should **not be treated as established compliance requirements**. Promotion to core requires additional independent observation, validated detection approaches, or broader community confirmation of the failure pattern. ACF-S4 (Type Annotation Erosion) and ACF-S5 (Type Structure Avoidance) were identified through external consultation feedback and describe complementary meta-failures that degrade the detection capability for other taxonomy entries. ACF-R4's generative mechanism is described in §2.4(a). ACF-T4 and ACF-R6 were identified through a structured audit of agent-generated commits against project-specific semantic rules; they describe failures that occur during maintenance-phase work (refactoring, remediation, auditing, triage) rather than during initial code generation. All five entries appear at the end of this appendix. -[^wardline-coverage]: Coverage by the companion specification's reference implementation as built, per its own coverage table (companion specification, Part I §2.1). "Covered"/"Partial" — one or more shipped rules address the mode. "Not covered" — no rule fires and no inference addresses the mode. "Not covered (bespoke only)" — the mode is detected in the case study project by its project-specific institutional rules (§8.3, §8.7), which were not generalised into the portable tool. "N/A (design constraint)" — a process threat the tool addresses by design posture (opt-in silence, a corpus-enforced false-positive gate) rather than by rules. Five core entries are covered or partially covered, clustered around trust flow and exception discipline. The *Detection* column, by contrast, rates detectability by any means, including bespoke rules and approaches no tool yet implements. +**[C] Portable tool coverage.** Coverage by the companion specification's reference implementation as built, per its own coverage table (companion specification, Part I §2.1). "Covered"/"Partial" — one or more shipped rules address the mode. "Not covered" — no rule fires and no inference addresses the mode. "Not covered (bespoke only)" — the mode is detected in the case study project by its project-specific institutional rules (§8.3, §8.7), which were not generalised into the portable tool. "N/A (design constraint)" — a process threat the tool addresses by design posture (opt-in silence, a corpus-enforced false-positive gate) rather than by rules. Five core entries are covered or partially covered, clustered around trust flow and exception discipline. The *Detection* column, by contrast, rates detectability by any means, including bespoke rules and approaches no tool yet implements. -[^failure-layers]: The primary generative mechanism for each entry, mapped to the failure-layer distinction in §2.4(h). *Training bias* — the model's priors encode the pattern as universally correct; persists across sessions and models with shared lineages. *Context collapse* — the model loses or displaces project-specific context during generation; addressable through session management and checkpoint controls. *Process volume* — the failure is in the review process, not the code; addressable through capacity planning and automated pre-screening. Some entries involve multiple layers; the column reflects the primary mechanism. The distinction matters for control selection: a control that addresses one layer may provide false reassurance against another (§2.4(h)). +**[A] Failure layer.** The primary generative mechanism for each entry, mapped to the failure-layer distinction in §2.4(h). *Training bias* — the model's priors encode the pattern as universally correct; persists across sessions and models with shared lineages. *Context collapse* — the model loses or displaces project-specific context during generation; addressable through session management and checkpoint controls. *Process volume* — the failure is in the review process, not the code; addressable through capacity planning and automated pre-screening. Some entries involve multiple layers; the column reflects the primary mechanism. The distinction matters for control selection: a control that addresses one layer may provide false reassurance against another (§2.4(h)). -[^r3b-detection]: The "Partial" rating reflects R3a (Verification Substitution), which has identifiable detection signatures. R3b (Compensating Control Dependency) has no practical detection method — the fragility is invisible until the compensating control is removed. The composite "Partial" understates the R3b gap. +**[F] R3b detection.** The "Partial" rating reflects R3a (Verification Substitution), which has identifiable detection signatures. R3b (Compensating Control Dependency) has no practical detection method — the fragility is invisible until the compensating control is removed. The composite "Partial" understates the R3b gap. -[^t1-detection]: No widely deployed tool detects this. Project-specific pattern matching (§8.3) provides limited intra-function proxy coverage — sufficient to catch some instances but not the cross-function taint flows that characterise the full failure mode. The "None" rating reflects the absence of generally available detection, not the impossibility of detection. +**[E] T1 detection.** No widely deployed tool detects this. Project-specific pattern matching (§8.3) provides limited intra-function proxy coverage — sufficient to catch some instances but not the cross-function taint flows that characterise the full failure mode. The "None" rating reflects the absence of generally available detection, not the impossibility of detection. -[^s3-stride]: Primary STRIDE category is Spoofing. The detailed entry below notes an Elevation of Privilege consequence — the structural impersonation that S3 enables can result in implicit privilege grants — but the entry is classified under S because the mechanism (false structural identity) is spoofing, not elevation. +**[D] S3 STRIDE category.** Primary STRIDE category is Spoofing. The detailed entry below notes an Elevation of Privilege consequence — the structural impersonation that S3 enables can result in implicit privilege grants — but the entry is classified under S because the mechanism (false structural identity) is spoofing, not elevation. -[^relation-categories]: Three categories: *Agent-specific* — a failure mode that arises from the generative properties of agentic coding, not observed (or observed only rarely) in human-authored code. *Known class, agent-amplified* — a failure class already catalogued in human-authored code, but produced at higher frequency, greater consistency, or harder-to-detect form by agents. *Workflow Pattern* entries (provisional) describe failures in multi-session or multi-agent coordination rather than in generated code. +**[B] Relation categories.** Three categories: *Agent-specific* — a failure mode that arises from the generative properties of agentic coding, not observed (or observed only rarely) in human-authored code. *Known class, agent-amplified* — a failure class already catalogued in human-authored code, but produced at higher frequency, greater consistency, or harder-to-detect form by agents. *Workflow Pattern* entries (provisional) describe failures in multi-session or multi-agent coordination rather than in generated code. **Type** indicates whether the entry describes a code-level pattern (addressable with technical controls), a process-level threat (addressable with management controls), or both. This taxonomy intentionally includes both because the threat model's compounding mechanism (§3.3) depends on their interaction — code-level failures and process-level degradation reinforce each other through feedback loops that a split taxonomy would obscure. diff --git a/source/chapters/wardline-01-04-the-trust-lattice.md b/source/chapters/wardline-01-04-the-trust-lattice.md index 33f1d88..890a52b 100644 --- a/source/chapters/wardline-01-04-the-trust-lattice.md +++ b/source/chapters/wardline-01-04-the-trust-lattice.md @@ -13,8 +13,8 @@ It is also the single largest piece of the designed specification (archived) tha | Rank | State | Set by | Meaning | |---:|---|---|---| | 0 | `INTEGRAL` | You — `@trusted` (default) | Fully trusted data the application produces and relies on. | -| 1 | `ASSURED` | You — `@trusted(level="ASSURED")`, `@trust_boundary(to_level="ASSURED")` | Trusted after validation; a notch below integral. | -| 2 | `GUARDED` | You — `@trust_boundary(to_level="GUARDED")`; also the bundled stdlib table | Partially checked: passed a shape or format guard, not fully assured. | +| 1 | `ASSURED` | You —
`@trusted` or
`@trust_boundary`
(ASSURED) | Trusted after validation; a notch below integral. | +| 2 | `GUARDED` | You —
`@trust_boundary`
(GUARDED); also the bundled stdlib table | Partially checked: passed a shape or format guard, not fully assured. | | 3 | `UNKNOWN_ASSURED` | The engine (never produced — see §4.4) | Semantically validated, provenance unestablished. | | 4 | `UNKNOWN_GUARDED` | The engine (never produced — see §4.4) | Shape-validated, provenance unestablished. | | 5 | `EXTERNAL_RAW` | You — `@external_boundary`; also the stdlib table | Raw untrusted data crossing into the system from outside. | diff --git a/source/pdf/.gitignore b/source/pdf/.gitignore index 56a3e39..90afd26 100644 --- a/source/pdf/.gitignore +++ b/source/pdf/.gitignore @@ -7,5 +7,6 @@ # Compiled PDFs source/*.pdf -# Mermaid build temp dir (cleaned up by build scripts, but just in case) -.mermaid-tmp/ +# Generated Mermaid source and SVG assets. These stay on disk so the generated +# .typ intermediates remain independently compilable. +.assets/ diff --git a/source/pdf/README.md b/source/pdf/README.md new file mode 100644 index 0000000..b9af806 --- /dev/null +++ b/source/pdf/README.md @@ -0,0 +1,57 @@ +# PDF publication pipeline + +The public PDFs are generated from the tracked Markdown in `source/chapters/`, +converted by Pandoc, and typeset by Typst. The build targets Typst 0.14's +tagged-PDF model and requires PDF/UA-1 conformance for every publication. + +## Requirements + +- Pandoc 3.2 or newer +- Typst 0.14.0 or newer +- Mermaid CLI 11 or newer (`mmdc`) +- Poppler utilities (`pdfinfo`, `pdffonts`, and `pdftotext`) + +The repository currently builds with Pandoc 3.9.0.2, Typst 0.14.2, and Mermaid CLI +11.16.0. Keep CI and local versions aligned when changing the template. + +## Commands + +```bash +# Build all five PDFs into source/pdf/ and run structural checks +./source/pdf/build-all.sh + +# Build, verify, and update the public artifacts used by MkDocs +./source/pdf/build-all.sh --publish + +# Rebuild and fail if the checked-in public PDFs are stale +./source/pdf/build-all.sh --check +``` + +The legacy per-profile commands remain available for focused iteration: + +```bash +./source/pdf/build-community.sh --pdf +./source/pdf/build-wardline-community.sh --pdf +./source/pdf/build-lite.sh sdag --pdf +``` + +## Design and accessibility contract + +- Markdown chapters remain the source of truth; ignored generated monoliths + are never publication inputs. +- Mermaid diagrams are strict build dependencies and render as vector SVG with + contextual alternative text. +- Metadata lives in `metadata-*.yaml` and includes a fixed source date epoch so + repeated builds are byte-reproducible. +- Covers use a semantic level-one heading. Tables use header cells, repeated + page furniture is marked as an artifact, body copy is ragged right, and text + never drops below 9 pt. +- Typst compilation always uses `--pdf-standard ua-1`. This is a strong + automated gate, not a substitute for release-time testing with veraPDF or + PAC and a screen reader. +- `--publish` copies artifacts only after all five documents build and pass + metadata, tagging, text-extraction, page-count, and font-embedding checks. + +Generated `.typ`, PDF, and Mermaid asset files under `source/pdf/` are ignored. +They are retained locally so the intermediate Typst documents can be inspected +and recompiled while diagnosing layout issues. diff --git a/source/pdf/build-all.sh b/source/pdf/build-all.sh new file mode 100755 index 0000000..b031ed9 --- /dev/null +++ b/source/pdf/build-all.sh @@ -0,0 +1,109 @@ +#!/bin/bash +# Canonical publication entry point for the five public PDFs. +# +# Usage: +# ./source/pdf/build-all.sh # build and verify working outputs +# ./source/pdf/build-all.sh --publish # build, verify, then update docs/pdf/ +# ./source/pdf/build-all.sh --check # fail if docs/pdf/ is out of date + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" +PUBLISH_DIR="$PROJECT_DIR/docs/pdf" + +MODE="build" +case "${1:-}" in + "") ;; + --publish) MODE="publish" ;; + --check) MODE="check" ;; + -h|--help) + sed -n '2,8p' "$0" | sed 's/^# *//' + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + exit 2 + ;; +esac + +version_at_least() { + local actual="$1" + local required="$2" + [[ "$(printf '%s\n%s\n' "$required" "$actual" | sort -V | head -n 1)" == "$required" ]] +} + +require_tool() { + local command_name="$1" + local required="$2" + local actual + + if ! command -v "$command_name" &>/dev/null; then + echo "[error] Required command not found: $command_name" >&2 + exit 1 + fi + actual=$("$command_name" --version 2>&1 | head -n 1 | grep -oE '[0-9]+(\.[0-9]+){1,2}' | head -n 1) + if [[ -z "$actual" ]] || ! version_at_least "$actual" "$required"; then + echo "[error] $command_name >= $required is required; found ${actual:-unknown}" >&2 + exit 1 + fi + echo " $command_name $actual" +} + +echo "Checking publication toolchain..." +require_tool pandoc 3.2 +require_tool typst 0.14.0 +require_tool mmdc 11.0.0 +command -v pdfinfo &>/dev/null || { echo "[error] pdfinfo is required" >&2; exit 1; } +command -v pdffonts &>/dev/null || { echo "[error] pdffonts is required" >&2; exit 1; } +command -v pdftotext &>/dev/null || { echo "[error] pdftotext is required" >&2; exit 1; } + +"$SCRIPT_DIR/build-community.sh" --pdf +"$SCRIPT_DIR/build-wardline-community.sh" --pdf +"$SCRIPT_DIR/build-lite.sh" --pdf + +PDFS=( + threat-model-discussion-paper-community.pdf + wardline-companion-community.pdf + governing-ai-generated-code.pdf + reviewing-ai-generated-code.pdf + document-suite-map.pdf +) + +echo "Verifying publication artifacts..." +for name in "${PDFS[@]}"; do + pdf="$SCRIPT_DIR/$name" + info=$(pdfinfo "$pdf" 2>/dev/null) + grep -q '^Tagged: *yes$' <<< "$info" || { echo "[error] $name is not tagged" >&2; exit 1; } + grep -Eq '^Pages: *[1-9][0-9]*$' <<< "$info" || { echo "[error] $name has no pages" >&2; exit 1; } + grep -Eq '^Title: *[^[:space:]]' <<< "$info" || { echo "[error] $name has no PDF title" >&2; exit 1; } + pdftotext "$pdf" - 2>/dev/null | awk '/[[:alnum:]]/ { found=1 } END { exit !found }' || { echo "[error] $name has no extractable text" >&2; exit 1; } + if ! pdffonts "$pdf" 2>/dev/null | awk 'NR > 2 && ($6 != "yes" || $7 != "yes") { bad=1 } END { exit bad }'; then + echo "[error] $name contains a font that is not embedded or subset" >&2 + exit 1 + fi + pages=$(sed -n 's/^Pages: *//p' <<< "$info") + echo " [ok] $name ($pages pages, tagged PDF/UA-1)" +done + +if [[ "$MODE" == "publish" ]]; then + echo "Publishing verified PDFs to docs/pdf/..." + for name in "${PDFS[@]}"; do + cp "$SCRIPT_DIR/$name" "$PUBLISH_DIR/$name" + done +elif [[ "$MODE" == "check" ]]; then + stale=0 + for name in "${PDFS[@]}"; do + if ! cmp -s "$SCRIPT_DIR/$name" "$PUBLISH_DIR/$name"; then + echo " [stale] docs/pdf/$name" >&2 + stale=1 + fi + done + if [[ "$stale" -ne 0 ]]; then + echo "[error] Published PDFs are stale; run ./source/pdf/build-all.sh --publish" >&2 + exit 1 + fi + echo "Published PDFs are current." +fi + +echo "Done." diff --git a/source/pdf/build-common.sh b/source/pdf/build-common.sh index c7c6222..0046854 100755 --- a/source/pdf/build-common.sh +++ b/source/pdf/build-common.sh @@ -3,49 +3,74 @@ # Sourced by build-community.sh, build-wardline-community.sh, and build-lite.sh. # --- Mermaid rendering --- -# Renders mermaid code blocks in a markdown file to PNG images. +# Renders Mermaid code blocks in a Markdown file to accessible SVG figures. # Arguments: $1 = body markdown file, $2 = mermaid temp directory, $3 = script directory render_mermaid() { local body_md="$1" local mermaid_dir="$2" local script_dir="$3" - if ! command -v mmdc &>/dev/null; then - echo " [skip] mmdc not found — mermaid blocks will render as code" + if ! grep -q '^```mermaid$' "$body_md"; then return 0 fi + if ! command -v mmdc &>/dev/null; then + echo " [error] mmdc is required because this document contains Mermaid diagrams" >&2 + return 1 + fi + echo "Rendering Mermaid diagrams..." local puppeteer_cfg="$mermaid_dir/puppeteer.json" + local mermaid_cfg="$mermaid_dir/mermaid.json" echo '{ "args": ["--no-sandbox"] }' > "$puppeteer_cfg" + echo '{ "htmlLabels": false, "flowchart": { "htmlLabels": false } }' > "$mermaid_cfg" python3 -c " import re, sys, subprocess, os body_md = sys.argv[1] mermaid_dir = sys.argv[2] puppeteer_cfg = os.path.join(mermaid_dir, 'puppeteer.json') +mermaid_cfg = os.path.join(mermaid_dir, 'mermaid.json') with open(body_md, 'r') as f: content = f.read() +def clean_text(value): + value = re.sub(r'<[^>]+>', ' ', value) + value = value.replace(chr(96), '') + value = re.sub(r'[*_#]', '', value) + return re.sub(r'\s+', ' ', value).strip() + +def diagram_alt(source, start, diagram): + headings = re.findall(r'^#{1,6}\s+(.+)$', source[:start], flags=re.MULTILINE) + context = clean_text(headings[-1]) if headings else 'the document' + labels = [] + for groups in re.findall(r'\[([^\]]+)\]|\(([^)]+)\)|\{([^}]+)\}', diagram): + label = clean_text(next((part for part in groups if part), '')) + if label and label not in labels: + labels.append(label) + detail = ' Key elements: ' + ', '.join(labels[:6]) + '.' if labels else '' + return ('Diagram for ' + context + '.' + detail)[:320] + def render_mermaid(match): idx = render_mermaid.counter render_mermaid.counter += 1 mmd_path = os.path.join(mermaid_dir, f'diagram-{idx}.mmd') - png_path = os.path.join(mermaid_dir, f'diagram-{idx}.png') + svg_path = os.path.join(mermaid_dir, f'diagram-{idx}.svg') with open(mmd_path, 'w') as f: f.write(match.group(1)) result = subprocess.run( - ['mmdc', '-i', mmd_path, '-o', png_path, '-b', 'white', - '--quiet', '-p', puppeteer_cfg, '-s', '3'], + ['mmdc', '-i', mmd_path, '-o', svg_path, '-b', 'white', + '-t', 'neutral', '-c', mermaid_cfg, '--quiet', '-p', puppeteer_cfg], capture_output=True, text=True ) if result.returncode != 0: print(f' [warn] mmdc failed for diagram {idx}: {result.stderr.strip()}', file=sys.stderr) return match.group(0) # keep original on failure - print(f' [mermaid] rendered diagram-{idx}.png') - rel_path = os.path.relpath(png_path, sys.argv[3]) - return f'![Diagram {idx + 1}]({rel_path}){{width=75%}}' + print(f' [mermaid] rendered diagram-{idx}.svg') + rel_path = os.path.relpath(svg_path, sys.argv[3]) + alt = diagram_alt(content, match.start(), match.group(1)).replace('[', '(').replace(']', ')') + return f'![{alt}]({rel_path}){{width=82%}}' render_mermaid.counter = 0 content = re.sub(r'\`\`\`mermaid\n(.*?)\n\`\`\`', render_mermaid, content, flags=re.DOTALL) @@ -111,9 +136,45 @@ def fix_columns(match): content = re.sub(r'columns: \([^)]+\)', fix_columns, content) +# Pandoc wraps every Typst table in figure(..., kind: table). In tagged PDF, +# the figure body becomes an artifact; footnotes or links inside that artifact +# are forbidden by PDF/UA and the table semantics are obscured. Pandoc's writer +# emits this stable wrapper even for uncaptioned tables, so unwrap it here. +figure_start = '#figure(\n [#table(' +figure_end = ' )]\n , kind: table\n )' +if content.count(figure_start) != content.count(figure_end): + raise SystemExit('unbalanced Pandoc table figure wrappers') +table_count = content.count(figure_start) +content = content.replace(figure_start, '#table(') +content = content.replace(figure_end, ' )') + +# Pandoc 3.9 maps Markdown image alt text to a Typst figure caption. Patch +# releases differ on whether they also populate image(alt:), which Typst 0.14 +# requires for PDF/UA. Normalise both forms to the contextual caption so the +# image has one stable, machine-readable alternative rather than duplicate +# arguments. +figure_pattern = re.compile( + r'#figure\((?Pimage\(\x22(?P\.assets/[^\x22]+\.(?:svg|png))\x22, ' + r'width: (?P[^,)]+)(?:, alt: .*?)?\)),' + r'\n caption: \[\n(?P.*?)\n \]\n\)', + flags=re.DOTALL, +) + +def add_image_alt(match): + caption = re.sub(r'\s+', ' ', match.group('caption')).strip() + image_call = 'image(' + json.dumps(match.group('path'), ensure_ascii=False) + ', width: ' + match.group('width') + accessible_call = image_call + f', alt: {json.dumps(caption, ensure_ascii=False)})' + return match.group(0).replace(match.group('image_call'), accessible_call, 1) + +content, accessible_figure_count = figure_pattern.subn(add_image_alt, content) + +lost_headings = re.findall(r'\\#\\#\\#\s+(?:[0-9]+\.|[A-Z]\.)', content) +if lost_headings: + raise SystemExit(f'heading-like text was not parsed as a heading: {lost_headings[0]}') + with open(sys.argv[1], 'w') as f: f.write(content) -print(' [post-process] Fixed table alignment and column widths') +print(f' [post-process] Exposed {table_count} semantic tables and tagged {accessible_figure_count} figures') " "$output_typ" "$table_overrides" "$mode" } @@ -128,6 +189,7 @@ run_pandoc() { echo "Generating Typst..." pandoc "$body_md" \ + --from=markdown \ --to=typst \ --template="$template" \ --metadata-file="$metadata" \ @@ -141,16 +203,27 @@ run_pandoc() { # --- Typst compilation --- # Compiles a .typ file to PDF. # Arguments: $1 = script directory, $2 = typst root directory, -# $3 = .typ filename, $4 = .pdf filename +# $3 = .typ filename, $4 = .pdf filename, $5 = metadata YAML compile_pdf() { local script_dir="$1" local root_dir="$2" local typ_file="$3" local pdf_file="$4" + local metadata="$5" + local source_date_epoch + + source_date_epoch=$(sed -n 's/^source-date-epoch: *"\([0-9][0-9]*\)"/\1/p' "$metadata") + if [[ -z "$source_date_epoch" ]]; then + echo " [error] source-date-epoch is missing from $metadata" >&2 + return 1 + fi - echo "Compiling PDF..." + echo "Compiling accessible PDF..." cd "$script_dir" - # Note: --pdf-standard ua-1 removed pending alt text for Mermaid-rendered PNGs - typst compile --root "$root_dir" "$typ_file" "$pdf_file" + typst compile \ + --root "$root_dir" \ + --pdf-standard ua-1 \ + --creation-timestamp "$source_date_epoch" \ + "$typ_file" "$pdf_file" echo " -> $script_dir/$pdf_file" } diff --git a/source/pdf/build-community.sh b/source/pdf/build-community.sh index c72c4f2..8ab8847 100755 --- a/source/pdf/build-community.sh +++ b/source/pdf/build-community.sh @@ -12,20 +12,28 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/build-common.sh" THREAT_MODEL_DIR="$(dirname "$SCRIPT_DIR")" -SOURCE="$THREAT_MODEL_DIR/agentic-code-threat-model-discussion-paper.md" +CHAPTERS_DIR="$THREAT_MODEL_DIR/chapters" TEMPLATE="$SCRIPT_DIR/pandoc-typst-community.typ" METADATA="$SCRIPT_DIR/metadata-community.yaml" OUTPUT_TYP="$SCRIPT_DIR/threat-model-discussion-paper-community.typ" OUTPUT_PDF="$SCRIPT_DIR/threat-model-discussion-paper-community.pdf" # Strip the metadata header from Markdown -BODY_MD=$(mktemp) -MERMAID_DIR="$SCRIPT_DIR/.mermaid-tmp" +FULL_MD=$(mktemp --suffix=.md) +BODY_MD=$(mktemp --suffix=.md) +MERMAID_DIR="$SCRIPT_DIR/.assets/threat-model" mkdir -p "$MERMAID_DIR" -trap 'rm -f "$BODY_MD"; rm -rf "$MERMAID_DIR"' EXIT +trap 'rm -f "$FULL_MD" "$BODY_MD"' EXIT + +# The tracked chapters are canonical. Joining with explicit blank lines avoids +# Markdown headings being absorbed into the preceding paragraph at boundaries. +for chapter in "$CHAPTERS_DIR"/sdag-*.md; do + sed -n '1,$p' "$chapter" >> "$FULL_MD" + printf '\n\n' >> "$FULL_MD" +done # Keep everything from "## Abstract" onward -sed -n '/^## Abstract$/,$p' "$SOURCE" > "$BODY_MD" +sed -n '/^## Abstract$/,$p' "$FULL_MD" > "$BODY_MD" # Strip the manual Table of Contents section — Typst #outline() handles it sed -i '/^## Table of Contents$/,/^---$/d' "$BODY_MD" @@ -44,7 +52,7 @@ TABLE_OVERRIDES='{"Summary Table": "8%, 20%, 8%, 12%, 15%, 8%, 29%"}' postprocess_tables "$OUTPUT_TYP" "$TABLE_OVERRIDES" if [[ "${1:-}" == "--pdf" ]]; then - compile_pdf "$SCRIPT_DIR" "$THREAT_MODEL_DIR" "$(basename "$OUTPUT_TYP")" "$(basename "$OUTPUT_PDF")" + compile_pdf "$SCRIPT_DIR" "$THREAT_MODEL_DIR" "$(basename "$OUTPUT_TYP")" "$(basename "$OUTPUT_PDF")" "$METADATA" fi echo "Done." diff --git a/source/pdf/build-lite.sh b/source/pdf/build-lite.sh index 0e58985..a9e6c7f 100755 --- a/source/pdf/build-lite.sh +++ b/source/pdf/build-lite.sh @@ -67,7 +67,7 @@ build_document() { # Prepare body: strip the H1 title line (metadata provides the title page) # and strip horizontal rules (Typst sections provide structure) - BODY_MD=$(mktemp) + BODY_MD=$(mktemp --suffix=.md) trap 'rm -f "$BODY_MD"' RETURN # Remove the first H1 heading (title) — the template renders the title page from metadata @@ -95,6 +95,10 @@ build_document() { sed -i '/^> This is a navigation guide/d' "$BODY_MD" sed -i '/^> This is a draft discussion paper/d' "$BODY_MD" + # PDF companions live together in docs/pdf; keep the link useful after + # Pandoc conversion instead of embedding a source-only Markdown path. + sed -i 's|(governing-ai-generated-code.md)|(governing-ai-generated-code.pdf)|g' "$BODY_MD" + # Convert MkDocs admonitions (!!! type "title") to blockquotes for pandoc python3 -c " import re, sys @@ -135,7 +139,7 @@ with open(sys.argv[1], 'w') as f: postprocess_tables "$output_typ" '{}' 'lite' if $BUILD_PDF; then - compile_pdf "$SCRIPT_DIR" "$(dirname "$SCRIPT_DIR")" "$(basename "$output_typ")" "$(basename "$output_pdf")" + compile_pdf "$SCRIPT_DIR" "$(dirname "$SCRIPT_DIR")" "$(basename "$output_typ")" "$(basename "$output_pdf")" "$metadata" fi echo "" diff --git a/source/pdf/build-wardline-community.sh b/source/pdf/build-wardline-community.sh index d815932..27a0544 100755 --- a/source/pdf/build-wardline-community.sh +++ b/source/pdf/build-wardline-community.sh @@ -12,34 +12,34 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/build-common.sh" THREAT_MODEL_DIR="$(dirname "$SCRIPT_DIR")" -SOURCE="$THREAT_MODEL_DIR/wardline-companion-document.md" +CHAPTERS_DIR="$THREAT_MODEL_DIR/chapters" TEMPLATE="$SCRIPT_DIR/pandoc-typst-community.typ" METADATA="$SCRIPT_DIR/metadata-wardline-community.yaml" OUTPUT_TYP="$SCRIPT_DIR/wardline-companion-community.typ" OUTPUT_PDF="$SCRIPT_DIR/wardline-companion-community.pdf" # Strip the metadata header from Markdown -BODY_MD=$(mktemp) -MERMAID_DIR="$SCRIPT_DIR/.mermaid-tmp" +FULL_MD=$(mktemp --suffix=.md) +BODY_MD=$(mktemp --suffix=.md) +MERMAID_DIR="$SCRIPT_DIR/.assets/wardline" mkdir -p "$MERMAID_DIR" -trap 'rm -f "$BODY_MD"; rm -rf "$MERMAID_DIR"' EXIT +trap 'rm -f "$FULL_MD" "$BODY_MD"' EXIT + +for chapter in "$CHAPTERS_DIR"/wardline-*.md; do + sed -n '1,$p' "$chapter" >> "$FULL_MD" + printf '\n\n' >> "$FULL_MD" +done # Keep the full document but strip front matter metadata blocks -sed -n '/^## Wardline/,$p' "$SOURCE" > "$BODY_MD" +sed -n '/^## Wardline/,$p' "$FULL_MD" > "$BODY_MD" -# Strip document-level ## headings — title page handles these -sed -i '/^## Wardline: A Classification Framework/d' "$BODY_MD" -sed -i '/^## Wardline for Python:/d' "$BODY_MD" -sed -i '/^## Wardline for Java:/d' "$BODY_MD" -sed -i '/^## Wardline Practitioner Guide/d' "$BODY_MD" +# Strip document-level headings — the title page handles these. +sed -i '/^## Wardline: An As-Built Specification$/d' "$BODY_MD" +sed -i '/^### Semantic Trust-Boundary Enforcement$/d' "$BODY_MD" # Strip horizontal rules — Typst sections provide structure sed -i '/^---$/d' "$BODY_MD" -# Promote "How to read" preamble heading — after stripping ## part titles, -# this ### heading would be level 3 with no level 2 before it (PDF/UA violation) -sed -i 's/^### How to read this \(document\|guide\)$/## How to read this \1/' "$BODY_MD" - # Strip front matter metadata lines rendered by the title page sed -i \ -e '/^\*\*Status:\*\*/d' \ @@ -56,6 +56,10 @@ sed -i \ -e '/^\*\*Sibling binding:\*\*/d' \ -e '/^\*\*Protective Marking:\*\*/d' \ -e '/^\*\*Prepared by:\*\*/d' \ + -e '/^\*\*Describes:\*\*/d' \ + -e '/^\*\*Document type:\*\*/d' \ + -e '/^\*\*Implementation:\*\*/d' \ + -e '/^\*\*Language frontends:\*\*/d' \ "$BODY_MD" # Strip the markdown Table of Contents — the Typst template generates its own via #outline() @@ -68,11 +72,29 @@ with open(sys.argv[1], 'w') as f: f.write(content) " "$BODY_MD" +# The tracked source nests all substantive sections below its document title. +# With the title moved to the cover, promote the remaining hierarchy by one +# level so major specification clauses become H2 and subsections become H3. +python3 -c " +import re, sys +with open(sys.argv[1], 'r') as f: + content = f.read() +content = re.sub( + r'^(#{3,6})(?= )', + lambda match: match.group(1)[1:], + content, + flags=re.MULTILINE, +) +with open(sys.argv[1], 'w') as f: + f.write(content) +" "$BODY_MD" + render_mermaid "$BODY_MD" "$MERMAID_DIR" "$SCRIPT_DIR" run_pandoc "$BODY_MD" "$TEMPLATE" "$METADATA" "$OUTPUT_TYP" # Table overrides for the Wardline companion TABLE_OVERRIDES='{ + "INTEGRAL": "7%, 22%, 39%, 32%", "Trusted assertion": "15%, 15%, 30%, 40%", "Not Applicable": "12%, 14%, 14%, 14%, 46%", "Institutional Knowledge": "18%, 30%, 18%, 34%", @@ -82,7 +104,7 @@ TABLE_OVERRIDES='{ postprocess_tables "$OUTPUT_TYP" "$TABLE_OVERRIDES" if [[ "${1:-}" == "--pdf" ]]; then - compile_pdf "$SCRIPT_DIR" "$THREAT_MODEL_DIR" "$(basename "$OUTPUT_TYP")" "$(basename "$OUTPUT_PDF")" + compile_pdf "$SCRIPT_DIR" "$THREAT_MODEL_DIR" "$(basename "$OUTPUT_TYP")" "$(basename "$OUTPUT_PDF")" "$METADATA" fi echo "Done." diff --git a/source/pdf/metadata-community.yaml b/source/pdf/metadata-community.yaml index 936a81e..c740dc2 100644 --- a/source/pdf/metadata-community.yaml +++ b/source/pdf/metadata-community.yaml @@ -2,7 +2,11 @@ title: "Semantic Defects in AI-Generated Code: Assurance Frameworks for AI-Assis author: "John Morrissey" date: "9 August 2026" version: "Draft for Comment v0.3.0" +document-type: "OPEN CONSULTATION · DISCUSSION PAPER" +running-title: "Semantic Defects in AI-Generated Code" +description: "A threat model and assurance framework for semantic defects in AI-generated code used in high-stakes systems." keywords: "AI-generated code, threat model, semantic defects, STRIDE, software assurance, high-stakes systems, agentic coding" +source-date-epoch: "1786233600" disclaimer: "This is a draft discussion paper circulated for peer review. It is independent work published in a personal capacity and does not constitute official guidance or policy of any government body. It does not mandate or recommend specific controls for any agency, system, or project. Views and analysis are the author's own." version-table: - version: "0.1.0" diff --git a/source/pdf/metadata-governing-ai-generated-code.yaml b/source/pdf/metadata-governing-ai-generated-code.yaml index 9003b85..8321a57 100644 --- a/source/pdf/metadata-governing-ai-generated-code.yaml +++ b/source/pdf/metadata-governing-ai-generated-code.yaml @@ -3,6 +3,10 @@ subtitle: "Companion Briefing to *Semantic Defects in AI-Generated Code*" author: "John Morrissey" date: "9 August 2026" version: "Draft for Comment v0.3.0" +document-type: "EXECUTIVE BRIEFING" +running-title: "Governing AI-Generated Code" +description: "An accessible briefing on semantic risk and proportionate assurance for AI-generated code." keywords: "AI-generated code, governance, semantic risk, software assurance, high-stakes systems, agentic coding" +source-date-epoch: "1786233600" disclaimer: "This is a draft discussion paper circulated for peer review. It is independent work published in a personal capacity and does not constitute official guidance or policy of any government body. It does not mandate or recommend specific controls for any agency, system, or project. Views and analysis are the author's own." parent-paper: "Semantic Defects in AI-Generated Code: Assurance Frameworks for AI-Assisted Development in High-Stakes Code Paths (v0.3.0)" diff --git a/source/pdf/metadata-reviewing-ai-generated-code.yaml b/source/pdf/metadata-reviewing-ai-generated-code.yaml index aa15809..3c0db43 100644 --- a/source/pdf/metadata-reviewing-ai-generated-code.yaml +++ b/source/pdf/metadata-reviewing-ai-generated-code.yaml @@ -3,6 +3,10 @@ subtitle: "Companion Briefing to *Semantic Defects in AI-Generated Code*" author: "John Morrissey" date: "9 August 2026" version: "Draft for Comment v0.3.0" +document-type: "PRACTICAL GUIDE" +running-title: "Reviewing AI-Generated Code" +description: "A practical guide to reviewing AI-generated code for semantic defects that conventional checks miss." keywords: "AI-generated code, code review, code authors, practical guide, semantic defects" +source-date-epoch: "1786233600" disclaimer: "This is a draft discussion paper circulated for peer review. It is independent work published in a personal capacity and does not constitute official guidance or policy of any government body. It does not mandate or recommend specific controls for any agency, system, or project. Views and analysis are the author's own." parent-paper: "Semantic Defects in AI-Generated Code: Assurance Frameworks for AI-Assisted Development in High-Stakes Code Paths (v0.3.0)" diff --git a/source/pdf/metadata-suite-map.yaml b/source/pdf/metadata-suite-map.yaml index f1de237..01ca94a 100644 --- a/source/pdf/metadata-suite-map.yaml +++ b/source/pdf/metadata-suite-map.yaml @@ -3,6 +3,10 @@ subtitle: "Reading Paths for *Semantic Defects in AI-Generated Code* and Compani author: "John Morrissey" date: "9 August 2026" version: "Draft for Comment v0.3.0" +document-type: "READING GUIDE" +running-title: "Document Suite Map" +description: "A role-based map of the Semantic Defects publication suite and its recommended reading paths." keywords: "document suite, reading guide, AI-generated code, threat model, wardline" +source-date-epoch: "1786233600" disclaimer: "This is a navigation guide to the discussion paper suite. It is independent work published in a personal capacity and does not constitute official guidance or policy of any government body. Views and analysis are the author's own." parent-paper: "Semantic Defects in AI-Generated Code: Assurance Frameworks for AI-Assisted Development in High-Stakes Code Paths (v0.3.0)" diff --git a/source/pdf/metadata-wardline-community.yaml b/source/pdf/metadata-wardline-community.yaml index e9816f8..908d486 100644 --- a/source/pdf/metadata-wardline-community.yaml +++ b/source/pdf/metadata-wardline-community.yaml @@ -3,7 +3,11 @@ subtitle: "Semantic Trust-Boundary Enforcement" author: "John Morrissey" date: "8 August 2026" version: "DRAFT v1.0.0-draft - as-built" +document-type: "AS-BUILT TECHNICAL SPECIFICATION" +running-title: "Wardline As-Built Specification" +description: "The as-built specification of Wardline's trust lattice, declarations, rule catalogue, gate semantics, and verification properties." keywords: "wardline, semantic trust boundaries, static analysis, trust lattice, code assurance, AI-generated code" +source-date-epoch: "1786147200" disclaimer: "This is a draft discussion paper circulated for peer review. It is independent work published in a personal capacity and does not constitute official guidance or policy of any government body. It does not mandate or recommend specific controls for any agency, system, or project. Views and analysis are the author's own." parent-paper: "Semantic Defects in AI-Generated Code: Assurance Frameworks for AI-Assisted Development in High-Stakes Code Paths (v0.3.0)" version-table: diff --git a/source/pdf/pandoc-typst-community.typ b/source/pdf/pandoc-typst-community.typ index f0054ae..1eefffa 100644 --- a/source/pdf/pandoc-typst-community.typ +++ b/source/pdf/pandoc-typst-community.typ @@ -36,6 +36,15 @@ $endif$ $if(keywords)$ pdf-keywords: "$keywords$", $endif$ +$if(description)$ + pdf-description: "$description$", +$endif$ +$if(running-title)$ + running-title: [$running-title$], +$endif$ +$if(version)$ + running-version: [$version$], +$endif$ $if(date)$ date: [$date$], $endif$ @@ -54,42 +63,47 @@ $endif$ // ===================================================================== #page(header: none, footer: none, numbering: none)[ - #v(7cm) + #v(2.2cm) - // Decorative rule — provides visual weight at the head of the title page - #line(length: 100%, stroke: 2pt + rgb("#2C3E5D")) - #v(0.8cm) + #text(size: 9pt, weight: "bold", tracking: 0.12em, fill: rgb("#2D7371"))[ + $if(document-type)$$document-type$$else$OPEN CONSULTATION$endif$ + ] + + #v(1.1cm) #set text(hyphenate: false) #set par(justify: false) - #text(size: 22pt, weight: "bold", fill: rgb("#2C3E5D"))[$if(title)$$title$$endif$] + #heading(level: 1, outlined: false)[$if(title)$$title$$endif$] - #v(0.8cm) + #v(0.5cm) $if(subtitle)$ - #text(size: 15pt, fill: rgb("#414141"))[$subtitle$] + #text(size: 15pt, fill: rgb("#56616E"))[$subtitle$] - #v(0.6cm) + #v(0.8cm) $endif$ - #text(size: 14pt)[Discussion Paper --- $if(version)$$version$$endif$] + #line(length: 7cm, stroke: 2pt + rgb("#315C87")) - #v(1.2cm) + #v(1.1cm) - #table( - columns: (auto, 1fr), - stroke: none, - fill: none, - align: left, - inset: (x: 8pt, y: 4pt), - [*Date:*], [$if(date)$$date$$endif$], - [*Prepared by:*], [$if(author)$$author$$endif$], - ) + #block(fill: rgb("#EAF1F5"), radius: 4pt, inset: 14pt, width: 100%)[ + #grid( + columns: (auto, 1fr), + column-gutter: 14pt, + row-gutter: 7pt, + [*Version*], [$if(version)$$version$$endif$], + [*Date*], [$if(date)$$date$$endif$], + [*Author*], [$if(author)$$author$$endif$], + ) + ] #v(1fr) - #text(size: 9pt, fill: rgb("#414141"))[ - $if(disclaimer)$$disclaimer$$else$This is a discussion paper. It presents a threat model and preliminary analysis, not final guidance. Comments and contributions are welcome.$endif$ + #block(stroke: (left: 3pt + rgb("#2D7371")), inset: (left: 12pt, y: 7pt), width: 100%)[ + #text(size: 9pt, fill: rgb("#56616E"))[ + $if(disclaimer)$$disclaimer$$else$This is a discussion paper. It presents a threat model and preliminary analysis, not final guidance. Comments and contributions are welcome.$endif$ + ] ] ] @@ -99,22 +113,21 @@ $endif$ $if(version-table)$ #page(header: none, footer: none, numbering: none)[ + #heading(level: 1, outlined: false)[Document control] #v(0.5cm) - #text(size: 16pt, weight: "bold", fill: rgb("#2C3E5D"))[Document Control] - #v(1cm) #table( columns: (auto, auto, 1fr), inset: 8pt, stroke: (x, y) => { - if y == 0 { (bottom: 1.2pt + rgb("#2C3E5D")) } - else { (bottom: 0.5pt + luma(200)) } + if y == 0 { (bottom: 1.2pt + rgb("#315C87")) } + else { (bottom: 0.5pt + rgb("#CBD5DE")) } }, fill: (x, y) => { - if y == 0 { rgb("#2C3E5D").lighten(90%) } + if y == 0 { rgb("#EAF1F5") } else { none } }, - table.cell(stroke: none)[*Version*], table.cell(stroke: none)[*Date*], table.cell(stroke: none)[*Changes*], + table.header([*Version*], [*Date*], [*Changes*]), $for(version-table)$ [$version-table.version$], [$version-table.date$], [#text(size: 9pt)[$version-table.changes$]], $endfor$ @@ -129,14 +142,10 @@ $endif$ #set page(numbering: "i") #counter(page).update(1) -// ToC title styled as front matter — no navy rule or forced page break +// The explicit heading gives the tagged PDF a navigable front-matter landmark. +#heading(level: 1, outlined: false)[Contents] #outline( - title: text( - font: ("TeX Gyre Heros", "Liberation Sans", "DejaVu Sans"), - size: 20pt, - weight: "bold", - fill: rgb("#2C3E5D"), - )[Contents], + title: none, depth: 4, indent: 1.5em, ) diff --git a/source/pdf/pandoc-typst-lite.typ b/source/pdf/pandoc-typst-lite.typ index 8f338af..9d0af69 100644 --- a/source/pdf/pandoc-typst-lite.typ +++ b/source/pdf/pandoc-typst-lite.typ @@ -36,6 +36,15 @@ $endif$ $if(keywords)$ pdf-keywords: "$keywords$", $endif$ +$if(description)$ + pdf-description: "$description$", +$endif$ +$if(running-title)$ + running-title: [$running-title$], +$endif$ +$if(version)$ + running-version: [$version$], +$endif$ $if(date)$ date: [$date$], $endif$ @@ -54,45 +63,50 @@ $endif$ // ===================================================================== #page(header: none, footer: none, numbering: none)[ - #v(7cm) + #v(2.2cm) - // Decorative rule — provides visual weight at the head of the title page - #line(length: 100%, stroke: 2pt + rgb("#2C3E5D")) - #v(0.8cm) + #text(size: 9pt, weight: "bold", tracking: 0.12em, fill: rgb("#2D7371"))[ + $if(document-type)$$document-type$$else$BRIEFING$endif$ + ] + + #v(1.1cm) #set text(hyphenate: false) #set par(justify: false) - #text(size: 22pt, weight: "bold", fill: rgb("#2C3E5D"))[$if(title)$$title$$endif$] + #heading(level: 1, outlined: false)[$if(title)$$title$$endif$] - #v(0.8cm) + #v(0.5cm) $if(subtitle)$ - #text(size: 15pt, fill: rgb("#414141"))[$subtitle$] + #text(size: 15pt, fill: rgb("#56616E"))[$subtitle$] - #v(0.6cm) + #v(0.8cm) $endif$ - #text(size: 14pt)[Discussion Paper --- $if(version)$$version$$endif$] - - #v(1.2cm) - - #table( - columns: (auto, 1fr), - stroke: none, - fill: none, - align: left, - inset: (x: 8pt, y: 4pt), - [*Date:*], [$if(date)$$date$$endif$], - [*Prepared by:*], [$if(author)$$author$$endif$], - $if(parent-paper)$ - [*Parent paper:*], [#text(size: 10pt)[$parent-paper$]], - $endif$ - ) + #line(length: 7cm, stroke: 2pt + rgb("#315C87")) + + #v(1.1cm) + + #block(fill: rgb("#EAF1F5"), radius: 4pt, inset: 14pt, width: 100%)[ + #grid( + columns: (auto, 1fr), + column-gutter: 14pt, + row-gutter: 7pt, + [*Version*], [$if(version)$$version$$endif$], + [*Date*], [$if(date)$$date$$endif$], + [*Author*], [$if(author)$$author$$endif$], + $if(parent-paper)$ + [*Related work*], [#text(size: 10pt)[$parent-paper$]], + $endif$ + ) + ] #v(1fr) - #text(size: 9pt, fill: rgb("#414141"))[ - $if(disclaimer)$$disclaimer$$else$This is a discussion paper. It presents a threat model and preliminary analysis, not final guidance. Comments and contributions are welcome.$endif$ + #block(stroke: (left: 3pt + rgb("#2D7371")), inset: (left: 12pt, y: 7pt), width: 100%)[ + #text(size: 9pt, fill: rgb("#56616E"))[ + $if(disclaimer)$$disclaimer$$else$This is a discussion paper. It presents a threat model and preliminary analysis, not final guidance. Comments and contributions are welcome.$endif$ + ] ] ] diff --git a/source/pdf/template.typ b/source/pdf/template.typ index 0ad75d6..0486288 100644 --- a/source/pdf/template.typ +++ b/source/pdf/template.typ @@ -1,12 +1,18 @@ -// Typst template for long-form discussion papers +// Typst template for the Semantic Defects publication suite. +// Requires Typst >= 0.14.0. // // Used via pandoc: pandoc input.md -t typst --template=pandoc-typst.typ --metadata-file=metadata.yaml -// --- Brand Colours --- -#let dta-navy = rgb("#2C3E5D") -#let dta-body = rgb("#333333") -#let dta-code-bg = rgb("#F5F5F5") -#let dta-rule-blue = rgb("#2E5090") +// A restrained, high-contrast palette for screen and print. Colour is never +// the only carrier of meaning; links are also underlined and callouts use rules. +#let ink = rgb("#18212F") +#let muted = rgb("#56616E") +#let accent = rgb("#315C87") +#let accent-dark = rgb("#234564") +#let teal = rgb("#2D7371") +#let paper-blue = rgb("#EAF1F5") +#let code-bg = rgb("#F4F6F8") +#let rule = rgb("#CBD5DE") // --- conf function (called by pandoc's template wrapper) --- #let conf( @@ -24,6 +30,9 @@ pdf-title: none, pdf-author: none, pdf-keywords: none, + pdf-description: none, + running-title: none, + running-version: none, // Custom metadata passed via pandoc doc, ) = { @@ -36,25 +45,36 @@ title: if pdf-title != none { pdf-title } else { none }, author: if pdf-author != none { (pdf-author,) } else { () }, keywords: if pdf-keywords != none { pdf-keywords.split(", ") } else { () }, + description: pdf-description, date: auto, ) // --- Page setup --- set page( paper: "a4", - margin: (top: 3cm, bottom: 3cm, left: 3cm, right: 3cm), + margin: (top: 2.8cm, bottom: 2.8cm, left: 2.7cm, right: 2.7cm), fill: white, - header: { - line(length: 100%, stroke: 0.4pt + luma(180)) + header: context { + grid( + columns: (1fr, auto), + align: (left, right), + text(size: 9pt, fill: muted)[#if running-title != none { running-title }], + text(size: 9pt, fill: muted)[#if running-version != none { running-version }], + ) + v(0.25em) + line(length: 100%, stroke: 0.6pt + rule) }, footer: context { - let page-num = counter(page).display() - line(length: 100%, stroke: 0.4pt + luma(180)) - v(0.2em) + // Read the counter value directly: display() is a locatable reference, + // which PDF/UA correctly forbids inside an artifact. + let page-num = str(counter(page).get().first()) + line(length: 100%, stroke: 0.6pt + rule) + v(0.25em) grid( - columns: (1fr, 1fr, 1fr), - align: (left, center, right), - [], [], text(size: 9pt)[#page-num], + columns: (1fr, auto), + align: (left, right), + text(size: 9pt, fill: muted, "semanticdefects.foundryside.dev"), + text(size: 9pt, weight: "medium", fill: ink, page-num), ) }, ) @@ -67,21 +87,19 @@ set text( font: ("Libertinus Serif", "TeX Gyre Termes", "Liberation Serif", "DejaVu Serif"), size: 11pt, - fill: dta-body, + fill: ink, lang: lang, region: region, hyphenate: true, ) // --- Paragraph spacing --- - // Justified body text with controlled hyphenation for formal government documents. - // "optimized" linebreaks provide some implicit widow/orphan mitigation by choosing - // better break points across the full paragraph. - // Note: par(costs: (widow: ..., orphan: ...)) requires Typst 0.15+ — add when available. + // Ragged-right copy avoids rivers in long technical passages and is easier + // to track for readers with dyslexia or low vision. set par( - leading: 0.9em, + leading: 0.78em, spacing: 1.1em, - justify: true, + justify: false, linebreaks: "optimized", ) @@ -92,13 +110,23 @@ // Prevent headings from appearing at the bottom of a page with no following content show heading: set block(sticky: true) - // Level 1 = ## in markdown (part titles) — strong visual separation with rule - show heading.where(level: 1): it => { + // Front-matter H1s are semantic landmarks but stay within their composed page. + show heading.where(level: 1, outlined: false): it => { + text( + font: ("TeX Gyre Heros", "Liberation Sans", "DejaVu Sans"), + size: 21pt, + weight: "bold", + fill: ink, + )[#it.body] + } + + // Level 1 = ## in markdown (part titles) — strong visual separation with rule. + show heading.where(level: 1, outlined: true): it => { pagebreak(weak: true) v(1.5cm) - line(length: 100%, stroke: 1.5pt + dta-navy) + line(length: 100%, stroke: 2pt + accent) v(0.6em) - text(font: ("TeX Gyre Heros", "Liberation Sans", "DejaVu Sans"), size: 20pt, weight: "bold", fill: dta-navy)[#it.body] + text(font: ("TeX Gyre Heros", "Liberation Sans", "DejaVu Sans"), size: 21pt, weight: "bold", fill: ink)[#it.body] v(1em) } @@ -106,7 +134,7 @@ show heading.where(level: 2): it => { v(1.5em) block(breakable: false)[ - #text(font: ("TeX Gyre Heros", "Liberation Sans", "DejaVu Sans"), size: 14pt, weight: "bold", fill: dta-navy)[#it.body] + #text(font: ("TeX Gyre Heros", "Liberation Sans", "DejaVu Sans"), size: 14pt, weight: "bold", fill: accent-dark)[#it.body] #v(0.4em) ] } @@ -115,7 +143,7 @@ show heading.where(level: 3): it => { v(0.8em) block(breakable: false)[ - #text(font: ("TeX Gyre Heros", "Liberation Sans", "DejaVu Sans"), size: 12pt, weight: "bold", fill: dta-navy)[#it.body] + #text(font: ("TeX Gyre Heros", "Liberation Sans", "DejaVu Sans"), size: 12pt, weight: "bold", fill: ink)[#it.body] #v(0.3em) ] } @@ -124,13 +152,13 @@ show heading.where(level: 4): it => { v(0.6em) block(breakable: false)[ - #text(font: ("TeX Gyre Heros", "Liberation Sans", "DejaVu Sans"), size: 11pt, weight: "bold", fill: dta-navy)[#it.body] + #text(font: ("TeX Gyre Heros", "Liberation Sans", "DejaVu Sans"), size: 11pt, weight: "bold", fill: ink)[#it.body] #v(0.2em) ] } // --- Links --- - show link: it => text(fill: dta-rule-blue)[#it] + show link: it => text(fill: accent-dark)[#underline(it)] // --- Code blocks --- // Noto Sans Mono is more compact than DejaVu, fitting longer lines before wrapping. @@ -139,20 +167,20 @@ set text(font: ("Noto Sans Mono", "DejaVu Sans Mono", "Liberation Mono"), size: 9pt) block( width: 100%, - fill: dta-code-bg, + fill: code-bg, inset: (x: 12pt, y: 10pt), radius: 3pt, breakable: true, - stroke: 0.5pt + luma(200), + stroke: (left: 2pt + teal, rest: 0.5pt + rule), it, ) } // --- Inline code --- show raw.where(block: false): it => { - set text(font: ("Noto Sans Mono", "DejaVu Sans Mono", "Liberation Mono"), size: 9.5pt) + set text(font: ("Noto Sans Mono", "DejaVu Sans Mono", "Liberation Mono"), size: 9pt) box( - fill: dta-code-bg, + fill: code-bg, inset: (x: 3pt, y: 0pt), outset: (y: 2pt), radius: 2pt, @@ -164,13 +192,13 @@ set table( inset: (x: 8pt, y: 6pt), stroke: (x, y) => { - if y == 0 { (bottom: 1.2pt + dta-navy) } - else { (bottom: 0.5pt + luma(200)) } + if y == 0 { (bottom: 1.2pt + accent-dark) } + else { (bottom: 0.5pt + rule) } }, fill: (x, y) => { - if y == 0 { dta-navy.lighten(90%) } + if y == 0 { paper-blue } else if calc.odd(y) { white } - else { luma(240) } + else { rgb("#F8FAFB") } }, ) @@ -180,21 +208,23 @@ set table.cell(breakable: true) // Bold header row text - show table.cell.where(y: 0): set text(weight: "bold", size: 9pt, fill: dta-navy, hyphenate: true) + show table.cell.where(y: 0): set text(weight: "bold", size: 9pt, fill: accent-dark, hyphenate: true) // Pandoc wraps tables in figure blocks — make them breakable across pages show figure.where(kind: table): set block(breakable: true, width: 100%) show figure.where(kind: table): set align(left) // Table captions smaller than body text - show figure.caption: set text(size: 8.5pt) + show figure.caption: set text(size: 9pt, fill: muted) // --- Block quotes --- show quote.where(block: true): it => { block( - inset: (left: 1.5em, top: 0.5em, bottom: 0.5em), - stroke: (left: 2.5pt + dta-navy.lighten(60%)), - text(style: "italic", fill: dta-body.lighten(15%))[#it.body], + width: 100%, + inset: (left: 1.2em, right: 1.2em, top: 0.8em, bottom: 0.8em), + fill: paper-blue, + stroke: (left: 3pt + teal), + text(fill: ink)[#it.body], ) } @@ -205,7 +235,7 @@ // --- Footnotes --- // Smaller footnote text with a visible separator rule set footnote.entry(separator: line(length: 30%, stroke: 0.5pt + luma(180))) - show footnote.entry: set text(size: 8.5pt) + show footnote.entry: set text(size: 9pt) // Emit the document body doc diff --git a/source/pdf/tests/test_postprocess_typst.py b/source/pdf/tests/test_postprocess_typst.py new file mode 100644 index 0000000..b2e19f8 --- /dev/null +++ b/source/pdf/tests/test_postprocess_typst.py @@ -0,0 +1,49 @@ +import subprocess +import tempfile +import unittest +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parents[3] +BUILD_COMMON = PROJECT_ROOT / "source" / "pdf" / "build-common.sh" + + +class TypstPostprocessTests(unittest.TestCase): + def test_existing_pandoc_image_alt_is_normalised_without_duplication(self) -> None: + source = """#figure(image(\".assets/wardline/diagram-0.svg\", width: 82.0%, alt: \"Pandoc-native alt.\"), + caption: [ +Canonical caption. + ] +) +""" + + with tempfile.TemporaryDirectory() as temp_dir: + typst_path = Path(temp_dir) / "fixture.typ" + typst_path.write_text(source, encoding="utf-8") + + subprocess.run( + [ + "bash", + "-c", + 'source "$1"; postprocess_tables "$2" "{}"', + "bash", + str(BUILD_COMMON), + str(typst_path), + ], + check=True, + cwd=PROJECT_ROOT, + ) + + processed = typst_path.read_text(encoding="utf-8") + + self.assertEqual(processed.count("alt:"), 1) + self.assertIn('alt: "Canonical caption."', processed) + self.assertIn( + '#figure(image(".assets/wardline/diagram-0.svg", width: 82.0%, ' + 'alt: "Canonical caption."),', + processed, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/website/public/pdf/document-suite-map.pdf b/website/public/pdf/document-suite-map.pdf index 16c8b81..caebac9 100644 Binary files a/website/public/pdf/document-suite-map.pdf and b/website/public/pdf/document-suite-map.pdf differ diff --git a/website/public/pdf/governing-ai-generated-code.pdf b/website/public/pdf/governing-ai-generated-code.pdf index ce0c309..3dfed6e 100644 Binary files a/website/public/pdf/governing-ai-generated-code.pdf and b/website/public/pdf/governing-ai-generated-code.pdf differ diff --git a/website/public/pdf/reviewing-ai-generated-code.pdf b/website/public/pdf/reviewing-ai-generated-code.pdf index ef2cbeb..2954e84 100644 Binary files a/website/public/pdf/reviewing-ai-generated-code.pdf and b/website/public/pdf/reviewing-ai-generated-code.pdf differ diff --git a/website/public/pdf/threat-model-discussion-paper-community.pdf b/website/public/pdf/threat-model-discussion-paper-community.pdf index 2e706bd..8ab3f98 100644 Binary files a/website/public/pdf/threat-model-discussion-paper-community.pdf and b/website/public/pdf/threat-model-discussion-paper-community.pdf differ diff --git a/website/public/pdf/wardline-companion-community.pdf b/website/public/pdf/wardline-companion-community.pdf index d9a3593..54c446e 100644 Binary files a/website/public/pdf/wardline-companion-community.pdf and b/website/public/pdf/wardline-companion-community.pdf differ