@decantr/cli 3.0.0-next.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,7 @@ Use `decantr setup` when you are unsure which path applies. It detects whether t
22
22
  Use `decantr scan` when you want a zero-commit Brownfield preview. It reads local files, detects framework/routes/styling/static-hosting/assistant-rule signals, previews typed Contract graph readiness in memory when a Decantr contract already exists, reports the contract capsule source-handle count and limit, prints a terminal report, and writes no `.decantr` files or report artifacts. Add `--json` when automation needs the `ScanReportV1` payload.
23
23
  Use `decantr new` for a greenfield workspace in a fresh directory. With a starter-kit blueprint or archetype it uses the runnable adapter and Decantr CSS; without certified vocabulary content it creates a contract-only workspace unless you explicitly pass `--adoption=decantr-css`.
24
24
  Use `decantr adopt` when you already have an app and want Decantr governance without adopting a blueprint. Brownfield attach is proposal-driven: Decantr inventories the app, writes an observed essence proposal, hydrates hosted execution packs when online, and only applies the contract when you explicitly accept or merge it.
25
- Use `decantr doctor` when the next step is unclear, `decantr task` before asking an LLM to modify a route, `decantr verify` after the edit, and `decantr ci` in required automation. Use `decantr graph` when you want the Decantr 3 typed Contract graph, typed graph diff summary, manifest, content-addressed snapshot history, and cache-friendly contract capsule written under `.decantr/graph`; the capsule includes a bounded SourceArtifact path index so agents can discover valid file-impact handles without reading the full snapshot, and `--capsule-source-limit <count>` can tune that index for large repos. Add `--route /feed --task "improve loading" --json` when you want the exact task-ranked route-scoped subgraph an agent should inspect before editing, `--node cmp:button --impact --json` when you need the graph-shaped blast radius for a component, token, rule, finding, or source artifact, or `--file src/app/page.tsx --impact --json` when the agent knows the source file it is about to change. Use `--snapshot-id <id>` to inspect a replayable history snapshot and `--compare-to <id> --include-diff-ops --json` to compare the selected/current graph against a prior snapshot. Use `decantr codify --from-audit --style-bridge` when you want project-owned UI patterns, local rules, and token/class bridge mappings such as button/card/shell/theme standards to appear in future task context and verification. Once accepted, that local law is the first Hybrid lane: the app still owns source and styling, but Decantr treats accepted local patterns, rules, and style bridge mappings as project authority.
25
+ Use `decantr doctor` when the next step is unclear, `decantr task` before asking an LLM to modify a route, `decantr verify` after the edit, and `decantr ci` in required automation. Use `decantr graph` when you want the Decantr 3 typed Contract graph, typed graph diff summary, manifest, content-addressed snapshot history, and cache-friendly contract capsule written under `.decantr/graph`; the capsule includes a bounded SourceArtifact path index so agents can discover valid file-impact handles without reading the full snapshot, and `--capsule-source-limit <count>` can tune that index for large repos. Add `--route /feed --task "improve loading" --json` when you want the exact task-ranked route-scoped subgraph an agent should inspect before editing, `--node cmp:button --impact --json` when you need the graph-shaped blast radius for a component, token, rule, finding, or source artifact, or `--file src/app/page.tsx --impact --json` when the agent knows the source file it is about to change. Use `--snapshot-id <id>` to inspect a replayable history snapshot and `--compare-to <id> --include-diff-ops --json` to compare the selected/current graph against a prior snapshot. Use `decantr codify --from-audit --style-bridge` when you want project-owned UI patterns, optional `behavior_obligations`, local rules, and token/class bridge mappings such as button/card/shell/theme standards to appear in future task context and verification. Once accepted, that local law is the first Hybrid lane: the app still owns source and styling, but Decantr treats accepted local patterns, behavior obligations, rules, and style bridge mappings as project authority.
26
26
  In monorepos, app-scoped commands accept `--project <app-path>`. `setup` shows attach guidance before adoption and the day-two loop after adoption. Hosted pack hydration also follows the essence path: `decantr registry compile-packs apps/web/decantr.essence.json --write-context` writes into `apps/web/.decantr/context`. In contract-only/offline Brownfield, deferred hosted packs are optional context unless a present manifest references missing files.
27
27
  Use `decantr init`, `decantr analyze`, `decantr check`, and `decantr health` as advanced primitives when you need direct control over one step.
28
28
 
@@ -91,9 +91,9 @@ pnpm exec decantr codify --map-pattern hero --project apps/web
91
91
  pnpm exec decantr ci init --project apps/web
