@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.
- package/LICENSE +21 -0
- package/README.md +42 -0
- package/dist/align-dir.d.ts +55 -0
- package/dist/align-dir.d.ts.map +1 -0
- package/dist/align-dir.js +184 -0
- package/dist/align-dir.js.map +1 -0
- package/dist/commands/agent.d.ts +14 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +142 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/baseline.d.ts +4 -0
- package/dist/commands/baseline.d.ts.map +1 -0
- package/dist/commands/baseline.js +57 -0
- package/dist/commands/baseline.js.map +1 -0
- package/dist/commands/build-report.d.ts +4 -0
- package/dist/commands/build-report.d.ts.map +1 -0
- package/dist/commands/build-report.js +122 -0
- package/dist/commands/build-report.js.map +1 -0
- package/dist/commands/build.d.ts +73 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +289 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/check.d.ts +31 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +203 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/doctor.d.ts +20 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +173 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/explain.d.ts +3 -0
- package/dist/commands/explain.d.ts.map +1 -0
- package/dist/commands/explain.js +54 -0
- package/dist/commands/explain.js.map +1 -0
- package/dist/commands/export-ir.d.ts +21 -0
- package/dist/commands/export-ir.d.ts.map +1 -0
- package/dist/commands/export-ir.js +33 -0
- package/dist/commands/export-ir.js.map +1 -0
- package/dist/commands/init.d.ts +13 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +93 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/skill.d.ts +20 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +23 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/telemetry.d.ts +52 -0
- package/dist/commands/telemetry.d.ts.map +1 -0
- package/dist/commands/telemetry.js +238 -0
- package/dist/commands/telemetry.js.map +1 -0
- package/dist/composition-root.d.ts +14 -0
- package/dist/composition-root.d.ts.map +1 -0
- package/dist/composition-root.js +18 -0
- package/dist/composition-root.js.map +1 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +39 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/init/claude-md.d.ts +6 -0
- package/dist/init/claude-md.d.ts.map +1 -0
- package/dist/init/claude-md.js +42 -0
- package/dist/init/claude-md.js.map +1 -0
- package/dist/init/config-comment.d.ts +7 -0
- package/dist/init/config-comment.d.ts.map +1 -0
- package/dist/init/config-comment.js +44 -0
- package/dist/init/config-comment.js.map +1 -0
- package/dist/init/detect-components.d.ts +16 -0
- package/dist/init/detect-components.d.ts.map +1 -0
- package/dist/init/detect-components.js +121 -0
- package/dist/init/detect-components.js.map +1 -0
- package/dist/init/gitignore.d.ts +8 -0
- package/dist/init/gitignore.d.ts.map +1 -0
- package/dist/init/gitignore.js +27 -0
- package/dist/init/gitignore.js.map +1 -0
- package/dist/init/render-config.d.ts +18 -0
- package/dist/init/render-config.d.ts.map +1 -0
- package/dist/init/render-config.js +57 -0
- package/dist/init/render-config.js.map +1 -0
- package/dist/init/suggest-layers.d.ts +16 -0
- package/dist/init/suggest-layers.d.ts.map +1 -0
- package/dist/init/suggest-layers.js +65 -0
- package/dist/init/suggest-layers.js.map +1 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +182 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/program.d.ts +11 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +199 -0
- package/dist/program.js.map +1 -0
- package/dist/skill/bullet-grammar.d.ts +2 -0
- package/dist/skill/bullet-grammar.d.ts.map +1 -0
- package/dist/skill/bullet-grammar.js +12 -0
- package/dist/skill/bullet-grammar.js.map +1 -0
- package/dist/skill/cli-inventory.d.ts +16 -0
- package/dist/skill/cli-inventory.d.ts.map +1 -0
- package/dist/skill/cli-inventory.js +17 -0
- package/dist/skill/cli-inventory.js.map +1 -0
- package/dist/skill/condensed.d.ts +2 -0
- package/dist/skill/condensed.d.ts.map +1 -0
- package/dist/skill/condensed.js +24 -0
- package/dist/skill/condensed.js.map +1 -0
- package/dist/skill/dsl-verbs.d.ts +2 -0
- package/dist/skill/dsl-verbs.d.ts.map +1 -0
- package/dist/skill/dsl-verbs.js +10 -0
- package/dist/skill/dsl-verbs.js.map +1 -0
- package/dist/skill/fix-loop-protocol.d.ts +18 -0
- package/dist/skill/fix-loop-protocol.d.ts.map +1 -0
- package/dist/skill/fix-loop-protocol.js +38 -0
- package/dist/skill/fix-loop-protocol.js.map +1 -0
- package/dist/skill/gates.d.ts +2 -0
- package/dist/skill/gates.d.ts.map +1 -0
- package/dist/skill/gates.js +20 -0
- package/dist/skill/gates.js.map +1 -0
- package/dist/skill/install.d.ts +25 -0
- package/dist/skill/install.d.ts.map +1 -0
- package/dist/skill/install.js +49 -0
- package/dist/skill/install.js.map +1 -0
- package/dist/skill/render.d.ts +15 -0
- package/dist/skill/render.d.ts.map +1 -0
- package/dist/skill/render.js +35 -0
- package/dist/skill/render.js.map +1 -0
- package/dist/skill/rule-kinds.d.ts +12 -0
- package/dist/skill/rule-kinds.d.ts.map +1 -0
- package/dist/skill/rule-kinds.js +47 -0
- package/dist/skill/rule-kinds.js.map +1 -0
- package/dist/skill/static-sections.d.ts +5 -0
- package/dist/skill/static-sections.d.ts.map +1 -0
- package/dist/skill/static-sections.js +74 -0
- package/dist/skill/static-sections.js.map +1 -0
- package/dist/skill/version-stamp.d.ts +22 -0
- package/dist/skill/version-stamp.d.ts.map +1 -0
- package/dist/skill/version-stamp.js +29 -0
- package/dist/skill/version-stamp.js.map +1 -0
- package/dist/telemetry/check-event.d.ts +7 -0
- package/dist/telemetry/check-event.d.ts.map +1 -0
- package/dist/telemetry/check-event.js +29 -0
- package/dist/telemetry/check-event.js.map +1 -0
- package/dist/telemetry/index.d.ts +8 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +8 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/telemetry/process-context.d.ts +9 -0
- package/dist/telemetry/process-context.d.ts.map +1 -0
- package/dist/telemetry/process-context.js +10 -0
- package/dist/telemetry/process-context.js.map +1 -0
- package/dist/telemetry/recorder.d.ts +43 -0
- package/dist/telemetry/recorder.d.ts.map +1 -0
- package/dist/telemetry/recorder.js +61 -0
- package/dist/telemetry/recorder.js.map +1 -0
- package/dist/telemetry/resolve.d.ts +19 -0
- package/dist/telemetry/resolve.d.ts.map +1 -0
- package/dist/telemetry/resolve.js +18 -0
- package/dist/telemetry/resolve.js.map +1 -0
- package/dist/telemetry/ruleset-hash.d.ts +10 -0
- package/dist/telemetry/ruleset-hash.d.ts.map +1 -0
- package/dist/telemetry/ruleset-hash.js +12 -0
- package/dist/telemetry/ruleset-hash.js.map +1 -0
- package/dist/telemetry/session.d.ts +6 -0
- package/dist/telemetry/session.d.ts.map +1 -0
- package/dist/telemetry/session.js +9 -0
- package/dist/telemetry/session.js.map +1 -0
- package/dist/telemetry/violations.d.ts +10 -0
- package/dist/telemetry/violations.d.ts.map +1 -0
- package/dist/telemetry/violations.js +36 -0
- package/dist/telemetry/violations.js.map +1 -0
- 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 @@
|
|
|
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"}
|