Skip to content

feat!: v3 — typed Model/Format API, chevrotain parsers, JSON/SARIF contract - #19

Open
ChS23 wants to merge 380 commits into
Byndyusoft:mainfrom
ChS23:refactor/v3-foundations
Open

feat!: v3 — typed Model/Format API, chevrotain parsers, JSON/SARIF contract#19
ChS23 wants to merge 380 commits into
Byndyusoft:mainfrom
ChS23:refactor/v3-foundations

Conversation

@ChS23

@ChS23 ChS23 commented May 20, 2026

Copy link
Copy Markdown
Contributor

v3-рефакторинг закончен и живёт на npm как aact@beta (3.0.0-beta.29).

Supersedes: #18 (дисциплина тестов + DX-улучшения — целиком включено как
основа), #15 (экранирование URL в resources/ — устарело после
переименования resources/fixtures/).

⚠️ Это полная переработка (425 файлов, ~75k строк). Ревьюить по слоям,
не по файлам — порядок в «Как ревьюить» ниже. Готов провести разбор или
ответить по любому разделу асинхронно.

Зачем v3

v2 держался на regex-лоадерах: хрупко, без source-locations, со
stringly-typed опциями правил и без машинного контракта, на который могли бы
опираться CI и AI-агенты. v3 заменяет это типизированными Model/Format API,
chevrotain-парсерами с UTF-16 ranges, range-based --fix и стабильным
--json/--sarif envelope — один контракт для людей, CI и агентов.

TL;DR

Новая Model API (типизированные kinds, Record-индексация, validateModel),
новый capability-based Format API (load/generate/fix как опциональные
возможности), chevrotain-парсеры для PUML + Structurizr DSL с UTF-16 ranges,
range-based --fix. Унифицированный --json envelope (schemaVersion: 1) и
--sarif v2.1.0 для GitHub Code Scanning. Четыре новые команды (diff,
model, rule explain, view), новый формат model-json для
LLM-генерируемых архитектур и diff-baseline'ов, skill-installer для
AI-агентов и опциональный companion-пакет @aact/view (браузерный
workbench).

Как ревьюить

Точки входа, чтобы 75k строк не пугали:

  1. src/index.ts — вся публичная поверхность одним файлом
  2. src/model/types.ts — Model (ядро, на нём держится всё)
  3. src/cli/output/types.ts — контракт envelope (JSON/SARIF/exit-коды)
  4. src/formats/structurizr/ — один парсер целиком; остальные по образцу
    (грамматика и scope-policy — src/formats/structurizr/parser/grammar.md)
  5. src/rules/crud.ts — каноничное правило (check + fix + options)

