ai-forge-cli 3.0.2__tar.gz → 3.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {ai_forge_cli-3.0.2/src/ai_forge_cli.egg-info → ai_forge_cli-3.0.4}/PKG-INFO +5 -1
  2. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/README.md +4 -0
  3. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/pyproject.toml +1 -1
  4. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4/src/ai_forge_cli.egg-info}/PKG-INFO +5 -1
  5. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/ai_forge_cli.egg-info/SOURCES.txt +4 -0
  6. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/assets/audit_template.html +264 -0
  7. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/commands/__init__.py +2 -1
  8. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/commands/audit.py +219 -3
  9. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/commands/context.py +54 -7
  10. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/commands/init.py +35 -0
  11. ai_forge_cli-3.0.4/src/cli/commands/knowledge.py +69 -0
  12. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/crawler.py +190 -0
  13. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/forge.py +2 -1
  14. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/resources/skills/forge-build/SKILL.md +47 -7
  15. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/resources/skills/forge-business/SKILL.md +24 -0
  16. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/resources/skills/forge-hydrate/SKILL.md +9 -0
  17. ai_forge_cli-3.0.4/src/cli/resources/skills/forge-orchestrator/SKILL.md +81 -0
  18. ai_forge_cli-3.0.4/src/cli/resources/skills/forge-orchestrator/agents/openai.yaml +4 -0
  19. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/resources/skills/forge-review/SKILL.md +28 -0
  20. ai_forge_cli-3.0.4/src/cli/resources/skills/forge-schema/FRAMEWORK_REUSE_DRAFT.md +61 -0
  21. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/resources/skills/forge-schema/SKILL.md +111 -12
  22. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/resources/skills/forge-security/SKILL.md +17 -2
  23. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/tests/test_context.py +113 -0
  24. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/tests/test_crawler.py +72 -0
  25. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/MANIFEST.in +0 -0
  26. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/setup.cfg +0 -0
  27. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/setup.py +0 -0
  28. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/ai_forge_cli.egg-info/dependency_links.txt +0 -0
  29. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/ai_forge_cli.egg-info/entry_points.txt +0 -0
  30. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/ai_forge_cli.egg-info/requires.txt +0 -0
  31. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/ai_forge_cli.egg-info/top_level.txt +0 -0
  32. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/__init__.py +0 -0
  33. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/__main__.py +0 -0
  34. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/assets/forge_full_logo.drawio.svg +0 -0
  35. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/assets/forge_white_small.drawio.svg +0 -0
  36. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/commands/base.py +0 -0
  37. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/commands/crawl.py +0 -0
  38. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/common.py +0 -0
  39. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/resources/FRAMEWORK_V4.md +0 -0
  40. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/resources/SCHEMA_REFERENCE_V4.md +0 -0
  41. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/resources/skills/forge-build/agents/openai.yaml +0 -0
  42. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/resources/skills/forge-business/agents/openai.yaml +0 -0
  43. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/resources/skills/forge-hydrate/agents/openai.yaml +0 -0
  44. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/resources/skills/forge-review/agents/openai.yaml +0 -0
  45. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/resources/skills/forge-schema/agents/openai.yaml +0 -0
  46. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/resources/skills/forge-security/agents/openai.yaml +0 -0
  47. {ai_forge_cli-3.0.2 → ai_forge_cli-3.0.4}/src/cli/yaml_io.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-forge-cli
3
- Version: 3.0.2
3
+ Version: 3.0.4
4
4
  Summary: Forge V4 architecture context CLI
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -36,6 +36,8 @@ scoped context, validation, and audit artifacts to the active skill.
36
36
  - `forge crawl`: extract the merged V4 model from central files and code annotations
37
37
  - `forge context`: render scoped context for a system, flow, container, entity,
38
38
  component, operation, or data shape
39
+ - `forge knowledge`: list supporting Markdown knowledge docs attached to Forge
40
+ refs such as containers, flows, entities, components, and operations
39
41
  - `forge audit`: generate a self-contained architecture audit dashboard
40
42
 
41
43
  ## What Forge Is
@@ -44,6 +46,8 @@ scoped context, validation, and audit artifacts to the active skill.
44
46
  - a skills-first workflow for moving from business discovery to one thin build slice
45
47
  - a scoped context generator for build, review, and security tasks
46
48
  - an audit artifact generator for human review
49
+ - a lightweight index for repo-native delivery knowledge such as runbooks,
50
+ testing notes, security notes, operations notes, and domain glossaries
47
51
 
48
52
  ## What Forge Is Not
49
53
 
@@ -21,6 +21,8 @@ scoped context, validation, and audit artifacts to the active skill.
21
21
  - `forge crawl`: extract the merged V4 model from central files and code annotations
22
22
  - `forge context`: render scoped context for a system, flow, container, entity,
23
23
  component, operation, or data shape
24
+ - `forge knowledge`: list supporting Markdown knowledge docs attached to Forge
25
+ refs such as containers, flows, entities, components, and operations
24
26
  - `forge audit`: generate a self-contained architecture audit dashboard
25
27
 
26
28
  ## What Forge Is
@@ -29,6 +31,8 @@ scoped context, validation, and audit artifacts to the active skill.
29
31
  - a skills-first workflow for moving from business discovery to one thin build slice
30
32
  - a scoped context generator for build, review, and security tasks
31
33
  - an audit artifact generator for human review
34
+ - a lightweight index for repo-native delivery knowledge such as runbooks,
35
+ testing notes, security notes, operations notes, and domain glossaries
32
36
 
33
37
  ## What Forge Is Not
34
38
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ai-forge-cli"
7
- version = "3.0.2"
7
+ version = "3.0.4"
8
8
  description = "Forge V4 architecture context CLI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-forge-cli
3
- Version: 3.0.2
3
+ Version: 3.0.4
4
4
  Summary: Forge V4 architecture context CLI
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -36,6 +36,8 @@ scoped context, validation, and audit artifacts to the active skill.
36
36
  - `forge crawl`: extract the merged V4 model from central files and code annotations
37
37
  - `forge context`: render scoped context for a system, flow, container, entity,
38
38
  component, operation, or data shape
39
+ - `forge knowledge`: list supporting Markdown knowledge docs attached to Forge
40
+ refs such as containers, flows, entities, components, and operations
39
41
  - `forge audit`: generate a self-contained architecture audit dashboard
40
42
 
41
43
  ## What Forge Is
@@ -44,6 +46,8 @@ scoped context, validation, and audit artifacts to the active skill.
44
46
  - a skills-first workflow for moving from business discovery to one thin build slice
45
47
  - a scoped context generator for build, review, and security tasks
46
48
  - an audit artifact generator for human review
49
+ - a lightweight index for repo-native delivery knowledge such as runbooks,
50
+ testing notes, security notes, operations notes, and domain glossaries
47
51
 
48
52
  ## What Forge Is Not
49
53
 
@@ -23,6 +23,7 @@ src/cli/commands/base.py
23
23
  src/cli/commands/context.py
24
24
  src/cli/commands/crawl.py
25
25
  src/cli/commands/init.py
26
+ src/cli/commands/knowledge.py
26
27
  src/cli/resources/FRAMEWORK_V4.md
27
28
  src/cli/resources/SCHEMA_REFERENCE_V4.md
28
29
  src/cli/resources/skills/forge-build/SKILL.md
@@ -31,8 +32,11 @@ src/cli/resources/skills/forge-business/SKILL.md
31
32
  src/cli/resources/skills/forge-business/agents/openai.yaml
32
33
  src/cli/resources/skills/forge-hydrate/SKILL.md
33
34
  src/cli/resources/skills/forge-hydrate/agents/openai.yaml
35
+ src/cli/resources/skills/forge-orchestrator/SKILL.md
36
+ src/cli/resources/skills/forge-orchestrator/agents/openai.yaml
34
37
  src/cli/resources/skills/forge-review/SKILL.md
35
38
  src/cli/resources/skills/forge-review/agents/openai.yaml
39
+ src/cli/resources/skills/forge-schema/FRAMEWORK_REUSE_DRAFT.md
36
40
  src/cli/resources/skills/forge-schema/SKILL.md
37
41
  src/cli/resources/skills/forge-schema/agents/openai.yaml
38
42
  src/cli/resources/skills/forge-security/SKILL.md
@@ -598,6 +598,247 @@
598
598
  .decision-field li + li {
599
599
  margin-top: 2px;
600
600
  }
601
+ .knowledge-type-grid {
602
+ display: grid;
603
+ grid-auto-flow: column;
604
+ grid-auto-columns: minmax(86px, 1fr);
605
+ gap: 8px;
606
+ margin-bottom: 18px;
607
+ overflow-x: auto;
608
+ padding-bottom: 2px;
609
+ }
610
+ .knowledge-type-grid .card {
611
+ min-width: 0;
612
+ padding: 10px 12px;
613
+ border-radius: 8px;
614
+ }
615
+ .knowledge-type-grid .card h3 {
616
+ margin: 0 0 6px;
617
+ font-size: 13px;
618
+ line-height: 1.15;
619
+ overflow-wrap: anywhere;
620
+ }
621
+ .knowledge-type-grid .card p {
622
+ margin: 0;
623
+ font-size: 12px;
624
+ line-height: 1.15;
625
+ }
626
+ .related-knowledge-block {
627
+ display: grid;
628
+ gap: 14px;
629
+ }
630
+ .knowledge-doc-grid {
631
+ display: grid;
632
+ gap: 14px;
633
+ }
634
+ .knowledge-type-group {
635
+ display: grid;
636
+ gap: 10px;
637
+ min-width: 0;
638
+ }
639
+ .knowledge-type-group-summary {
640
+ display: grid;
641
+ grid-template-columns: minmax(0, 1fr) auto 18px;
642
+ gap: 10px;
643
+ align-items: center;
644
+ padding: 8px 12px;
645
+ border: 1px solid var(--border);
646
+ border-radius: 8px;
647
+ background: var(--panel-alt);
648
+ cursor: pointer;
649
+ list-style: none;
650
+ }
651
+ .knowledge-type-group-summary::-webkit-details-marker {
652
+ display: none;
653
+ }
654
+ .knowledge-type-group-summary::marker {
655
+ content: "";
656
+ }
657
+ .knowledge-type-group-summary span:first-child {
658
+ color: var(--text);
659
+ font-size: 13px;
660
+ font-weight: 700;
661
+ }
662
+ .knowledge-type-group-summary strong {
663
+ display: inline-flex;
664
+ align-items: center;
665
+ justify-content: center;
666
+ min-width: 24px;
667
+ min-height: 20px;
668
+ padding: 0 7px;
669
+ border: 1px solid var(--border);
670
+ border-radius: 999px;
671
+ background: var(--panel);
672
+ color: var(--muted);
673
+ font-size: 12px;
674
+ line-height: 1;
675
+ }
676
+ .knowledge-type-group-docs {
677
+ display: grid;
678
+ gap: 10px;
679
+ }
680
+ .related-knowledge-block {
681
+ margin-top: 18px;
682
+ }
683
+ .knowledge-inline-header {
684
+ margin-bottom: 0;
685
+ }
686
+ .knowledge-doc-card {
687
+ padding: 0;
688
+ overflow: hidden;
689
+ min-width: 0;
690
+ }
691
+ .knowledge-doc-summary {
692
+ display: grid;
693
+ grid-template-columns: minmax(0, 1fr) minmax(260px, auto) 18px;
694
+ gap: 4px 14px;
695
+ align-items: start;
696
+ padding: 12px 14px;
697
+ background: var(--panel);
698
+ cursor: pointer;
699
+ list-style: none;
700
+ min-width: 0;
701
+ }
702
+ .knowledge-doc-summary::-webkit-details-marker {
703
+ display: none;
704
+ }
705
+ .knowledge-doc-summary::marker {
706
+ content: "";
707
+ }
708
+ .knowledge-doc-summary:hover {
709
+ background: var(--hover);
710
+ }
711
+ .knowledge-doc-identity,
712
+ .knowledge-doc-meta {
713
+ min-width: 0;
714
+ }
715
+ .knowledge-doc-identity {
716
+ display: contents;
717
+ }
718
+ .knowledge-doc-summary h4 {
719
+ grid-column: 1;
720
+ grid-row: 1;
721
+ margin: 0;
722
+ font-size: 14px;
723
+ line-height: 1.15;
724
+ }
725
+ .knowledge-doc-summary span {
726
+ color: var(--muted);
727
+ font-size: 12px;
728
+ line-height: 1.15;
729
+ }
730
+ .knowledge-doc-meta {
731
+ display: grid;
732
+ grid-template-columns: auto auto;
733
+ align-items: start;
734
+ justify-content: end;
735
+ gap: 4px 6px;
736
+ grid-column: 2;
737
+ grid-row: 1;
738
+ color: var(--muted);
739
+ font-size: 12px;
740
+ text-align: left;
741
+ }
742
+ .knowledge-doc-meta strong,
743
+ .knowledge-doc-meta em {
744
+ display: inline-flex;
745
+ align-items: center;
746
+ min-height: 20px;
747
+ padding: 0 7px;
748
+ border: 1px solid var(--border);
749
+ border-radius: 999px;
750
+ background: var(--panel-alt);
751
+ font-style: normal;
752
+ font-weight: 600;
753
+ }
754
+ .knowledge-doc-meta-row {
755
+ grid-column: 1;
756
+ display: grid;
757
+ grid-template-columns: 42px minmax(0, 1fr);
758
+ gap: 8px;
759
+ align-items: baseline;
760
+ min-width: 0;
761
+ text-align: left;
762
+ }
763
+ .knowledge-doc-identity .knowledge-doc-meta-row:nth-of-type(1) {
764
+ grid-row: 2;
765
+ }
766
+ .knowledge-doc-identity .knowledge-doc-meta-row:nth-of-type(2) {
767
+ grid-row: 3;
768
+ }
769
+ .knowledge-doc-meta-row span {
770
+ color: var(--subtle);
771
+ font-size: 11px;
772
+ font-weight: 700;
773
+ line-height: 1.15;
774
+ text-transform: uppercase;
775
+ }
776
+ .knowledge-doc-meta-row p {
777
+ margin: 0;
778
+ color: var(--muted);
779
+ font-size: 12px;
780
+ line-height: 1.15;
781
+ }
782
+ .knowledge-doc-file {
783
+ grid-column: 2;
784
+ grid-row: 3;
785
+ justify-self: end;
786
+ align-self: baseline;
787
+ color: var(--muted);
788
+ font-size: 12px;
789
+ line-height: 1.15;
790
+ max-width: 420px;
791
+ overflow-wrap: anywhere;
792
+ text-align: right;
793
+ }
794
+ .knowledge-doc-summary .entity-shape-chevron {
795
+ grid-column: 3;
796
+ grid-row: 1;
797
+ margin-top: 2px;
798
+ }
799
+ .knowledge-doc-summary .entity-shape-chevron::before,
800
+ .knowledge-type-group-summary .entity-shape-chevron::before {
801
+ content: "▶";
802
+ }
803
+ .knowledge-doc-card[open] > .knowledge-doc-summary .entity-shape-chevron::before,
804
+ .knowledge-type-group[open] > .knowledge-type-group-summary .entity-shape-chevron::before {
805
+ content: "▼";
806
+ }
807
+ .knowledge-doc-body {
808
+ padding: 0 18px 16px;
809
+ color: var(--text);
810
+ border-top: 1px solid var(--border);
811
+ }
812
+ .knowledge-doc-body h1,
813
+ .knowledge-doc-body h2,
814
+ .knowledge-doc-body h3,
815
+ .knowledge-doc-body h4 {
816
+ margin: 16px 0 8px;
817
+ font-size: 15px;
818
+ line-height: 1.25;
819
+ }
820
+ .knowledge-doc-body p,
821
+ .knowledge-doc-body li {
822
+ color: var(--muted);
823
+ font-size: 13px;
824
+ line-height: 1.5;
825
+ }
826
+ .knowledge-doc-body p {
827
+ margin: 8px 0 0;
828
+ }
829
+ .knowledge-doc-body ul {
830
+ margin: 8px 0 0;
831
+ padding-left: 20px;
832
+ }
833
+ .knowledge-doc-body pre {
834
+ margin: 10px 0 0;
835
+ padding: 12px;
836
+ border: 1px solid var(--border);
837
+ border-radius: 6px;
838
+ background: var(--panel-alt);
839
+ overflow-x: auto;
840
+ font-size: 12px;
841
+ }
601
842
  .annotation-filter {
602
843
  display: flex;
603
844
  flex-wrap: wrap;
@@ -2314,6 +2555,24 @@
2314
2555
  .record-meta {
2315
2556
  text-align: left;
2316
2557
  }
2558
+ .knowledge-doc-summary {
2559
+ grid-template-columns: minmax(0, 1fr) 18px;
2560
+ }
2561
+ .knowledge-doc-meta {
2562
+ grid-column: 1 / -1;
2563
+ justify-content: start;
2564
+ margin-top: 6px;
2565
+ }
2566
+ .knowledge-doc-file {
2567
+ grid-column: 1 / -1;
2568
+ grid-row: auto;
2569
+ justify-self: start;
2570
+ max-width: none;
2571
+ text-align: left;
2572
+ }
2573
+ .knowledge-doc-summary .entity-shape-chevron {
2574
+ grid-column: 2;
2575
+ }
2317
2576
  .runtime-container-header {
2318
2577
  grid-template-columns: minmax(0, 1fr);
2319
2578
  }
@@ -2346,6 +2605,7 @@
2346
2605
  <button class="tab" data-group="Flows">Flows</button>
2347
2606
  <button class="tab" data-group="Runtime">Runtime</button>
2348
2607
  <button class="tab" data-group="Data">Data</button>
2608
+ <button class="tab" data-group="Knowledge">Knowledge</button>
2349
2609
  <button class="tab" data-group="Validation">Validation</button>
2350
2610
  <button class="tab" data-group="Quality Assurance">Quality Assurance</button>
2351
2611
  </nav>
@@ -2369,6 +2629,9 @@
2369
2629
  </div>
2370
2630
  <div data-slot="data-body">__DATA_BODY__</div>
2371
2631
  </section>
2632
+ <section class="section" id="knowledge-overview" data-group="Knowledge">
2633
+ <div data-slot="knowledge-body">__KNOWLEDGE_BODY__</div>
2634
+ </section>
2372
2635
  <section class="section" id="verticals-overview" data-group="Validation">
2373
2636
  <div data-slot="verticals-body">__VERTICALS_BODY__</div>
2374
2637
  </section>
@@ -2422,6 +2685,7 @@
2422
2685
  System: 'system-overview',
2423
2686
  Runtime: 'runtime-overview',
2424
2687
  Data: 'data-overview',
2688
+ Knowledge: 'knowledge-overview',
2425
2689
  Validation: 'verticals-overview',
2426
2690
  'Quality Assurance': 'quality-assurance-overview',
2427
2691
  };
