@spikedpunch/align-cli 0.1.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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/dist/align-dir.d.ts +55 -0
  4. package/dist/align-dir.d.ts.map +1 -0
  5. package/dist/align-dir.js +184 -0
  6. package/dist/align-dir.js.map +1 -0
  7. package/dist/commands/agent.d.ts +14 -0
  8. package/dist/commands/agent.d.ts.map +1 -0
  9. package/dist/commands/agent.js +142 -0
  10. package/dist/commands/agent.js.map +1 -0
  11. package/dist/commands/baseline.d.ts +4 -0
  12. package/dist/commands/baseline.d.ts.map +1 -0
  13. package/dist/commands/baseline.js +57 -0
  14. package/dist/commands/baseline.js.map +1 -0
  15. package/dist/commands/build-report.d.ts +4 -0
  16. package/dist/commands/build-report.d.ts.map +1 -0
  17. package/dist/commands/build-report.js +122 -0
  18. package/dist/commands/build-report.js.map +1 -0
  19. package/dist/commands/build.d.ts +73 -0
  20. package/dist/commands/build.d.ts.map +1 -0
  21. package/dist/commands/build.js +289 -0
  22. package/dist/commands/build.js.map +1 -0
  23. package/dist/commands/check.d.ts +31 -0
  24. package/dist/commands/check.d.ts.map +1 -0
  25. package/dist/commands/check.js +203 -0
  26. package/dist/commands/check.js.map +1 -0
  27. package/dist/commands/doctor.d.ts +20 -0
  28. package/dist/commands/doctor.d.ts.map +1 -0
  29. package/dist/commands/doctor.js +173 -0
  30. package/dist/commands/doctor.js.map +1 -0
  31. package/dist/commands/explain.d.ts +3 -0
  32. package/dist/commands/explain.d.ts.map +1 -0
  33. package/dist/commands/explain.js +54 -0
  34. package/dist/commands/explain.js.map +1 -0
  35. package/dist/commands/export-ir.d.ts +21 -0
  36. package/dist/commands/export-ir.d.ts.map +1 -0
  37. package/dist/commands/export-ir.js +33 -0
  38. package/dist/commands/export-ir.js.map +1 -0
  39. package/dist/commands/init.d.ts +13 -0
  40. package/dist/commands/init.d.ts.map +1 -0
  41. package/dist/commands/init.js +93 -0
  42. package/dist/commands/init.js.map +1 -0
  43. package/dist/commands/skill.d.ts +20 -0
  44. package/dist/commands/skill.d.ts.map +1 -0
  45. package/dist/commands/skill.js +23 -0
  46. package/dist/commands/skill.js.map +1 -0
  47. package/dist/commands/telemetry.d.ts +52 -0
  48. package/dist/commands/telemetry.d.ts.map +1 -0
  49. package/dist/commands/telemetry.js +238 -0
  50. package/dist/commands/telemetry.js.map +1 -0
  51. package/dist/composition-root.d.ts +14 -0
  52. package/dist/composition-root.d.ts.map +1 -0
  53. package/dist/composition-root.js +18 -0
  54. package/dist/composition-root.js.map +1 -0
  55. package/dist/config.d.ts +31 -0
  56. package/dist/config.d.ts.map +1 -0
  57. package/dist/config.js +39 -0
  58. package/dist/config.js.map +1 -0
  59. package/dist/index.d.ts +3 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +4 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/init/claude-md.d.ts +6 -0
  64. package/dist/init/claude-md.d.ts.map +1 -0
  65. package/dist/init/claude-md.js +42 -0
  66. package/dist/init/claude-md.js.map +1 -0
  67. package/dist/init/config-comment.d.ts +7 -0
  68. package/dist/init/config-comment.d.ts.map +1 -0
  69. package/dist/init/config-comment.js +44 -0
  70. package/dist/init/config-comment.js.map +1 -0
  71. package/dist/init/detect-components.d.ts +16 -0
  72. package/dist/init/detect-components.d.ts.map +1 -0
  73. package/dist/init/detect-components.js +121 -0
  74. package/dist/init/detect-components.js.map +1 -0
  75. package/dist/init/gitignore.d.ts +8 -0
  76. package/dist/init/gitignore.d.ts.map +1 -0
  77. package/dist/init/gitignore.js +27 -0
  78. package/dist/init/gitignore.js.map +1 -0
  79. package/dist/init/render-config.d.ts +18 -0
  80. package/dist/init/render-config.d.ts.map +1 -0
  81. package/dist/init/render-config.js +57 -0
  82. package/dist/init/render-config.js.map +1 -0
  83. package/dist/init/suggest-layers.d.ts +16 -0
  84. package/dist/init/suggest-layers.d.ts.map +1 -0
  85. package/dist/init/suggest-layers.js +65 -0
  86. package/dist/init/suggest-layers.js.map +1 -0
  87. package/dist/mcp/server.d.ts +7 -0
  88. package/dist/mcp/server.d.ts.map +1 -0
  89. package/dist/mcp/server.js +182 -0
  90. package/dist/mcp/server.js.map +1 -0
  91. package/dist/program.d.ts +11 -0
  92. package/dist/program.d.ts.map +1 -0
  93. package/dist/program.js +199 -0
  94. package/dist/program.js.map +1 -0
  95. package/dist/skill/bullet-grammar.d.ts +2 -0
  96. package/dist/skill/bullet-grammar.d.ts.map +1 -0
  97. package/dist/skill/bullet-grammar.js +12 -0
  98. package/dist/skill/bullet-grammar.js.map +1 -0
  99. package/dist/skill/cli-inventory.d.ts +16 -0
  100. package/dist/skill/cli-inventory.d.ts.map +1 -0
  101. package/dist/skill/cli-inventory.js +17 -0
  102. package/dist/skill/cli-inventory.js.map +1 -0
  103. package/dist/skill/condensed.d.ts +2 -0
  104. package/dist/skill/condensed.d.ts.map +1 -0
  105. package/dist/skill/condensed.js +24 -0
  106. package/dist/skill/condensed.js.map +1 -0
  107. package/dist/skill/dsl-verbs.d.ts +2 -0
  108. package/dist/skill/dsl-verbs.d.ts.map +1 -0
  109. package/dist/skill/dsl-verbs.js +10 -0
  110. package/dist/skill/dsl-verbs.js.map +1 -0
  111. package/dist/skill/fix-loop-protocol.d.ts +18 -0
  112. package/dist/skill/fix-loop-protocol.d.ts.map +1 -0
  113. package/dist/skill/fix-loop-protocol.js +38 -0
  114. package/dist/skill/fix-loop-protocol.js.map +1 -0
  115. package/dist/skill/gates.d.ts +2 -0
  116. package/dist/skill/gates.d.ts.map +1 -0
  117. package/dist/skill/gates.js +20 -0
  118. package/dist/skill/gates.js.map +1 -0
  119. package/dist/skill/install.d.ts +25 -0
  120. package/dist/skill/install.d.ts.map +1 -0
  121. package/dist/skill/install.js +49 -0
  122. package/dist/skill/install.js.map +1 -0
  123. package/dist/skill/render.d.ts +15 -0
  124. package/dist/skill/render.d.ts.map +1 -0
  125. package/dist/skill/render.js +35 -0
  126. package/dist/skill/render.js.map +1 -0
  127. package/dist/skill/rule-kinds.d.ts +12 -0
  128. package/dist/skill/rule-kinds.d.ts.map +1 -0
  129. package/dist/skill/rule-kinds.js +47 -0
  130. package/dist/skill/rule-kinds.js.map +1 -0
  131. package/dist/skill/static-sections.d.ts +5 -0
  132. package/dist/skill/static-sections.d.ts.map +1 -0
  133. package/dist/skill/static-sections.js +74 -0
  134. package/dist/skill/static-sections.js.map +1 -0
  135. package/dist/skill/version-stamp.d.ts +22 -0
  136. package/dist/skill/version-stamp.d.ts.map +1 -0
  137. package/dist/skill/version-stamp.js +29 -0
  138. package/dist/skill/version-stamp.js.map +1 -0
  139. package/dist/telemetry/check-event.d.ts +7 -0
  140. package/dist/telemetry/check-event.d.ts.map +1 -0
  141. package/dist/telemetry/check-event.js +29 -0
  142. package/dist/telemetry/check-event.js.map +1 -0
  143. package/dist/telemetry/index.d.ts +8 -0
  144. package/dist/telemetry/index.d.ts.map +1 -0
  145. package/dist/telemetry/index.js +8 -0
  146. package/dist/telemetry/index.js.map +1 -0
  147. package/dist/telemetry/process-context.d.ts +9 -0
  148. package/dist/telemetry/process-context.d.ts.map +1 -0
  149. package/dist/telemetry/process-context.js +10 -0
  150. package/dist/telemetry/process-context.js.map +1 -0
  151. package/dist/telemetry/recorder.d.ts +43 -0
  152. package/dist/telemetry/recorder.d.ts.map +1 -0
  153. package/dist/telemetry/recorder.js +61 -0
  154. package/dist/telemetry/recorder.js.map +1 -0
  155. package/dist/telemetry/resolve.d.ts +19 -0
  156. package/dist/telemetry/resolve.d.ts.map +1 -0
  157. package/dist/telemetry/resolve.js +18 -0
  158. package/dist/telemetry/resolve.js.map +1 -0
  159. package/dist/telemetry/ruleset-hash.d.ts +10 -0
  160. package/dist/telemetry/ruleset-hash.d.ts.map +1 -0
  161. package/dist/telemetry/ruleset-hash.js +12 -0
  162. package/dist/telemetry/ruleset-hash.js.map +1 -0
  163. package/dist/telemetry/session.d.ts +6 -0
  164. package/dist/telemetry/session.d.ts.map +1 -0
  165. package/dist/telemetry/session.js +9 -0
  166. package/dist/telemetry/session.js.map +1 -0
  167. package/dist/telemetry/violations.d.ts +10 -0
  168. package/dist/telemetry/violations.d.ts.map +1 -0
  169. package/dist/telemetry/violations.js +36 -0
  170. package/dist/telemetry/violations.js.map +1 -0
  171. package/package.json +58 -0