Дальше «по объёму» (фикстуры, snapshot'ы, examples/) — беглым взглядом.

Риски / trade-offs (осознанно)

  • Размер. Полная переработка одним бранчем — построчное ревью
    нереалистично; отсюда порядок чтения выше. AI-ревьюеры на таком объёме
    покрывают лишь часть файлов — это ревью для людей.
  • Scope намеренно узкий. Только C4 static + System Landscape + Dynamic.
    Deployment view / ArchiMate / UML / BPMN — осознанно вне scope.
  • Пробелы парсера задокументированы (не «молча неверные данные») —
    список в grammar.md (раздел про парсеры ниже).
  • schemaVersion: 1 заморожен до GA — bump'ы только пост-GA.

Что изменилось — по областям

Model layer (breaking)

  • Типизированные union'ы ElementKind / BoundaryKind
  • Element.external: boolean — ортогонален kind, заменил варианты *_Ext
  • model.elements / model.boundariesReadonly<Record<name, T>> для O(1) поиска
  • Переименован интерфейс ContainerElement (литерал kind: "Container" сохраняется для C4 level-2)
  • Добавлен WorkspaceMetadata (Structurizr workspace "..." extends "...")
  • validateModel + 9 типизированных видов ModelIssue
  • Хелперы: getElement, targetOf, walkBoundaries, formatLocation, isDatabaseElement, isDatabaseKind

Format API (breaking)

  • Format = { name, defaultPattern?, load?, generate?, fix? } — все возможности опциональны, type-guard'ы canLoad / canGenerate / canFix
  • defaultPattern принимает string | readonly string[] — compose объявляет все 4 канонических имени, structurizr — ["workspace.json", "*.dsl"] для автоопределения DSL-источников
  • Ленивый registry с dynamic import'ами
  • 5 форматов:
    • plantuml — load + generate + fix
    • structurizr — load + generate + fix (полный round-trip)
    • kubernetes — load + generate (Phase 2: load для детекции дрейфа через aact diff arch.dsl ./k8s/)
    • compose — load + generate (для scaffold'инга и round-trip)
    • model-json — load + generate

Парсеры (chevrotain)

C4-PUML парсер с UTF-16 ranges — заменяет зависимость plantuml-parser.

Structurizr DSL парсер — полная поверхность §«In scope» из grammar.md:

  • workspace + model + 6 видов элементов + body-statements (description / technology / tags / tag / url / properties / perspectives)
  • явные + implicit-source + -/> relationships, ключевое слово this, иерархические ссылки (bank.api)
  • в body workspace переопределения name "..." / description "..." (побеждает последнее)
  • директивы: !include / !const / !var / !identifiers / !impliedRelationships (в форме с ! и без)
  • подстановка ${...} (fixed-point, 16 итераций), многострочные продолжения через \
  • reopen-форма, разделитель групп, проброс свойств вложенных групп
  • использование archetype-алиаса (<id> = <alias> "Name") с цепочкой дефолтов: description/technology по fallback (источник побеждает), tags/properties/perspectives аддитивно
  • archetype в форме kind-default (archetypes { softwareSystem { … } } без имени-алиаса) — дефолты на все элементы данного kind
  • проброс property/perspective из тела archetype на элемент
  • ключевое слово Element в слоте идентификатора для симметрии (element = element "X")
  • CustomElement, дефолтные tags на kind, проброс предков при !impliedRelationships true
  • opaque-блоки (views/styles/configuration/branding/terminology/themes/archetypes/!docs/!adrs/!plugin/!script) → strip на pre-parse с сохранением source-range
  • семейство deployment → strip с info-issue на каждое вхождение
  • удалённые конструкции (!ref/!extend/!constant/enterprise) → parseErrors с подсказкой замены

Structurizr DSL generator — генерит workspace.dsl из канонической Model. Round-trip parity: parse → Model → emit → parse = идентичная Model (с намеренным расхождением "" / undefined). Покрывает workspace, все виды элементов, вложенность boundary, body-statements, реконструкцию properties + perspectives, внешние системы, relationships с placeholder-семантикой.

Паритет с reference-фикстурами проверён против DslTests.test_archetypes, test_archetypesForDefaults, секции модели big-bank-plc, многострочных продолжений, getting-started.

Задокументированные пробелы (намеренные, не «молча неверные данные»): relationship-archetypes (a --https-> b), проброс тела селектора, round-trip сырого содержимого opaque-блоков, реконструкция !include в generator.

aact view — браузерный workbench (@aact/view, опц. зависимость)

  • Companion-пакет, опциональная зависимость от ядра aact
  • Svelte 5 SPA, ELK для раскладки, Svelte Flow для рендера, chokidar live-reload через WebSocket
  • 3 режима (Drill / Expand / Flat), фильтр рёбер по Bounded-Context, ссылка на исходник DSL через AACT_FILE_OPENER
  • Per-session auth-токен защищает /api/model + WebSocket-upgrade (строго в пределах /api/ws)
  • Визуальный язык по C4-палитре Simon Brown
  • Полный спек в packages/view/DESIGN.md

--fix engine (breaking)

  • SourceEdit — discriminated union: replace / remove / insert-after / insert-before
  • applyEdits — чистый splicer, детекция конфликтов через EditConflict[]
  • FixContext<O> — bag-of-args (model, violations, syntax, options)
  • FormatSyntax.relationDecl(from, to, opts) (раньше — позиционно)

CLI envelope contract

  • CliEnvelope<TData> с schemaVersion: 1 (freeze-policy: bump'ы только пост-GA)
  • ~30 типизированных DiagnosticKind (model.* / config.* / format.* / fix.* / skill.* / view.* / internal.*)
  • 3 reporter'а: Human / JSON / SARIF
  • Exit-коды: 0 чисто / 1 нарушения / 2 ошибка инструмента
  • Envelope ошибки (data: null) — spec-канонический SARIF invocations[].toolExecutionNotifications[]

Contract freeze (финальный проход перед заморозкой schemaVersion: 1)

  • ruleId — единый ключ во всём envelope (violations[], suggestedFixes[], rules[], SARIF result.ruleId); общий RuleMetadata для check --json и rule list (+ helpUri)
  • CheckSummary{ passed, failed, violations } — правила против находок (убран двусмысленный total)
  • severity — один словарь error | warning | info для Diagnostic и CheckViolation (значения пока все error → per-rule severity приедет аддитивно без bump); SARIF маппит info → note, GitHub — info → notice; ToolError/exit-2 → error
  • Неизвестное правило в config.rules → жёсткая ошибка (exit 2); один общий резолвер правил для check / rule list / rule explain
  • SARIF-паспорта выровнены с JSON: helpUri на основе ADR, camelCase-идентификаторы model.* (model.danglingRelation)
  • config.generate — по форматам (generate.<format>); aact generate передаёт срез в format.generate; артефакт в stdout запрещён в любом не-текстовом режиме вывода
  • Объекты форматов экспортятся из root (plantumlFormat, …); схема model-json аддитивна (additionalProperties снят, ломающее → aact-model-v2.json)
  • AnalysisReport и под-типы → readonly; generate files[].bytes → в UTF-8 байтах

Новые CLI-команды

  • aact view — запускает локальный браузерный workbench (см. выше)
  • aact model — нормализованный граф (текстовая сводка, --json полный граф, --sarif проблемы загрузчика)
  • aact diff <baseline> [<current>] — структурный diff для PR-ревью с детекцией переименований (similarity ≥0.7, выводится как confidence), multiset-сопоставление relations, pair-collapse при смене technology, RFC 6902 --with-patch опционально. Входы: файл / git-ref <ref>:<path> / stdin
  • aact rule explain <name> — rationale + good/bad-примеры + ссылка на ADR + helpUri

Существующие команды — миграция на envelope

  • aact check — текст + --json + --sarif (готово для GitHub Code Scanning)
  • aact analyze — переписан под структурные метрики (см. ниже)
  • aact generate — резолвер sink'а (stdout / --output - / --output dir/), проверка коллизии с JSON
  • aact rule list — envelope --json, без тихого глотания ошибок
  • aact init--json возвращает InitData, не перезаписывает файлы
  • aact skill install — ставит agent-skill в основные skill-пути (--claude / --cline / --codex / --cursor / --copilot / --all)

Переработка analyze

  • Убран захардкоженный sync/async-счётчик с ["http","grpc","tcp"] (давал 0/0 на большинстве PUML)
  • Добавлено: elementsByKind, relationsByStyle: {sync,async,unspecified} (по тегам, с опциональным fallback по technology), boundaries[].{syncCoupling,asyncCoupling,unspecifiedCoupling,ratio}, fanIn/fanOut top-N с фильтром exclude, cycles: {count, smallest} через Tarjan SCC
  • Новая секция AactConfig.analyze: syncTechnologies / asyncTechnologies / exclude.{tags,namePatterns} / topN

Правила

  • 8 встроенных: acl, acyclic, apiGateway, cohesion, commonReuse, crud, dbPerService, stableDependencies
  • Каждое — один файл: объект RuleDefinition с inline check + опциональным fix
  • rationale + examples + adrPath? на каждое правило (используются в aact rule explain)
  • Определение роли по имени: picomatch-globs с brace-expansion — опции *NamePatterns для acl/apiGateway/crud/dbPerService покрывают legacy-архивы и AI-генерённые диаграммы без явных тегов
  • Все типы *Options экспортируются
  • relatedLocations для вторичных якорей (рёбра-аксессоры общей БД, цели внешних систем, участники цикла)
  • isDatabaseElement унифицирован — теперь ContainerDb || ComponentDb (раньше только ContainerDb), используется в analyze + правилах + генерации k8s

Гиперссылки

  • OSC 8 на нарушениях, с per-terminal URL-схемой:
    • TERM_PROGRAM=vscode или CURSOR_TRACE_IDfile://abs:line:col
    • TERM_PROGRAM=zed → обычный текст (у Zed свой автодетект путей)
    • иначе → <scheme>://file/abs:line:col, схема через env AACT_FILE_OPENER (vscode / vscode-insiders / cursor / windsurf / zed / none)
  • Якоря в аннотациях GitHub Actions

Документация

  • AGENTS.md (гайд верхнего уровня для AI-агентов), CLAUDE.md и .github/copilot-instructions.md — симлинки
  • README.md (RU) + README.en.md (EN) с двуязычным переключателем
  • docs/format-coverage.md — матрица по форматам (load/generate/fix на каждое поле)
  • docs/parser/ — phase-0 инвентаризация + грамматики
  • src/formats/structurizr/parser/grammar.md + README.md (актуальное состояние парсера, scope-policy, оставшиеся пробелы)
  • 5 ADR (Anti-corruption Layer, Common Reuse Principle, Database per CRUD-service, Target Architecture, шаблон)
  • CHANGELOG.md через changelogen (формат Keep a Changelog)
  • examples/custom-rules/ — рабочий пример: defineRule + defineConfig<const C> + тесты

Library API (src/index.ts)

Экспортируется: 8 правил + их *Options, Model + всё семейство типов, analyzeArchitecture + AnalysisReport, registry форматов + type-guard'ы, контракт CliEnvelope, полная поверхность SARIF v2.1.0, data-shape'ы команд (CheckData, ModelData, DiffData, RuleListData, RuleExplainData, GenerateData, InitData, SkillData), computeDiff (чистая функция для library-юзеров), defineConfig + defineRule.

Breaking changes (live в aact@beta)

  1. Интерфейс ContainerElement. Литерал kind: "Container" сохраняется для C4 level-2
  2. Violation.element: stringtarget: string + дискриминатор targetKind: "element" | "boundary"
  3. FormatSyntax.relationDecl(from, to, label?, ...)(from, to, opts: { description?, technology?, tags? })
  4. AnalysisReport.syncApiCalls / asyncApiCallsrelationsByStyle: {sync,async,unspecified} (глобально) + per-boundary syncCoupling / asyncCoupling / unspecifiedCoupling
  5. AnalyzeOptions.apiTechnologies → пара syncTechnologies + asyncTechnologies
  6. Форматтер kubernetes: утилиты v2 (loadDeployConfigs, mapFromConfigs, DeployConfig) убраны; v3 даёт полноценную возможность load для детекции дрейфа
  7. CheckViolation / FixResult несут ruleId (был rule); CheckSummary.totalviolations; типы CheckRuleMetadata / RuleInfo → единый RuleMetadata
  8. Встроенные правила теперь opt-in (бегут только указанные в config.rules); неизвестное имя в config.rules → exit 2
  9. config.generate по форматам: boundaryLabelgenerate.plantuml.boundaryLabel; aact generate запрещает артефакт в stdout в любом не-текстовом режиме

Статус релиза

npm dist-tags: latest: 2.1.5 (нетронут), beta: 3.0.0-beta.29.

29 beta-релизов с момента разделения. Каждый задокументирован в CHANGELOG.md.

План тестов

  • Unit: 1855 тестов, проекты vitest (unit / integration / e2e)
  • Integration: 42 теста на 8 example-фикстурах (banking-plantuml, ecommerce-structurizr, microservices-structurizr, common-reuse-plantuml, custom-rules, violations-demo)
  • E2E: 45 тестов против собранного dist/cli/index.mjs в чистом tmpdir
  • Порог покрытия 95/85/95/95 (statements/branches/functions/lines), проверяется на CI
  • Mutation-тестирование через Stryker (ручной прогон)
  • Линт чистый: eslint + prettier + boundaries + sonarjs + unicorn + import-x
  • pnpm publint — санити package.json
  • pnpm knip — без неиспользуемых экспортов
  • Паритет с reference-фикстурами для Structurizr DSL — DslTests.test_archetypes / test_archetypesForDefaults дают идентичную наблюдаемую Model

Закрывает

Closes #8 — рендеринг //-ссылок в PlantUML (</size// в SVG) починен в генераторе и парсере (ручной + сгенерированный puml)
Closes #7 — автогенерация архитектуры вынесена из юнит-теста в CLI (aact generate)
Closes #6 — у всех паттернов в patterns.md проставлены ссылки на unit- и example-тесты

ChS23 added 30 commits May 19, 2026 01:16
…ner.name

Aligns the chevrotain DSL parser with the Model contract documented
in src/model/types.ts:101 — "Container.name — PlantUML alias /
Structurizr structurizr.dsl.identifier". The DSL parser previously
stored the display name in Container.name (matching what the user
wrote in `softwareSystem "Bank"`), leaving aact's three loaders
asymmetric:

  - JSON loader: Container.name = `dslId(c.id, props)` (short id)
  - PUML loader: Container.name = `el.alias` (short alias)
  - DSL parser: Container.name = `element.name.value` (display)  ← off

Now uniform: every loader keys the Model by the short identifier the
user writes in source, while `Container.label` carries the display
name. Match impact:

  - `aact check --fix` on `.dsl` sources now actually patches the
    workspace file. fix.syntax patterns like `orders_crud -> orders_db`
    finally match the DSL text (previously they were built from
    display name `"Orders CRUD"`, which doesn't appear there).
  - identifierMap stores `lowercased lookup key → canonical short id`
    so case-insensitive resolution still works and returns the same
    identifier the rest of the Model is keyed by.

handleLeaf and handleBoundary now take the resolved `name` as an
explicit parameter rather than recomputing from `element.name.value`.

All 12 DSL-parser test files updated to use short-id keys
(`model.containers["bank"]` not `["Bank"]`, etc.); relation.to
values updated; Boundary.containerNames / boundaryNames arrays
updated; reference-fixture test reads big-bank-plc.dsl by its DSL
ids (`customer`, `internetBankingSystem`, `apiApplication`, …).

808/808 tests pass (1 skipped); empirically verified that running
`aact check --fix` against examples/ecommerce-structurizr/workspace.dsl
deletes the three offending CRUD→DB edges as intended.
Reference parser's IdentifiersRegister.register throws when the same
identifier maps to two distinct elements (`a = container "X"` then
`a = container "Y"` later). aact surfaces the same situation as a
ModelIssue so the linter shows the collision without aborting the
parse — last-write-wins on the identifier map keeps subsequent
resolution sane.

Case-insensitive detection mirrors `IdentifiersRegister.getElement`'s
equalsIgnoreCase behaviour (so `BANK` colliding with `bank` flags
too). Reopen blocks (`bank { description "..." }`) don't trigger the
issue because handleReopen never calls handleElement on the target.

  - new ModelIssue variant `duplicate-identifier`
  - parserIssues array threaded through collectModelChild → handleElement
  - toModel returns merged `[...parserIssues, ...validateModel issues]`

3 new tests, 181/181 parser tests pass.
Reference `GroupParser` reads `structurizr.groupSeparator` from the
model's `properties { }` block and joins nested group names with it:

  properties {
    "structurizr.groupSeparator" /
  }
  group "Outer" {
    group "Inner" {
      api = container "API"   // properties.group → "Outer/Inner"
    }
    db = container "DB"        // properties.group → "Outer"
  }

A new `GroupContext` threads two values through collectModelChild →
handleElement → handleGroup: the separator (looked up once at toModel
top) and the in-flight composed group path. Innermost group stamps
its full composed name onto its members; outer group's own pass
skips elements already carrying `properties.group` so it doesn't
clobber deeper stamps.

Without a separator, nested elements get the innermost group name
only — matches reference fixture `groups-nested.dsl` behaviour
without the property.

modelBodyItem now also surfaces workspace-scope `properties { }` as
a `PropertiesBlock` ModelChildNode (previously dropped on the
floor), which is how the separator is read.
Reference: StructurizrDslParser.java:690-691 — when the GROUP_TOKEN
appears inside a ComponentDslContext (or container body), it's a
property assignment, not a nested element declaration. The body
form `component "X" { group "Web Layer" }` makes
`Component.properties.group = "Web Layer"`.

aggregateBody now recognises the GroupNode AST shape with empty
`members` (no `{ }` block on the group) and routes it to the
properties bag instead of leaving it as a phantom child element.

Reference fixture: groups-nested.dsl:13-22 uses exactly this form.
`bank { db = container "DB" }` on an already-declared Boundary now
adds the new container to the target's containerNames / boundaryNames
list, matching the reference parser's behaviour of routing
reopen-body element declarations through the regular element handler
with the target as parent identifier path.

  - handleReopen filters body into bodyStatements, relationships,
    and newElements (the third category previously dropped silently)
  - For a Boundary target: snapshot containers/boundaries lengths
    before, run handleElement on each newElement, patch the target
    Boundary's name lists with the names added during that run
  - For a Container target: run handleElement so the new elements
    exist in the Model, but leave at top-level scope (reference
    would promote the Container to a Boundary; we leave that to a
    future commit since it requires reshaping an existing
    Container into a Boundary mid-build)

185/185 parser tests pass.
Reference: StructurizrDslParser.java:1385-1414 — every token passes
through a substitution step that replaces \`\${NAME}\` with the value
of a matching \`!const\`/\`!var\` declaration. The pattern allows
\`[a-zA-Z0-9_.-]+\` for the name.

We do this once, pre-lex: scan the source for \`!const NAME "VALUE"\`
and \`!var NAME "VALUE"\` (also accepts \`"""..."""\` text-block
values), then iterate \`\${NAME}\` replacements over the source to a
fixed point so chained refs (\`A \"\${B}\" / B "X"\`) resolve.
Bounded to 16 passes — anything beyond that is cyclic, and the user
sees the unresolved \`\${...}\` token verbatim, matching reference
behaviour.

5 new tests, 190/190 parser tests pass.
…space

Reference parsers expose workspace-level metadata via
`Workspace.getName() / getDescription()` (WorkspaceParserTests.java:
31-47). aact previously ignored it — `workspace "Big Bank plc"
"Internet Banking Demo" { ... }` parsed cleanly but the metadata
landed nowhere.

  - New optional `Model.workspace: WorkspaceMetadata` field
    ({ name?, description?, extendsTarget? }) — formats without a
    workspace header (PUML) leave it undefined.
  - buildModel accepts a `workspace` input and freezes it into the
    Model.
  - Structurizr DSL parser's toModel reads workspace.name /
    description / extendsTarget from the AST and passes them through.

3 new tests; 823/823 full suite passes.
…gaps

Two more reference constructs the linter doesn't interpret but does
need to parse-around so larger fixtures don't fail wholesale:

  - `archetypes { ... }` — reference declares alias→base-kind +
    default values here; without it the `archetypes` keyword token
    would derail parsing. Added to OPAQUE_KEYWORDS for balance-brace
    skip.
  - `!element` / `!elements` / `!relationship` / `!relationships`
    selector blocks — body would normally attach to the selected
    elements, but the linter doesn't apply that today. Block strip
    keeps selector-bearing fixtures parseable.

Inventory in grammar.md updated: moved 7 items from "open" to
"closed" (substitution, nested-group separator, group-as-property,
reopen-new-nested, identifier re-registration, workspace metadata,
+ these two strips). Remaining gaps are now:
  - archetype USAGE form (`<alias> <id> "name"`, inverse of regular
    declaration) — needs grammar surgery, beta defers
  - selector body propagation (apply tags from `!element { ... }` to
    the matched element) — beta defers
  - empty `""` vs `undefined` for missing description/technology —
    deliberate TS-idiom divergence

826/826 tests pass.
…lity

Agent-led validation of grammar.md against the Java reference
surfaced two real code gaps and four documentation imprecisions.
This commit closes the code gaps and updates the doc.

Real code fixes (verified empirically):

  - `tag` is a syntactic alias for `tags` in the reference
    (`StructurizrDslParser.java:612` dispatches both to
    `ModelItemParser.parseTags`). The parser previously treated
    `tag` as a single-arg-no-split form, so `tag "a,b"` made one
    tag "a,b" instead of two, and `tag "x" "y"` dropped "y". Now
    `tagStmt` accepts AT_LEAST_ONE StringLiteral and toModel splits
    each arg as CSV — uniform with `tagsStmt`.

  - `!const` / `!var` accepted at any scope. Reference dispatcher
    (`StructurizrDslParser.java:1255-1265`) applies no context
    guard — directives are valid top-level, in workspace, in model,
    AND in element bodies. The `bodyStatement` rule now lists
    `directive` as an alternative.

Grammar.md fidelity updates (no code impact):

  - Archetype body grammar: removed `url` (reference's
    ArchetypeParser does not expose `parseUrl`), added optional
    `metadata` for `element`-based (CustomElement) archetypes,
    cited `StructurizrDslParser.java:642, 807-808` for evidence.
  - Archetype base keywords: replaced bogus `relationship` with
    `->` (the relationship-archetype form, `archetypes.dsl:28`
    declares as `https = -> { ... }`; reference dispatch is
    `isRelationshipKeywordOrArchetype`).
  - `!const` / `!var` scope claim corrected (was: "workspace /
    model only"; reality: any scope).
  - `!identifiers` ordering clarified (convention, not enforced).
  - String literal escape note corrected (`Tokenizer.java:32-46`
    recognises only `\"`; no `\n` / `\t` / `\\` decoding).
  - Block comment line range tightened to `:281-289`.

Two new parser tests pin the tag/tags aliasing and !const-in-body
behaviour against future regression. 829/830 full suite passes.
- New parser stack mirrors Structurizr: tokens / preParse / parser / visitor / toModel / index
- Five byte-length-preserving pre-lex passes keep SourceLocation aligned with source bytes
- SourceLocation now lands on every Container / Boundary / Relation
- grammar.md §8.1 documents the arithmetic strip for `$index=Index()-N`
- 86 new tests; 14/14 in-scope reference fixtures roundtrip cleanly
- load.ts becomes a thin file-I/O wrapper around parseSource
- Delete src/formats/plantuml/lib/filterElements.ts
- Drop plantuml-parser 0.4.0 dependency
- Roundtrip identity preserved across 12 reference fixtures
- output/types.ts + cli/loadModel.ts: add `model.duplicateIdentifier` diagnostic
- structurizr/preParse.ts: narrow `out.at(-1)` / `out.at(-2)` via locals
- Multi-line opaque macros work today; pin behaviour against regression
- Backslash-continuation preprocessor is a known gap; pin parse-error surface
- Correct stale "rare limitation" note in preParse.ts header
- Rewrite v3.0.0-beta.6 description to CLI-only scope
- New v3.0.0-beta.7 section for both parsers + drop plantuml-parser dep
Borrow the location from the first-use Rel call so dangling-source
diagnostics point at the actual reference site instead of nowhere.
Brings PUML parser SourceLocation coverage to 100%.
- Violation/CheckViolation carry optional SourceLocation
- flattenViolations falls back to container's location
- linkSourceLocation wraps container name via terminal-link
- GitHub annotations get file=/line=/col= for inline PR comments
- formatLocation exported for non-terminal renderers
- 5 rules emit Violation.sourceLocation pointing at the offending
  edge / boundary (acl, acyclic, crud, dbPerService, cohesion)
- flattenViolations falls back to boundary location for cohesion
- renderViolationsTable rewritten in eslint format:
  `path:line:col  error  rule  container: message`
- OSC8 hyperlink on the location column in TTY-with-hyperlinks
- acl/apiGateway/crud/dbPerService get `*NamePatterns?: string[]`
  options — picomatch globs with brace expansion
- rules treat container as repo/acl if name matches even without
  an explicit tag (legacy archives, agent-generated diagrams)
- crud.fix rewires through existing name-matched repo and adds the
  canonical tag in a single pass — no duplicate `_repo` container
- defaults exposed via `src/rules/lib/namingPatterns.ts` helper
Citty prepends parent path automatically — supplying meta.name lets
`<cmd> --help` print `USAGE aact <cmd> [OPTIONS]` instead of falling
back to argv[1] (the full dist path). One small UX polish across all
6 commands.
- Vocab alignment with C4: `Element` is the universal aggregator
  (Person/System/Container/Component); `kind: "Container"` literal stays.
- Renames: `Model.containers`→`elements`, `allContainers`→`allElements`,
  `getContainer`→`getElement`, `Boundary.containerNames`→`elementNames`,
  `Violation.container`→`element` (+ JSON envelope field), `ContainerKind`→`ElementKind`,
  `ModelIssue` field & kind renames, `DiagnosticKind` values.
- In-process tests for citty wrapper via `runCommand` + `process.exit` spy
  (citty's own pattern). Adds coverage for `issueToDiagnostic`, OSC8
  hyperlinks, envelope/humanReporter edges, loadConfig failure paths,
  skill error variants + `renderSkillText`.
- Coverage back over 95/85/95/95 floor (95.87 / 86 / 96.68 / 95.29).
`orders_repo` ships in the default architecture.puml without an explicit
`$tags="repo"` — `crud`'s default `repoNamePatterns` (`*_{repo,…}` glob)
picks it up by name. Scaffold now surfaces two violations after init
(`crud` + `dbPerService`), both auto-fixed in one `--fix` pass — closer
to importing a legacy archive than the prior single-rule demo.
SourceEdit is now a discriminated union by `kind` (replace / remove /
insert-after / insert-before) anchored on `SourceLocation` byte
ranges. The applier is a pure byte-splicer that returns
`{ content, applied, conflicts }` — no more ambiguous-pattern warns,
overlapping edits surface as `fix.editConflict` diagnostics instead
of silent drops.

- RuleDefinition.fix takes a single `FixContext<O>` (model, violations,
  syntax, options) — future args land additively.
- FormatSyntax (was SourceSyntax) trimmed to content-builders
  `containerDecl` / `relationDecl`; the `*Pattern` regex helpers are
  removed.
- CLI surfaces edit conflicts through diagnostics with kept/skipped
  ranges so partial fixes are visible, not hidden.
- Tests for fix functions load through the real chevrotain parser so
  byte offsets match what production sees.
…tags}

The old positional `(from, to, tech?, tags?)` signature treated the
third arg as PUML positional 3 (label slot), so every rule fix that
passed `rel.technology` to preserve it actually clobbered the
description. The opts-object form maps cleanly to PUML
`Rel(from, to, label, techn, ...)` and Structurizr DSL
`from -> to "description" "technology"` — both fields land in their
correct slots and survive a `--fix` rewire intact.

Rules (acl, crud, dbPerService) updated to pass description +
technology + tags through `opts`. Custom-rule authors emitting
relations: replace `relationDecl(a, b, "tech")` with
`relationDecl(a, b, { technology: "tech" })`.
The field's JSDoc said "byte offset", but every producer and
consumer in v3 (chevrotain lexer, `String.prototype.slice` in
applyEdits, OSC8 hyperlinks) operates in UTF-16 code units — the
JS string unit. A naive consumer that read "byte" literally would
land mid-glyph on cyrillic / emoji / CJK content. Math was always
right; only the doc lied.

Regression tests pin the invariant through `applyEdits` directly
and through a full `crud --fix` rewrite on PUML with Russian and
emoji labels — non-ASCII before the edit point doesn't shift
subsequent ranges.
- Exclude .parser-refs/ from eslint (Java repo refs fetched on
  demand by scripts/fetch-parser-refs.sh — not our code).
- Stale test descriptions referencing the old ModelIssue.kind
  values updated to match the post-Element-rename names.
- Replaced an `it.skip` placeholder with `it.todo` so vitest stops
  warning about disabled tests; the rationale comment is preserved.

Net effect: `pnpm exec eslint .` goes from 1 error / 16 warnings to
0 errors / 15 warnings (remaining warnings are cognitive-complexity
in parser code — out of scope for v3 stable).
ChS23 added 3 commits June 21, 2026 11:39
runnable list missed custom-rules; test-suite list missed common-reuse-plantuml
- bump aact + @aact/view to 3.0.0 (GA); @aact/view aact dep ^3.0.0
- CHANGELOG: Unreleased → v3.0.0 (2026-06-21); schemaVersion 1 now frozen
@ChS23 ChS23 changed the title feat!: v3 — new Model + Format API, chevrotain parsers, agent-facing output contract feat!: v3 — typed Model/Format API, chevrotain parsers, JSON/SARIF contract Jun 21, 2026
ChS23 added 24 commits June 21, 2026 12:08
- configuring-rules: add 'role detection by name' section + name-pattern defaults in table
- proofread all guides for needless RU/EN mixing (Review→Ревью, fallback→запасной вариант, etc.)
The DSL and workspace.json loaders stamped "Element" plus a kind tag
(Container, Software System, ...) on every element and "Relationship" on
relations, mirroring the reference parser. Those duplicate the typed
`kind` field, no rule reads them, and PlantUML/kubernetes/compose never
produced them — so the same model now yields the same tags whatever
format it loads from. The Structurizr generator re-derives them from
`kind`, so round-trips stay faithful.

- diff strips the styling tags when comparing (defense for legacy .aact.json)
- humanize keeps acronyms uppercase: Orders API, not Orders Api
These commands now take a file or directory path positionally, the way
`aact diff` already does. It overrides config.source, and with no
aact.config.ts it stands in for one — `aact model architecture.dsl`,
`aact check ./k8s/` — with the format auto-detected from the path
(directory -> kubernetes). Ad-hoc `check` with no config runs every
built-in rule, so it lints out of the box.
Walk through drift between architecture-as-code and a live cluster:
`aact diff architecture.dsl ./k8s/`, linting manifests directly, and a
drift gate for CI. The kubernetes-drift example pairs an intended C4
model with a deployed cluster carrying planted structural and technology
drift, and is covered by an integration test.
External duplicates the typed `external` field the same way the kind
tags duplicate `kind` — no rule reads it (rules check `external`), and
the generator re-derives it. Only Structurizr emitted it; k8s/compose
set the field alone, so cross-format diff of external elements showed
spurious `-[External]` noise.

buildModel is the one constructor every loader (incl. model-json) flows
through, so it now strips implicit tags there — Model.tags is uniform
across all formats by construction. The per-loader and diff-level
stripping stay as defense for hand-built models.
…↔ IaC)

"aact ↔ Kubernetes" was too narrow — k8s is one IaC target (Compose is
another), and the diff is cross-format. Reframe around the concept:
Architecture-as-Code (Structurizr/PlantUML) vs Infrastructure-as-Code
(k8s/Compose), with k8s as the worked example.

Also drop the misleading "k8s works in two directions" line — load +
generate is the general Format API. The IaC-specific point is that the
two directions aren't equal: load (infra → model) is reliable; generate
(model → infra) is a lossy approximation, unlike AaC round-trips.
Replaces the legacy `name:` / `environment:` deploy-config with real
`apps/v1` Deployment / StatefulSet + `v1` Service / Namespace (current
stable k8s API). DB/queue kinds become StatefulSets, relations become
env-var Service references, boundaries become Namespaces.

It round-trips: `generate` → `load` reproduces the model — `aact.*`
annotations preserve kind / technology / tags / name. Still a structural
scaffold, not a deployment source (no resources / probes / secrets).
`dbConnectionTemplate` now substitutes `{service}` / `{db}`.
Drop the "Флагман" framing and English filler (round-trip / override /
expose / ad-hoc / scope …) to match the other guides' tone, and rewrite
the generate section now that it emits real, round-tripping manifests.
Covers driving aact from code through the public package surface: load a
model, run built-in and `defineRule` rules, computeDiff two models,
analyzeArchitecture, and generate — every snippet exercised end-to-end by
examples/library-api/library-api.test.ts.
A Model loaded from Structurizr or kubernetes can carry hyphenated names
(`orders-repo`), but C4-PlantUML aliases are identifiers — the generated
`.puml` was unparseable (`Container(orders-repo, …)` threw on re-parse).
`toAlias` normalises every alias and relation endpoint to `[A-Za-z0-9_]`,
so generate output round-trips. Surfaced while verifying generate across
all five formats.
Maps the cascade-coupling-reduction principle (the aact author's Habr
article) onto the tool: model the hierarchy as nested C4 boundaries, read
per-level cohesion/coupling with `aact analyze`, and assert with
`analyzeArchitecture` that coupling doesn't grow up the levels
(cohesion ≥ coupling ≥ coupling-escaping-to-parent). Reuses the existing
examples/banking-plantuml/ccr.test.ts; every number in the guide is real.
Split the overloaded "В команде" into "В CI и команде" (gates: SARIF,
conformance) and "Расширение и встраивание" (custom rules, library API),
and refresh the intro coverage line to mention conformance, CCR and the
library API.
The five-minute onboarding path with real scaffolded output: `aact init`
writes a config + a starter architecture with a seeded CRUD violation,
`aact check` surfaces it, `aact check --fix` rewires it through the
existing repo (one range-based fix resolves both crud and dbPerService),
re-check is clean. Listed first under "С чего начать".
Point readers at the full rule-config guide right in the "Что в конфиге"
section, not only from "Дальше".
…skill)

How an AI agent drives aact: the stable CliEnvelope (schemaVersion 1),
exit codes 0/1/2 as the gate, `model --json` as the inspection surface,
`check --json` (ruleId / summary / sourceLocation / suggestedFixes),
`rule list` / `rule explain` metadata, and installing the aact-architect
skill. Closes the model / rule / skill command-coverage gap. Every shape
is captured from real `--json` output.
…f to getting-started

- cascade-coupling: nested-boundary C4 diagram (rendered from
  fixtures/architecture/boundaries.puml — the example the guide walks through)
- architecture-conformance: intended Shop C4 diagram, dogfooded via
  `aact generate --format plantuml` → plantuml (also exercises the hyphen-alias fix)
- getting-started: reuse the existing docs/demo/demo.gif (init → check → fix → analyze)

Brings the conceptual / flagship guides up to the visual bar of analyze /
custom-rules / explore-view. Pure-CLI guides (check, library-api, agent-contract)
stay text/code as before.
- root barrel is an explicit allow-list (no export * for config/model);
  AactConfigSchema and isDuplicateElement are no longer public
- loadBaseline + DiffInputError move out of CLI internals into the diff
  module; CLI maps DiffInputError to ToolError at the boundary
- ci: user-smoke asserts k8s/*.yaml, matching what generate writes
- view: clean dist before build so the npm tarball drops stale src

BREAKING CHANGE: AactConfigSchema and isDuplicateElement are no longer
exported from the package root; author config via defineConfig.
- generate joins model/check/analyze in taking an ad-hoc positional
  source; works with no aact.config.ts
- positional is the input (format auto-detected from path); --format
  stays the target format
- regenerate command reference + e2e coverage
- generic Container with no inferable image now gets build.context "."
  so the output is valid Compose (spec requires image or build)
- preserve technology as an aact.technology label when there's no image,
  so generate -> load round-trips it instead of dropping it
PlantUML reads `//` as italic Creole, so a raw URL emitted into a label,
description, technology, or property rendered as broken `[https: …]</size>//`.
Escape `//` → `~//` in rendered-text args; leave `$link` / `$sprite` metadata
intact. The loader decodes the escape so the Model keeps the real value and
generate → load round-trips. Also swaps two preParse regexes for a
hand-written constant scanner.
- structurizr load + diff hungarian: extract-function split, behaviour
  identical (verified against the prior implementation)
- compose word-splitter: hand-scan kebab/snake/camel boundaries, no regex
- type external YAML/JSON/path inputs as `unknown` with explicit guards
The C4L2 / generated fixtures carried raw `https://` in relation
technology, which PlantUML rendered as broken `[https: …]</size>//` in
the committed SVGs (visible in the README). Escape `//` → `~//` in the
sources — the same form the generator emits and the loader decodes — and
regenerate the diagrams. Closes the render defect from the old
url-escaping work on the v3 asset side.
ELK ships as one large prebundled layout chunk, isolated under the vendor
chunk. Raise `chunkSizeWarningLimit` to 1500 so that vendor chunk stops
warning while unrelated growth still surfaces. Add a minimal
`svelte.config.js`.
Three resolveRedirectTarget cases emit a warning; mock `consola.warn`
like the other cases in this file already do, to keep test output clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants