Poprawki w scan-xxxx

This commit is contained in:
Marcin Wojas
2026-05-20 18:26:26 +02:00
parent 37d92acfe0
commit bd3750078c
4 changed files with 298 additions and 24 deletions
+25 -15
View File
@@ -45,15 +45,25 @@ Algorytm:
w l.mn. („Dokumenty handlowe"), bo opisują tabelę. Fallback: jeśli klasy `*Table` brak
lub nie ma atrybutu, czytane są te same atrybuty z klasy `*Row`. Wartością jest pierwszy
parametr `string` konstruktora atrybutu.
- `Guided` = `tak`, gdy klasa `*Table` dziedziczy (bezpośrednio lub pośrednio) z `GuidedTable`
albo `ExportedTable`. Tabele oznaczone `Guided=tak`**rootami drzewa obiektów**
stanowią korzeń paczki danych (`Datapack`/`GuidedRow`/`ExportedRow`) i to one są obsługiwane
przez mechanizm synchronizacji i eksportu/importu. Tabele bez tej flagi to elementy
szczegółowe (subrowy, info-rowy), które są częścią paczki danej tabeli-korzenia, ale nie
stanowią samodzielnego rootu.
- `Guided` — rozróżnia trzy stany:
- `root` — klasa `*Table` dziedziczy (bezpośrednio lub pośrednio) z `GuidedTable`
albo `ExportedTable`. Tabele te są **korzeniami drzewa obiektów** — stanowią root
paczki danych (`Datapack`/`GuidedRow`/`ExportedRow`) i to one są obsługiwane
przez mechanizm synchronizacji i eksportu/importu.
- `child: Pole→TypRow` — tabela jest częścią drzewa innego rootu; pole rekordu
z atrybutem `[ColumnInfo(GuidedRelation=…)]` wskazuje na tabelę nadrzędną.
`Pole` to nazwa pola w `*Record`, `TypRow` to konkretny typ `*Row` odczytany
z odpowiadającej property w klasie `*Row` (w `*Record` pole ma zwykle typ `IRow`).
- pusta wartość — tabela szczegółowa (subrow, info-row) niewchodząca w skład żadnego
drzewa guided.
- `Konfig` = `konfig`, gdy `*Table` ma `[TableInfo(IsConfig=true)]`. Tabele konfiguracyjne
żyją w osobnej sesji (`ExecuteConfig`) i mają inne reguły zapisu niż tabele operacyjne.
- `Interfaces` = lista nazw interfejsów zadeklarowanych w `[TableInfo(Interfaces = new[] { … })]`.
Soneta używa ich jako **relacji interfejsowych** — pole typu `IXxx` może referować rekord
z dowolnej tabeli deklarującej `IXxx` w swoim `TableInfo`.
- Dla samego modułu (`*Module`) Tytuł/Opis czytane są analogicznie z atrybutów na klasie modułu.
6. Wypisz markdown: sekcja `##` per moduł (z jego `Caption`/`Description` jeśli są), w każdej
sekcji tabela `RowType | TableType | Tytuł | Opis`.
sekcji tabela `RowType | TableType | Guided | Konfig | Interfaces | Tytuł | Opis`.
## Wymagania
@@ -89,14 +99,14 @@ Znaleziono modułów: 37
- Opis: Moduł handlowy obsługujący dokumenty sprzedaży, zakupu, zamówień i innych operacji handlowych...
- Tabel: 62
| RowType | TableType | Guided | Tytuł | Opis |
|---------|-----------|--------|-------|------|
| DefDokHandlowego | DefDokHandlowych | tak | Definicje dokumentów handlowych | Konfigurowalna definicja (szablon) dokumentu handlowego... |
| DefRelacjiHandlowej | DefRelHandlowych | tak | Definicje relacji handlowych | Konfigurowalna definicja relacji między dokumentami handlowymi... |
| DokumentHandlowy | DokHandlowe | tak | Dokumenty handlowe | Główna tabela dokumentów handlowych (faktury, paragony, zamówienia, korekty, umowy itp.)... |
| DokumentHandlowyKoszt | DokHandloweKoszt | | Koszty dodatkowe | Koszt dodatkowy przypisany do dokumentu handlowego... |
| DrukarkaFiskalna | DrukarkiFiskalne | tak | Lista drukarek fiskalnych | Konfiguracja drukarki fiskalnej... |
| ... | ... | ... | ... | ... |
| RowType | TableType | Guided | Konfig | Interfaces | Tytuł | Opis |
|---------|-----------|--------|--------|------------|-------|------|
| DefDokHandlowego | DefDokHandlowych | root | konfig | | Definicje dokumentów handlowych | Konfigurowalna definicja (szablon) dokumentu handlowego... |
| DefRelacjiHandlowej | DefRelHandlowych | root | konfig | | Definicje relacji handlowych | Konfigurowalna definicja relacji między dokumentami handlowymi... |
| DokumentHandlowy | DokHandlowe | root | | IDokument, IKontrahentRef | Dokumenty handlowe | Główna tabela dokumentów handlowych (faktury, paragony, zamówienia, korekty, umowy itp.)... |
| DokumentHandlowyKoszt | DokHandloweKoszt | child: Dokument→DokumentHandlowy | | | Koszty dodatkowe | Koszt dodatkowy przypisany do dokumentu handlowego... |
| DrukarkaFiskalna | DrukarkiFiskalne | root | konfig | | Lista drukarek fiskalnych | Konfiguracja drukarki fiskalnej... |
| ... | ... | ... | ... | ... | ... | ... |
_Łącznie tabel: 1196_
```
+37 -3
View File
@@ -41,7 +41,24 @@ Algorytm:
- znajdź klasę biznesową (`DefinicjaNumeracji`) oraz typ `*Module+DefinicjaNumeracjiRow` (mogą być w innym module — np. `CoreModule`);
- powtórz całą procedurę (kroki 58) dla tego rekordu, używając prefiksu `Numeracja.` w kluczach wyników (`Numeracja.Pole1`, `Numeracja.Pole2`, …).
Rekurencja działa dowolnie głęboko (subrow w subrowie). Pętle (rekord zawierający siebie pośrednio) są zabezpieczone przez zbiór odwiedzonych typów.
10. Wypisz tabelę markdown na stdout (kolumny: `Pole | Typ | Rodzaj | Tytuł | Opis`).
10. **Metadane tabeli** — dodatkowo do nagłówka trafiają:
- `Tabela konfiguracyjna: Tak/Nie` — czytane z `[TableInfo(IsConfig=true)]` na zagnieżdżonej
klasie `*Module.*Table` (atrybut siedzi tam, nie na top-levelowym typie zwracanym przez
property `Table` w `*Row`).
- `Guided: root` — gdy `*Table` dziedziczy z `GuidedTable`/`ExportedTable`.
- `Guided: child — nadrzędna przez pole \`X\` → \`Y\`` — gdy w rekordzie istnieje pole
z `[ColumnInfo(GuidedRelation=…)]` wskazujące tabelę nadrzędną w drzewie obiektów.
- `Implementuje interfejsy: …` — lista interfejsów z `[TableInfo(Interfaces=…)]` tej tabeli.
11. **Relacje interfejsowe** — skrypt buduje globalny indeks `interfejs → lista tabel implementujących`
(iteracja po wszystkich `*Module.*Table` we wszystkich referencjach). Dla każdego pola, którego
typ jest interfejsem występującym w tym indeksie (heurystyka: nazwa zaczyna się od `I` + wielka
litera), kolumna `Rodzaj` dostaje znacznik `iface-ref`, a po głównej tabeli pól wypisywana
jest sekcja `## Relacje interfejsowe` z listą `Pole | Interfejs | Tabele implementujące`.
Pozwala to od razu zobaczyć alternatywy, do których pole może wskazywać.
12. **Znacznik `guided-parent`** — pole rekordu z atrybutem `[ColumnInfo(GuidedRelation=…)]`
dostaje w kolumnie `Rodzaj` dodatkowy tag `guided-parent`, sygnalizując, że to ono trzyma
referencję do rootu drzewa.
13. Wypisz tabelę markdown na stdout (kolumny: `Pole | Typ | Rodzaj | Tytuł | Opis`).
## Wymagania
@@ -70,6 +87,9 @@ dotnet script ~/.claude/skills/soneta-programming/scripts/scan-props.csx \
```markdown
# Pola i właściwości klasy biznesowej: `Soneta.Handel.DokumentHandlowy`
Nazwa tabeli: `DokHandlowe`
Tabela konfiguracyjna: Nie
Guided: root
Implementuje interfejsy: `IDokument`, `IKontrahentRef`
- pola bazodanowe: 128
- pola kalkulowane (z klas biznesowych): 388
@@ -78,14 +98,28 @@ Nazwa tabeli: `DokHandlowe`
|------|-----|--------|-------|------|
| Brutto | `decimal` | bazodanowe | Brutto | Wartość brutto dokumentu |
| DataDokumentu | `System.DateTime` | bazodanowe | Data dokumentu | |
| Kontrahent | `Soneta.Kontrahenci.Kontrahent` | bazodanowe | Kontrahent | |
| Kontrahent | `Soneta.Kontrahenci.Kontrahent` | bazodanowe, iface-ref | Kontrahent | |
| Netto | `decimal` | bazodanowe | Netto | |
| Numer | `string` | bazodanowe | Numer | |
| SaldoWaluta | `decimal` | | Saldo w walucie | |
| ... | ... | ... | ... | ... |
## Relacje interfejsowe
Pola, których typ jest interfejsem zadeklarowanym w `[TableInfo(Interfaces=...)]` innych tabel.
Pole może wskazywać na rekord dowolnej z poniższych tabel.
| Pole | Interfejs | Tabele implementujące |
|------|-----------|------------------------|
| Kontrahent | `IKontrahent` | `Kontrahent`, `Pracownik`, `Urzad` |
```
Kolumna `Rodzaj` ma wartość `bazodanowe` dla pól rekordu lub jest pusta dla właściwości kalkulowanych.
Kolumna `Rodzaj` jest kombinacją znaczników rozdzielonych przecinkami:
- `bazodanowe` — pole rekordu (`*Record`); brak znacznika = property kalkulowana klasy biznesowej.
- `guided-parent` — pole z `[ColumnInfo(GuidedRelation=…)]` trzymające referencję do nadrzędnej
tabeli w drzewie obiektów guided.
- `iface-ref` — typ pola jest interfejsem zadeklarowanym w `[TableInfo(Interfaces=…)]` innej tabeli;
konkretne tabele docelowe są wymienione w sekcji `## Relacje interfejsowe` pod tabelą pól.
## Kody wyjścia
+83 -6
View File
@@ -112,8 +112,8 @@ foreach (var module in modules)
continue;
}
Console.WriteLine("| RowType | TableType | Guided | Tytuł | Opis |");
Console.WriteLine("|---------|-----------|--------|-------|------|");
Console.WriteLine("| RowType | TableType | Guided | Konfig | Interfaces | Tytuł | Opis |");
Console.WriteLine("|---------|-----------|--------|--------|------------|-------|------|");
foreach (var row in rowClasses)
{
var rowType = row.Name.EndsWith("Row")
@@ -136,11 +136,19 @@ foreach (var module in modules)
if (string.IsNullOrEmpty(description))
description = GetAttributeFirstString(row, "DescriptionAttribute");
var guided = tableCls != null && InheritsFromGuidedOrExportedTable(tableCls)
? "tak"
: "";
var isGuidedRoot = tableCls != null && InheritsFromGuidedOrExportedTable(tableCls);
var guided = isGuidedRoot ? "root" : "";
if (!isGuidedRoot)
{
var recordCls = module.GetTypeMembers(rowType + "Record").FirstOrDefault();
var parent = FindGuidedParent(recordCls, row);
if (!string.IsNullOrEmpty(parent)) guided = "child: " + parent;
}
Console.WriteLine($"| {rowType} | {tableType} | {guided} | {EscapeCell(caption)} | {EscapeCell(description)} |");
var konfig = IsConfigTable(tableCls) ? "konfig" : "";
var interfaces = string.Join(", ", GetTableInterfaces(tableCls));
Console.WriteLine($"| {rowType} | {tableType} | {guided} | {konfig} | {EscapeCell(interfaces)} | {EscapeCell(caption)} | {EscapeCell(description)} |");
totalRows++;
}
Console.WriteLine();
@@ -185,6 +193,75 @@ static ISymbol FindMemberInherited(INamedTypeSymbol type, string name)
return null;
}
// Zwraca opis nadrzędnej tabeli w strukturze guided dla tabel guided-child.
// Pole rekordu oznaczone [ColumnInfo(GuidedRelation=RelationGuidedType.GuidedParent)] wskazuje
// kierunek relacji; konkretny typ Row pobieramy z property o tej samej nazwie w klasie *Row
// (w *Record pole ma zwykle typ IRow, więc bez Row nie da się ustalić konkretu).
static string FindGuidedParent(INamedTypeSymbol recordCls, INamedTypeSymbol rowCls)
{
if (recordCls == null) return "";
foreach (var f in recordCls.GetMembers().OfType<IFieldSymbol>())
{
foreach (var a in f.GetAttributes())
{
var an = a.AttributeClass?.Name;
if (an != "ColumnInfoAttribute" && an != "ColumnInfo") continue;
var hasGuided = a.NamedArguments.Any(na => na.Key == "GuidedRelation"
&& na.Value.Kind == TypedConstantKind.Enum
&& na.Value.Value is int v && v != 0);
if (!hasGuided) continue;
var propType = "?";
if (rowCls != null)
{
for (var rc = rowCls; rc != null && rc.SpecialType != SpecialType.System_Object; rc = rc.BaseType)
{
var p = rc.GetMembers(f.Name).OfType<IPropertySymbol>().FirstOrDefault();
if (p != null) { propType = p.Type.Name; break; }
}
}
return f.Name + "→" + propType;
}
}
return "";
}
// Lista interfejsów biznesowych z [TableInfo(Interfaces = new[] { "I1", "I2", ... })].
// Soneta używa ich jako "relacji interfejsowych" — pole typu IXxx może referować dowolny
// rekord z tabeli, która deklaruje IXxx w swoim TableInfo.
static System.Collections.Generic.IEnumerable<string> GetTableInterfaces(INamedTypeSymbol tableCls)
{
if (tableCls == null) yield break;
foreach (var a in tableCls.GetAttributes())
{
if (a.AttributeClass?.Name != "TableInfoAttribute" && a.AttributeClass?.Name != "TableInfo")
continue;
foreach (var na in a.NamedArguments)
{
if (na.Key != "Interfaces" || na.Value.Kind != TypedConstantKind.Array) continue;
foreach (var el in na.Value.Values)
{
if (el.Value is string s && !string.IsNullOrEmpty(s)) yield return s;
}
}
}
}
static bool IsConfigTable(INamedTypeSymbol tableCls)
{
if (tableCls == null) return false;
foreach (var a in tableCls.GetAttributes())
{
if (a.AttributeClass?.Name != "TableInfoAttribute" && a.AttributeClass?.Name != "TableInfo")
continue;
foreach (var na in a.NamedArguments)
{
if (na.Key == "IsConfig" && na.Value.Value is bool b)
return b;
}
}
return false;
}
static string GetAttributeFirstString(ISymbol symbol, string attributeTypeName)
{
if (symbol == null) return "";
+153
View File
@@ -105,6 +105,7 @@ topLevelClasses.TryGetValue(recordBaseName, out mainBusinessClass);
// Nazwa tabeli wyciągana z typu zwracanego przez property `Table` w klasie XxxxRow.
string tableTypeName = null;
bool isConfigTable = false;
var rowClass = enclosing?.GetTypeMembers(recordBaseName + "Row").FirstOrDefault();
if (rowClass != null)
{
@@ -118,6 +119,20 @@ if (rowClass != null)
}
}
}
// Atrybut [TableInfo(IsConfig=true)] siedzi na klasie zagnieżdżonej XxxxModule.XxxxTable
// (nie na top-level typie tabeli zwracanym przez property `Table` w *Row).
var nestedTableCls = enclosing?.GetTypeMembers(recordBaseName + "Table").FirstOrDefault();
if (nestedTableCls != null)
isConfigTable = IsConfigTable(nestedTableCls);
// Wyznacz status guided: root (dziedziczy po GuidedTable/ExportedTable) lub child→ParentRow
// (pole rekordu z [ColumnInfo(GuidedRelation=...)]). Pole zapamiętujemy też w guidedParentField,
// żeby oznaczyć je później w tabeli pól.
var isGuidedRoot = nestedTableCls != null && InheritsFromGuidedOrExportedTable(nestedTableCls);
string guidedParentField = null;
string guidedParentType = null;
if (!isGuidedRoot)
(guidedParentField, guidedParentType) = FindGuidedParent(foundRecord, rowClass);
// Klucz: nazwa pola z notacją kropkową dla subrowów; Wartość: (typ, czyBazodanowe, tytuł, opis)
var merged = new SortedDictionary<string, (string Type, bool IsDb, string Caption, string Description)>(StringComparer.Ordinal);
@@ -138,6 +153,38 @@ else
if (!string.IsNullOrEmpty(tableTypeName))
{
Console.WriteLine($"Nazwa tabeli: `{tableTypeName}`");
Console.WriteLine($"Tabela konfiguracyjna: {(isConfigTable ? "Tak" : "Nie")}");
if (isGuidedRoot)
Console.WriteLine("Guided: root");
else if (guidedParentField != null)
Console.WriteLine($"Guided: child — nadrzędna przez pole `{guidedParentField}` → `{guidedParentType}`");
var thisInterfaces = nestedTableCls != null ? GetTableInterfaces(nestedTableCls).ToList() : new System.Collections.Generic.List<string>();
if (thisInterfaces.Count > 0)
Console.WriteLine($"Implementuje interfejsy: {string.Join(", ", thisInterfaces.Select(i => "`" + i + "`"))}");
}
// Indeks interfejs → lista tabel implementujących, na potrzeby pokazania alternatyw
// dla pól o typie interfejsowym (relacje interfejsowe Soneta). Klasy *Table są zagnieżdżone
// w *Module — iterujemy po top-level *Module i pobieramy ich nested types.
var interfaceImpls = new SortedDictionary<string, System.Collections.Generic.List<string>>(StringComparer.Ordinal);
foreach (var asmRef in compilation.References)
{
if (compilation.GetAssemblyOrModuleSymbol(asmRef) is not IAssemblySymbol asm) continue;
foreach (var top in EnumerateAllTypes(asm.GlobalNamespace))
{
if (top.ContainingType != null || !top.Name.EndsWith("Module")) continue;
foreach (var t in top.GetTypeMembers())
{
if (!t.Name.EndsWith("Table")) continue;
foreach (var iface in GetTableInterfaces(t))
{
if (!interfaceImpls.TryGetValue(iface, out var list))
interfaceImpls[iface] = list = new System.Collections.Generic.List<string>();
var rowName = t.Name.Substring(0, t.Name.Length - "Table".Length);
list.Add(rowName);
}
}
}
}
Console.WriteLine();
var dbCount = merged.Values.Count(v => v.IsDb);
@@ -147,13 +194,48 @@ Console.WriteLine($"- pola kalkulowane (z klas biznesowych): {calcCount}");
Console.WriteLine();
Console.WriteLine("| Pole | Typ | Rodzaj | Tytuł | Opis |");
Console.WriteLine("|------|-----|--------|-------|------|");
var interfaceFields = new System.Collections.Generic.List<(string Field, string IfaceShort, System.Collections.Generic.List<string> Impls)>();
foreach (var kv in merged)
{
var rodzaj = kv.Value.IsDb ? "bazodanowe" : "";
if (guidedParentField != null && kv.Key == guidedParentField)
rodzaj = string.IsNullOrEmpty(rodzaj) ? "guided-parent" : rodzaj + ", guided-parent";
var shortType = ShortTypeName(kv.Value.Type);
if (shortType.StartsWith("I") && shortType.Length > 1 && char.IsUpper(shortType[1])
&& interfaceImpls.TryGetValue(shortType, out var impls))
{
rodzaj = string.IsNullOrEmpty(rodzaj) ? "iface-ref" : rodzaj + ", iface-ref";
interfaceFields.Add((kv.Key, shortType, impls));
}
Console.WriteLine($"| {kv.Key} | `{kv.Value.Type}` | {rodzaj} | {EscapeCell(kv.Value.Caption)} | {EscapeCell(kv.Value.Description)} |");
}
if (interfaceFields.Count > 0)
{
Console.WriteLine();
Console.WriteLine("## Relacje interfejsowe");
Console.WriteLine();
Console.WriteLine("Pola, których typ jest interfejsem zadeklarowanym w `[TableInfo(Interfaces=...)]` innych tabel.");
Console.WriteLine("Pole może wskazywać na rekord dowolnej z poniższych tabel.");
Console.WriteLine();
Console.WriteLine("| Pole | Interfejs | Tabele implementujące |");
Console.WriteLine("|------|-----------|------------------------|");
foreach (var f in interfaceFields)
{
Console.WriteLine($"| {f.Field} | `{f.IfaceShort}` | {string.Join(", ", f.Impls.Select(i => "`" + i + "`"))} |");
}
}
return 0;
static string ShortTypeName(string fullName)
{
if (string.IsNullOrEmpty(fullName)) return "";
var lt = fullName.IndexOf('<');
if (lt >= 0) fullName = fullName.Substring(0, lt);
var dot = fullName.LastIndexOf('.');
return dot >= 0 ? fullName.Substring(dot + 1) : fullName;
}
static void ScanRecord(
INamedTypeSymbol record,
string prefix,
@@ -267,6 +349,77 @@ static IEnumerable<IPropertySymbol> EnumerateInheritedProperties(INamedTypeSymbo
}
}
static bool InheritsFromGuidedOrExportedTable(INamedTypeSymbol type)
{
for (var t = type.BaseType; t != null && t.SpecialType != SpecialType.System_Object; t = t.BaseType)
{
if (t.Name == "GuidedTable" || t.Name == "ExportedTable") return true;
}
return false;
}
static (string field, string parentType) FindGuidedParent(INamedTypeSymbol recordCls, INamedTypeSymbol rowCls)
{
if (recordCls == null) return (null, null);
foreach (var f in recordCls.GetMembers().OfType<IFieldSymbol>())
{
foreach (var a in f.GetAttributes())
{
var an = a.AttributeClass?.Name;
if (an != "ColumnInfoAttribute" && an != "ColumnInfo") continue;
var hasGuided = a.NamedArguments.Any(na => na.Key == "GuidedRelation"
&& na.Value.Kind == TypedConstantKind.Enum
&& na.Value.Value is int v && v != 0);
if (!hasGuided) continue;
var propType = "?";
if (rowCls != null)
{
for (var rc = rowCls; rc != null && rc.SpecialType != SpecialType.System_Object; rc = rc.BaseType)
{
var p = rc.GetMembers(f.Name).OfType<IPropertySymbol>().FirstOrDefault();
if (p != null) { propType = p.Type.Name; break; }
}
}
return (f.Name, propType);
}
}
return (null, null);
}
static System.Collections.Generic.IEnumerable<string> GetTableInterfaces(INamedTypeSymbol tableCls)
{
if (tableCls == null) yield break;
foreach (var a in tableCls.GetAttributes())
{
if (a.AttributeClass?.Name != "TableInfoAttribute" && a.AttributeClass?.Name != "TableInfo")
continue;
foreach (var na in a.NamedArguments)
{
if (na.Key != "Interfaces" || na.Value.Kind != TypedConstantKind.Array) continue;
foreach (var el in na.Value.Values)
{
if (el.Value is string s && !string.IsNullOrEmpty(s)) yield return s;
}
}
}
}
static bool IsConfigTable(INamedTypeSymbol tableCls)
{
if (tableCls == null) return false;
foreach (var a in tableCls.GetAttributes())
{
if (a.AttributeClass?.Name != "TableInfoAttribute" && a.AttributeClass?.Name != "TableInfo")
continue;
foreach (var na in a.NamedArguments)
{
if (na.Key == "IsConfig" && na.Value.Value is bool b)
return b;
}
}
return false;
}
static string GetAttributeFirstString(ISymbol symbol, string attributeTypeName)
{
if (symbol == null) return "";