@@ -0,0 +1,203 @@
1
+ import { assertNoCustomHostRules, buildMcpCheckPayload, renderViolationMessage, } from '@spikedpunch/align-core';
2
+ import { loadConfig } from '../config.js';
3
+ import { createOrchestrator } from '../composition-root.js';
4
+ import { readBaseline, readGeneratedRules, readRulesetIr, readRulesLock, writeBaseline } from '../align-dir.js';
5
+ import { verifyFrozenRules } from './build.js';
6
+ import { buildCheckEvent, computeAndPersistViolationTransitions, computeRulesetIrHash, createTelemetryRecorder } from '../telemetry/index.js';
7
+ /** Carried Stage 3 affordance (approved ahead of Stage 4): when generated rules are active
8
+ * (`.align/generated-rules.json` + `.align/rules.lock.json` both present, ADR 011), surface a
9
+ * one-line summary so a human/agent reading `align check` output knows doc-built rules are in
10
+ * force without having to separately inspect `.align/`. Trusted mode only — `--untrusted`'s
11
+ * effective ruleset was already merged into the exported artifact at `align export-ir` time, so
12
+ * re-reading the live `.align/generated-rules.json` here would describe the wrong ruleset (and
13
+ * would be a config-adjacent filesystem read done for cosmetics, not a scan need). */
14
+ function generatedRulesSummary(rootDir) {
15
+ const generated = readGeneratedRules(rootDir);
16
+ const lock = readRulesLock(rootDir);
17
+ if (generated === undefined || lock === undefined || generated.rules.length === 0)
18
+ return undefined;
19
+ return { count: generated.rules.length, doc: lock.docPath, builtAt: new Date(lock.builtAt).toISOString().slice(0, 10) };
20
+ }
21
+ /**
22
+ * `align check` — FRESH scan every run (ADR 005: rescan-on-check, no caching of any kind).
23
+ * Exit 0 only on a fully green verdict (and, with `--frozen-rules`, no doc/generated-rules
24
+ * drift); 1 on red or error (ADR 008: error is environmental and halts/escalates, but from a
25
+ * shell's perspective both are "not safe to proceed"); 1 on a `--untrusted` refuse (missing IR
26
+ * artifact, custom.host present) — refusal is also "not safe to proceed", just before a scan ever
27
+ * starts.
28
+ */
29
+ export async function runCheck(rootDir, options) {
30
+ if (options.untrusted === true && options.frozenRules === true) {
31
+ console.error('--untrusted and --frozen-rules cannot be combined: frozen-rules verification reads the live ' +
32
+ 'align.config.ts/.align/generated-rules.json/.align/rules.lock.json trio to detect drift, which ' +
33
+ "is exactly the trusted-mode filesystem state --untrusted's committed-IR-only contract excludes. " +
34
+ 'Run `align build --verify` (or plain `align check --frozen-rules`) in a trusted checkout instead.');
35
+ return 1;
36
+ }
37
+ return options.untrusted === true ? runUntrustedCheck(rootDir, options) : runTrustedCheck(rootDir, options);
38
+ }
39
+ async function runTrustedCheck(rootDir, options) {
40
+ const { ruleset, excludes, hostRules, telemetry } = await loadConfig(rootDir);
41
+ const { orchestrator, baselineStore } = createOrchestrator(ruleset, readBaseline(rootDir), hostRules);
42
+ const recorder = createTelemetryRecorder(rootDir, 'check', options.telemetryPreConfig, telemetry);
43
+ const rulesetIrHash = computeRulesetIrHash(ruleset);
44
+ const wallStart = performance.now();
45
+ const run = await orchestrator.check({ rootDir, excludes });
46
+ const wallMs = performance.now() - wallStart;
47
+ persistMovedBaseline(rootDir, run, baselineStore);
48
+ let effectiveRun = run;
49
+ if (options.frozenRules === true) {
50
+ const frozen = verifyFrozenRules(rootDir);
51
+ effectiveRun = {
52
+ ...run,
53
+ // A false 'green' verdict is a severity-zero bug class (ARCHITECTURE.md's stated
54
+ // invariant) — drift/divergence must flip the VERDICT ITSELF, not just the exit code, so
55
+ // `--json` consumers (agents, CI) reading `verdict` alone never get a lying "green" while
56
+ // `advisories` quietly explains why they shouldn't have trusted it.
57
+ verdict: !frozen.ok && run.verdict === 'green' ? 'red' : run.verdict,
58
+ advisories: [...run.advisories, ...frozen.advisories],
59
+ };
60
+ }
61
+ recordCheckTelemetry(rootDir, recorder, effectiveRun, wallMs, rulesetIrHash, 'check');
62
+ return emit(effectiveRun, options, generatedRulesSummary(rootDir));
63
+ }
64
+ /**
65
+ * The one place `check`'s telemetry gets recorded (both trusted and `--untrusted` paths funnel
66
+ * through this) — a `check` event unconditionally, plus one `violation-appeared`/
67
+ * `violation-resolved` event per fingerprint transition against `.align/telemetry-state.json`
68
+ * (only computed/persisted when the recorder is actually enabled — an OFF run must never touch
69
+ * that file, IMPLEMENTATION_PLAN.md's telemetry spec). An `error`/`gate-error` event replaces the
70
+ * `check` event when the verdict itself is `'error'` (a gate genuinely couldn't produce a
71
+ * trustworthy result — ADR 008), since a latency/violation-count number for a run that didn't
72
+ * really complete would misrepresent the distribution the summarizer command computes.
73
+ */
74
+ function recordCheckTelemetry(rootDir, recorder, run, wallMs, rulesetIrHash, command) {
75
+ if (!recorder.enabled)
76
+ return;
77
+ if (run.verdict === 'error') {
78
+ const errorGate = run.gates.find((g) => g.status === 'error');
79
+ recorder.record({ kind: 'error', errorKind: 'gate-error', message: shortMessage(errorGate?.errorMessage ?? 'unknown gate error'), command }, { ...(rulesetIrHash !== undefined ? { rulesetIrHash } : {}) });
80
+ return;
81
+ }
82
+ recorder.record(buildCheckEvent(run, wallMs), { ...(rulesetIrHash !== undefined ? { rulesetIrHash } : {}) });
83
+ const violations = run.gates.flatMap((g) => g.violations);
84
+ for (const event of computeAndPersistViolationTransitions(rootDir, violations)) {
85
+ recorder.record(event, { ...(rulesetIrHash !== undefined ? { rulesetIrHash } : {}) });
86
+ }
87
+ }
88
+ /** SHORT message only — no secrets, no file contents (IMPLEMENTATION_PLAN.md's telemetry spec).
89
+ * `errorMessage` on a `GateResult` is already environmental/plain-string (never LLM-facing prose
90
+ * embedding source text, `gates/types.ts`), but is truncated defensively here anyway since it can
91
+ * originate from an arbitrary thrown `Error`'s `.message`. */
92
+ function shortMessage(message, maxLength = 200) {
93
+ return message.length > maxLength ? `${message.slice(0, maxLength)}…` : message;
94
+ }
95
+ /**
96
+ * `align check --untrusted` (ADR 014). Everything above `orchestrator.check` is deliberately
97
+ * different code paths from `runTrustedCheck`, not a shared branch inside it — the whole point is
98
+ * that `loadConfig` (which dynamically imports align.config.ts) is never even referenced in this
99
+ * function's call graph. `hostPredicates` is always the empty map here — safe unconditionally
100
+ * because `assertNoCustomHostRules` below already refused any ruleset that would have needed one.
101
+ */
102
+ async function runUntrustedCheck(rootDir, options) {
103
+ // Built before the IR artifact is even read, from flags/`ALIGN_TELEMETRY` only — `--untrusted`
104
+ // never calls `loadConfig` (ADR 014), so `align.config.ts`'s `telemetry` export can never
105
+ // contribute to this decision under this mode, by the same design that keeps `hostRules` out of
106
+ // reach here. This lets a refusal itself be recorded (`untrusted-refusal`) below.
107
+ const recorder = createTelemetryRecorder(rootDir, 'check --untrusted', options.telemetryPreConfig, undefined);
108
+ let exported;
109
+ try {
110
+ exported = readRulesetIr(rootDir, options.ir);
111
+ }
112
+ catch (err) {
113
+ const message = `${err instanceof Error ? err.message : String(err)} — refusing to run. A corrupted or ` +
114
+ 'hand-edited IR artifact is never treated as absent (that would silently drop rules); re-run ' +
115
+ '`align export-ir` in a trusted checkout to regenerate it.';
116
+ console.error(`align check --untrusted: ${message}`);
117
+ recorder.record({ kind: 'error', errorKind: 'untrusted-refusal', message: shortMessage(message), command: 'check --untrusted' });
118
+ return 1;
119
+ }
120
+ if (exported === undefined) {
121
+ const path = options.ir ?? '.align/ruleset-ir.json';
122
+ const message = `no committed IR ruleset found at ${path}. --untrusted cannot execute align.config.ts, so there is ` +
123
+ 'nothing to check it against. Run `align export-ir` in a trusted checkout to produce it, or run ' +
124
+ '`align check` without --untrusted only on repos you trust to execute code.';
125
+ console.error(`align check --untrusted: ${message}`);
126
+ recorder.record({ kind: 'error', errorKind: 'untrusted-refusal', message: shortMessage(message), command: 'check --untrusted' });
127
+ return 1;
128
+ }
129
+ try {
130
+ assertNoCustomHostRules(exported.ruleset.rules);
131
+ }
132
+ catch (err) {
133
+ const message = err instanceof Error ? err.message : String(err);
134
+ console.error(`align check --untrusted: ${message}`);
135
+ recorder.record({ kind: 'error', errorKind: 'untrusted-refusal', message: shortMessage(message), command: 'check --untrusted' });
136
+ return 1;
137
+ }
138
+ const rulesetIrHash = computeRulesetIrHash(exported.ruleset);
139
+ const { orchestrator, baselineStore } = createOrchestrator(exported.ruleset, readBaseline(rootDir), new Map());
140
+ const wallStart = performance.now();
141
+ const run = await orchestrator.check({ rootDir, excludes: exported.excludes });
142
+ const wallMs = performance.now() - wallStart;
143
+ persistMovedBaseline(rootDir, run, baselineStore);
144
+ recordCheckTelemetry(rootDir, recorder, run, wallMs, rulesetIrHash, 'check --untrusted');
145
+ return emit(run, options, undefined);
146
+ }
147
+ function persistMovedBaseline(rootDir, run, baselineStore) {
148
+ // Move-transfer (ADR 006) mutated the in-memory store during `check` — persist so a rename
149
+ // doesn't need a separate `align baseline prune` run to stop being reported every time.
150
+ if (run.advisories.some((a) => a.kind === 'baseline-moved')) {
151
+ writeBaseline(rootDir, baselineStore.snapshot());
152
+ }
153
+ }
154
+ function emit(run, options, generatedRules) {
155
+ if (options.json) {
156
+ const payload = buildMcpCheckPayload(run);
157
+ const withGeneratedRules = generatedRules === undefined ? payload : { ...payload, generatedRules: { ...generatedRules } };
158
+ process.stdout.write(`${JSON.stringify(withGeneratedRules, null, 2)}\n`);
159
+ return run.verdict === 'green' ? 0 : 1;
160
+ }
161
+ printHuman(run, generatedRules);
162
+ return run.verdict === 'green' ? 0 : 1;
163
+ }
164
+ function printHuman(run, generatedRules) {
165
+ for (const gate of run.gates) {
166
+ const label = `${gate.gate}`.padEnd(12);
167
+ if (gate.status === 'error') {
168
+ console.log(` ${label} ERROR ${gate.errorMessage ?? ''}`);
169
+ continue;
170
+ }
171
+ if (gate.status === 'skipped') {
172
+ console.log(` ${label} skipped`);
173
+ continue;
174
+ }
175
+ const suffix = gate.baselinedCount > 0 ? ` (${gate.baselinedCount} baselined)` : '';
176
+ console.log(` ${label} ${gate.status === 'green' ? 'green ' : 'RED '} ${gate.violations.length} violation(s)${suffix}`);
177
+ }
178
+ if (generatedRules !== undefined) {
179
+ console.log(` +${generatedRules.count} rules from ${generatedRules.doc} (built ${generatedRules.builtAt})`);
180
+ }
181
+ const violations = run.gates.flatMap((g) => g.violations);
182
+ if (violations.length > 0) {
183
+ console.log('');
184
+ for (const v of violations) {
185
+ console.log(` ${v.file}:${v.range.startLine} [${v.ruleId}] ${renderViolationMessage(v)}`);
186
+ }
187
+ }
188
+ console.log('');
189
+ if (run.advisories.length > 0) {
190
+ for (const a of run.advisories)
191
+ console.log(` advisory (${a.kind}): ${a.message}`);
192
+ }
193
+ // R1 (greenfield mode): a distinct line near the verdict — not buried in `align explain`/
194
+ // `doctor` — so a check-agent's own loop sees "green because compliant" vs. "green because
195
+ // empty" as different states (registry.ts's `findUngroundedComponents` doc comment names the
196
+ // false-green hole this closes).
197
+ if (run.ungroundedComponents.length > 0) {
198
+ const names = run.ungroundedComponents.map((c) => c.name).join(', ');
199
+ console.log(`⚠ ${run.ungroundedComponents.length} component(s) matched no files (ungrounded, provisionally green): ${names}`);
200
+ }
201
+ console.log(`verdict: ${run.verdict}`);
202
+ }
203
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { type Advisory, type UncertaintyMarker } from '@spikedpunch/align-core';
2
+ export interface DoctorOptions {
3
+ readonly json: boolean;
4
+ }
5
+ export interface DoctorJsonPayload {
6
+ readonly advisories: readonly Advisory[];
7
+ readonly uncertainty: {
8
+ readonly total: number;
9
+ readonly detail: readonly UncertaintyMarker[];
10
+ };
11
+ }
12
+ /**
13
+ * `align doctor` — read-only advisory survey (Stage 2; `--json` per-specifier uncertainty detail
14
+ * added Stage 3). Unlike `align check`, doctor never fails a build: it's a diagnostic tool for the
15
+ * humans/agents configuring align on a repo, not a gate. Exit code is always 0; every failure mode
16
+ * downgrades to an advisory instead of throwing, since a misconfigured repo is exactly the case
17
+ * doctor exists to help someone understand.
18
+ */
19
+ export declare function runDoctor(rootDir: string, options?: DoctorOptions): Promise<number>;
20
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAEA,OAAO,EAAyE,KAAK,QAAQ,EAAE,KAAK,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAqBvJ,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,UAAU,EAAE,SAAS,QAAQ,EAAE,CAAC;IACzC,QAAQ,CAAC,WAAW,EAAE;QACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;KAC/C,CAAC;CACH;AA+HD;;;;;;GAMG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,aAA+B,GAAG,OAAO,CAAC,MAAM,CAAC,CAc1G"}
@@ -0,0 +1,173 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { buildUncertaintyAdvisories, findUngroundedComponents, toComponentName } from '@spikedpunch/align-core';
4
+ import { TypeScriptPlugin, UNMAPPED_COMPONENT, findDeadAliases, findOrphanedPackages } from '@spikedpunch/align-plugin-typescript';
5
+ import { loadConfig } from '../config.js';
6
+ import { ALIGN_VERSION } from '../telemetry/index.js';
7
+ import { parseSkillVersionMarker } from '../skill/version-stamp.js';
8
+ const UNMAPPED_EXAMPLES = 5;
9
+ /** Stage 2 live-probe DX finding, carried into Stage 3: the agent had to script against the
10
+ * scanner API to get per-specifier uncertainty detail — `buildUncertaintyAdvisories` only
11
+ * produces grouped counts. `--json` exposes the raw markers directly, capped so a repo with
12
+ * thousands of uncertain specifiers can't blow up the payload (ADR 007 discipline extended to
13
+ * doctor's own output). */
14
+ const UNCERTAINTY_DETAIL_CAP = 50;
15
+ /** Stage 5 polish, evidence: `RULESET_REPORT.md` (kluster ruleset exercise) logged 47
16
+ * orphaned-package advisories and 34 dead-alias hits as DX friction in the human-readable
17
+ * `align doctor` output — a wall of near-identical lines with no way to see "how many kinds of
18
+ * problem do I have" at a glance. Human output now shows the first N per kind + a "and M more"
19
+ * pointer to `--json`, which stays complete (uncapped except for the pre-existing per-specifier
20
+ * uncertainty detail cap above). */
21
+ const DOCTOR_HUMAN_DISPLAY_CAP = 10;
22
+ /**
23
+ * `align skill --install` writes a point-in-time snapshot (`install.ts`/`version-stamp.ts`) that
24
+ * goes stale as align evolves — a SKILL.md installed before some feature shipped won't mention it,
25
+ * with nothing telling the human or agent the snapshot is behind. `doctor` is the natural
26
+ * read-only advisory surface for that: no advisory if there is no installed file (nothing to be
27
+ * stale about), a distinct message for a pre-stamping install (no marker at all) versus a stamped
28
+ * install that's simply behind the running binary's own `ALIGN_VERSION`. Deliberately simple
29
+ * string inequality, not semver comparison — any mismatch, including a missing marker, means
30
+ * "refresh"; there's no notion of a snapshot being "close enough".
31
+ */
32
+ function buildStaleSkillAdvisory(rootDir) {
33
+ const filePath = path.join(rootDir, '.claude', 'skills', 'align', 'SKILL.md');
34
+ if (!fs.existsSync(filePath))
35
+ return undefined;
36
+ const content = fs.readFileSync(filePath, 'utf8');
37
+ const installedVersion = parseSkillVersionMarker(content);
38
+ if (installedVersion === undefined) {
39
+ return {
40
+ kind: 'stale-skill',
41
+ message: 'installed align skill snapshot predates version stamping — run `align skill --install` to refresh',
42
+ };
43
+ }
44
+ if (installedVersion !== ALIGN_VERSION) {
45
+ return {
46
+ kind: 'stale-skill',
47
+ message: `installed align skill snapshot is v${installedVersion} (current: v${ALIGN_VERSION}) — run \`align skill --install\` to refresh`,
48
+ };
49
+ }
50
+ return undefined;
51
+ }
52
+ async function collectDoctorReport(rootDir) {
53
+ const advisories = [];
54
+ let uncertain = [];
55
+ const staleSkill = buildStaleSkillAdvisory(rootDir);
56
+ if (staleSkill !== undefined)
57
+ advisories.push(staleSkill);
58
+ const loaded = await loadConfig(rootDir).catch((err) => {
59
+ advisories.push({
60
+ kind: 'config-error',
61
+ message: `Could not load align.config.ts: ${err instanceof Error ? err.message : String(err)}`,
62
+ });
63
+ return undefined;
64
+ });
65
+ const excludes = loaded?.excludes ?? [];
66
+ if (loaded !== undefined) {
67
+ const { ruleset, excludes: loadedExcludes } = loaded;
68
+ const plugin = new TypeScriptPlugin();
69
+ const graph = await plugin.scanner.scan({ rootDir, components: ruleset.components, excludes: loadedExcludes }).catch((err) => {
70
+ advisories.push({ kind: 'scan-error', message: err instanceof Error ? err.message : String(err) });
71
+ return undefined;
72
+ });
73
+ if (graph !== undefined) {
74
+ uncertain = graph.uncertain;
75
+ advisories.push(...buildUncertaintyAdvisories(graph.uncertain));
76
+ const unmapped = graph.nodes.filter((n) => n.component === UNMAPPED_COMPONENT);
77
+ if (unmapped.length > 0) {
78
+ const examples = unmapped.slice(0, UNMAPPED_EXAMPLES).map((n) => n.file);
79
+ const more = unmapped.length > UNMAPPED_EXAMPLES ? `, +${unmapped.length - UNMAPPED_EXAMPLES} more` : '';
80
+ advisories.push({
81
+ kind: 'unmapped-files',
82
+ message: `${unmapped.length} file(s) matched no component selector: ${examples.join(', ')}${more}.`,
83
+ });
84
+ }
85
+ const seenComponents = new Set(graph.nodes.map((n) => n.component));
86
+ // R4 (greenfield mode, IMPLEMENTATION_PLAN.md Design Reserve): ungrounded components (empty
87
+ // policy `'allow'`/`'until-populated'`, currently zero classified files) get a
88
+ // policy-specific suggestion — `doctor` is the proactive advisory surface (R1's `align
89
+ // check` line is the always-visible one; this is the "what do I do about it" detail).
90
+ for (const ungrounded of findUngroundedComponents(ruleset.components, seenComponents)) {
91
+ const suggestion = ungrounded.policy === 'until-populated'
92
+ ? 'expected for architecture-first authoring — rules load now and enforce automatically once files land under this selector; if that never happens, double-check the glob.'
93
+ : "permanently tolerated (empty: 'allow') — if this component was meant to be populated by now, check the selector, or switch to empty: 'until-populated' so doctor flags the transition.";
94
+ advisories.push({
95
+ kind: 'ungrounded-component',
96
+ message: `Component '${ungrounded.name}' (selector: ${ungrounded.selector}, empty: '${ungrounded.policy}') matched zero files — ${suggestion}`,
97
+ });
98
+ }
99
+ // The auto-arm/populated half of R2: a `'until-populated'` component that now HAS files no
100
+ // longer needs the marker — the empty-check has already stopped firing for it (nothing to
101
+ // fix functionally), but the marker itself is stale documentation an author should clean up.
102
+ for (const name of Object.keys(ruleset.components)) {
103
+ const def = ruleset.components[toComponentName(name)];
104
+ if (def === undefined || def.empty !== 'until-populated')
105
+ continue;
106
+ if (!seenComponents.has(toComponentName(name)))
107
+ continue; // still empty — not this advisory
108
+ const fileCount = graph.nodes.filter((n) => n.component === toComponentName(name)).length;
109
+ advisories.push({
110
+ kind: 'until-populated-now-populated',
111
+ message: `Component '${name}' is now populated (${fileCount} file(s) classified) — remove its empty: 'until-populated' marker, it's no longer needed.`,
112
+ });
113
+ }
114
+ }
115
+ }
116
+ for (const alias of findDeadAliases(rootDir, excludes)) {
117
+ advisories.push({
118
+ kind: 'dead-alias',
119
+ message: `${alias.tsconfig}: alias '${alias.alias}' -> '${alias.target}' does not resolve to an existing path.`,
120
+ });
121
+ }
122
+ for (const pkg of findOrphanedPackages(rootDir, excludes)) {
123
+ advisories.push({
124
+ kind: 'workspace-orphaned-package',
125
+ message: `${pkg.dir} (package '${pkg.name}') is on disk but not covered by any pnpm-workspace.yaml glob.`,
126
+ });
127
+ }
128
+ return { advisories, uncertain };
129
+ }
130
+ /**
131
+ * `align doctor` — read-only advisory survey (Stage 2; `--json` per-specifier uncertainty detail
132
+ * added Stage 3). Unlike `align check`, doctor never fails a build: it's a diagnostic tool for the
133
+ * humans/agents configuring align on a repo, not a gate. Exit code is always 0; every failure mode
134
+ * downgrades to an advisory instead of throwing, since a misconfigured repo is exactly the case
135
+ * doctor exists to help someone understand.
136
+ */
137
+ export async function runDoctor(rootDir, options = { json: false }) {
138
+ const { advisories, uncertain } = await collectDoctorReport(rootDir);
139
+ if (options.json) {
140
+ const payload = {
141
+ advisories,
142
+ uncertainty: { total: uncertain.length, detail: uncertain.slice(0, UNCERTAINTY_DETAIL_CAP) },
143
+ };
144
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
145
+ return 0;
146
+ }
147
+ printReport(advisories);
148
+ return 0; // advisory tool — never fails the build
149
+ }
150
+ function printReport(advisories) {
151
+ if (advisories.length === 0) {
152
+ console.log('align doctor: no advisories.');
153
+ return;
154
+ }
155
+ console.log(`align doctor: ${advisories.length} advisory(ies)\n`);
156
+ const byKind = new Map();
157
+ for (const a of advisories) {
158
+ const list = byKind.get(a.kind);
159
+ if (list === undefined)
160
+ byKind.set(a.kind, [a]);
161
+ else
162
+ list.push(a);
163
+ }
164
+ for (const [kind, list] of byKind) {
165
+ console.log(` ${kind} (${list.length}):`);
166
+ for (const a of list.slice(0, DOCTOR_HUMAN_DISPLAY_CAP))
167
+ console.log(` - ${a.message}`);
168
+ if (list.length > DOCTOR_HUMAN_DISPLAY_CAP) {
169
+ console.log(` ... and ${list.length - DOCTOR_HUMAN_DISPLAY_CAP} more (use --json for all)`);
170
+ }
171
+ }
172
+ }
173
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { McpExplainRulePayload } from '@spikedpunch/align-core';
2
+ export declare function buildExplainPayload(rootDir: string, ruleId: string): Promise<McpExplainRulePayload | undefined>;
3
+ //# sourceMappingURL=explain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explain.d.ts","sourceRoot":"","sources":["../../src/commands/explain.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAoB,MAAM,yBAAyB,CAAC;AAOvF,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAmCrH"}
@@ -0,0 +1,54 @@
1
+ import { buildViolationMermaid, evaluateRule, toComponentName, toRuleId } from '@spikedpunch/align-core';
2
+ import { TypeScriptPlugin } from '@spikedpunch/align-plugin-typescript';
3
+ import { loadConfig } from '../config.js';
4
+ const EXAMPLES_PER_COMPONENT = 3;
5
+ export async function buildExplainPayload(rootDir, ruleId) {
6
+ const { ruleset, excludes, hostRules } = await loadConfig(rootDir);
7
+ const rule = ruleset.rules.find((r) => r.id === ruleId);
8
+ if (rule === undefined)
9
+ return undefined;
10
+ const plugin = new TypeScriptPlugin();
11
+ const graph = await plugin.scanner.scan({ rootDir, components: ruleset.components, excludes });
12
+ const filesByComponent = new Map();
13
+ for (const node of graph.nodes) {
14
+ const list = filesByComponent.get(node.component) ?? [];
15
+ if (list.length < EXAMPLES_PER_COMPONENT)
16
+ list.push(node.file);
17
+ filesByComponent.set(node.component, list);
18
+ }
19
+ const componentNames = collectComponentNames(rule);
20
+ const components = componentNames.map((name) => ({
21
+ name: toComponentName(name),
22
+ exampleFiles: filesByComponent.get(name) ?? [],
23
+ }));
24
+ // Mermaid is explain-only (ADR 007: pull-on-demand, never in align_check/align_violations).
25
+ // Diagram ONE representative violation instance, if the rule currently has any — a rule with
26
+ // no live violations has no "offending path" to visualize.
27
+ const violations = evaluateRule(rule, graph, ruleset.components, hostRules);
28
+ const firstViolation = violations[0];
29
+ const mermaid = firstViolation === undefined ? undefined : buildViolationMermaid(firstViolation);
30
+ return {
31
+ ruleId: toRuleId(rule.id),
32
+ kind: rule.kind,
33
+ ...(rule.provenance.because === undefined ? {} : { because: rule.provenance.because }),
34
+ components,
35
+ ...(mermaid === undefined ? {} : { mermaid }),
36
+ };
37
+ }
38
+ function collectComponentNames(rule) {
39
+ if (rule.kind === 'arch.no-dependency')
40
+ return [String(rule['from']), String(rule['to'])];
41
+ if (rule.kind === 'arch.no-cycles') {
42
+ const scope = rule['scope'];
43
+ return scope === 'repo' ? [] : [String(scope)];
44
+ }
45
+ if (rule.kind === 'arch.layers') {
46
+ const layers = rule['layers'];
47
+ return [...new Set(layers.flatMap((l) => [l.layer, ...l.canDependOn]))];
48
+ }
49
+ if (rule.kind === 'arch.metric')
50
+ return [String(rule['target'])];
51
+ // `security.manifest.*` (ADR 013): no `ComponentRef` — falls through to the default `[]`.
52
+ return [];
53
+ }
54
+ //# sourceMappingURL=explain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explain.js","sourceRoot":"","sources":["../../src/commands/explain.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ export interface ExportIrOptions {
2
+ /** Overrides the default `.align/ruleset-ir.json` output location (absolute or repo-root-relative). */
3
+ readonly out?: string;
4
+ }
5
+ /**
6
+ * `align export-ir` (ADR 014) — runs ONCE in a trusted context (imports align.config.ts exactly
7
+ * like `align check` already does) and writes the resulting EFFECTIVE ruleset — hand-authored
8
+ * rules plus any `.align/generated-rules.json` rules already merged in, same `loadConfig` path
9
+ * `align check` uses — as portable JSON. `align check --untrusted` reads this file and never
10
+ * imports align.config.ts at all. Re-run this command (in a trusted checkout, e.g. CI before an
11
+ * agent gets an untrusted clone, or as a pre-commit/release step) whenever align.config.ts or the
12
+ * doc it's built from changes — the artifact is a snapshot, not a live view.
13
+ *
14
+ * Deliberately does not export `hostRules` — predicate functions cannot survive a JSON boundary
15
+ * (ADR 002 amendment) and are unconditionally unavailable under --untrusted regardless
16
+ * (`assertNoCustomHostRules`). A ruleset containing `custom.host` rules exports successfully (this
17
+ * command has no opinion on trusted-mode rule content) but prints an advisory naming them, since
18
+ * `align check --untrusted` against this exact file will refuse to run until they're removed.
19
+ */
20
+ export declare function runExportIr(rootDir: string, options?: ExportIrOptions): Promise<number>;
21
+ //# sourceMappingURL=export-ir.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export-ir.d.ts","sourceRoot":"","sources":["../../src/commands/export-ir.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,eAAe;IAC9B,uGAAuG;IACvG,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBjG"}
@@ -0,0 +1,33 @@
1
+ import { buildExportedRuleset } from '@spikedpunch/align-core';
2
+ import { loadConfig } from '../config.js';
3
+ import { writeRulesetIr } from '../align-dir.js';
4
+ /**
5
+ * `align export-ir` (ADR 014) — runs ONCE in a trusted context (imports align.config.ts exactly
6
+ * like `align check` already does) and writes the resulting EFFECTIVE ruleset — hand-authored
7
+ * rules plus any `.align/generated-rules.json` rules already merged in, same `loadConfig` path
8
+ * `align check` uses — as portable JSON. `align check --untrusted` reads this file and never
9
+ * imports align.config.ts at all. Re-run this command (in a trusted checkout, e.g. CI before an
10
+ * agent gets an untrusted clone, or as a pre-commit/release step) whenever align.config.ts or the
11
+ * doc it's built from changes — the artifact is a snapshot, not a live view.
12
+ *
13
+ * Deliberately does not export `hostRules` — predicate functions cannot survive a JSON boundary
14
+ * (ADR 002 amendment) and are unconditionally unavailable under --untrusted regardless
15
+ * (`assertNoCustomHostRules`). A ruleset containing `custom.host` rules exports successfully (this
16
+ * command has no opinion on trusted-mode rule content) but prints an advisory naming them, since
17
+ * `align check --untrusted` against this exact file will refuse to run until they're removed.
18
+ */
19
+ export async function runExportIr(rootDir, options = {}) {
20
+ const { ruleset, excludes } = await loadConfig(rootDir);
21
+ const exported = buildExportedRuleset(ruleset, excludes);
22
+ const writtenTo = writeRulesetIr(rootDir, exported, options.out);
23
+ const componentCount = Object.keys(exported.ruleset.components).length;
24
+ console.log(`align export-ir: wrote ${exported.ruleset.rules.length} rule(s), ${componentCount} component(s) to ${writtenTo}`);
25
+ const hostRuleIds = exported.ruleset.rules.filter((r) => r.kind === 'custom.host').map((r) => r.id);
26
+ if (hostRuleIds.length > 0) {
27
+ console.log(` advisory: ${hostRuleIds.length} custom.host rule(s) exported (${hostRuleIds.join(', ')}) — ` +
28
+ `\`align check --untrusted\` will refuse to run against this file until they are removed ` +
29
+ `(a host predicate is code, and --untrusted never has a predicate registry to consult).`);
30
+ }
31
+ return 0;
32
+ }
33
+ //# sourceMappingURL=export-ir.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export-ir.js","sourceRoot":"","sources":["../../src/commands/export-ir.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAOjD;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAe,EAAE,UAA2B,EAAE;IAC9E,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,CAAC;IACxD,MAAM,QAAQ,GAAG,oBAAoB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACzD,MAAM,SAAS,GAAG,cAAc,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IAEjE,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC;IACvE,OAAO,CAAC,GAAG,CAAC,0BAA0B,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,aAAa,cAAc,oBAAoB,SAAS,EAAE,CAAC,CAAC;IAE/H,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACpG,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3B,OAAO,CAAC,GAAG,CACT,eAAe,WAAW,CAAC,MAAM,kCAAkC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM;YAC7F,0FAA0F;YAC1F,wFAAwF,CAC3F,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,CAAC;AACX,CAAC"}
@@ -0,0 +1,13 @@
1
+ export interface InitOptions {
2
+ readonly acceptExisting: boolean;
3
+ readonly nonInteractive?: boolean;
4
+ /** R4 (greenfield mode, IMPLEMENTATION_PLAN.md Design Reserve): force every detected component
5
+ * to `empty: 'until-populated'` regardless of today's file count — for a repo that's
6
+ * architecture-first from commit zero (components declared, zero files under any of them yet).
7
+ * Without this flag, `runInit` still auto-detects per-component zero-file matches and marks
8
+ * only those — this flag is for the "every component is empty right now" case auto-detection
9
+ * alone already covers, made explicit for a human who wants to say so up front. */
10
+ readonly greenfield?: boolean;
11
+ }
12
+ export declare function runInit(rootDir: string, options: InitOptions): Promise<number>;
13
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC;;;;;uFAKmF;IACnF,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,wBAAsB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAmGpF"}