feat!: v3 — typed Model/Format API, chevrotain parsers, JSON/SARIF contract - #19
Open
ChS23 wants to merge 380 commits into
Open
feat!: v3 — typed Model/Format API, chevrotain parsers, JSON/SARIF contract#19ChS23 wants to merge 380 commits into
ChS23 wants to merge 380 commits into
Conversation
…enerator + tests" This reverts commit a5f56b1.
…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).
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
- 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.
This was referenced Jun 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
v3-рефакторинг закончен и живёт на npm как
aact@beta(3.0.0-beta.29).Supersedes: #18 (дисциплина тестов + DX-улучшения — целиком включено как
основа), #15 (экранирование URL в
resources/— устарело послепереименования
resources/→fixtures/).Зачем v3
v2 держался на regex-лоадерах: хрупко, без source-locations, со
stringly-typed опциями правил и без машинного контракта, на который могли бы
опираться CI и AI-агенты. v3 заменяет это типизированными Model/Format API,
chevrotain-парсерами с UTF-16 ranges, range-based
--fixи стабильным--json/--sarifenvelope — один контракт для людей, 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. Унифицированный--jsonenvelope (schemaVersion: 1) и--sarifv2.1.0 для GitHub Code Scanning. Четыре новые команды (diff,model,rule explain,view), новый форматmodel-jsonдляLLM-генерируемых архитектур и diff-baseline'ов, skill-installer для
AI-агентов и опциональный companion-пакет
@aact/view(браузерныйworkbench).
Как ревьюить
Точки входа, чтобы 75k строк не пугали:
src/index.ts— вся публичная поверхность одним файломsrc/model/types.ts— Model (ядро, на нём держится всё)src/cli/output/types.ts— контракт envelope (JSON/SARIF/exit-коды)src/formats/structurizr/— один парсер целиком; остальные по образцу(грамматика и scope-policy —
src/formats/structurizr/parser/grammar.md)src/rules/crud.ts— каноничное правило (check + fix + options)Дальше «по объёму» (фикстуры, snapshot'ы,
examples/) — беглым взглядом.Риски / trade-offs (осознанно)
нереалистично; отсюда порядок чтения выше. AI-ревьюеры на таком объёме
покрывают лишь часть файлов — это ревью для людей.
Deployment view / ArchiMate / UML / BPMN — осознанно вне scope.
список в
grammar.md(раздел про парсеры ниже).schemaVersion: 1заморожен до GA — bump'ы только пост-GA.Что изменилось — по областям
Model layer (breaking)
ElementKind/BoundaryKindElement.external: boolean— ортогонален kind, заменил варианты*_Extmodel.elements/model.boundaries—Readonly<Record<name, T>>для O(1) поискаContainer→Element(литералkind: "Container"сохраняется для C4 level-2)WorkspaceMetadata(Structurizrworkspace "..." extends "...")validateModel+ 9 типизированных видовModelIssuegetElement,targetOf,walkBoundaries,formatLocation,isDatabaseElement,isDatabaseKindFormat API (breaking)
Format = { name, defaultPattern?, load?, generate?, fix? }— все возможности опциональны, type-guard'ыcanLoad / canGenerate / canFixdefaultPatternпринимаетstring | readonly string[]— compose объявляет все 4 канонических имени, structurizr —["workspace.json", "*.dsl"]для автоопределения DSL-источниковplantuml— load + generate + fixstructurizr— 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:-/>relationships, ключевое словоthis, иерархические ссылки (bank.api)name "..."/description "..."(побеждает последнее)!include/!const/!var/!identifiers/!impliedRelationships(в форме с!и без)${...}(fixed-point, 16 итераций), многострочные продолжения через\<id> = <alias> "Name") с цепочкой дефолтов: description/technology по fallback (источник побеждает), tags/properties/perspectives аддитивноarchetypes { softwareSystem { … } }без имени-алиаса) — дефолты на все элементы данного kindElementв слоте идентификатора для симметрии (element = element "X")!impliedRelationships true!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, опц. зависимость)AACT_FILE_OPENER/api/model+ WebSocket-upgrade (строго в пределах/api/ws)packages/view/DESIGN.md--fixengine (breaking)SourceEdit— discriminated union:replace / remove / insert-after / insert-beforeapplyEdits— чистый 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)DiagnosticKind(model.*/config.*/format.*/fix.*/skill.*/view.*/internal.*)0чисто /1нарушения /2ошибка инструментаdata: null) — spec-канонический SARIFinvocations[].toolExecutionNotifications[]Contract freeze (финальный проход перед заморозкой
schemaVersion: 1)ruleId— единый ключ во всём envelope (violations[],suggestedFixes[],rules[], SARIFresult.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 →errorconfig.rules→ жёсткая ошибка (exit 2); один общий резолвер правил дляcheck/rule list/rule explainhelpUriна основе ADR, camelCase-идентификаторыmodel.*(model.danglingRelation)config.generate— по форматам (generate.<format>);aact generateпередаёт срез вformat.generate; артефакт в stdout запрещён в любом не-текстовом режиме выводаplantumlFormat, …); схема model-json аддитивна (additionalPropertiesснят, ломающее →aact-model-v2.json)AnalysisReportи под-типы →readonly;generatefiles[].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>/ stdinaact 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/), проверка коллизии с JSONaact rule list— envelope--json, без тихого глотания ошибокaact init—--jsonвозвращаетInitData, не перезаписывает файлыaact skill install— ставит agent-skill в основные skill-пути (--claude/--cline/--codex/--cursor/--copilot/--all)Переработка analyze
["http","grpc","tcp"](давал0/0на большинстве PUML)elementsByKind,relationsByStyle: {sync,async,unspecified}(по тегам, с опциональным fallback по technology),boundaries[].{syncCoupling,asyncCoupling,unspecifiedCoupling,ratio},fanIn/fanOuttop-N с фильтромexclude,cycles: {count, smallest}через Tarjan SCCAactConfig.analyze:syncTechnologies / asyncTechnologies / exclude.{tags,namePatterns} / topNПравила
acl,acyclic,apiGateway,cohesion,commonReuse,crud,dbPerService,stableDependenciesRuleDefinitionс inline check + опциональным fixrationale+examples+adrPath?на каждое правило (используются вaact rule explain)*NamePatternsдля acl/apiGateway/crud/dbPerService покрывают legacy-архивы и AI-генерённые диаграммы без явных тегов*OptionsэкспортируютсяrelatedLocationsдля вторичных якорей (рёбра-аксессоры общей БД, цели внешних систем, участники цикла)isDatabaseElementунифицирован — теперьContainerDb || ComponentDb(раньше только ContainerDb), используется в analyze + правилах + генерации k8sГиперссылки
TERM_PROGRAM=vscodeилиCURSOR_TRACE_ID→file://abs:line:colTERM_PROGRAM=zed→ обычный текст (у Zed свой автодетект путей)<scheme>://file/abs:line:col, схема через envAACT_FILE_OPENER(vscode/vscode-insiders/cursor/windsurf/zed/none)Документация
AGENTS.md(гайд верхнего уровня для AI-агентов),CLAUDE.mdи.github/copilot-instructions.md— симлинкиdocs/format-coverage.md— матрица по форматам (load/generate/fix на каждое поле)docs/parser/— phase-0 инвентаризация + грамматикиsrc/formats/structurizr/parser/grammar.md+README.md(актуальное состояние парсера, scope-policy, оставшиеся пробелы)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)Container→Element. Литералkind: "Container"сохраняется для C4 level-2Violation.element: string→target: string+ дискриминаторtargetKind: "element" | "boundary"FormatSyntax.relationDecl(from, to, label?, ...)→(from, to, opts: { description?, technology?, tags? })AnalysisReport.syncApiCalls/asyncApiCalls→relationsByStyle: {sync,async,unspecified}(глобально) + per-boundarysyncCoupling/asyncCoupling/unspecifiedCouplingAnalyzeOptions.apiTechnologies→ параsyncTechnologies+asyncTechnologieskubernetes: утилиты v2 (loadDeployConfigs,mapFromConfigs,DeployConfig) убраны; v3 даёт полноценную возможностьloadдля детекции дрейфаCheckViolation/FixResultнесутruleId(былrule);CheckSummary.total→violations; типыCheckRuleMetadata/RuleInfo→ единыйRuleMetadataconfig.rules); неизвестное имя вconfig.rules→ exit 2config.generateпо форматам:boundaryLabel→generate.plantuml.boundaryLabel;aact generateзапрещает артефакт в stdout в любом не-текстовом режимеСтатус релиза
npm dist-tags:latest: 2.1.5(нетронут),beta: 3.0.0-beta.29.29 beta-релизов с момента разделения. Каждый задокументирован в
CHANGELOG.md.План тестов
dist/cli/index.mjsв чистом tmpdirpnpm publint— санити package.jsonpnpm knip— без неиспользуемых экспортовDslTests.test_archetypes/test_archetypesForDefaultsдают идентичную наблюдаемую ModelЗакрывает
Closes #8 — рендеринг
//-ссылок в PlantUML (</size//в SVG) починен в генераторе и парсере (ручной + сгенерированный puml)Closes #7 — автогенерация архитектуры вынесена из юнит-теста в CLI (
aact generate)Closes #6 — у всех паттернов в
patterns.mdпроставлены ссылки на unit- и example-тесты