etymd 0.2.2 → 0.3.1
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/CHANGELOG.md +70 -2
- package/README.md +145 -37
- package/dist/{approve-VC4V3OPW.js → approve-3B2UL7LG.js} +6 -4
- package/dist/audit-4YB3K62W.js +12 -0
- package/dist/{brief-TZ45MNHD.js → brief-6EEXGQUW.js} +6 -4
- package/dist/chunk-CRJHIXJH.js +5 -0
- package/dist/{chunk-LVLZLCND.js → chunk-DKC7MLWG.js} +1 -1
- package/dist/chunk-EF6BPBG6.js +22 -0
- package/dist/{chunk-BH37LELV.js → chunk-HOLFW5QY.js} +8 -5
- package/dist/chunk-LT67FU2Y.js +59 -0
- package/dist/{chunk-IW6K5HLV.js → chunk-MEJ4MBVL.js} +2 -2
- package/dist/{chunk-EUIHYD6F.js → chunk-MMPV67FW.js} +2 -2
- package/dist/{chunk-ANCCGYSH.js → chunk-NMZ3RHWW.js} +31 -59
- package/dist/{chunk-FZSIOCOZ.js → chunk-R74SJ7HS.js} +1 -1
- package/dist/{chunk-LBRNZZZF.js → chunk-TADKOW6P.js} +1 -99
- package/dist/{chunk-KMAJIQHC.js → chunk-V6NZEIHF.js} +7 -5
- package/dist/{chunk-MWIK6ML3.js → chunk-VFUYQLW7.js} +141 -25
- package/dist/{chunk-KBF3SW3N.js → chunk-WWBF4Y7K.js} +1 -1
- package/dist/chunk-YLNQQZ5F.js +102 -0
- package/dist/cli.js +50 -16
- package/dist/config-MX7OYI7F.js +4 -0
- package/dist/{context-GYCLR3TG.js → context-4DGXO7JR.js} +5 -3
- package/dist/doctor-T76ZNL4S.js +19 -0
- package/dist/{fleet-N3G5ZIGR.js → fleet-WOPKM2Z3.js} +248 -10
- package/dist/gates-WIGDS5VF.js +262 -0
- package/dist/generate-I75IEXXF.js +6 -0
- package/dist/index.d.ts +122 -33
- package/dist/index.js +737 -257
- package/dist/{init-4CQOLFQG.js → init-NSYVVMF2.js} +9 -5
- package/dist/ledger-TMIV45AA.js +5 -0
- package/dist/scan-BAD2UJAL.js +5 -0
- package/dist/{scan-CYOOGJFG.js → scan-COEPZJBO.js} +6 -4
- package/dist/screen-U7JMWZPS.js +134 -0
- package/package.json +2 -2
- package/dist/audit-LZJRJS5V.js +0 -9
- package/dist/doctor-6JDAHGDT.js +0 -16
- package/dist/gates-G5BCMRRV.js +0 -71
- package/dist/ledger-RHQJH5EA.js +0 -4
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { applyFiles } from './chunk-EF6BPBG6.js';
|
|
3
|
+
import { derivedCommands, planWorkflow, runPrefix, isSafeGateCommand } from './chunk-VFUYQLW7.js';
|
|
4
|
+
import { readConfig, CONFIG_FILE, configPath } from './chunk-NMZ3RHWW.js';
|
|
5
|
+
import { section, print, glyph, theme, renderPlan } from './chunk-TADKOW6P.js';
|
|
6
|
+
import { scanProject } from './chunk-HOLFW5QY.js';
|
|
7
|
+
import './chunk-DKC7MLWG.js';
|
|
8
|
+
import './chunk-R74SJ7HS.js';
|
|
9
|
+
import './chunk-CRJHIXJH.js';
|
|
10
|
+
import { readText, git } from './chunk-YLNQQZ5F.js';
|
|
11
|
+
import { promises } from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { confirm, isCancel, cancel, select, multiselect } from '@clack/prompts';
|
|
14
|
+
|
|
15
|
+
function detectIndent(raw) {
|
|
16
|
+
const m = raw.match(/\n(\s+)"/);
|
|
17
|
+
if (!m) return 2;
|
|
18
|
+
const ws = m[1];
|
|
19
|
+
return ws.includes(" ") ? " " : ws.length;
|
|
20
|
+
}
|
|
21
|
+
function mergeScriptInto(raw, key, value) {
|
|
22
|
+
let doc;
|
|
23
|
+
try {
|
|
24
|
+
doc = JSON.parse(raw);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return {
|
|
27
|
+
result: {
|
|
28
|
+
outcome: "unparsable",
|
|
29
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const scripts = doc.scripts ?? {};
|
|
34
|
+
const current = scripts[key];
|
|
35
|
+
if (typeof current === "string") {
|
|
36
|
+
if (current === value) return { result: { outcome: "unchanged" } };
|
|
37
|
+
return { result: { outcome: "conflict", existing: current } };
|
|
38
|
+
}
|
|
39
|
+
const indent = detectIndent(raw);
|
|
40
|
+
const next = { ...doc, scripts: { ...scripts, [key]: value } };
|
|
41
|
+
const trailing = raw.endsWith("\n") ? "\n" : "";
|
|
42
|
+
return { text: JSON.stringify(next, null, indent) + trailing, result: { outcome: "added" } };
|
|
43
|
+
}
|
|
44
|
+
async function ensurePackageScript(pkgPath, key, value) {
|
|
45
|
+
const raw = await readText(pkgPath);
|
|
46
|
+
if (raw === null) return { outcome: "absent" };
|
|
47
|
+
const { text, result } = mergeScriptInto(raw, key, value);
|
|
48
|
+
if (text) await promises.writeFile(pkgPath, text, "utf8");
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/commands/gates.ts
|
|
53
|
+
var PUBLISH_GATE_SCRIPT = "scripts/artifact-check.sh";
|
|
54
|
+
var PUBLISH_GATE_VALUE = `./${PUBLISH_GATE_SCRIPT}`;
|
|
55
|
+
function publishGateKey(route) {
|
|
56
|
+
return route === "vscode" ? "vscode:prepublish" : "prepublishOnly";
|
|
57
|
+
}
|
|
58
|
+
function printGateSummary(gates, facts, source) {
|
|
59
|
+
const run2 = runPrefix(facts.packageManager);
|
|
60
|
+
const cmds = gates.commands.length ? gates.commands.map((c) => `${run2} ${c}`).join(", ") : "none detected";
|
|
61
|
+
print(` ${glyph.bullet} ${theme.dim("pre-push runs")} ${theme.info(cmds)}`);
|
|
62
|
+
print(` ${glyph.bullet} ${theme.dim("audit fails on")} ${theme.info(gates.failOn)}`);
|
|
63
|
+
print(
|
|
64
|
+
` ${glyph.bullet} ${theme.dim("publish screen")} ${theme.info(gates.publishGate ? "yes" : "no")}${gates.publishGate && !facts.publishable ? theme.dim(" (overridden)") : ""}`
|
|
65
|
+
);
|
|
66
|
+
if (source.recorded || source.willRecord) {
|
|
67
|
+
print(` ${theme.dim(`change any of these later in ${CONFIG_FILE}`)}`);
|
|
68
|
+
} else {
|
|
69
|
+
print(
|
|
70
|
+
` ${theme.dim(`derived from this repo \u2014 write ${CONFIG_FILE} (or run without --yes) to pin them`)}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function customize(current, facts) {
|
|
75
|
+
const c = facts.commands;
|
|
76
|
+
const available = [c.formatCheck, c.typecheck, c.lint, c.test].filter(
|
|
77
|
+
(k) => Boolean(k)
|
|
78
|
+
);
|
|
79
|
+
const picked = available.length ? await multiselect({
|
|
80
|
+
message: "Which commands should the pre-push gate run?",
|
|
81
|
+
required: false,
|
|
82
|
+
initialValues: current.commands,
|
|
83
|
+
options: available.map((k) => ({
|
|
84
|
+
value: k,
|
|
85
|
+
label: `${runPrefix(facts.packageManager)} ${k}`,
|
|
86
|
+
hint: isSafeGateCommand(c.raw[k]) ? void 0 : "writes \u2014 allowed only if you pick it"
|
|
87
|
+
}))
|
|
88
|
+
}) : [];
|
|
89
|
+
if (isCancel(picked)) return void 0;
|
|
90
|
+
const tier = await select({
|
|
91
|
+
message: "Fail the push on which finding tier?",
|
|
92
|
+
initialValue: current.failOn,
|
|
93
|
+
options: [
|
|
94
|
+
{ value: "risk", label: "risk \u2014 only what makes an agent do the wrong thing" },
|
|
95
|
+
{ value: "gap", label: "gap \u2014 also dead references and missing safeguards" },
|
|
96
|
+
{ value: "polish", label: "polish \u2014 everything" }
|
|
97
|
+
]
|
|
98
|
+
});
|
|
99
|
+
if (isCancel(tier)) return void 0;
|
|
100
|
+
const publish = await confirm({
|
|
101
|
+
message: "Screen the published artifact? (the only check that sees what actually ships)",
|
|
102
|
+
initialValue: current.publishGate ?? facts.publishable
|
|
103
|
+
});
|
|
104
|
+
if (isCancel(publish)) return void 0;
|
|
105
|
+
const commands = picked;
|
|
106
|
+
return {
|
|
107
|
+
commands,
|
|
108
|
+
failOn: tier,
|
|
109
|
+
publishGate: publish,
|
|
110
|
+
// Picking a writing command IS the override — recorded so the generator honors it.
|
|
111
|
+
allowWriting: commands.filter((k) => !isSafeGateCommand(c.raw[k]))
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function mergeGateSection(previous, gates) {
|
|
115
|
+
const merged = { ...previous, ...gates };
|
|
116
|
+
const why = previous._why;
|
|
117
|
+
if (!why) return merged;
|
|
118
|
+
const next = gates;
|
|
119
|
+
const kept = Object.fromEntries(
|
|
120
|
+
Object.entries(why).filter(
|
|
121
|
+
([field]) => JSON.stringify(previous[field]) === JSON.stringify(next[field])
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
if (Object.keys(kept).length) merged._why = kept;
|
|
125
|
+
else delete merged._why;
|
|
126
|
+
return merged;
|
|
127
|
+
}
|
|
128
|
+
async function writeGateConfig(root, gates) {
|
|
129
|
+
const target = configPath(root);
|
|
130
|
+
const existing = await readText(target);
|
|
131
|
+
let doc = {};
|
|
132
|
+
if (existing) {
|
|
133
|
+
try {
|
|
134
|
+
doc = JSON.parse(existing);
|
|
135
|
+
} catch {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
doc.gates = mergeGateSection(doc.gates ?? {}, gates);
|
|
140
|
+
await promises.mkdir(path.dirname(target), { recursive: true });
|
|
141
|
+
await promises.writeFile(target, JSON.stringify(doc, null, 2) + "\n", "utf8");
|
|
142
|
+
}
|
|
143
|
+
async function run(opts) {
|
|
144
|
+
const facts = await scanProject(opts.cwd);
|
|
145
|
+
if (opts.ci) {
|
|
146
|
+
section("CI review gate");
|
|
147
|
+
print(
|
|
148
|
+
` ${glyph.partial} ${theme.dim("The advisory AI-review CI job (bring-your-own-key) ships in a later release \u2014 installing the free local gates below.")}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if (!facts.git.isRepo) {
|
|
152
|
+
print(
|
|
153
|
+
` ${glyph.bad} ${theme.dim("Not a git repository \u2014 local hooks need git. Run `git init` first.")}`
|
|
154
|
+
);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (facts.hooks.source === "husky" || facts.hooks.source === "husky-legacy" || facts.hooks.source === "custom") {
|
|
158
|
+
print(
|
|
159
|
+
` ${glyph.partial} ${theme.warn(`${facts.hooks.source} already manages hooks here.`)} ${theme.dim("git runs ONE hook path \u2014 installing .githooks + core.hooksPath takes over. Fold the existing hook logic in first.")}`
|
|
160
|
+
);
|
|
161
|
+
if (!opts.yes) {
|
|
162
|
+
const go = await confirm({ message: "Proceed with .githooks anyway?", initialValue: false });
|
|
163
|
+
if (isCancel(go) || !go) {
|
|
164
|
+
cancel("No changes made.");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const existingPrePush = await readText(path.join(opts.cwd, ".githooks", "pre-push"));
|
|
170
|
+
const { config, present: hasConfig } = await readConfig(opts.cwd);
|
|
171
|
+
const configSource = { recorded: hasConfig, willRecord: !opts.yes };
|
|
172
|
+
let gateConfig = {
|
|
173
|
+
...config.gates,
|
|
174
|
+
commands: config.gates.commands.length ? config.gates.commands : derivedCommands(facts, existingPrePush ?? void 0),
|
|
175
|
+
publishGate: config.gates.publishGate ?? facts.publishable
|
|
176
|
+
};
|
|
177
|
+
const plan = async () => {
|
|
178
|
+
const files = await planWorkflow(opts.cwd, facts, {
|
|
179
|
+
agents: false,
|
|
180
|
+
gates: true,
|
|
181
|
+
gateConfig
|
|
182
|
+
});
|
|
183
|
+
return files.filter((f) => f.executable);
|
|
184
|
+
};
|
|
185
|
+
let gateFiles = await plan();
|
|
186
|
+
renderPlan(gateFiles);
|
|
187
|
+
printGateSummary(gateConfig, facts, configSource);
|
|
188
|
+
if (!opts.yes) {
|
|
189
|
+
const choice = await select({
|
|
190
|
+
message: "Install these gates?",
|
|
191
|
+
initialValue: "yes",
|
|
192
|
+
options: [
|
|
193
|
+
{ value: "yes", label: "yes \u2014 install as shown" },
|
|
194
|
+
{ value: "customize", label: "customize \u2014 change what the gates run" },
|
|
195
|
+
{ value: "no", label: "cancel" }
|
|
196
|
+
]
|
|
197
|
+
});
|
|
198
|
+
if (isCancel(choice) || choice === "no") {
|
|
199
|
+
cancel("No changes made.");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (choice === "customize") {
|
|
203
|
+
const customized = await customize(gateConfig, facts);
|
|
204
|
+
if (!customized) {
|
|
205
|
+
cancel("No changes made.");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
gateConfig = customized;
|
|
209
|
+
gateFiles = await plan();
|
|
210
|
+
renderPlan(gateFiles);
|
|
211
|
+
printGateSummary(gateConfig, facts, configSource);
|
|
212
|
+
}
|
|
213
|
+
await writeGateConfig(opts.cwd, gateConfig);
|
|
214
|
+
}
|
|
215
|
+
const overwrite = /* @__PURE__ */ new Set();
|
|
216
|
+
for (const f of gateFiles) {
|
|
217
|
+
if (!f.exists) continue;
|
|
218
|
+
if (!f.differs) {
|
|
219
|
+
overwrite.add(f.path);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (opts.yes) continue;
|
|
223
|
+
const ow = await confirm({
|
|
224
|
+
message: `${f.path} exists with different content (hand-edited?). Overwrite with the pack version?`,
|
|
225
|
+
initialValue: false
|
|
226
|
+
});
|
|
227
|
+
if (isCancel(ow)) {
|
|
228
|
+
cancel("No changes made.");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (ow) overwrite.add(f.path);
|
|
232
|
+
}
|
|
233
|
+
const result = await applyFiles(opts.cwd, gateFiles, overwrite);
|
|
234
|
+
await git(opts.cwd, ["config", "core.hooksPath", ".githooks"]);
|
|
235
|
+
section("Done");
|
|
236
|
+
for (const w of result.written) print(` ${glyph.ok} ${theme.dim("wrote")} ${theme.info(w)}`);
|
|
237
|
+
for (const sk of result.skipped)
|
|
238
|
+
print(` ${glyph.bullet} ${theme.dim("kept (hand-edited)")} ${theme.dim(sk)}`);
|
|
239
|
+
print(` ${glyph.ok} ${theme.dim("set")} ${theme.code("core.hooksPath = .githooks")}`);
|
|
240
|
+
const wrotePublishGate = result.written.includes(PUBLISH_GATE_SCRIPT);
|
|
241
|
+
if (wrotePublishGate) {
|
|
242
|
+
const pkgPath = path.join(opts.cwd, "package.json");
|
|
243
|
+
const PUBLISH_GATE_KEY = publishGateKey(facts.publishRoute);
|
|
244
|
+
{
|
|
245
|
+
const merged = await ensurePackageScript(pkgPath, PUBLISH_GATE_KEY, PUBLISH_GATE_VALUE);
|
|
246
|
+
if (merged.outcome === "added")
|
|
247
|
+
print(` ${glyph.ok} ${theme.dim("wired")} ${theme.code(PUBLISH_GATE_KEY)}`);
|
|
248
|
+
else if (merged.outcome === "conflict")
|
|
249
|
+
print(
|
|
250
|
+
` ${glyph.partial} ${theme.warn(`${PUBLISH_GATE_KEY} already runs \`${merged.existing}\``)} ${theme.dim("\u2014 left as is; chain the screen into it by hand if you want both.")}`
|
|
251
|
+
);
|
|
252
|
+
else if (merged.outcome === "unchanged")
|
|
253
|
+
print(` ${glyph.bullet} ${theme.dim(`${PUBLISH_GATE_KEY} already wired`)}`);
|
|
254
|
+
else
|
|
255
|
+
print(
|
|
256
|
+
` ${glyph.partial} ${theme.dim(`package.json ${merged.outcome}${merged.detail ? `: ${merged.detail}` : ""} \u2014 wire ${PUBLISH_GATE_KEY} by hand`)}`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export { mergeGateSection, run };
|
package/dist/index.d.ts
CHANGED
|
@@ -70,7 +70,7 @@ interface GitFacts {
|
|
|
70
70
|
husky: boolean;
|
|
71
71
|
/** Distinct commit authors in recent history — the solo-vs-team profile signal. */
|
|
72
72
|
recentAuthors?: number;
|
|
73
|
-
/** Ticket-key prefix inferred from recent commit subjects (e.g. "
|
|
73
|
+
/** Ticket-key prefix inferred from recent commit subjects (e.g. "PROJ"). */
|
|
74
74
|
ticketKey?: string;
|
|
75
75
|
}
|
|
76
76
|
interface HookFacts {
|
|
@@ -104,6 +104,22 @@ interface ProjectFacts {
|
|
|
104
104
|
files: string[];
|
|
105
105
|
};
|
|
106
106
|
hooks: HookFacts;
|
|
107
|
+
/**
|
|
108
|
+
* This project would publish something if asked — a manifest exists and does not declare
|
|
109
|
+
* `private: true`.
|
|
110
|
+
*
|
|
111
|
+
* A GUESS, not a fact: npm treats a missing `private` as publishable, which is right about
|
|
112
|
+
* npm's semantics and wrong about a local fork nobody will ever publish. It seeds the plan;
|
|
113
|
+
* `gates.publishGate` in `.etymd/config.json` overrides it for good.
|
|
114
|
+
*/
|
|
115
|
+
publishable: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* How this project publishes, when it does. The route decides which manifest key fires a
|
|
118
|
+
* publish-time gate: npm runs `prepublishOnly`, while `vsce` ignores that key entirely and
|
|
119
|
+
* runs `vscode:prepublish` — so wiring the npm key into an extension is a gate that silently
|
|
120
|
+
* never runs.
|
|
121
|
+
*/
|
|
122
|
+
publishRoute: "npm" | "vscode" | "none";
|
|
107
123
|
artifacts: DetectedArtifact[];
|
|
108
124
|
/** Optional: baselines approved by older versions predate this fact. */
|
|
109
125
|
freshness?: FreshnessFacts;
|
|
@@ -159,35 +175,6 @@ declare const EXTRACTION_THRESHOLD: number;
|
|
|
159
175
|
declare function isAlwaysAppliedCursorRule(text: string): boolean;
|
|
160
176
|
declare function measureContext(root: string, perFileWords?: number): Promise<ContextBudget>;
|
|
161
177
|
|
|
162
|
-
interface GeneratedFile {
|
|
163
|
-
path: string;
|
|
164
|
-
contents: string;
|
|
165
|
-
/** Present on disk already — apply will skip or ask before overwriting. */
|
|
166
|
-
exists: boolean;
|
|
167
|
-
/** The existing file's content differs from what the pack would generate (hand-edited or stale). */
|
|
168
|
-
differs?: boolean;
|
|
169
|
-
/** Hooks need the executable bit. */
|
|
170
|
-
executable?: boolean;
|
|
171
|
-
label: string;
|
|
172
|
-
}
|
|
173
|
-
interface PlanOptions {
|
|
174
|
-
/** Scaffold a minimal AGENTS.md (init only offers this when none exists). */
|
|
175
|
-
agents: boolean;
|
|
176
|
-
gates: boolean;
|
|
177
|
-
}
|
|
178
|
-
/** Build the file set an onboarding would write, flagging which exist and which differ. */
|
|
179
|
-
declare function planWorkflow(root: string, facts: ProjectFacts, opts: PlanOptions): Promise<GeneratedFile[]>;
|
|
180
|
-
|
|
181
|
-
interface ApplyResult {
|
|
182
|
-
written: string[];
|
|
183
|
-
skipped: string[];
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Write a planned file set. Idempotent by default: an existing file is skipped unless `overwrite`
|
|
187
|
-
* names it — etymd never clobbers hand-authored contracts without consent.
|
|
188
|
-
*/
|
|
189
|
-
declare function applyFiles(root: string, files: GeneratedFile[], overwrite?: Set<string>): Promise<ApplyResult>;
|
|
190
|
-
|
|
191
178
|
declare const CONFIG_FILE: string;
|
|
192
179
|
interface InstructionScope {
|
|
193
180
|
/** Extra instruction files to audit beyond the auto-detected set (globs, repo-relative). */
|
|
@@ -207,10 +194,29 @@ interface StateBudgets {
|
|
|
207
194
|
/** Chars in a state doc past which the file is a finding (session hooks truncate ~10k). */
|
|
208
195
|
maxChars: number;
|
|
209
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* What the generated gates should contain. Every field here is DERIVED from the repo on a first
|
|
199
|
+
* run and written into the plan, not asked: a wrong guess costs a slightly slow hook or a
|
|
200
|
+
* slightly strict audit, and both are one edit away. Prompting is reserved for the choices where
|
|
201
|
+
* a wrong default is expensive — trust (a permanent leak) and the publish gate (a write into
|
|
202
|
+
* package.json). Recording the derivation here is what makes `gates` re-runnable without
|
|
203
|
+
* re-deciding, and what a drift check compares against.
|
|
204
|
+
*/
|
|
205
|
+
interface GateConfig {
|
|
206
|
+
/** Command keys the pre-push gate runs, in order. Empty = whatever the scan finds. */
|
|
207
|
+
commands: string[];
|
|
208
|
+
/** Tier the audit step fails on: risk | gap | polish. */
|
|
209
|
+
failOn: string;
|
|
210
|
+
/** Emit the publish-time screen. Unset = derive from whether the repo publishes. */
|
|
211
|
+
publishGate?: boolean;
|
|
212
|
+
/** Commands allowed into a gate despite looking like they write (an explicit override). */
|
|
213
|
+
allowWriting: string[];
|
|
214
|
+
}
|
|
210
215
|
interface EtymdConfig {
|
|
211
216
|
instructions: InstructionScope;
|
|
212
217
|
context: ContextBudgets;
|
|
213
218
|
state: StateBudgets;
|
|
219
|
+
gates: GateConfig;
|
|
214
220
|
}
|
|
215
221
|
interface LoadedConfig {
|
|
216
222
|
config: EtymdConfig;
|
|
@@ -228,6 +234,43 @@ declare function configPath(root: string): string;
|
|
|
228
234
|
*/
|
|
229
235
|
declare function readConfig(root: string): Promise<LoadedConfig>;
|
|
230
236
|
|
|
237
|
+
interface GeneratedFile {
|
|
238
|
+
path: string;
|
|
239
|
+
contents: string;
|
|
240
|
+
/** Present on disk already — apply will skip or ask before overwriting. */
|
|
241
|
+
exists: boolean;
|
|
242
|
+
/** The existing file's content differs from what the pack would generate (hand-edited or stale). */
|
|
243
|
+
differs?: boolean;
|
|
244
|
+
/** Hooks need the executable bit. */
|
|
245
|
+
executable?: boolean;
|
|
246
|
+
label: string;
|
|
247
|
+
}
|
|
248
|
+
interface PlanOptions {
|
|
249
|
+
/** Scaffold a minimal AGENTS.md (init only offers this when none exists). */
|
|
250
|
+
agents: boolean;
|
|
251
|
+
gates: boolean;
|
|
252
|
+
/**
|
|
253
|
+
* Emit the publish-time screen. Defaults to `facts.publishable` — set it explicitly to
|
|
254
|
+
* override the derivation (a repo that publishes by a route npm cannot see, or one that
|
|
255
|
+
* declines the door).
|
|
256
|
+
*/
|
|
257
|
+
publishGate?: boolean;
|
|
258
|
+
/** Recorded gate choices; absent means derive everything from the scan. */
|
|
259
|
+
gateConfig?: GateConfig;
|
|
260
|
+
}
|
|
261
|
+
/** Build the file set an onboarding would write, flagging which exist and which differ. */
|
|
262
|
+
declare function planWorkflow(root: string, facts: ProjectFacts, opts: PlanOptions): Promise<GeneratedFile[]>;
|
|
263
|
+
|
|
264
|
+
interface ApplyResult {
|
|
265
|
+
written: string[];
|
|
266
|
+
skipped: string[];
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Write a planned file set. Idempotent by default: an existing file is skipped unless `overwrite`
|
|
270
|
+
* names it — etymd never clobbers hand-authored contracts without consent.
|
|
271
|
+
*/
|
|
272
|
+
declare function applyFiles(root: string, files: GeneratedFile[], overwrite?: Set<string>): Promise<ApplyResult>;
|
|
273
|
+
|
|
231
274
|
type FindingTier = "risk" | "gap" | "polish";
|
|
232
275
|
type Effort = "S" | "M" | "L";
|
|
233
276
|
type Confidence = "high" | "medium" | "low";
|
|
@@ -395,6 +438,35 @@ interface AuditResult {
|
|
|
395
438
|
declare function runAudit(root: string, opts?: AuditOptions): Promise<AuditResult>;
|
|
396
439
|
|
|
397
440
|
type FleetProfile = "personal" | "corp";
|
|
441
|
+
/**
|
|
442
|
+
* How exposed an entry's history is, or could plausibly become. A SAFETY PREDICATE, not a label:
|
|
443
|
+
* it decides whether content screening applies, so absence must never read as the permissive
|
|
444
|
+
* answer. `checkManifest` treats an undeclared non-corp entry as a finding — a repo published
|
|
445
|
+
* without ever answering the question is exactly the case a default would hide.
|
|
446
|
+
*
|
|
447
|
+
* `public-repo` already public — outside-contribution surface.
|
|
448
|
+
* `public-bound` private today, plausibly public later. Screened as hard as public, because
|
|
449
|
+
* publishing exposes ALL history: the scrub must precede the first commit,
|
|
450
|
+
* not the visibility flip.
|
|
451
|
+
* `private` not destined to be published. An ANSWER, not the absence of one.
|
|
452
|
+
*
|
|
453
|
+
* Corp entries do not carry it — `profile: "corp"` implies the answer (machine-pinned, never
|
|
454
|
+
* publishable), so requiring it there would be ceremony.
|
|
455
|
+
*/
|
|
456
|
+
type FleetTrust = "public-repo" | "public-bound" | "private";
|
|
457
|
+
/**
|
|
458
|
+
* Fleet-wide orientation: the one entry every other entry is guided by.
|
|
459
|
+
*
|
|
460
|
+
* Declared ONCE at the manifest level rather than as a per-entry link, because the relationship
|
|
461
|
+
* is constant — a per-entry field carries no information and can be forgotten, which is exactly
|
|
462
|
+
* how three entries came to be silently unlinked. Hoisting it makes the orphan state
|
|
463
|
+
* unrepresentable instead of merely detectable. A fleet with no orientation root simply omits
|
|
464
|
+
* the block; etymd never assumes one exists.
|
|
465
|
+
*/
|
|
466
|
+
interface FleetOrientation {
|
|
467
|
+
/** Registered name of the orientation root. Only the root itself is exempt from being guided. */
|
|
468
|
+
root: string;
|
|
469
|
+
}
|
|
398
470
|
interface FleetContract {
|
|
399
471
|
state?: string;
|
|
400
472
|
decisions?: string;
|
|
@@ -411,9 +483,24 @@ interface FleetEntry {
|
|
|
411
483
|
path?: string;
|
|
412
484
|
/** Remote name of the upstream this repo forks — freshness measures fork-authored commits. */
|
|
413
485
|
upstream?: string;
|
|
414
|
-
/**
|
|
415
|
-
|
|
486
|
+
/**
|
|
487
|
+
* How exposed this entry is — see {@link FleetTrust}. `"public-repo"` marks an
|
|
488
|
+
* outside-contribution surface (hygiene needles apply). Undeclared on a non-corp entry is a
|
|
489
|
+
* `fleet check` finding, never a silent "private".
|
|
490
|
+
*/
|
|
491
|
+
trust?: FleetTrust;
|
|
492
|
+
/** A declared `trust` value outside the vocabulary — preserved verbatim so the check can name it. */
|
|
493
|
+
trustRaw?: string;
|
|
416
494
|
staleAfterDays?: number;
|
|
495
|
+
/**
|
|
496
|
+
* How this entry's git hooks are governed. `"none"` declares that generated gates are
|
|
497
|
+
* deliberately absent — a prose repo with nothing mechanically checkable, or one whose gate is
|
|
498
|
+
* hand-written on purpose. Wall findings cannot be quieted through the ledger (004: their only
|
|
499
|
+
* honest resolution is fixing them), which is right for leak and partition conditions but
|
|
500
|
+
* wrong for a state a fleet legitimately chooses; without this, a settled decision is
|
|
501
|
+
* re-reported on every sweep until the report is ignored.
|
|
502
|
+
*/
|
|
503
|
+
gates?: string;
|
|
417
504
|
/** Per-entry state char-budget override (ships unset — the schema slot exists). */
|
|
418
505
|
stateBudget?: number;
|
|
419
506
|
contract: FleetContract;
|
|
@@ -437,6 +524,8 @@ interface FleetManifest {
|
|
|
437
524
|
dir: string;
|
|
438
525
|
/** Resolved absolute fleet root (registry shape), when declared. */
|
|
439
526
|
root?: string;
|
|
527
|
+
/** Fleet-wide orientation root, when the manifest declares one. Absent = this fleet has none. */
|
|
528
|
+
orientation?: FleetOrientation;
|
|
440
529
|
machineProfile?: FleetProfile;
|
|
441
530
|
corpHosts: string[];
|
|
442
531
|
labels: Record<string, string>;
|
|
@@ -656,7 +745,7 @@ declare const gateIntegrityLens: Lens;
|
|
|
656
745
|
* change meaning. Stamped into facts, baselines, and generated artifacts so drift against the
|
|
657
746
|
* pack is computable and `harvest` has something to diff.
|
|
658
747
|
*/
|
|
659
|
-
declare const PACK_VERSION = "
|
|
748
|
+
declare const PACK_VERSION = "3";
|
|
660
749
|
|
|
661
750
|
declare const VERSION: string;
|
|
662
751
|
declare const NAME: string;
|