92
92
  ```
93
93
 
94
- Assistant rule integration is preview-first: `--assistant-bridge=preview` writes `.decantr/context/assistant-bridge.md`, `decantr rules preview` prints the bridge, and `--assistant-bridge=apply` or `decantr rules apply` mutates supported rule files with idempotent marked blocks. When `.decantr/graph/contract-capsule.json` exists, `decantr task` includes it in the read list and JSON payload so CLI-only agents can load the typed Contract capsule and its SourceArtifact path index before route-specific edits. When `.decantr/graph/graph.snapshot.json` exists, `decantr task --json` also includes `graph.routeContext`: the route node, scoped page/shell/pattern/component/token nodes, local law/style bridge nodes, open findings/evidence IDs, deterministic ranked nodes boosted by the task text, and the supporting edges. If git changed files resolve to SourceArtifact nodes, `decantr task --json` also includes `graph.changedFileContext` so an agent can see source-file blast radius before editing or repairing the current diff.
94
+ Assistant rule integration is preview-first: `--assistant-bridge=preview` writes `.decantr/context/assistant-bridge.md`, `decantr rules preview` prints the bridge, and `--assistant-bridge=apply` or `decantr rules apply` mutates supported rule files with idempotent marked blocks. When `.decantr/graph/contract-capsule.json` exists, `decantr task` includes it in the read list and JSON payload so CLI-only agents can load the typed Contract capsule and its SourceArtifact path index before route-specific edits. When `.decantr/graph/graph.snapshot.json` exists, `decantr task --json` also includes `graph.routeContext`: the route node, scoped page/shell/pattern/component/token nodes, local law/style bridge nodes, behavior-obligation LocalRule nodes, open findings/evidence IDs, deterministic ranked nodes boosted by the task text, and the supporting edges. Accepted `.decantr/local-patterns.json` `behavior_obligations` also appear in `localLaw.patterns[].behaviorObligations` and `localLaw.behaviorObligations` so agents see project-owned dialog/form obligations before editing. If git changed files resolve to SourceArtifact nodes, `decantr task --json` also includes `graph.changedFileContext` so an agent can see source-file blast radius before editing or repairing the current diff.
95
95
 
96
- `decantr scan` is different from the mutating Brownfield primitives: it is look-don't-touch reconnaissance for fit, route/style evidence, GitHub Pages hints, typed Contract graph readiness, capsule source-handle bounds, and next-command guidance. For attached Essence V4 apps it derives the graph preview in memory, reports stale/missing `.decantr/graph` artifacts, and still writes nothing. `decantr analyze` writes `.decantr/doctrine-map.json`, a ranked source-precedence map across security/data, architecture, design-system, workflow/CI, feature/business, assistant-specific, stale, and unsafe-to-cite evidence. It also writes `.decantr/brownfield-intelligence.json`, `.decantr/theme-inventory.json`, and `.decantr/enrichment-backlog.md`. The proposal groups routes into observed semantic domains such as auth, RBAC, billing, reporting, facilities, settings, and public surfaces across Next App/Pages Router, React Router, Angular Router, SvelteKit, Vue Router, and Nuxt file routes. Existing styling systems such as Tailwind, Bootstrap, MUI, Chakra, plain CSS, and Decantr CSS are observed as evidence instead of replaced. Theme variants are observed in the theme inventory without changing Essence V4. `decantr adopt` writes the first typed Contract graph baseline before verification so Project Health, task context, and agents can anchor to graph artifacts immediately. `decantr codify --from-audit` proposes `.decantr/local-patterns.proposal.json` and `.decantr/rules.proposal.json` with Hybrid authority guidance, source-derived button/card/form/theme evidence, variant hints, and confidence tiers; `decantr codify --style-bridge` proposes `.decantr/style-bridge.proposal.json`, mapping Decantr intent to project-owned tokens/classes without requiring `@decantr/css`; `decantr codify --map-pattern <slug>` maps a hosted or bundled registry pattern into an advisory local-law proposal without changing source. After review, `decantr codify --accept` promotes whichever proposals exist. `decantr doctor` reports whether the app is contract-only, Hybrid local law, style bridge, Decantr CSS, or Hybrid composition. `decantr task` prints that authority block and warns before mixing runtimes or adding Decantr CSS to a non-Decantr-CSS app. `decantr suggest --from-code` surfaces accepted local patterns and style bridge mappings from the app root or selected `--project`, and `decantr ci` prints accepted local-rule findings plus style bridge status in text, markdown, and JSON reports. `decantr verify --brownfield --local-patterns` uses the Brownfield guard layer plus accepted local law to flag actionable missing doctrine coverage, unsafe context, missing assistant bridges, style drift, raw local-rule violations, and unsafe defaults without treating current database migrations as stale docs.
96
+ `decantr scan` is different from the mutating Brownfield primitives: it is look-don't-touch reconnaissance for fit, route/style evidence, GitHub Pages hints, typed Contract graph readiness, capsule source-handle bounds, and next-command guidance. For attached Essence V4 apps it derives the graph preview in memory, reports stale/missing `.decantr/graph` artifacts, and still writes nothing. `decantr analyze` writes `.decantr/doctrine-map.json`, a ranked source-precedence map across security/data, architecture, design-system, workflow/CI, feature/business, assistant-specific, stale, and unsafe-to-cite evidence. It also writes `.decantr/brownfield-intelligence.json`, `.decantr/theme-inventory.json`, and `.decantr/enrichment-backlog.md`. The proposal groups routes into observed semantic domains such as auth, RBAC, billing, reporting, facilities, settings, and public surfaces across Next App/Pages Router, React Router, Angular Router, SvelteKit, Vue Router, and Nuxt file routes. Existing styling systems such as Tailwind, Bootstrap, MUI, Chakra, plain CSS, and Decantr CSS are observed as evidence instead of replaced. Theme variants are observed in the theme inventory without changing Essence V4. `decantr adopt` writes the first typed Contract graph baseline before verification so Project Health, task context, and agents can anchor to graph artifacts immediately. `decantr codify --from-audit` proposes `.decantr/local-patterns.proposal.json` and `.decantr/rules.proposal.json` with Hybrid authority guidance, source-derived button/card/form/theme evidence, variant hints, confidence tiers, and optional `behavior_obligations` for form controls and destructive confirmation dialogs when source evidence is strong; `decantr codify --style-bridge` proposes `.decantr/style-bridge.proposal.json`, mapping Decantr intent to project-owned tokens/classes without requiring `@decantr/css`; `decantr codify --map-pattern <slug>` maps a hosted or bundled registry pattern into an advisory local-law proposal without changing source. After review, `decantr codify --accept` promotes whichever proposals exist. `decantr doctor` reports whether the app is contract-only, Hybrid local law, style bridge, Decantr CSS, or Hybrid composition. `decantr task` prints that authority block and warns before mixing runtimes or adding Decantr CSS to a non-Decantr-CSS app. `decantr suggest --from-code` surfaces accepted local patterns and style bridge mappings from the app root or selected `--project`, and `decantr ci` prints accepted local-rule findings, behavior-obligation findings, plus style bridge status in text, markdown, and JSON reports. `decantr verify --brownfield --local-patterns` uses the Brownfield guard layer plus accepted local law to flag actionable missing doctrine coverage, unsafe context, missing assistant bridges, behavior-obligation drift, style drift, raw local-rule violations, and unsafe defaults without treating current database migrations as stale docs.
97
97
 
98
98
  ## What It Does
99
99
 
@@ -102,7 +102,7 @@ Assistant rule integration is preview-first: `--assistant-bridge=preview` writes
102
102
  - guides users through human workflow commands: setup, adopt, doctor, task, verify, ci, and codify
103
103
  - supports explicit workflow lanes: greenfield blueprint, greenfield contract-only, brownfield adoption, Hybrid local law, Hybrid style bridge, Hybrid Decantr CSS, and hybrid composition
104
104
  - generates execution-pack context files for AI coding assistants
105
- - generates typed Contract graph artifacts, replayable snapshot history, graph diffs, manifests, source-file impact context, style-bridge Token nodes, and `contract-capsule.json` for agent sessions
105
+ - generates typed Contract graph artifacts, replayable snapshot history, graph diffs, manifests, source-file impact context, style-bridge Token nodes, behavior-obligation LocalRule nodes, and `contract-capsule.json` for agent sessions
106
106
  - audits projects against Decantr contracts
107
107
  - produces local Project Health reports, Evidence Bundles, workspace health, and a localhost Studio dashboard for end-user drift triage
108
108
  - audits local vocabulary repositories with Content Health reports for schema, reference, and quality coverage
@@ -181,7 +181,7 @@ decantr showcase verification --json
181
181
 
182
182
  `decantr ci` is the blessed non-mutating automation gate. It runs the Project Health surface with adoption-mode-aware local law checks and emits a schema-backed CI report. `decantr ci init` writes root GitHub workflows or portable generic snippets using the detected package manager and pinned local CLI command instead of `@latest`; if the root manifest has not pinned Decantr yet, it prints the exact install command first.
183
183
 
184
- `decantr health` remains the advanced project observability primitive. It composes the existing verifier audit, guard checks, brownfield route drift checks, runtime evidence, component reuse drift, accepted style bridge drift, typed Contract graph freshness, and execution-pack files into a `ProjectHealthReport` with a status, score, route summary, pack summary, findings, stable diagnostic codes, typed repair IDs, and AI-ready remediation prompts. The graph freshness slice emits `GRAPH001` / `regenerate-typed-graph` when an attached app has missing, stale, or non-derivable `.decantr/graph` artifacts. The component reuse slice emits `COMP001` / `import-existing-component` when production source locally redeclares a common primitive that already exists as an exported reusable component, and `COMP010` / `replace-raw-control-with-local-component` when production JSX renders raw controls such as `<button>` while the project already owns a reusable primitive. The style bridge slice emits `TOKEN010` / `replace-arbitrary-style-with-bridge-token` when production JSX, common class helpers, hardcoded inline color styles, or hardcoded visual values in CSS/module stylesheets bypass `.decantr/style-bridge.json` after it has been accepted as project-owned style authority. The baseline slice emits `VISUAL010` / `review-visual-baseline-drift` when `--since-baseline` detects changed screenshot hashes. When `.decantr/graph/graph.snapshot.json` exists, each finding is anchored to the most specific graph node Decantr can resolve, and JSON, markdown, text output, repair prompts, and Evidence Bundles carry that anchor. `decantr graph` also writes content-addressed history snapshots under `.decantr/graph/snapshots/` so repeated graph runs can be replayed across an AI edit sequence. When `.decantr/analysis.json` exists, `decantr graph` links observed routes/pages to implementation source artifacts and links exported reusable component declarations to their source files. When browser evidence writes `.decantr/evidence/visual-manifest.json`, `decantr graph` ingests it as local route/page Evidence nodes without uploading screenshots. When `.decantr/evidence/latest.json` exists, `decantr graph` can also materialize saved findings, evidence strings, graph anchors, repair IDs, and referenced repair/read target files as typed graph nodes and edges. When `.decantr/health-baseline-diff.json` exists, baseline changed files become file-level temporal evidence in the graph.
184
+ `decantr health` remains the advanced project observability primitive. It composes the existing verifier audit, guard checks, brownfield route drift checks, runtime evidence, component reuse drift, accepted style bridge drift, accepted behavior-obligation checks, typed Contract graph freshness, and execution-pack files into a `ProjectHealthReport` with a status, score, route summary, pack summary, findings, stable diagnostic codes, typed repair IDs, and AI-ready remediation prompts. The graph freshness slice emits `GRAPH001` / `regenerate-typed-graph` when an attached app has missing, stale, or non-derivable `.decantr/graph` artifacts. The component reuse slice emits `COMP001` / `import-existing-component` when production source locally redeclares a common primitive that already exists as an exported reusable component, and `COMP010` / `replace-raw-control-with-local-component` when production JSX renders raw controls such as `<button>` while the project already owns a reusable primitive. The behavior-obligation slice emits `A11Y010`, `A11Y011`, `INT010`, `INT011`, `INT012`, `INT013`, and `COMP020` for high-confidence dialog/form regressions such as missing accessible names, missing label associations, missing visible destructive consequence copy, missing cancel affordances, missing submitting guards, implicit form button types, or bypassed project-owned interaction primitives. The style bridge slice emits `TOKEN010` / `replace-arbitrary-style-with-bridge-token` when production JSX, common class helpers, hardcoded inline color styles, or hardcoded visual values in CSS/module stylesheets bypass `.decantr/style-bridge.json` after it has been accepted as project-owned style authority. The baseline slice emits `VISUAL010` / `review-visual-baseline-drift` when `--since-baseline` detects changed screenshot hashes. When `.decantr/graph/graph.snapshot.json` exists, each finding is anchored to the most specific graph node Decantr can resolve, and JSON, markdown, text output, repair prompts, and Evidence Bundles carry that anchor. `decantr graph` also writes content-addressed history snapshots under `.decantr/graph/snapshots/` so repeated graph runs can be replayed across an AI edit sequence. When `.decantr/analysis.json` exists, `decantr graph` links observed routes/pages to implementation source artifacts and links exported reusable component declarations to their source files. When browser evidence writes `.decantr/evidence/visual-manifest.json`, `decantr graph` ingests it as local route/page Evidence nodes without uploading screenshots. When `.decantr/evidence/latest.json` exists, `decantr graph` can also materialize saved findings, evidence strings, graph anchors, repair IDs, and referenced repair/read target files as typed graph nodes and edges. When `.decantr/health-baseline-diff.json` exists, baseline changed files become file-level temporal evidence in the graph.
185
185
 
186
186
  ```bash
187
187
  decantr verify
package/dist/bin.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-BDY4JCFH.js";
2
+ import "./chunk-GN63PIKK.js";
3
3
  import "./chunk-SIDKK73N.js";
4
- import "./chunk-GXC5IJQY.js";
5
- import "./chunk-WUIFEHWM.js";
6
- import "./chunk-MNZKOZW7.js";
4
+ import "./chunk-B2PJDAMS.js";
5
+ import "./chunk-4NDOHCYY.js";
6
+ import "./chunk-7Z74ETDR.js";
@@ -3,7 +3,7 @@ import {
3
3
  sendProjectHealthCiFailedTelemetry,
4
4
  sendProjectHealthPromptTelemetry,
5
5
  sendProjectHealthReportTelemetry
6
- } from "./chunk-MNZKOZW7.js";
6
+ } from "./chunk-7Z74ETDR.js";
7
7
 
8
8
  // src/commands/health.ts
9
9
  import { execFileSync as execFileSync2 } from "child_process";
@@ -117,9 +117,9 @@ import { createHash } from "crypto";
117
117
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
118
118
  import { dirname as dirname2, isAbsolute as isAbsolute2, join as join4, relative as relative2 } from "path";
119
119
  import {
120
+ buildContractCapsuleFromSnapshot,
120
121
  buildGraphImpactContext,
121
122
  buildGraphRouteContext,
122
- buildContractCapsuleFromSnapshot,
123
123
  buildGraphSnapshotFromEssence,
124
124
  diffGraphSnapshots,
125
125
  GRAPH_DIFF_SCHEMA_URL,
@@ -182,6 +182,129 @@ function readLocalPatternPack(projectRoot) {
182
182
  function readLocalRuleManifest(projectRoot) {
183
183
  return readJsonFile(localRulesPath(projectRoot));
184
184
  }
185
+ function stringArray(value) {
186
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
187
+ }
188
+ function localBehaviorSeverity(value) {
189
+ return value === "info" || value === "warn" || value === "error" ? value : null;
190
+ }
191
+ function summarizeLocalPatternBehaviorObligations(pattern) {
192
+ const contract = pattern.behavior_obligations;
193
+ if (!contract || typeof contract !== "object" || Array.isArray(contract)) return null;
194
+ if (!Array.isArray(contract.obligations)) return null;
195
+ const patternId = typeof pattern.id === "string" && pattern.id.trim() ? pattern.id : "unknown";
196
+ const obligations = contract.obligations.map((obligation, index) => {
197
+ if (!obligation || typeof obligation !== "object" || Array.isArray(obligation)) {
198
+ return null;
199
+ }
200
+ const id = typeof obligation.id === "string" && obligation.id.trim() ? obligation.id.trim() : `obligation-${index + 1}`;
201
+ const label = typeof obligation.label === "string" && obligation.label.trim() ? obligation.label.trim() : id;
202
+ const evidence = typeof obligation.evidence === "string" && obligation.evidence.trim() ? obligation.evidence.trim() : null;
203
+ return {
204
+ id,
205
+ label,
206
+ severity: localBehaviorSeverity(obligation.severity),
207
+ evidence
208
+ };
209
+ }).filter(
210
+ (entry) => Boolean(entry)
211
+ );
212
+ if (obligations.length === 0) return null;
213
+ return {
214
+ patternId,
215
+ patternRole: typeof contract.pattern_role === "string" ? contract.pattern_role : typeof pattern.role === "string" ? pattern.role : null,
216
+ intent: typeof contract.intent === "string" && contract.intent.trim() ? contract.intent.trim() : null,
217
+ modalities: stringArray(contract.modalities),
218
+ states: stringArray(contract.states),
219
+ riskProfile: stringArray(contract.risk_profile),
220
+ obligations,
221
+ testHints: stringArray(contract.test_hints),
222
+ componentPaths: stringArray(pattern.componentPaths)
223
+ };
224
+ }
225
+ function formControlBehaviorObligations(evidence) {
226
+ if (evidence.formComponents.length === 0 && evidence.formSourceEvidence.length === 0) {
227
+ return void 0;
228
+ }
229
+ return {
230
+ intent: "Preserve accessible, explicit form behavior when AI edits inputs, labels, validation, or form actions.",
231
+ pattern_role: "form-control",
232
+ modalities: ["keyboard", "pointer", "screen-reader", "touch"],
233
+ states: ["empty", "focused", "valid", "invalid", "submitting", "disabled"],
234
+ risk_profile: ["label-loss", "accidental-submit", "validation-context-loss"],
235
+ obligations: [
236
+ {
237
+ id: "label-associated",
238
+ label: "Inputs, selects, and textareas keep an associated visible or programmatic label.",
239
+ severity: "warn",
240
+ evidence: "static"
241
+ },
242
+ {
243
+ id: "explicit-form-button-type",
244
+ label: 'Buttons inside forms declare type="button" or type="submit" explicitly.',
245
+ severity: "warn",
246
+ evidence: "static"
247
+ },
248
+ {
249
+ id: "project-form-primitive",
250
+ label: "Use project-owned form/control primitives when they exist.",
251
+ severity: "info",
252
+ evidence: "primitive"
253
+ }
254
+ ],
255
+ test_hints: [
256
+ "keyboard form path smoke test",
257
+ "label association assertion",
258
+ "non-submit action does not submit form"
259
+ ]
260
+ };
261
+ }
262
+ function confirmationDialogBehaviorObligations() {
263
+ return {
264
+ intent: "Confirm destructive account or data actions without accidental execution.",
265
+ pattern_role: "confirmation-dialog",
266
+ modalities: ["keyboard", "pointer", "screen-reader", "touch"],
267
+ states: ["closed", "opening", "open", "submitting", "error", "success"],
268
+ risk_profile: ["accidental-destruction", "focus-loss", "screen-reader-context-loss"],
269
+ obligations: [
270
+ {
271
+ id: "project-dialog-primitive",
272
+ label: "Use the project-owned Dialog or AlertDialog primitive instead of one-off overlays.",
273
+ severity: "warn",
274
+ evidence: "primitive"
275
+ },
276
+ {
277
+ id: "accessible-name",
278
+ label: "Dialog has an accessible name.",
279
+ severity: "error",
280
+ evidence: "static"
281
+ },
282
+ {
283
+ id: "visible-consequence",
284
+ label: "Destructive consequence is stated in visible text.",
285
+ severity: "warn",
286
+ evidence: "static"
287
+ },
288
+ {
289
+ id: "cancel-affordance",
290
+ label: "Dialog exposes a clear cancel/close affordance before the destructive action.",
291
+ severity: "warn",
292
+ evidence: "static"
293
+ },
294
+ {
295
+ id: "submitting-guard",
296
+ label: "Submitting state prevents accidental repeated destructive execution.",
297
+ severity: "info",
298
+ evidence: "static"
299
+ }
300
+ ],
301
+ test_hints: [
302
+ "keyboard interaction smoke test",
303
+ "focus return assertion",
304
+ "destructive action requires explicit confirmation"
305
+ ]
306
+ };
307
+ }
185
308
  function createBrownfieldCodifyProposal(input) {
186
309
  const sourceFiles = input.fromAudit ? listSourceFiles(input.projectRoot, 800) : [];
187
310
  const evidence = summarizeSourceEvidence(input.projectRoot, sourceFiles);
@@ -326,6 +449,7 @@ function createBrownfieldCodifyProposal(input) {
326
449
  "Start by mapping field wrappers, labels, error states, and disabled states from source."
327
450
  ]
328
451
  },
452
+ behavior_obligations: formControlBehaviorObligations(evidence),
329
453
  evidence: evidence.formComponents.length ? evidence.formComponents : ["Add form field wrapper paths and validation examples."],
330
454
  forbiddenAlternatives: [
331
455
  "Unlabeled one-off inputs or validation states that do not match the app standard."
@@ -356,7 +480,39 @@ function createBrownfieldCodifyProposal(input) {
356
480
  forbiddenAlternatives: [
357
481
  "Component-local theme forks that bypass shared theme providers or tokens."
358
482
  ]
359
- }
483
+ },
484
+ ...evidence.dialogComponents.length > 0 || evidence.dialogSourceEvidence.length > 0 ? [
485
+ {
486
+ id: "confirmation-dialog",
487
+ label: "Confirmation dialogs",
488
+ role: "Destructive confirmations, modal decisions, and interruptive dialog actions",
489
+ appliesTo: [
490
+ "destructive confirmations",
491
+ "account deletion",
492
+ "data removal",
493
+ "modal decisions"
494
+ ],
495
+ componentPaths: evidence.dialogComponents,
496
+ decide: "Define which Dialog/AlertDialog primitive, destructive Button variant, consequence copy, cancel affordance, and submitting guard the app owns.",
497
+ sourceEvidence: evidence.dialogSourceEvidence,
498
+ confidence: confidenceForEvidence({
499
+ componentCount: evidence.dialogComponents.length,
500
+ sourceEvidenceCount: evidence.dialogSourceEvidence.length
501
+ }),
502
+ enforcement: {
503
+ level: "warn",
504
+ status: "proposal",
505
+ notes: [
506
+ "Only enforce statically checkable obligations. Focus trapping and screen-reader behavior should stay in project tests unless source evidence is explicit."
507
+ ]
508
+ },
509
+ behavior_obligations: confirmationDialogBehaviorObligations(),
510
+ evidence: evidence.dialogComponents.length ? evidence.dialogComponents : evidence.dialogSourceEvidence.map((entry) => entry.file),
511
+ forbiddenAlternatives: [
512
+ "One-off overlays for destructive actions without reviewed dialog primitives."
513
+ ]
514
+ }
515
+ ] : []
360
516
  ],
361
517
  starterRules: [
362
518
  "Prefer project-owned wrappers for repeated primitives once they exist.",
@@ -597,6 +753,29 @@ function validateLocalLaw(projectRoot) {
597
753
  `Local pattern ${id} maps hosted registry guidance but has no project-owned component path yet.`
598
754
  );
599
755
  }
756
+ if (pattern.behavior_obligations && (typeof pattern.behavior_obligations !== "object" || Array.isArray(pattern.behavior_obligations))) {
757
+ warnings.push(
758
+ `Local pattern ${id || "unknown"} has behavior_obligations, but it is not an object.`
759
+ );
760
+ } else if (pattern.behavior_obligations && !Array.isArray(pattern.behavior_obligations.obligations)) {
761
+ warnings.push(
762
+ `Local pattern ${id || "unknown"} has behavior_obligations without an obligations array.`
763
+ );
764
+ } else if (pattern.behavior_obligations?.obligations) {
765
+ for (const [index, obligation] of pattern.behavior_obligations.obligations.entries()) {
766
+ if (!obligation || typeof obligation !== "object" || Array.isArray(obligation)) {
767
+ warnings.push(
768
+ `Local pattern ${id || "unknown"} has malformed behavior obligation at index ${index}.`
769
+ );
770
+ continue;
771
+ }
772
+ if (typeof obligation.id !== "string" || !obligation.id.trim()) {
773
+ warnings.push(
774
+ `Local pattern ${id || "unknown"} has a behavior obligation missing an id.`
775
+ );
776
+ }
777
+ }
778
+ }
600
779
  }
