gg_dna 4.1.2
gg_dna: ^4.1.2 copied to clipboard
The DNA engine - resolves DNA packages declared as dev-dependencies, merges their dna folders and instantiates configs, docs, scripts and agent skills into the target repo.
gg_dna #
gg_dna is the DNA engine — it is not the DNA itself. The DNA (the
guides, scripts, configurations and agent skills a project inherits)
lives in separate DNA packages: base_dna, dna_dart, dna-ts,
ds-dna, … Each of them ships a dna/ folder that mirrors a project
root.
This engine resolves the DNA packages a project depends on, merges their
dna/ folders into one tree and copies the result to its real locations
(.vscode/settings.json, LICENSE, doc/, .claude/skills/, …).
These copies are called instances — and a placed test guarantees on
every test run that they always match the generated originals.
Quick start #
-
Declare DNA packages as dev-dependencies:
// package.json (TypeScript projects) { "devDependencies": { "dna-ts": "^1.0.0" } }# pubspec.yaml (Dart projects) dev_dependencies: dna_dart: ^1.0.0 gg_dna: ^4.0.0 -
Install (
pnpm install/dart pub get) and run once:gg_dna initThis places the DNA wrapper test (
test/dna/dna_test.dartand/ortest/dna/dna.spec.ts) and adna/_dna.jsonskeleton withlayerspre-filled from the DNA packages you have installed. Commit. -
Run your tests. The first run instantiates the DNA and commits what it generated as
#gg: generated DNA. From now on every test run keeps the project in sync.
Distribution: dev-dependencies + inheritance tree #
DNAs are normal packages (pnpm, for Dart-reachable DNAs additionally pub). A DNA declares its parent DNAs as regular dependencies — pnpm and pub therefore install the whole inheritance tree transitively; gg_dna clones nothing.
Resolution. Layers are named by the package name they are declared
under in pubspec.yaml/package.json, never by a path. Lock files
(pubspec.lock, pnpm-lock.yaml) supply the ecosystem, the resolved
version and the list of installed names; node_modules/ and
.dart_tool/package_config.json supply the folder — a lock file pins an
identity, not a location, and reconstructing pub-cache or pnpm-store
paths would mean reimplementing package-manager internals. For local
development nothing DNA-specific is needed: gg_localize_refs writes
pubspec_overrides.yaml/pnpm-workspace.yaml, and the engine follows
the resolution that produces.
A DNA published to both registries collapses to one layer: the npm scope
is dropped when folding a name to its identity, so @tssuite/base-dna,
base_dna and base-dna are the same layer. node wins when both are
installed; a warning fires when the two copies carry different dna/
trees.
Order: layers is the single source of truth — parents before
children, diamonds deduplicated (first topological position wins), cycles
are errors. The last layer wins.
Configuration: dna/_dna.json #
The only place DNA configuration lives. It sits inside dna/ because
that is the one folder both ecosystems publish — pub drops every path
with a leading dot, so a config below .gg/ never reaches a
pub-installed consumer. The engine only ever reads this file; what it
writes goes to dna/_generated.json.
A project without a dna/ folder and without this file is simply not a
DNA project. A dna/ folder without _dna.json is a hard error —
the config is what declares whether that folder is hand-authored
("role": "dna") or engine-generated, and guessing project would let
the engine overwrite a hand-written DNA. Run gg_dna init to place it.
{
"version": 1, // required
"role": "project", // "dna" for DNA packages themselves
"layers": ["base_dna", "dna_dart"],
"vars": { "projectName": "my_project" },
"claude": { "claudeMdInclude": ["doc/conventions"] },
}
role: "project"(default):dna/is fully generated by the engine.role: "dna": the repo authors itsdna/by hand; it is the last (winning) layer of its own instantiation and is never overwritten. This is also the mark that makes a package a DNA layer at all — adna/folder alone does not.layerslists package names in application order. A dependency that is not listed is not a layer.
With role: "dna" the repo's own dna/ is applied unconditionally and
always last — with an empty layers, with one layer, with a whole tree
of them, and even when a declared layer is another copy of the same
package. It always contributes its files and always wins a conflict.
The replica layout #
dna/ mirrors the project root:
| Path in the DNA | Instantiated to |
|---|---|
dna/dot-vscode/settings.json |
.vscode/settings.json |
dna/LICENSE |
LICENSE |
dna/doc/develop.md |
doc/develop.md |
dna/scripts/create-branch.js |
scripts/create-branch.js |
dna/dot-claude/skills/init/SKILL.md |
.claude/skills/init/SKILL.md |
dna/_vars.json |
— (private) |
- Dotfiles are escaped with a
dot-prefix.dart pub publishsilently drops every path with a leading dot, so a DNA that shipsdna/.vscode/loses it the moment it is consumed from pub. The escape is decoded when instantiating;dna/itself keeps it, because that is what gets republished. A layer shipping literal dotfiles is warned about.dot-is the only accepted form: the placed test rejects adot_-escaped path indna/before instantiating and names the rename (dna/dot_vscode→dna/dot-vscode). - Private: path segments starting with
_(e.g._vars.json) stay insidedna/and are never instantiated. - Public: everything else becomes an instance.
- Consumed by the engine itself:
*.overrides.md,*.overrides.jsonsidecars, plus the two manifests —dna/_dna.json(yours) anddna/_generated.json(the engine's: layers, hashes, and the project files the DNA owns). The effective variables are neither: they are content and live indna/_vars.json. - Forbidden instance targets:
.git/**andCLAUDE.md(the latter is managed via theclaudeblock below).
The placed test: instantiate + verify in one #
Every test run executes the engine (Dart: in-process via the gg_dna
dev-dependency; TypeScript: via the npm package @tssuite/gg_dna-js, the
gg_dna engine compiled to WebAssembly with node callbacks injected):
-
Instance modified by hand → the test fails and leaves the file untouched. The report names the DNA source the file is generated from, so the fix is obvious:
Generated files modified by hand: Move edits from .vscode/settings.json to dna_dart/dna/dot-vscode/settings.overrides.json. -
DNA updated (new dependency versions, changed local DNA) → the engine rewrites
dna/, the instances and its bookkeeping and commits exactly those files as#gg: generated DNA— generated content is machine-owned and never clutters your working tree. Without a repository or a git identity the files stay for a manual commit and the run fails once. -
Everything up to date → green, no writes.
-
Per-file guard: every existing file a run would overwrite or delete must be committed. If one of them carries uncommitted work (modified, staged or untracked), the run fails without writing and reports each file the same way —
Move edits from <instance> to <DNA source>.(headline: Generated files carry invalid changes:) Unrelated dirty files never block a run, so every overwrite stays recoverable via git.
Existing project files that a DNA also ships are adopted (overwritten — git history is the backup, which is exactly what the per-file guard enforces). Instances no longer produced by any DNA are removed, together with folders they leave empty; locally modified ones are kept with a warning.
Markdown overrides #
Unchanged from 4.0, now across the whole replica: ## [@tag] Heading
marks a replaceable section, {{@tag:default}} a replaceable string. A
higher layer ships X.overrides.md next to the same path with
heading-form or <!-- @tag --> … <!-- @tag --> blocks;
global.overrides.md in the dna/ root rewrites string placeholders in
all merged .md files. Markers survive layer application; the final
render strips them. Content in code fences and inline code is immune.
JSON overrides #
A same-path X.json in a later layer replaces the file. A sidecar
X.overrides.json merges field-wise:
- objects deep-merge (default), scalars replace
nulldeletes the key"key!"replaces the value outright (no merge)"key+"joins arrays (append, deduplicated)
// base_dna: dna/dot-vscode/extensions.json
{ "recommendations": ["esbenp.prettier-vscode"] }
// dna_dart: dna/dot-vscode/extensions.overrides.json
{ "recommendations+": ["dart-code.dart-code"] }
// instance: .vscode/extensions.json
{ "recommendations": ["esbenp.prettier-vscode", "dart-code.dart-code"] }
JSONC input (comments, trailing commas) is tolerated; structurally
patched files are re-emitted comment-free, untouched files are copied
byte-identical. YAML supports whole-file replacement only —
X.overrides.yaml is an error.
Variables #
Defined in dna/_vars.json (camelCase keys without prefix), deep-
merged across all layers, finally overridden by vars in the target's
dna/_dna.json:
{ "copyrightHolder": "ggsuite", "projectName": "unnamed" }
References carry the dna prefix and are replaced case-adaptively in
every text file of the merged tree:
| Reference | Replacement |
|---|---|
dnaProjectName |
camelCase (myProject) |
DnaProjectName |
PascalCase (MyProject) — class names |
dna_project_name |
snake_case (my_project) |
DNA_PROJECT_NAME |
SCREAMING_SNAKE (MY_PROJECT) |
dna-project-name |
kebab-case (my-project) |
Non-identifier values (spaces, sentences — e.g. "MEGA TARGET") are
inserted verbatim for every form. Unknown references stay literal.
File naming #
DNA files are instantiated under exactly the name they carry in the DNA
layer — no case conversion happens. Author each file with the name the
target project should see (the dot escape dot-vscode/ → .vscode/ is
the only path rewriting the engine performs).
CLAUDE.md and skills #
- Skills are plain instances:
dna/dot-claude/skills/<name>/SKILL.md→.claude/skills/<name>/SKILL.md. CLAUDE.mdkeeps the managed block:claude.claudeMdIncludelists files/folders (human documentation!) that get one@-import line each between<!-- gg_dna:claude_md:start/end -->. Content outside the block is never touched. All documentation is written for humans — the AI consumes the same files.
Engine API #
import 'package:gg_dna/gg_dna.dart';
await runDnaTest(); // what the placed test calls
final result = instantiateDna( // programmatic access
host: IoDnaHost(),
targetRoot: '.',
baseVersion: ggDnaVersion,
);
The engine core is free of dart:io/Process — all host access goes
through the injectable DnaHost interface (IoDnaHost for the CLI and
Dart tests, callback-based hosts for the WebAssembly bridge
@tssuite/gg_dna-js).
Migration from 4.x #
5.0 reads nothing but the new format — there is no compatibility path, so every repository is moved over by hand once.
- Move
.gg/dna.jsontodna/_dna.json, add"version": 1, renameordertolayersand list every layer explicitly (nothing is inferred from dependencies any more). Delete.gg/dna.jsonand the!.gg/dna.jsonline from.gitignore. - Drop the
dependenciespath overrides — gg_localize_refs already pointspubspec_overrides.yaml/pnpm-workspace.yamlat the sibling checkouts. - Flatten
config.claude.claude_md.includetoclaude.claudeMdInclude. - In DNA repos: declare
"role": "dna"(adna/folder alone no longer marks a package as a layer), rename every dotfile belowdna/to thedot-form (dna/.vscode/→dna/dot-vscode/), and remove".gg/dna.json"from the npmfilesarray. - Delete
dna/_instances.json— it lives insidedna/_generated.jsonnow. The first run re-adopts the existing instances, so commit before running it. - Switch npm repositories to pnpm;
package-lock.jsonandyarn.lockare not read.