import type { Serializer } from "./serializer.js"; import type { Declarable } from "./declarable.js"; import type { LintRule } from "./lint/rule.js"; import type { RuleSpec } from "./lint/declarative.js"; import type { PostSynthCheck } from "./lint/post-synth.js"; import type { TemplateParser, TemplateIR } from "./import/parser.js"; import type { TypeScriptGenerator } from "./import/generator.js"; import type { ArtifactIntegrity } from "./lexicon-integrity.js"; import type { OkfFile } from "./okf.js"; import type { CompletionContext, CompletionItem, HoverContext, HoverInfo, CodeActionContext, CodeAction } from "./lsp/types.js"; import type { McpToolContribution, McpResourceContribution } from "./mcp/types.js"; import type { DriverComponent } from "./components/driver.js"; import type { EmulatorDeclaration } from "./op/emulator-lifecycle.js"; import type { OwnershipChannel, OwnershipMarker } from "./ownership.js"; import type { LexiconConfigSchema } from "./lexicon-config.js"; import type { RuleMeta } from "./audit/catalog.js"; import type { ReferenceCatalog } from "./graph-refs.js"; import type { IREdge } from "./graph-ir.js"; import type { DescribeResourcesResult, UnobservedReason } from "./observation.js"; import type { DeepNormalizationHooks, DeepObservationResult } from "./deep-observation.js"; import type { OwnerChainVerdict } from "./owner-chain.js"; import type { CommandGroup } from "./cli/command-group.js"; export type { CommandGroup, CommandGroupCommand, CommandGroupContext } from "./cli/command-group.js"; export type { ReferenceCatalog, IdentityRule, RefRule } from "./graph-refs.js"; export type { IREdge } from "./graph-ir.js"; export type { DescribeResourcesResult, ObservationResult, NormalizedObservation, UnobservedEntity, UnobservedReason, } from "./observation.js"; export type { DeepObservationResult, DeepResourceObservation, NormalizedDeepObservation, DeepNormalizationHooks, DeepNode, DeepArrayElement, DeepSide, } from "./deep-observation.js"; /** * Manifest for a packaged lexicon — metadata embedded in the tarball. * * chant #1067 decided each optional field below explicitly rather than * leaving them all equally unchecked: * * - `chantVersion` — now validated for presence/shape by `chant dev * check-lexicon` (a lexicon's `dist/manifest.json` must declare one). * NOT validated for compatibility against the core version actually * running — that needs a live check at plugin-load time (a different * surface: `loadPlugin`/`loadPlugins` in `./cli/plugins.ts`), which stays * a deliberate non-goal here. * - `namespace` — deliberately NOT validated for cross-lexicon uniqueness in * #1067. `chant dev check-lexicon ` only ever inspects one lexicon at * a time, so it structurally can't catch a collision between two * *different* lexicons. The natural home is `checkConflicts` (./cli/ * conflict-check.ts), which already detects cross-lexicon rule-id/skill/ * MCP-tool/MCP-resource collisions when multiple plugins load together — * but `namespace` isn't on the runtime `LexiconPlugin` surface at all * (manifest-only, baked in at package time), so extending that check would * mean adding a new field to `LexiconPlugin` itself. Deferred rather than * done under time pressure alongside a concurrent, unrelated change to * this same interface (chant #1064). * - `pseudoParameters` — deliberately NOT validated against what the * lexicon actually exports in #1067, unlike `intrinsics` (see * `IntrinsicDef.isTag` below and `./cli/commands/check-lexicon- * intrinsics.ts`). The same static-analysis approach would apply — check * each declared pseudo-parameter's short name against the real exported * namespace object's properties (`AWS.StackName`, `Azure.ResourceGroupName`, * ...) — but unlike `intrinsics()`, `pseudoParameters(): string[]` is a * flat list of dotted strings with no structural link back to a specific * export/property declaration, making the same trick more involved to * generalize correctly. Relevant to #1063 (folding cross-file references * into lexicon pseudo-parameter namespaces): once that lands, a wrong or * missing pseudo-parameter property would silently break folding exactly * the way #1039 did for intrinsics, which would be the moment this * deferral needs revisiting. */ export interface LexiconManifest { name: string; version: string; chantVersion?: string; namespace?: string; intrinsics?: IntrinsicDef[]; pseudoParameters?: Record; } /** * Metadata about when and how a package was generated. */ export interface PackageMetadata { generatedAt: string; chantVersion: string; generatorVersion: string; sourceSchemaCount: number; } /** * Container for all artifacts in a lexicon bundle. */ export interface BundleSpec { manifest: LexiconManifest; registry: string; typesDTS: string; rules: Map; skills: Map; /** OKF knowledge bundle over the registry and rules (#1060), written to dist/okf/. */ okf?: OkfFile[]; integrity?: ArtifactIntegrity; metadata?: PackageMetadata; } /** * Trigger that determines when a skill should be suggested. */ export interface SkillTrigger { type: "command" | "file-pattern" | "context"; value: string; } /** * Parameter definition for a skill. */ export interface SkillParameter { name: string; description: string; type: string; required?: boolean; default?: string; } /** * Example showing how a skill is used. */ export interface SkillExample { title: string; description?: string; input?: string; output?: string; } /** * Definition of a skill provided by a lexicon plugin. * Content is full markdown with optional YAML frontmatter. */ export interface SkillDefinition { readonly name: string; readonly description: string; readonly content: string; readonly triggers?: SkillTrigger[]; readonly parameters?: SkillParameter[]; readonly examples?: SkillExample[]; readonly preConditions?: string[]; readonly postConditions?: string[]; } /** * Definition of a lexicon-specific intrinsic function */ export interface IntrinsicDef { readonly name: string; readonly description?: string; readonly outputKey?: string; /** * Whether this intrinsic is authored as a JS tagged template (`` Sub`...` ``) * rather than a plain function call (`Ref(...)`). Required — chant #1067 — * because an omitted value silently defaulted to "not a tag" with no * signal that the registration had never been decided. That produced #1039 * in both directions at once: aws's `Sub` (a genuine tagged template, the * most-used intrinsic in the ecosystem) shipped with no `isTag` at all and * silently never folded, while gitlab's `reference()` (a plain call) * shipped with `isTag: true`. Both were wrong, and both shipped, because * nothing forced the declaration or checked it against how the intrinsic is * actually authored. `chant dev check-lexicon` now validates every * registration here against its real declaration (tagged-template * signature vs plain call) and against the package's own exports — see * `../cli/commands/check-lexicon-intrinsics.ts`. */ readonly isTag: boolean; /** * chant #1044 — opt this intrinsic's PLAIN-CALL form into folding * (`Ref(bucket)`, `Concat(a, b)` reduce to their intrinsic node instead of * falling the whole file back to the run path). * * Optional, and OFF unless a lexicon writes `true`. That default is the * point: `fold()` has no general `CallExpression` case by construction * (epic #1019), and this field is the only thing that admits one. It is a * closed, lexicon-declared allowlist, decided one intrinsic at a time — * never inferred from `isTag`, from the name, or from the call's shape. An * intrinsic with no `foldsAsCall` behaves exactly as it did before #1044. * * Only set it when calling the intrinsic is a pure function of its * arguments that builds a deterministic data envelope — the whole * correctness argument is that invoking it while folding is * indistinguishable from invoking it during a real run of the file. An * intrinsic that reads the environment, mutates state, or depends on * anything but its arguments does not qualify, and neither does a tagged * template (see {@link isTag}: the two forms are mutually exclusive, and * `chant dev check-lexicon` rejects `isTag: true` + `foldsAsCall: true`). * * Registration is by name. It is not permission to invoke whatever that * name happens to be bound to: `fold()` reduces the call to a symbolic * envelope executing nothing, and `../discovery/fold-import.ts` resolves * the name through the folding FILE'S OWN imports before invoking the real * function — so the function that runs while folding is the same one the * run path would have called, from the module the source itself named. */ readonly foldsAsCall?: boolean; } /** * Whether `chant build --fold` can ever fold a use of this intrinsic * (chant #1062, epic #1019) — in EITHER authored form. * * Two disjoint ways to qualify, one per form: * * - a registered tagged-template intrinsic (`Sub\`...\``) folds because * `foldTaggedTemplate` recognizes its tag and recurses into the interior * ({@link intrinsicTagFolds}); * - a registered plain-call intrinsic (`Ref(...)`, `Concat(...)`) folds * only when its lexicon opted it in with `foldsAsCall` * ({@link intrinsicCallFolds}, chant #1044). Before #1044 no plain call * folded at all, whatever it was named or registered as. * * This function is the single predicate the generated per-lexicon intrinsics * page (`../codegen/docs-sections.ts`'s "Folds?" column) calls — never a * restated copy that could silently drift from the code. `fold()` itself * calls the two form-specific predicates below rather than this one, because * it always knows which form it is looking at, and a tag must not fold as a * call (or vice versa) merely because the other form was opted in. * * Takes a structural `{ isTag?, foldsAsCall? }` rather than * `Pick` deliberately: `IntrinsicDef.isTag` is required * for new registrations (chant #1067), but these predicates also read off * untrusted, possibly-older parsed JSON (`ManifestJSON` in * `./codegen/docs-types.ts`, on disk as a published lexicon's * `dist/manifest.json`) that may predate either field. `undefined` there * means what it always did — not a tag, not opted in. */ export declare function intrinsicFolds(def: { isTag?: boolean; foldsAsCall?: boolean; }): boolean; /** True when this intrinsic's TAGGED-TEMPLATE form folds (`Sub\`...\``) — see {@link intrinsicFolds}. */ export declare function intrinsicTagFolds(def: { isTag?: boolean; }): boolean; /** * True when this intrinsic's PLAIN-CALL form folds (`Ref(...)`) — i.e. the * lexicon opted it in via {@link IntrinsicDef.foldsAsCall} (chant #1044). * * `isTag: true` disqualifies regardless: a tagged template is invoked as * `` Name`...` ``, so a call to it isn't the registered authoring form at * all. Keeping that here rather than trusting registrations means a lexicon * that declares both flags cannot quietly widen `fold()`'s call case — and * `chant dev check-lexicon` fails the registration outright. */ export declare function intrinsicCallFolds(def: { isTag?: boolean; foldsAsCall?: boolean; }): boolean; /** * Options passed to a MigrationSource by `chant migrate`. */ export interface MigrateOptions { /** Output format. */ emit?: "yaml" | "ts"; /** Recognise composite patterns when emitting. */ useComposites?: boolean; /** Source file path (for provenance display only). */ sourceFile?: string; /** Escalate needs-review diagnostics to errors. */ strict?: boolean; /** Run security-aware migration analysis (classify property fates + run * target security checks). Enabled by `chant migrate --validate`. */ security?: boolean; } /** * Result of `MigrationSource.transform()`. * * Provenance is a generic side channel: each record is `{ sourceKey, rule, * category, note?, ... }`. Diagnostics are SARIF-compatible records derived * from provenance (concrete shape lives in `packages/core/src/lint/rule.ts`). */ export interface MigrationResult { /** Rendered output (YAML by default, TS when emit: "ts"). */ output: string; /** Per-key provenance records (typed loosely at the core level). */ provenance: Array>; /** SARIF-shaped diagnostics. */ diagnostics: Array>; /** Markdown "Security posture" section, when security analysis ran (#306). */ securityPosture?: string; } /** * Edge that translates one lexicon's source format into this lexicon's IR * and output. Exposed via `LexiconPlugin.migrationSource(from)`. */ export interface MigrationSource { /** Lightweight detector: does this content look like the expected source? */ detect(content: string): boolean; /** Run the translation. */ transform(content: string, opts: MigrateOptions): Promise; } /** * Structured init template output from a lexicon plugin. */ export interface InitTemplateSet { /** Source files written to src/ */ src: Record; /** Application scaffold files written to project root */ root?: Record; /** Scripts merged into generated package.json */ scripts?: Record; } /** * Plugin interface for lexicon packages. * * Required lifecycle methods enforce consistency: every lexicon must support * generate, validate, coverage, and package operations. */ /** * Where a lexicon pins an upstream schema/spec version, and where to look for a * newer one — the lexicon's own declaration for the self-upgrade tooling * (`chant dev pinned-upgrade`), so core never hard-codes per-lexicon upstream * repos or version-constant locations. See ./codegen/pinned-upgrade.ts. */ export interface UpstreamPin { /** Source file (relative to the lexicon package root) holding the pinned version constant. */ readonly file: string; /** Regex whose first capture group is the current pinned version in `file`. */ readonly pattern: RegExp; /** Rebuild a line that matched `pattern`, substituting the new version. */ replace(newVersion: string, line: string): string; /** Where to query for the latest stable upstream tag. */ readonly upstream: { readonly owner: string; readonly repo: string; /** `releases` = published releases; `tags` = every git tag. */ readonly kind: "releases" | "tags"; /** Only consider tags ending with this suffix (e.g. "-ee" for GitLab). */ readonly tagSuffix?: string; }; } /** One CI job in a generated pipeline (generate mode) — a thin trigger for one component in one wave. */ export interface ComponentPipelineJob { /** CI job name (safe as a YAML key). */ jobName: string; /** The component this job triggers. */ component: string; /** The stage/wave this job runs in. */ stage: string; /** Direct dependency job names this job waits on (mirrors the component's `dependsOn`). */ needs: string[]; } /** Generic knobs for generate mode, applied once across every generated job (never per component). */ export interface ComponentPipelineOptions { /** Target environment threaded into the trigger command. */ env?: string; /** The trigger command per component, argv with `{name}` substituted. */ runCommand?: string[]; /** Extra script lines appended to every job. */ extraScript?: string[]; /** Script lines run before the trigger command in every job. */ beforeScript?: string[]; /** Container image every job runs in. */ image?: string; /** Top-level CI `variables:` block. */ variables?: Record; } /** The synthesized CI pipeline for a component graph (generate mode). */ export interface ComponentPipelineResult { /** The synthesized CI YAML (e.g. `.gitlab-ci.yml` content). */ yaml: string; /** Wave-ordered stage names (matches the driver's graph waves 1:1). */ stages: string[]; /** Every generated job, in emit order. */ jobs: ComponentPipelineJob[]; } /** * Live status of a single deploy unit (a CloudFormation stack, a K8s release, …) * addressed by its deployed name — the per-component presence signal * `chant components status --live` needs (#57). A component's deploy step carries * the exact unit name it targets (e.g. a `cfn-deploy` step's `stack`), which is * the identity in a multi-stack component project where `describeResources` * (entity-keyed, single-stack-per-env) can't see the component's own stack. */ export interface StackStatusObservation { /** The deploy-unit name queried (the stack name). */ stack: string; /** False when the unit does not exist yet — the pre-first-apply state. */ present: boolean; /** Provider-native status string, e.g. CloudFormation "CREATE_COMPLETE". */ status?: string; /** True when `status` is a terminal *success* state (deployed and healthy). */ healthy?: boolean; } /** * Context for a lexicon's {@link LexiconPlugin.buildRoots} hook (#1548). */ export interface BuildRootContext { /** * The directory the project configuration was loaded from (where * `chant.config.*` lives) — relative root paths resolve against it, NOT * against `sourceDir`, because a non-chant-source root (a kustomize * overlay tree) usually lives beside the typed source rather than inside * it. */ projectRoot: string; /** The resolved project configuration, for the lexicon's own namespace. */ config: Record; } /** * What a build-root render contributes to the build: entities keyed like * discovered ones, plus non-fatal diagnostics. */ export interface BuildRootContribution { entities: Map; warnings?: string[]; } export interface LexiconPlugin { /** Human-readable name (e.g. "aws", "gcp") */ readonly name: string; /** Serializer for build output */ readonly serializer: Serializer; /** Generate lexicon artifacts (types, lexicon JSON, runtime) from spec */ generate(options?: { verbose?: boolean; }): Promise; /** Validate generated lexicon artifacts */ validate(options?: { verbose?: boolean; }): Promise; /** Analyze lexicon coverage across resource dimensions */ coverage(options?: { verbose?: boolean; minOverall?: number; }): Promise; /** Package lexicon into distributable tarball */ package(options?: { verbose?: boolean; force?: boolean; }): Promise; /** * The shape of this lexicon's own `chant.config.ts` namespace — the top-level * key named after the lexicon (#1344). * * k8s reads `k8s.profiles..context`, temporal `temporal.profiles`, * forgejo `forgejo.runnerLabels` and `forgejo.actionsRoot`. All were * documented for users and declared nowhere: the config schema is * `.passthrough()`, so a typo was accepted and silently ignored, and the * `ChantConfig` interface is closed, so the documented examples did not * compile. * * Declaring the schema makes an unknown key inside the namespace an error * rather than a default, and gives the lexicon a single source to derive the * type it augments `ChantConfig` with — so the runtime rule and the * compile-time one cannot disagree. Omit it and the namespace keeps today's * passthrough. */ readonly configSchema?: LexiconConfigSchema; /** * Local emulator(s) (#920), if this lexicon has any: Floci for aws, floci-az * for azure, floci-gcp for gcp, mudflaps and spritzer for fly. Drives * `chant emulator up|down|status` and lets a consumer (behold `--local`) boot * one and point apply/observe at it — no cloud account. Absent when the * lexicon has no local emulator. * * One capability or several (#1345). fly ships two, and while this field held * exactly one, three of the repo's four emulators went undeclared and * `chant emulator up --all` booted only Floci — even though azure's and gcp's * wrappers already built the same spec this needs. */ readonly emulator?: EmulatorDeclaration; /** * A CLI verb group this lexicon contributes, mounted under `chant * ` (#1078). Core learns that a lexicon MAY contribute a command * group and learns nothing about what is inside it — it finds the group by * name and dispatches to the matched verb's handler wholesale, the same * "spec, not behavior" shape as {@link emulator}. Unlike `emulator`, which * core itself aggregates across every configured lexicon in one command * (`chant emulator up --all`), a command group is owned end-to-end by ONE * lexicon: `get -o wide -l app=x --field-selector` is irreducibly * Kubernetes vocabulary, not something core could generalize or merge * across plugins even if it wanted to. Absent when the lexicon contributes * no CLI surface — registering nothing here changes nothing else about how * the lexicon behaves; the build/fold path never calls this or invokes any * verb's handler, since command dispatch happens only in the CLI's own * entry point, never in discovery/build/fold. */ commands?(): CommandGroup; /** Return lint rules provided by this lexicon */ lintRules?(): LintRule[]; /** Return declarative rule specs for compilation via rule() */ declarativeRules?(): RuleSpec[]; /** Return post-synthesis checks for build validation */ postSynthChecks?(): PostSynthCheck[]; /** * Audit catalog metadata (title/tier/fix/authority/category) for this * lexicon's `postSynthChecks`, keyed by check id — the per-provider half of * `chant audit`'s rule catalog (#687, epic #350). Core aggregates these over * its static catalog via `resolveAuditCatalog` (./audit/catalog.ts). Omit * for lexicons whose rule metadata still lives in core's static catalog. */ auditCatalog?(): Record; /** * Machine-readable spec-coverage accounting for `check-lexicon` (#1330). * * `coverage()` prints a report for humans; this returns the one fact the * completeness gate cares about: which upstream spec kinds are neither * modeled as declarables nor on the lexicon's exclusion list. fountain held * this line in a lexicon-local vitest assertion (`coverage.test.ts`), which * is a convention rather than a contract — the same class of gap #1342 * closed for LSP providers. * * Implementations must work offline from committed snapshots (fountain * reads `spec/fountain-openapi.snapshot.json` plus its surface baseline): * `check-lexicon` runs on every PR, so no network I/O. Omit when the * lexicon has no kind-level spec accounting; the check passes vacuously. */ coverageReport?(): Promise<{ unaccountedKinds?: string[]; }>; /** Return intrinsic function definitions */ intrinsics?(): IntrinsicDef[]; /** Return pseudo-parameter names (e.g. "MyDomain::StackName") */ pseudoParameters?(): string[]; /** * Detect whether raw template content belongs to this lexicon. * @param data - Parsed JSON object from a template file * @returns true if this plugin can handle the template */ detectTemplate?(data: unknown): boolean; /** Return a parser for importing external templates into IR */ templateParser?(): TemplateParser; /** Return a generator for converting IR to TypeScript */ templateGenerator?(): TypeScriptGenerator; /** Return skills provided by this lexicon */ skills?(): SkillDefinition[]; /** Return source file templates for `chant init` project scaffolding */ initTemplates?(template?: string): InitTemplateSet; /** Optional initialization hook */ init?(): void | Promise; /** How this lexicon pins its upstream schema version + where to check for a newer one (self-upgrade tooling — see ./codegen/pinned-upgrade.ts). Omit for lexicons with no pinned upstream. */ readonly upstreamPin?: UpstreamPin; /** * Generate a CI pipeline from the discovered component graph — the * `chant build --components --generate ` seam. Only CI-provider * lexicons (gitlab, github, forgejo) implement this; core owns the generic * driver (discovery + graph waves) and dispatches here. Omit for lexicons * that are not CI providers. */ generateComponentPipeline?(components: DriverComponent[], options?: ComponentPipelineOptions): ComponentPipelineResult; /** * Render this lexicon's config-declared build roots into entities (#1548 * piece 3) — the seam for a build root that is NOT typed chant source. The * k8s lexicon implements it for `k8s.kustomize.roots`: each named * kustomization dir renders (`kustomize build`) at build time and the * documents come back as entities, which the build merges into the * discovered set BEFORE partitioning — so they are serialized by this * lexicon's serializer (ownership stamping included), seen by post-synth * checks, and observed by `describeResources` exactly like discovered * entities. * * Called once per top-level build (never for nested child projects), only * when the CLI wires it up (`collectBuildRootContributors` in * ./cli/plugins.ts → {@link BuildOptions.buildRoots}). A hook that has * nothing configured must return an empty map, not throw; a hook that * cannot render (missing binary, missing dir) should throw with a message * naming the problem — the build reports it as a build error, not a stack * trace. Omit for lexicons with no non-source build-root concept. */ buildRoots?(ctx: BuildRootContext): Promise; /** Provide completions for LSP */ completionProvider?(ctx: CompletionContext): CompletionItem[]; /** Provide hover information for LSP */ hoverProvider?(ctx: HoverContext): HoverInfo | undefined; /** Provide code actions for LSP */ codeActionProvider?(ctx: CodeActionContext): CodeAction[]; /** Generate documentation pages */ docs?(options?: { verbose?: boolean; }): Promise; /** Return MCP tool contributions */ mcpTools?(): McpToolContribution[]; /** Return MCP resource contributions */ mcpResources?(): McpResourceContribution[]; /** * Return a migration source for translating from another lexicon's * format into this lexicon. Returns undefined if `from` is not supported. * * Example: the gitlab lexicon implements `migrationSource("github")` to * translate `.github/workflows/*.yml` into `.gitlab-ci.yml`. */ migrationSource?(from: string): MigrationSource | undefined; /** * Query deployed resources and return API metadata. Opt-in. * * Use this when each chant entity has a 1:1 cloud equivalent — e.g. an * AWS CFN resource, a K8s object, an ARM resource, a Temporal namespace. * * **The observation contract (#1089).** Returning nothing for a declared * entity is a claim, and there are two different claims to make. Either the * provider was asked and reported the resource absent — which is what lets * the change set propose `create` — or the lexicon never looked, which must * not. An implementation that has a "did not look" case (no reader for the * kind, the read errored, no credentials, no cluster binding) must return the * {@link ObservationResult} envelope and name those entities in `unobserved` * with a total {@link UnobservedReason}. Warning on stderr is not enough: a * warning is invisible to `lifecycle plan`, which is where the wrong `create` * gets proposed. Returning the bare `name → ResourceMetadata` map is still * valid and means "everything I was asked about, I looked at". * * Throwing is the whole-lexicon failure (see the k8s cluster-binding refusal, * #1100): core catches it and marks every declared entity NOT-OBSERVED with * `read-failed`, so a failed read is never a list of creates. * * Ownership verdicts are total (#1089). When `owned` is requested and the * lexicon has no marker channel on this path, it must stamp * `ownership: "unknown"` on what it returns rather than degrading silently — * the change set never escalates `unknown` to a `delete`. * * An undeclared entry this method returns may carry {@link * ResourceMetadata.ownerChain} (#1077) — set it when the provider's own * parent/child graph (Kubernetes `ownerReferences`) shows this object's * chain reaching a declared entity, so the diff engine classifies it * `runtime` instead of `orphan`. Optional; a lexicon that never sets it * keeps every undeclared entry classified `orphan`, unchanged. * * `entities` carries the chant-side entity declarations for this lexicon, * keyed by chant entity name (e.g. the export name from a `*.ts` file). * Implementations that need to map cloud-side names back to chant entity * names (e.g. Temporal — server-side namespace `prod` ↔ chant entity `ns` * declared with `name: "prod"`) read this; implementations that already * have name parity (e.g. AWS CloudFormation logical IDs == chant entity * names) can ignore it. * * `entityNames` is preserved as a convenience for the simple case. */ describeResources?(options: { environment: string; buildOutput: string; entityNames: string[]; entities: Map; }>; /** * The deployed stack name to observe, for a multi-stack project where the * stack is not named after the environment (see `stacks` in {@link * ChantConfig}). When omitted, an implementation keeps its single-stack * convention (AWS: the stack named after `environment`). */ stack?: string; /** AWS region the stack is in (multi-region). When set, the observation * targets this region instead of the ambient one (#1161 follow-up). */ region?: string; /** * Restrict the result to chant-owned resources (those carrying the * ownership marker, #119). Where a lexicon has no durable marker channel, * it must log that ownership is unavailable rather than silently returning * everything. */ owned?: boolean; /** * Where to look for an entity whose declaration names no scope of its own * (#1629) — `chant graph --live --namespace `, `chant lifecycle diff * --live --namespace `. * * For a substrate with a namespace-like scope (Kubernetes today), a GitOps * estate splits the binding from the objects: one project declares * `spec.targetNamespace`, another declares bare objects the controller * stamps at apply time. Reading the second project has to be told where * the first put them, or it reads the substrate's default and reports a * running estate as absent. * * A default, never a rewrite: an entity that declares its own namespace * keeps it. Optional, and a lexicon with no such scope ignores it. */ namespace?: string; }): Promise; /** * Report the undeclared resources this estate *depends on* (#1273), as * opposed to the ones it manages. * * `describeResources` is scoped to what the stack declares. Anything the * estate references but does not declare — an account's default VPC route * tables, a shared subnet, networking owned by another team — is invisible to * it, so it never becomes a node, so no edge can reach it and no fold can * traverse it. That is why derived facts about un-modelled topology have had * to be computed inside lexicons and injected as attributes. * * The closure rule is depth one by reference, plus whatever chains this * lexicon's {@link referenceCatalog} declares as meaningful — so AWS follows * `SubnetId` → `RouteTableId` → `GatewayId` because the catalog says those * references matter, not because they happen to be reachable. Without a rule * a VPC transitively reaches most of an account. * * Every returned resource must carry {@link ResourceMetadata.referencedBy}, * naming the nodes that pulled it in. A dependency with no referrer is * unbounded discovery, which is what the closure rule exists to prevent. * * Optional and additive. A lexicon that does not implement it behaves exactly * as before, and a consumer that ignores dependencies sees what it always saw. */ observeDependencies?(options: { environment: string; /** Declared entities, for a lexicon that resolves references from source. */ entities: Map; }>; /** What {@link describeResources} just found — the roots of the closure. */ observed: Record; stack?: string; region?: string; }): Promise; /** * Kinds this lexicon can enumerate beyond the declared estate (#1278). * * Declared separately from {@link observeAmbient} so a caller can say that * ambient resources of a kind are POSSIBLE without paying for a scan to find * out. `chant search` uses it to point out that `--ambient` is relevant to * the kind just queried — an agent asking which security groups are unused * has no way to know that some are not in the answer at all. */ ambientKinds?(): string[]; /** * Report resources of a kind this estate manages that exist in the account * without being declared or referenced (#1278). * * `describeResources` answers "what do I manage" and `observeDependencies` * answers "what do I rely on". Neither can see a resource that is simply * *there* — an unattached security group, an orphaned volume — because both * resolve outward from the declared estate and an unused resource is reached * by nothing. * * That is a real question with no other answer: "which of my security groups * are unused" cannot be resolved from a state file at all, because a state * file knows only what it created. A lexicon that can enumerate a kind can * answer it. * * `kinds` bounds the scan to types the project actually declares, so a * project managing security groups is not made to enumerate the account. A * lexicon returns resources marked {@link ResourceMetadata.ambient}, and must * exclude anything already in `observed` — those are managed, not ambient. * * Optional and opt-in. A lexicon that does not implement it, or a caller that * does not ask, sees exactly what it saw before. */ observeAmbient?(options: { environment: string; /** Entity types the project declares — the bound on what to enumerate. */ kinds: string[]; /** Already-observed managed resources, to exclude. */ observed: Record; stack?: string; region?: string; }): Promise>; /** * Read the full live *property tree* for each declared entity (#1014). Opt-in, * and strictly deeper than {@link describeResources}, which reports existence * plus a handful of scrubbed outputs. A lexicon that implements neither, or * only the thin one, is unaffected — `lifecycle diff --live` gains * property-level entries only where this exists. * * The result is keyed by chant entity name, exactly like the thin read, and * carries the same NOT-OBSERVED map. That is the composition rule with #1089: * a deep read that fails for one entity says so with a total * {@link UnobservedReason}. It never returns a thin-but-clean tree, because a * clean tree is a claim that nothing drifted. * * Properties must be normalized before they are returned — run * `normalizeDeepProperties` (../deep-observation.ts) with this lexicon's own * {@link deepNormalizationHooks}, so the trees a consumer sees are already * free of arns, timestamps, status subtrees and unstable orderings. * * Throwing is the whole-lexicon failure, same as the thin read: core turns it * into `read-failed` for every declared entity. */ observeResourcesDeep?(options: { environment: string; buildOutput: string; entityNames: string[]; entities: Map; }>; /** Deployed stack to observe, for a multi-stack project (see `stacks` in {@link ChantConfig}). */ stack?: string; /** Region the stack is deployed in (#1267), mirroring the thin path's * `describeResources` (#1261). When omitted, an implementation keeps its * ambient-region default. */ region?: string; /** Restrict to chant-owned resources (#119). A lexicon with no marker channel on this path says so. */ owned?: boolean; }): Promise; /** * This lexicon's noise rules for deep observation (#1014): which fields are * read-only / server-populated / controller-managed / provider-defaulted, and * which arrays are sets. Data, not a method — core applies the same rules to * the *declared* tree, which no reader ever touches, and the two sides have * to be normalized identically to be comparable. * * Ships alongside {@link observeResourcesDeep}; a reader without hooks * produces a diff made almost entirely of noise. */ deepNormalizationHooks?: DeepNormalizationHooks; /** * Report the live status of one deploy unit by its deployed name. Opt-in. * * Complements {@link describeResources}: that observes a stack's *entities* * keyed by chant entity name (and assumes one stack per environment), which * can't see a multi-stack component project where each component owns its own * stack. `chant components status --live` resolves a component's deploy-step * target (e.g. a `cfn-deploy` step's `stack`) and calls this to learn whether * that unit is present and healthy — a component-level presence signal. * * Returns `null` when the lexicon cannot determine status (e.g. the provider * CLI failed for a reason other than "does not exist"); a genuinely absent * unit returns `{ present: false }`. */ describeStackStatus?(options: { environment: string; stack: string; }): Promise; /** * Enumerate the resources this lexicon would delete for one marker identity * (#1222). Opt-in, and read-only here: this method names the would-delete * set, it never deletes. `chant lifecycle teardown ` calls it to plan; * the execution half ({@link executeTeardown}) deletes from the same * enumeration. * * Selection is marker-scoped by construction. `marker` carries this * project's ownership stack plus the requested environment, and every * returned candidate must have been read carrying exactly that identity on * this lexicon's marker channel — managed-by present, stack equal, env * equal. A resource whose marker is absent, foreign-stack, or foreign-env is * not a candidate, ever. Core re-checks each candidate's `marker` and drops * mismatches, so a buggy implementation cannot widen the set. * * The #1089 discipline applies: a kind this lexicon stamps but cannot read * back (no reader for the kind, the read errored, no credentials) is a * `hole`, named with a total {@link UnobservedReason} — never silently * absent, because "absent from the plan" reads as "safe", and an unreadable * kind is unknown, not safe. * * A lexicon without this capability still takes part in teardown planning: * core falls back to {@link describeResources} and filters on * {@link ResourceMetadata.marker}. Implement this when that read is the * wrong shape for deletion — aws, whose thin read carries no tags and whose * teardown is stack-level, is the motivating case. */ teardownOwned?(options: { environment: string; /** The identity to select on: this project's ownership stack + the env being torn down. */ marker: OwnershipMarker; /** Deployed stack name, for a multi-stack project (see `stacks` in {@link ChantConfig}). */ stack?: string; /** Region that stack is deployed in (#1261's contract). */ region?: string; /** * Every deployed stack a multi-stack project declares (see `stacks` in * {@link ChantConfig}), for a lexicon whose enumeration is stack-shaped * (aws). When absent or empty, the single-stack convention applies: * `stack`, else the stack named after the environment. */ stacks?: Array<{ name: string; region?: string; }>; }): Promise; /** * Delete the teardown candidates core hands over — the execution half of * `chant lifecycle teardown --yes` (#1222). Opt-in, and the sibling of * {@link teardownOwned}: that method names the would-delete set, this one * deletes it. A lexicon that enumerates but does not implement this reports * its candidates as skipped rather than pretending. * * `candidates` is the marker-verified set core computed from the plan — an * implementation deletes those and only those, in whatever order its target * requires (k8s deletes namespaces last; fly deletes apps last). It never * re-widens the set: a live resource not in `candidates` is not this call's * business, whatever its labels say. * * Every candidate gets exactly one outcome per call, keyed by `name`: * `deleted` (including already-gone — deletion is idempotent), `failed` * (the delete errored; core runs one bounded retry pass over these), or * `not-prunable` with a reason (the live object no longer carries the * requested identity, the kind cannot be addressed, the target refuses). * A candidate the implementation says nothing about is reported as failed * by core — silence is never success. */ executeTeardown?(options: { environment: string; /** The identity everything was selected on: ownership stack + env. */ marker: OwnershipMarker; /** The marker-verified candidates to delete — from {@link teardownOwned} / the plan. */ candidates: TeardownCandidate[]; /** Deployed stack name, for a multi-stack project (see `stacks` in {@link ChantConfig}). */ stack?: string; /** Region that stack is deployed in (#1261's contract). */ region?: string; /** Every declared deployed stack, mirroring {@link teardownOwned} — how a * stack-shaped execution (aws) finds each candidate's region. */ stacks?: Array<{ name: string; region?: string; }>; }): Promise; /** * Where this lexicon can stamp and read chant's ownership marker (#1348). * Data, not a method. * * {@link ResourceMetadata.ownership} says a lexicon with no marker channel on * a read path must return `unknown` rather than degrade silently. That was an * obligation with no declaration behind it: a caller could not learn whether * `owned: true` was answerable except by asking and reading a warning on * stderr afterwards — and a warning is invisible to `lifecycle plan`, which is * where the wrong delete gets proposed. * * Declared per read path, because the answer differs by path. aws stamps tags * at synthesis and reads them on the deep observation and on live export, * while its `describeResources` is sourced from `describe-stack-resources`, * which returns no tags — so an `owned: true` thin read against aws can only * answer `unknown`, and does. * * Absent means no channel anywhere: every verdict must be `unknown`. */ readonly ownershipChannel?: OwnershipChannel; /** * Reference catalog for live edge reconstruction (#778). Declares how this * lexicon's observed resources reference each other — an identity map (which * attrs identify each kind) plus reference rules (which attr paths point at * other resources, as a peer edge or containment). Consumed by * `chant graph --live` to turn a bag of live nodes into a graph. Data, not a * method. Opt-in. */ referenceCatalog?: ReferenceCatalog; /** * Enrich live IR node attributes for edge reconstruction (#784). Returns * `nodeId → attributes` with cross-resource references resolved to the * referenced node id, so the reference resolver ({@link referenceCatalog}) can * match them. Opt-in — for lexicons whose `describeResources` metadata is too * thin to carry references (e.g. AWS CloudFormation, where it's sourced from * the fuller `exportResources` config). */ enrichLiveAttrs?(options: { environment: string; stack?: string; stacks?: Array; owned?: boolean; }): Promise>>; /** * List runtime artifacts in the given environment. Opt-in. * * Use this for lexicons whose chant entities describe *authoring* * primitives rather than 1:1 cloud resources — e.g. Helm (charts vs * releases), Docker (Compose vs running containers). The contract is * context-keyed: given an environment, list all artifacts visible there. * There is no `declared` comparison axis — `state diff --live` reports * added/removed/changed between snapshots, not vs. declared. * * `entities` is passed for cases where the lexicon needs to know what * was declared in order to scope its enumeration (e.g. a per-tenant * runtime where the declared entities name which tenants to query). */ listArtifacts?(options: { environment: string; entities: Map; }>; /** The deployed stack to scope enumeration to, for a multi-stack project * (see `stacks` in {@link ChantConfig}). Omitted for single-stack projects. */ stack?: string; }): Promise>; /** * Export full-fidelity import IR from a live API — the cloud→code primitive * that lets chant regenerate TypeScript from running cloud/cluster state. * * Deliberately separate from {@link describeResources}: that returns scrubbed * *output* metadata for diffing, this returns full *input* config — enough to * regenerate a resource, and therefore possibly containing secrets. The * scrubbing boundary stays single-purpose; never overload one method for both. * * The return type {@link ExportedTemplate} is branded distinct from the * observation types so a full-fidelity export can never flow into the * observation/`state` code paths by accident. * * `owned` is accepted now but inert until ownership marking exists (#119/#120). * * `verbatim` controls fidelity: by default an implementation strips * server-defaulted and server-managed fields to reach the declared shape * (the form a user would have authored). `verbatim: true` keeps them. Targets * whose live config is already the declared shape (e.g. a CloudFormation * original template) may ignore it. */ exportResources?(options: { environment: string; /** The deployed stack to export from, for a multi-stack project (see * `stacks` in {@link ChantConfig}, #932). When omitted, an implementation * keeps its single-stack convention (AWS: the stack named after * `environment`). */ stack?: string; /** AWS region the stack is in (multi-region estates). */ region?: string; selector?: ResourceSelector; owned?: boolean; verbatim?: boolean; }): Promise; } /** * One resource {@link LexiconPlugin.teardownOwned} would delete (#1222). * Identity only — no delete happens on this path. */ export interface TeardownCandidate { /** chant entity name where a declared mapping exists, else the provider-side name. */ name: string; /** Resource type (e.g. "AWS::S3::Bucket", "K8s::Apps::Deployment"). */ type: string; /** Provider-side identifier, when the read surfaces one. */ physicalId?: string; /** * The stack/env identity read off the resource's own marker — read back, * never inferred. Core verifies it equals the requested identity and drops * the candidate otherwise. */ marker: OwnershipMarker; } /** * One kind or entity a teardown enumeration could not read (#1089). A hole is * a claim of ignorance, not of absence: the plan must print it loudly, and the * execution half must refuse to call the env clean while holes exist. */ export interface TeardownHole { /** The unreadable kind or entity name. */ name: string; /** Resource type, when known. */ type?: string; /** Total verdict — the same vocabulary the observation envelope uses. */ reason: UnobservedReason; /** Human-readable detail: the failing command, the unsupported kind. */ detail?: string; } /** What {@link LexiconPlugin.teardownOwned} returns: the would-delete set plus its holes. */ export interface TeardownEnumeration { candidates: TeardownCandidate[]; /** Omit or leave empty when every stamped kind was readable. */ holes?: TeardownHole[]; } /** * One candidate's fate after {@link LexiconPlugin.executeTeardown} (#1222). * `name` keys it back to the candidate it answers for. */ export interface TeardownOutcome { /** The candidate's `name`, verbatim. */ name: string; /** Resource type, when the implementation carries it through. */ type?: string; /** Provider-side identifier, when known. */ physicalId?: string; /** * - `deleted` — gone, including already-gone (deletion is idempotent); * - `failed` — the delete errored (core retries these once); * - `not-prunable` — deliberately not deleted; `detail` says why. */ outcome: "deleted" | "failed" | "not-prunable"; /** The error for `failed`, the reason for `not-prunable`. */ detail?: string; } /** What {@link LexiconPlugin.executeTeardown} returns: one outcome per candidate. */ export interface TeardownExecution { outcomes: TeardownOutcome[]; } /** * The observation view of a lexicon — every capability except live export. * * State/observation code (snapshots, `state diff --live`) consumes lexicons * through this type so that `exportResources` is unreachable from those paths: * a full-fidelity {@link ExportedTemplate} (which may carry secrets) must never * be read where scrubbed {@link ResourceMetadata} is expected. A full * {@link LexiconPlugin} is assignable to this type; accessing `exportResources` * on it is a compile error. */ export type ObservationLexicon = Omit; /** * Narrows which live resources {@link LexiconPlugin.exportResources} and the * `owned` filter operate on. Both fields are optional; omit to export all. */ export interface ResourceSelector { /** Restrict to a single chant resource type (e.g. "AWS::S3::Bucket"). */ readonly type?: string; /** Restrict to a single resource name. */ readonly name?: string; } /** * Full-fidelity import IR read from a live API, branded distinct from the * scrubbed observation metadata. It IS a {@link TemplateIR} — it feeds the * existing `templateGenerator()` unchanged — but the phantom brand keeps it * from being passed where observation metadata is expected, and keeps * observation metadata from being passed where an export is expected. * * The brand is type-only; nothing is added at runtime. */ export type ExportedTemplate = TemplateIR & { /** Phantom marker — never present at runtime. */ readonly __fidelity?: "full-config"; }; /** * Metadata about a deployed resource, returned by describeResources. */ /** * What a lexicon saw outside its declared estate (#1273): the resources * something declared references, and how they connect. * * Kept as its own result rather than folded into `describeResources` so every * consumer downstream can tell "what I manage" from "what I depend on" without * disentangling them — the same reason the change set separates `orphan` from * `runtimeChildren`. */ export interface DependencyObservation { /** * Undeclared resources, keyed by a stable id. Physical id is the natural * choice: these have no logical name, and the key has to match between a live * read and a snapshot replay or an overlay double-counts them. */ resources: Record; /** * Relationships among the dependencies and back to the declared nodes that * reached them. Reported rather than reconstructed, because a reference * catalog can only resolve what it has an identity index for, and part of the * point here is expressing a hop the catalog does not model. */ edges?: IREdge[]; } export interface ResourceMetadata { /** Entity type (e.g. AWS::S3::Bucket, K8s::Apps::Deployment) */ type: string; /** Provider-assigned physical ID (ARN, resource ID, pod name) */ physicalId?: string; /** Provider-specific status string */ status: string; /** ISO timestamp of last update */ lastUpdated?: string; /** Cloud-assigned output properties */ attributes?: Record; /** * Live ownership verdict from the resource's marker (#119/#120). `owned` = * carries chant's marker; `foreign` = no marker; `unknown` = the lexicon has * no marker channel on this read path and says so rather than degrading * silently (#1089 — verdicts are total). Absent is read as `unknown`. The * change set reads this — never the snapshot — to decide whether an orphan is * a delete, and never escalates `unknown` to one. */ ownership?: "owned" | "foreign" | "unknown"; /** * The stack/env identity read off the resource's own ownership marker * (#1222) — the tags/labels/metadata chant stamped at synthesis, read back * verbatim on the same channel. This is what marker-scoped selection keys * on: {@link ownership} says "chant's", `marker` says *which* stack and env. * * Set only when the live model actually carries the channel and the * managed-by marker is present — an absent channel means an absent field, * never a guess. In particular aws's thin read is sourced from * `describe-stack-resources`, which returns no tags at all, so aws never * populates this here; aws teardown is stack-level and reads the stack's own * tags instead (later PR). A populated `marker` does not by itself imply an * {@link ownership} verdict on paths that do not declare a marker channel. */ marker?: OwnershipMarker; /** * Where this resource's owner-reference chain leads, for a live resource * that is not itself declared (#1077). A lexicon that maintains an * owner-reference graph (Kubernetes) sets this on an undeclared entry it * returns; the diff engine reads `{ root: "declared" }` as `runtime` * (a Pod a declared Deployment's controller created) rather than `orphan`. * Distinct from {@link ownership}: that is chant's own managed-by marker, * this is the provider's native parent/child graph — a runtime child * usually carries no chant marker of its own at all. Absent means the * lexicon supplies no chain, which is exactly today's behavior: every * undeclared live resource stays `orphan`. */ ownerChain?: OwnerChainVerdict; /** * The declared node ids that reference this resource (#1273), set on an * undeclared resource observed only because something declared points at it — * the account's default VPC route table an instance routes through, a shared * subnet, a network someone else owns. * * A third kind of observed-but-undeclared resource, beside {@link ownership} * and {@link ownerChain}. Not an orphan: nobody is going to adopt or delete * the account's default VPC, so offering it as a delete candidate is wrong. * Not a runtime child either — nothing declared created it. It gets a runtime * child's drift treatment for the same reason: it is not yours, it changes on * its own, and alerting on it is noise. * * Carries the referrers rather than a bare flag so the reason a resource was * pulled in is answerable, and so a closure can be walked back to the declared * entity that justified it. */ referencedBy?: string[]; /** * Observed in the account, of a kind this estate manages, but neither * declared nor referenced by anything declared (#1278). * * The third and last category of observed-but-undeclared resource, after * {@link ownerChain}'s runtime children and {@link referencedBy}'s * dependencies. A default security group AWS creates per VPC is the type * case: nothing declares it, nothing points at it, and it is exactly what * "which of my security groups are unused" is asking about. * * Opt-in, because finding these means asking the provider what exists rather * than resolving out from what is declared, which is a broader read and a * different claim about what an observation is. */ ambient?: boolean; } /** * Metadata about a runtime artifact, returned by listArtifacts. Same shape * as ResourceMetadata; the conceptual distinction is whether the lexicon's * chant entities have 1:1 runtime equivalents (resources) or whether the * runtime artifacts are created by tooling outside chant's entity model * (artifacts — e.g. Helm releases, Docker containers). */ export interface ArtifactMetadata { /** Artifact type (e.g. Helm::Release, Docker::Container) */ type: string; /** Server-side identifier */ physicalId?: string; /** Provider-specific status string */ status: string; /** ISO timestamp of last update */ lastUpdated?: string; /** Provider-specific properties */ attributes?: Record; } /** * Type guard to check if a value is a LexiconPlugin. * Checks for required lifecycle methods in addition to name/serializer. */ export declare function isLexiconPlugin(value: unknown): value is LexiconPlugin; //# sourceMappingURL=lexicon.d.ts.map