601
780
  }
602
781
  if (ruleManifest && !Array.isArray(ruleManifest.rules)) {
@@ -615,14 +794,20 @@ function validateLocalLaw(projectRoot) {
615
794
  function createLocalLawTaskSummary(projectRoot) {
616
795
  const patternPack = readLocalPatternPack(projectRoot);
617
796
  const ruleManifest = readLocalRuleManifest(projectRoot);
618
- const patterns = (patternPack?.patterns ?? []).map((pattern) => ({
619
- id: typeof pattern.id === "string" ? pattern.id : "unknown",
620
- role: typeof pattern.role === "string" ? pattern.role : null,
621
- componentPaths: Array.isArray(pattern.componentPaths) ? pattern.componentPaths.filter((entry) => typeof entry === "string") : [],
622
- confidenceTier: typeof pattern.confidence === "object" && pattern.confidence !== null && typeof pattern.confidence.tier === "string" ? pattern.confidence.tier : null,
623
- enforcementLevel: typeof pattern.enforcement === "object" && pattern.enforcement !== null && typeof pattern.enforcement.level === "string" ? pattern.enforcement.level : null,
624
- hostedPatternRefs: Array.isArray(pattern.hostedPatternRefs) ? pattern.hostedPatternRefs.map((ref) => typeof ref?.slug === "string" ? ref.slug : null).filter((slug) => Boolean(slug)) : []
625
- }));
797
+ const behaviorObligations = [];
798
+ const patterns = (patternPack?.patterns ?? []).map((pattern) => {
799
+ const behavior = summarizeLocalPatternBehaviorObligations(pattern);
800
+ if (behavior) behaviorObligations.push(behavior);
801
+ return {
802
+ id: typeof pattern.id === "string" ? pattern.id : "unknown",
803
+ role: typeof pattern.role === "string" ? pattern.role : null,
804
+ componentPaths: Array.isArray(pattern.componentPaths) ? pattern.componentPaths.filter((entry) => typeof entry === "string") : [],
805
+ confidenceTier: typeof pattern.confidence === "object" && pattern.confidence !== null && typeof pattern.confidence.tier === "string" ? pattern.confidence.tier : null,
806
+ enforcementLevel: typeof pattern.enforcement === "object" && pattern.enforcement !== null && typeof pattern.enforcement.level === "string" ? pattern.enforcement.level : null,
807
+ hostedPatternRefs: Array.isArray(pattern.hostedPatternRefs) ? pattern.hostedPatternRefs.map((ref) => typeof ref?.slug === "string" ? ref.slug : null).filter((slug) => Boolean(slug)) : [],
808
+ behaviorObligations: behavior
809
+ };
810
+ });
626
811
  const rules = (ruleManifest?.rules ?? []).map((rule) => ({
627
812
  id: rule.id,
628
813
  severity: rule.severity,
@@ -635,6 +820,7 @@ function createLocalLawTaskSummary(projectRoot) {
635
820
  patternCount: patterns.length,
636
821
  ruleCount: rules.length,
637
822
  patterns,
823
+ behaviorObligations,
638
824
  rules
639
825
  };
640
826
  }
@@ -732,6 +918,7 @@ function summarizeSourceEvidence(projectRoot, files) {
732
918
  const componentPaths = files.filter((file) => /(^|[/\\])components?([/\\]|$)|(^|[/\\])ui([/\\]|$)/i.test(file.relative)).map((file) => file.relative);
733
919
  const byName = (terms) => componentPaths.filter((file) => terms.some((term) => basename(file).toLowerCase().includes(term))).slice(0, 12);
734
920
  const themeComponents = componentPaths.filter((file) => /theme|provider|mode|appearance|tenant|brand/i.test(file)).slice(0, 12);
921
+ const dialogComponents = componentPaths.filter((file) => /dialog|modal|alert|confirm|sheet|drawer/i.test(file)).slice(0, 12);
735
922
  const shellComponents = files.filter((file) => /layout|shell|frame|app|root|nav|sidebar/i.test(basename(file.relative))).map((file) => file.relative).slice(0, 12);
736
923
  return {
737
924
  buttonComponents: byName(["button", "action"]),
@@ -776,6 +963,19 @@ function summarizeSourceEvidence(projectRoot, files) {
776
963
  "field",
777
964
  "select"
778
965
  ]),
966
+ dialogComponents,
967
+ dialogSourceEvidence: collectSourceEvidence(projectRoot, files, [
968
+ "dialog",
969
+ "alertdialog",
970
+ "modal",
971
+ "confirm",
972
+ "confirmation",
973
+ "delete",
974
+ "remove",
975
+ "destructive",
976
+ "irreversible",
977
+ "cannot be undone"
978
+ ]),
779
979
  themeSourceEvidence: collectSourceEvidence(projectRoot, files, [
780
980
  "theme",
781
981
  "dark",
@@ -988,13 +1188,13 @@ function readStyleBridge(projectRoot) {
988
1188
  function readStyleBridgeProposal(projectRoot) {
989
1189
  return readJsonFile2(styleBridgeProposalPath(projectRoot));
990
1190
  }
991
- function stringArray(value) {
1191
+ function stringArray2(value) {
992
1192
  return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
993
1193
  }
994
1194
  function readThemeInventory(projectRoot) {
995
1195
  const inventory = readJsonFile2(join3(projectRoot, ".decantr", "theme-inventory.json"));
996
1196
  return {
997
- modes: stringArray(inventory?.modes),
1197
+ modes: stringArray2(inventory?.modes),
998
1198
  variantIds: Array.isArray(inventory?.variants) ? inventory.variants.map((variant) => variant.id).filter((id) => typeof id === "string") : [],
999
1199
  darkModeDetected: typeof inventory?.darkModeDetected === "boolean" ? inventory.darkModeDetected : null
1000
1200
  };
@@ -1003,9 +1203,7 @@ function tokenHints(styling, terms) {
1003
1203
  return styling.cssVariables.filter((name) => terms.test(name)).slice(0, 12);
1004
1204
  }
1005
1205
  function readProjectPatternPack(projectRoot) {
1006
- return readLocalPatternPack(projectRoot) ?? readJsonFile2(
1007
- join3(projectRoot, ".decantr", "local-patterns.proposal.json")
1008
- );
1206
+ return readLocalPatternPack(projectRoot) ?? readJsonFile2(join3(projectRoot, ".decantr", "local-patterns.proposal.json"));
1009
1207
  }
1010
1208
  function classHintsForPattern(projectRoot, ids) {
1011
1209
  const pack = readProjectPatternPack(projectRoot);
@@ -1029,7 +1227,9 @@ function sourceEvidence(projectRoot, ids) {
1029
1227
  function colorTokenNames(styling) {
1030
1228
  const names = new Set(Object.keys(styling.colors ?? {}));
1031
1229
  for (const variable of styling.cssVariables) {
1032
- if (/color|bg|background|surface|text|foreground|border|primary|secondary|accent|muted/i.test(variable)) {
1230
+ if (/color|bg|background|surface|text|foreground|border|primary|secondary|accent|muted/i.test(
1231
+ variable
1232
+ )) {
1033
1233
  names.add(variable);
1034
1234
  }
1035
1235
  }
@@ -1038,7 +1238,9 @@ function colorTokenNames(styling) {
1038
1238
  function createStyleBridgeProposal(input) {
1039
1239
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
1040
1240
  const theme = readThemeInventory(input.projectRoot);
1041
- const routeCount = input.essence && typeof input.essence === "object" && "blueprint" in input.essence ? Object.keys(input.essence.blueprint?.routes ?? {}).length : 0;
1241
+ const routeCount = input.essence && typeof input.essence === "object" && "blueprint" in input.essence ? Object.keys(
1242
+ input.essence.blueprint?.routes ?? {}
1243
+ ).length : 0;
1042
1244
  const target = input.essence && typeof input.essence === "object" && "meta" in input.essence ? String(input.essence.meta?.target ?? "") || null : null;
1043
1245
  const darkModeDetected = theme.darkModeDetected ?? input.styling.darkMode;
1044
1246
  const themeModes = theme.modes.length > 0 ? theme.modes : darkModeDetected ? ["base", "dark"] : ["base"];
@@ -1084,7 +1286,10 @@ function createStyleBridgeProposal(input) {
1084
1286
  label: "Surfaces and cards",
1085
1287
  decantrIntent: "surface background, card treatment, border, radius, depth, and hover state",
1086
1288
  projectAuthority: "Use the app card/surface primitives, tokens, and accepted local law.",
1087
- tokenHints: tokenHints(input.styling, /surface|card|panel|bg|background|border|shadow|radius/i),
1289
+ tokenHints: tokenHints(
1290
+ input.styling,
1291
+ /surface|card|panel|bg|background|border|shadow|radius/i
1292
+ ),
1088
1293
  classHints: classHintsForPattern(input.projectRoot, ["surface-card"]),
1089
1294
  sourceEvidence: sourceEvidence(input.projectRoot, ["surface-card"]),
1090
1295
  guardrails: [
@@ -1097,7 +1302,10 @@ function createStyleBridgeProposal(input) {
1097
1302
  label: "Actions and buttons",
1098
1303
  decantrIntent: "primary, secondary, tertiary, destructive, icon-only, loading, and disabled action states",
1099
1304
  projectAuthority: "Use the app button/action primitives and local variant names.",
1100
- tokenHints: tokenHints(input.styling, /primary|secondary|accent|danger|error|destructive|focus/i),
1305
+ tokenHints: tokenHints(
1306
+ input.styling,
1307
+ /primary|secondary|accent|danger|error|destructive|focus/i
1308
+ ),
1101
1309
  classHints: classHintsForPattern(input.projectRoot, ["button"]),
1102
1310
  sourceEvidence: sourceEvidence(input.projectRoot, ["button"]),
1103
1311
  guardrails: [
@@ -1209,7 +1417,9 @@ function styleBridgeMatches(projectRoot, query) {
1209
1417
  if (!projectRoot) return [];
1210
1418
  const bridge = readStyleBridge(projectRoot);
1211
1419
  if (!bridge) return [];
1212
- const terms = query.toLowerCase().split(/[^a-z0-9]+/).filter((term) => term.length > 1).flatMap((term) => term.endsWith("s") && term.length > 3 ? [term, term.slice(0, -1)] : [term]);
1420
+ const terms = query.toLowerCase().split(/[^a-z0-9]+/).filter((term) => term.length > 1).flatMap(
1421
+ (term) => term.endsWith("s") && term.length > 3 ? [term, term.slice(0, -1)] : [term]
1422
+ );
1213
1423
  if (terms.length === 0) return [];
1214
1424
  return bridge.mappings.map((mapping) => {
1215
1425
  const haystack = [
@@ -1594,6 +1804,15 @@ function sourceArtifacts(projectRoot, componentReuseAudit = null) {
1594
1804
  hash: hashFile(rulesPath)
1595
1805
  });
1596
1806
  }
1807
+ const patternsPath = localPatternsPath(projectRoot);
1808
+ if (existsSync4(patternsPath)) {
1809
+ sources.push({
1810
+ id: "src:.decantr/local-patterns.json",
1811
+ kind: "local-pattern-manifest",
1812
+ path: ".decantr/local-patterns.json",
1813
+ hash: hashFile(patternsPath)
1814
+ });
1815
+ }
1597
1816
  const bridgePath = styleBridgePath(projectRoot);
1598
1817
  if (existsSync4(bridgePath)) {
1599
1818
  sources.push({
@@ -1673,7 +1892,11 @@ function sourceArtifacts(projectRoot, componentReuseAudit = null) {
1673
1892
  }
1674
1893
  });
1675
1894
  }
1676
- const importedFile = resolveImportSourcePath(projectRoot, importingFile, importReference.source);
1895
+ const importedFile = resolveImportSourcePath(
1896
+ projectRoot,
1897
+ importingFile,
1898
+ importReference.source
1899
+ );
1677
1900
  if (!importedFile) continue;
1678
1901
  const importedPath = join4(projectRoot, importedFile);
1679
1902
  if (!existsSync4(importedPath)) continue;
@@ -1865,6 +2088,63 @@ function augmentProjectGraph(snapshot, projectRoot, sources, componentReuseAudit
1865
2088
  });
1866
2089
  }
1867
2090
  }
2091
+ const localPatterns = readLocalPatternPack(projectRoot);
2092
+ if (localPatterns) {
2093
+ const sourceId = "src:.decantr/local-patterns.json";
2094
+ for (const pattern of localPatterns.patterns ?? []) {
2095
+ const behavior = summarizeLocalPatternBehaviorObligations(pattern);
2096
+ if (!behavior) continue;
2097
+ const patternNodeId = `pat:${graphSlug(behavior.patternId, "pattern")}`;
2098
+ for (const obligation of behavior.obligations) {
2099
+ const ruleId = `rule:behavior:${graphSlug(behavior.patternId, "pattern")}:${graphSlug(
2100
+ obligation.id,
2101
+ "obligation"
2102
+ )}`;
2103
+ addNode(nodes, {
2104
+ id: ruleId,
2105
+ type: "LocalRule",
2106
+ payload: {
2107
+ id: ruleId.replace(/^rule:/, ""),
2108
+ kind: "behavior-obligation",
2109
+ patternId: behavior.patternId,
2110
+ patternRole: behavior.patternRole,
2111
+ intent: behavior.intent,
2112
+ obligationId: obligation.id,
2113
+ label: obligation.label,
2114
+ severity: obligation.severity,
2115
+ evidence: obligation.evidence,
2116
+ modalities: behavior.modalities,
2117
+ states: behavior.states,
2118
+ riskProfile: behavior.riskProfile,
2119
+ testHints: behavior.testHints,
2120
+ componentPaths: behavior.componentPaths,
2121
+ manifest: {
2122
+ status: localPatterns.status,
2123
+ source: localPatterns.source,
2124
+ acceptedAt: localPatterns.acceptedAt
2125
+ }
2126
+ }
2127
+ });
2128
+ addEdge(edges, {
2129
+ src: ruleId,
2130
+ dst: snapshot.project_id,
2131
+ relation: "LOCAL_RULE_APPLIES_TO"
2132
+ });
2133
+ if (nodes.has(patternNodeId)) {
2134
+ addEdge(edges, {
2135
+ src: ruleId,
2136
+ dst: patternNodeId,
2137
+ relation: "LOCAL_RULE_APPLIES_TO"
2138
+ });
2139
+ }
2140
+ addEdge(edges, {
2141
+ src: ruleId,
2142
+ dst: sourceId,
2143
+ relation: "NODE_DERIVED_FROM_SOURCE"
2144
+ });
2145
+ }
2146
+ }
2147
+ }
1868
2148
  const styleBridge = readStyleBridge(projectRoot);
1869
2149
  if (styleBridge) {
1870
2150
  const sourceId = "src:.decantr/style-bridge.json";
@@ -1998,7 +2278,11 @@ function augmentProjectGraph(snapshot, projectRoot, sources, componentReuseAudit
1998
2278
  for (const importReference of componentReuseAudit?.imports ?? []) {
1999
2279
  const importingFile = projectRelativePath(projectRoot, importReference.file);
2000
2280
  if (!importingFile || !nodes.has(`src:${importingFile}`)) continue;
2001
- const importedFile = resolveImportSourcePath(projectRoot, importingFile, importReference.source);
2281
+ const importedFile = resolveImportSourcePath(
2282
+ projectRoot,
2283
+ importingFile,
2284
+ importReference.source
2285
+ );
2002
2286
  if (!importedFile || !nodes.has(`src:${importedFile}`)) continue;
2003
2287
  addEdge(edges, {
2004
2288
  src: `src:${importingFile}`,
@@ -2463,7 +2747,10 @@ function summaryPayload(artifacts, routeContext, impactContext, selected, compar
2463
2747
  evidence: selected.snapshot.summary.evidence
2464
2748
  } : void 0,
2465
2749
  comparison,
2466
- routeContext: routeContextPayload(routeContext ?? null, routeContext ? optionsRoute(routeContext) : void 0),
2750
+ routeContext: routeContextPayload(
2751
+ routeContext ?? null,
2752
+ routeContext ? optionsRoute(routeContext) : void 0
2753
+ ),
2467
2754
  impactContext: impactContextPayload(impactContext ?? null, impactSelection)
2468
2755
  };
2469
2756
  }
@@ -3008,7 +3295,7 @@ function sourceFromAuditFinding(finding) {
3008
3295
  if (category.includes("pack") || category.includes("review contract")) {
3009
3296
  return "pack";
3010
3297
  }
3011
- if (category.includes("interaction") || id.includes("interaction") || rule.includes("interaction")) {
3298
+ if (category.includes("interaction") || category.includes("behavior") || id.includes("interaction") || id.includes("behavior") || rule.includes("interaction")) {
3012
3299
  return "interaction";
3013
3300
  }
3014
3301
  if (category.includes("style bridge") || id.includes("style-bridge") || rule.includes("style-bridge")) {
@@ -3030,6 +3317,7 @@ function normalizeHealthCategory(category, source) {
3030
3317
  if (source === "design-token" || lower.includes("design-token")) return "Design Token";
3031
3318
  if (source === "style-bridge" || lower.includes("style bridge")) return "Style Bridge";
3032
3319
  if (source === "graph") return "Typed Contract Graph";
3320
+ if (lower.includes("behavior obligation")) return "Behavior Obligation";
3033
3321
  if (lower.includes("accessibility")) return "Accessibility";
3034
3322
  if (source === "runtime") return "Runtime";
3035
3323
  if (source === "browser") return "Visual Evidence";
@@ -1559,7 +1559,10 @@ async function captureCliTelemetryEvent(input) {
1559
1559
  }
1560
1560
  }
1561
1561
  async function sendCliCommandTelemetry(input) {
1562
- const projectRoot = resolveCliTelemetryProjectRoot(input.projectRoot ?? process.cwd(), input.args);
1562
+ const projectRoot = resolveCliTelemetryProjectRoot(
1563
+ input.projectRoot ?? process.cwd(),
1564
+ input.args
1565
+ );
1563
1566
  const command = normalizeCommand(input.args[0]);
1564
1567
  if (!isOptedIn(projectRoot) || !command || command === "help" || command === "version" || isHelpOrVersionProbe(input.args)) {
1565
1568
  return;
@@ -2157,11 +2160,11 @@ ${CYAN}Telemetry enabled.${RESET} Decantr will send privacy-filtered CLI product
2157
2160
  }
2158
2161
 
2159
2162
  export {
2163
+ scanStyling,
2160
2164
  loadBundledContentItem,
2161
2165
  loadBundledContentList,
2162
2166
  scanAmbientContext,
2163
2167
  scanRoutes,
2164
- scanStyling,
2165
2168
  createDoctrineMap,
2166
2169
  writeDoctrineMap,
2167
2170
  isOptedIn,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createProjectHealthReport,
3
3
  listWorkspaceAppCandidates
4
- } from "./chunk-WUIFEHWM.js";
4
+ } from "./chunk-4NDOHCYY.js";
5
5
 
6
6
  // src/commands/workspace.ts
7
7
  import { execFileSync } from "child_process";