@@ -2,5 +2,6 @@ from cli.commands.audit import register_audit
2
2
  from cli.commands.context import register_context
3
3
  from cli.commands.crawl import register_crawl
4
4
  from cli.commands.init import register_init
5
+ from cli.commands.knowledge import register_knowledge
5
6
 
6
- __all__ = ["register_context", "register_init", "register_audit", "register_crawl"]
7
+ __all__ = ["register_context", "register_init", "register_audit", "register_crawl", "register_knowledge"]
@@ -5,6 +5,7 @@ import errno
5
5
  import hashlib
6
6
  import json
7
7
  import platform
8
+ import re
8
9
  import subprocess
9
10
  from argparse import Namespace
10
11
  from html import escape
@@ -134,6 +135,7 @@ def render_v4_audit_html(result: ForgeCrawlResult) -> str:
134
135
  {"id": "system-overview", "group": "System", "label": "System"},
135
136
  {"id": "runtime-overview", "group": "Runtime", "label": "Runtime Overview"},
136
137
  {"id": "data-overview", "group": "Data", "label": "Entities & Data"},
138
+ {"id": "knowledge-overview", "group": "Knowledge", "label": "Knowledge"},
137
139
  {"id": "verticals-overview", "group": "Validation", "label": "Findings"},
138
140
  {"id": "quality-assurance-overview", "group": "Quality Assurance", "label": "Quality Assurance"},
139
141
  ]
@@ -147,7 +149,7 @@ def render_v4_audit_html(result: ForgeCrawlResult) -> str:
147
149
  "Flows",
148
150
  f"Flow: {flow['id']}",
149
151
  "Container-level flow across runtime boundaries.",
150
- _render_v4_flow_section(flow, model),
152
+ _render_v4_flow_section(flow, model) + _render_related_knowledge(model, [f"flow:{flow['id']}"]),
151
153
  str(system.get("id", "forge")),
152
154
  with_header=False,
153
155
  )
@@ -161,7 +163,7 @@ def render_v4_audit_html(result: ForgeCrawlResult) -> str:
161
163
  "Runtime",
162
164
  f"Container: {container['id']}",
163
165
  "Source-root components, data shapes, and extracted operations.",
164
- _render_v4_container_section(container),
166
+ _render_v4_container_section(container) + _render_related_knowledge(model, [f"container:{container['id']}"]),
165
167
  str(system.get("id", "forge")),
166
168
  with_header=False,
167
169
  )
@@ -175,7 +177,7 @@ def render_v4_audit_html(result: ForgeCrawlResult) -> str:
175
177
  "Data",
176
178
  f"Entity: {entity['id']}",
177
179
  "Business state, canonical shape, ownership, and persistence.",
178
- _render_v4_entity_section(entity, model),
180
+ _render_v4_entity_section(entity, model) + _render_related_knowledge(model, [f"entity:{entity['id']}"]),
179
181
  str(system.get("id", "forge")),
180
182
  with_header=False,
181
183
  )
@@ -215,6 +217,7 @@ def render_v4_audit_html(result: ForgeCrawlResult) -> str:
215
217
  "__SYSTEM_BODY__": _render_v4_system_section(model),
216
218
  "__RUNTIME_BODY__": _render_v4_runtime_section(model),
217
219
  "__DATA_BODY__": _render_v4_data_section(model),
220
+ "__KNOWLEDGE_BODY__": _render_v4_knowledge_section(model),
218
221
  "__VERTICALS_BODY__": _render_v4_findings_overview(model),
219
222
  "__QUALITY_ASSURANCE_BODY__": _render_quality_assurance_overview(str(system.get("id", "forge"))),
220
223
  "__DEPLOYMENT_BODY__": "",
@@ -238,6 +241,7 @@ def _render_v4_overview_section(model: dict[str, object]) -> str:
238
241
  ("System", str(system.get("id", "forge")), "system-overview"),
239
242
  ("Containers", str(summary.get("containers", 0)), "runtime-overview"),
240
243
  ("Flows", str(summary.get("container_flows", 0)), "flow-" + str(model["container_flows"][0]["id"]) if model["container_flows"] else "runtime-overview"),
244
+ ("Knowledge", str(summary.get("knowledge", 0)), "knowledge-overview"),
241
245
  ("Findings", str(summary.get("validation_findings", 0)), "verticals-overview"),
242
246
  ]
243
247
  summary_html = "".join(
@@ -336,6 +340,217 @@ def _render_v4_data_section(model: dict[str, object]) -> str:
336
340
  return '<div class="record-list">' + "".join(rows) + "</div>"
337
341
 
338
342
 
343
+ def _render_v4_knowledge_section(model: dict[str, object]) -> str:
344
+ knowledge = [doc for doc in model.get("knowledge", []) if isinstance(doc, dict)]
345
+ if not knowledge:
346
+ return (
347
+ _section_header(
348
+ "knowledge-overview",
349
+ f"{model['system'].get('id', 'forge')} / Knowledge",
350
+ "Knowledge",
351
+ "Supporting Markdown docs attached to Forge objects.",
352
+ )
353
+ + '<div class="card"><p>No knowledge docs found under <code>forge/knowledge</code>.</p></div>'
354
+ )
355
+ types = sorted({str(doc.get("type", "unknown") or "unknown") for doc in knowledge})
356
+ summary_cards = "".join(
357
+ '<div class="card card-defined">'
358
+ f'<h3>{escape(type_)}</h3>'
359
+ f'<p>{sum(1 for doc in knowledge if str(doc.get("type", "unknown") or "unknown") == type_)}</p>'
360
+ '</div>'
361
+ for type_ in types
362
+ )
363
+ docs = _knowledge_doc_groups(knowledge, expanded=True)
364
+ return (
365
+ _section_header(
366
+ "knowledge-overview",
367
+ f"{model['system'].get('id', 'forge')} / Knowledge",
368
+ "Knowledge",
369
+ "Runbooks, test suites, security notes, operations notes, glossaries, and guides attached to Forge refs.",
370
+ )
371
+ + f'<div class="knowledge-type-grid">{summary_cards}</div>'
372
+ + f'<div class="knowledge-doc-grid">{docs}</div>'
373
+ )
374
+
375
+
376
+ def _render_related_knowledge(model: dict[str, object], refs: list[str]) -> str:
377
+ knowledge = _knowledge_for_refs(model, refs)
378
+ if not knowledge:
379
+ return ""
380
+ return (
381
+ '<section class="related-knowledge-block">'
382
+ '<div class="section-header knowledge-inline-header">'
383
+ '<h3 class="section-title">Related Knowledge</h3>'
384
+ '<p class="section-description">Supporting docs attached to this Forge object.</p>'
385
+ '</div>'
386
+ f'{_knowledge_cards(knowledge, expanded=False)}'
387
+ '</section>'
388
+ )
389
+
390
+
391
+ def _knowledge_for_refs(model: dict[str, object], refs: list[str]) -> list[dict[str, object]]:
392
+ wanted = set(refs)
393
+ return [
394
+ doc
395
+ for doc in model.get("knowledge", [])
396
+ if isinstance(doc, dict) and wanted.intersection(doc.get("refs", []))
397
+ ]
398
+
399
+
400
+ def _knowledge_doc_groups(knowledge: list[dict[str, object]], *, expanded: bool) -> str:
401
+ grouped: dict[str, list[dict[str, object]]] = {}
402
+ for doc in knowledge:
403
+ type_ = str(doc.get("type", "unknown") or "unknown")
404
+ grouped.setdefault(type_, []).append(doc)
405
+
406
+ blocks: list[str] = []
407
+ for type_ in sorted(grouped):
408
+ docs = grouped[type_]
409
+ if len(docs) == 1:
410
+ blocks.append(_knowledge_cards(docs, expanded=expanded))
411
+ continue
412
+ open_attr = " open" if expanded else ""
413
+ blocks.append(
414
+ f'<details class="knowledge-type-group"{open_attr}>'
415
+ '<summary class="knowledge-type-group-summary">'
416
+ f'<span>{escape(type_)}</span>'
417
+ f'<strong>{len(docs)}</strong>'
418
+ '<span class="entity-shape-chevron" aria-hidden="true"></span>'
419
+ '</summary>'
420
+ '<div class="knowledge-type-group-docs">'
421
+ f'{_knowledge_cards(docs, expanded=expanded)}'
422
+ '</div>'
423
+ '</details>'
424
+ )
425
+ return "".join(blocks)
426
+
427
+
428
+ def _knowledge_cards(knowledge: list[dict[str, object]], *, expanded: bool) -> str:
429
+ if not knowledge:
430
+ return "<p>No related knowledge docs.</p>"
431
+ cards: list[str] = []
432
+ for doc in sorted(knowledge, key=lambda item: str(item.get("title") or item.get("path", "")).casefold()):
433
+ type_ = str(doc.get("type", "") or "unknown")
434
+ status = str(doc.get("status", "") or "")
435
+ refs = doc.get("refs", [])
436
+ tags = doc.get("tags", [])
437
+ body = str(doc.get("body") or doc.get("excerpt") or "").strip()
438
+ body_html = _markdown_to_html(body)
439
+ summary = (
440
+ '<div class="knowledge-doc-identity">'
441
+ f'<h4>{escape(str(doc.get("title") or doc.get("path", "")))}</h4>'
442
+ f'{_knowledge_doc_meta_row("Refs", refs)}'
443
+ f'{_knowledge_doc_meta_row("Tags", tags)}'
444
+ '</div>'
445
+ '<div class="knowledge-doc-meta">'
446
+ f'<strong>{escape(type_)}</strong>'
447
+ f'{f"<em>{escape(status)}</em>" if status else ""}'
448
+ '</div>'
449
+ f'<span class="knowledge-doc-file">{escape(str(doc.get("path", "")))}</span>'
450
+ '<span class="entity-shape-chevron" aria-hidden="true"></span>'
451
+ )
452
+ open_attr = " open" if expanded else ""
453
+ cards.append(
454
+ f'<details class="card knowledge-doc-card"{open_attr}>'
455
+ '<summary class="knowledge-doc-summary">'
456
+ f'{summary}'
457
+ '</summary>'
458
+ f'<article class="knowledge-doc-body">{body_html}</article>'
459
+ '</details>'
460
+ )
461
+ return "".join(cards)
462
+
463
+
464
+ def _knowledge_doc_meta_row(label: str, values: object) -> str:
465
+ if not isinstance(values, list):
466
+ return ""
467
+ value_text = ", ".join(
468
+ str(value)
469
+ for value in values
470
+ if str(value).strip()
471
+ )
472
+ if not value_text:
473
+ return ""
474
+ return (
475
+ '<div class="knowledge-doc-meta-row">'
476
+ f'<span>{escape(label)}</span>'
477
+ f'<p>{escape(value_text)}</p>'
478
+ '</div>'
479
+ )
480
+
481
+
482
+ def _markdown_to_html(markdown: str) -> str:
483
+ lines = markdown.splitlines()
484
+ html: list[str] = []
485
+ paragraph: list[str] = []
486
+ list_items: list[str] = []
487
+ list_tag = "ul"
488
+ code_lines: list[str] = []
489
+ in_code = False
490
+
491
+ def flush_paragraph() -> None:
492
+ if paragraph:
493
+ html.append(f"<p>{escape(' '.join(paragraph))}</p>")
494
+ paragraph.clear()
495
+
496
+ def flush_list() -> None:
497
+ if list_items:
498
+ html.append(f"<{list_tag}>" + "".join(f"<li>{escape(item)}</li>" for item in list_items) + f"</{list_tag}>")
499
+ list_items.clear()
500
+
501
+ for line in lines:
502
+ stripped = line.strip()
503
+ ordered_match = re.match(r"^(\d+)\.\s+(.*)$", stripped)
504
+ if stripped.startswith("```"):
505
+ flush_paragraph()
506
+ flush_list()
507
+ if in_code:
508
+ html.append(f"<pre><code>{escape(chr(10).join(code_lines))}</code></pre>")
509
+ code_lines.clear()
510
+ in_code = False
511
+ else:
512
+ in_code = True
513
+ continue
514
+ if in_code:
515
+ code_lines.append(line)
516
+ continue
517
+ if not stripped:
518
+ flush_paragraph()
519
+ flush_list()
520
+ continue
521
+ if list_items and (line.startswith(" ") or line.startswith("\t")) and not stripped.startswith(("- ", "* ")) and not ordered_match:
522
+ list_items[-1] = f"{list_items[-1]} {stripped}"
523
+ continue
524
+ if stripped.startswith("#"):
525
+ flush_paragraph()
526
+ flush_list()
527
+ level = min(len(stripped) - len(stripped.lstrip("#")), 4)
528
+ title = stripped[level:].strip()
529
+ html.append(f"<h{level}>{escape(title)}</h{level}>")
530
+ continue
531
+ if stripped.startswith(("- ", "* ")):
532
+ flush_paragraph()
533
+ if list_items and list_tag != "ul":
534
+ flush_list()
535
+ list_tag = "ul"
536
+ list_items.append(stripped[2:].strip())
537
+ continue
538
+ if ordered_match:
539
+ flush_paragraph()
540
+ if list_items and list_tag != "ol":
541
+ flush_list()
542
+ list_tag = "ol"
543
+ list_items.append(ordered_match.group(2).strip())
544
+ continue
545
+ flush_list()
546
+ paragraph.append(stripped)
547
+ flush_paragraph()
548
+ flush_list()
549
+ if in_code:
550
+ html.append(f"<pre><code>{escape(chr(10).join(code_lines))}</code></pre>")
551
+ return "".join(html) if html else "<p>No content.</p>"
552
+
553
+
339
554
  def _v4_data_overview_chips(entity: dict[str, object], persistence: dict[str, object] | None) -> str:
340
555
  chip_specs = [("chip-persistent", str(entity.get("category", "")))]
341
556
  if persistence:
@@ -2153,6 +2368,7 @@ def _toolbar_sections(sections: list[dict[str, str]]) -> list[dict[str, str]]:
2153
2368
  "system-overview",
2154
2369
  "runtime-overview",
2155
2370
  "data-overview",
2371
+ "knowledge-overview",
2156
2372
  "verticals-overview",
2157
2373
  "quality-assurance-overview",
2158
2374
  "deployment-overview",