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.
Files changed (38) hide show
  1. package/CHANGELOG.md +70 -2
  2. package/README.md +145 -37
  3. package/dist/{approve-VC4V3OPW.js → approve-3B2UL7LG.js} +6 -4
  4. package/dist/audit-4YB3K62W.js +12 -0
  5. package/dist/{brief-TZ45MNHD.js → brief-6EEXGQUW.js} +6 -4
  6. package/dist/chunk-CRJHIXJH.js +5 -0
  7. package/dist/{chunk-LVLZLCND.js → chunk-DKC7MLWG.js} +1 -1
  8. package/dist/chunk-EF6BPBG6.js +22 -0
  9. package/dist/{chunk-BH37LELV.js → chunk-HOLFW5QY.js} +8 -5
  10. package/dist/chunk-LT67FU2Y.js +59 -0
  11. package/dist/{chunk-IW6K5HLV.js → chunk-MEJ4MBVL.js} +2 -2
  12. package/dist/{chunk-EUIHYD6F.js → chunk-MMPV67FW.js} +2 -2
  13. package/dist/{chunk-ANCCGYSH.js → chunk-NMZ3RHWW.js} +31 -59
  14. package/dist/{chunk-FZSIOCOZ.js → chunk-R74SJ7HS.js} +1 -1
  15. package/dist/{chunk-LBRNZZZF.js → chunk-TADKOW6P.js} +1 -99
  16. package/dist/{chunk-KMAJIQHC.js → chunk-V6NZEIHF.js} +7 -5
  17. package/dist/{chunk-MWIK6ML3.js → chunk-VFUYQLW7.js} +141 -25
  18. package/dist/{chunk-KBF3SW3N.js → chunk-WWBF4Y7K.js} +1 -1
  19. package/dist/chunk-YLNQQZ5F.js +102 -0
  20. package/dist/cli.js +50 -16
  21. package/dist/config-MX7OYI7F.js +4 -0
  22. package/dist/{context-GYCLR3TG.js → context-4DGXO7JR.js} +5 -3
  23. package/dist/doctor-T76ZNL4S.js +19 -0
  24. package/dist/{fleet-N3G5ZIGR.js → fleet-WOPKM2Z3.js} +248 -10
  25. package/dist/gates-WIGDS5VF.js +262 -0
  26. package/dist/generate-I75IEXXF.js +6 -0
  27. package/dist/index.d.ts +122 -33
  28. package/dist/index.js +737 -257
  29. package/dist/{init-4CQOLFQG.js → init-NSYVVMF2.js} +9 -5
  30. package/dist/ledger-TMIV45AA.js +5 -0
  31. package/dist/scan-BAD2UJAL.js +5 -0
  32. package/dist/{scan-CYOOGJFG.js → scan-COEPZJBO.js} +6 -4
  33. package/dist/screen-U7JMWZPS.js +134 -0
  34. package/package.json +2 -2
  35. package/dist/audit-LZJRJS5V.js +0 -9
  36. package/dist/doctor-6JDAHGDT.js +0 -16
  37. package/dist/gates-G5BCMRRV.js +0 -71
  38. 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 };
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ export { derivedCommands, planWorkflow } from './chunk-VFUYQLW7.js';
3
+ import './chunk-NMZ3RHWW.js';
4
+ import './chunk-R74SJ7HS.js';
5
+ import './chunk-CRJHIXJH.js';
6
+ import './chunk-YLNQQZ5F.js';
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. "NGRE2E"). */
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
- /** `"public-repo"` marks an outside-contribution surface (hygiene needles apply). */
415
- trust?: string;
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 = "2";
748
+ declare const PACK_VERSION = "3";
660
749
 
661
750
  declare const VERSION: string;
662
751
  declare const NAME: string;