etymd 0.15.0 → 0.17.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/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # etymd
2
2
 
3
+ ## 0.17.0
4
+
5
+ ### Minor Changes
6
+
7
+ - The content screen's allow file accepts a `generated <path-regex>` record: a path declared
8
+ as regenerated data is still read and still screened, but vocabulary-class patterns drop
9
+ for it — secret-class patterns and the machine-path check stay — and the exempted paths
10
+ are named in the output, so a clean run never reads as fully screened. Provenance
11
+ (reason, date, author) is required.
12
+
13
+ ### Patch Changes
14
+
15
+ - 2bceef1: The fleet's gate-drift check now derives the pre-push audit tier the same way `etymd gates`
16
+ does. In a repo where no risk-tier rule can fire (no package manifest, no state doc) the
17
+ generator lowers the audit line to `--fail-on gap`; the drift comparison planned with the raw
18
+ config tier (`risk`), so its expectation permanently differed from the hook the generator
19
+ itself writes — a `gate-stale` finding on every repo of that shape, unclearable by the action
20
+ it names. The derivation now lives in `planWorkflow` itself, where the hook is built, so every
21
+ caller (the drift check, `etymd init`, the programmatic surface) generates identical bytes; a
22
+ tier the repo pinned in config is still never lowered.
23
+ - 7221fea: The content screen's machine-path rule now fires only at a path boundary (line start,
24
+ whitespace, a quote, `=`, `(`, `[` or `:`). A home path inside a longer token or a URL —
25
+ a test fixture's temp root (`$ROOT/home/‹x›/`), a docs URL with a home segment — is not a
26
+ machine path, and no longer prints the same advisory line on every push. A bare
27
+ `/Users/<name>/…` or `/home/<name>/…` still hits.
28
+
29
+ ## 0.16.0
30
+
31
+ ### Minor Changes
32
+
33
+ - dc77a1f: `contract.milestones` and `etymd fleet board` — a project declares its plan in `MILESTONES.md`
34
+ (`# Milestones`, then `| id | milestone | goal | status | next | effort | depends-on |`); the sweep
35
+ files a gap when a declared file is absent or off-shape; `fleet add` registers the file when
36
+ present; `fleet board --initiatives <file> --out <file>` renders every project's rows plus a ranked
37
+ initiatives table and totals, deterministic, guarded entries excluded, exit 1 on holes.
38
+ - b3fb81a: The fleet manifest's second profile is now `guarded` (it had another name before), with every derived name
39
+ following: `guardedHosts` in the local file, `--profile guarded`, the `guarded/<name>/` persistence
40
+ zone beside the manifest, and the guarded-worktree wall checks. No alias for the old value: a
41
+ manifest still carrying it fails `fleet check` and names the entry. Prose and examples describe the
42
+ feature as a second, guarded workspace whose entries stay alias-only and machine-pinned.
43
+ - 79af085: `etymd propose` — score the sweep's improvement findings and recurring classes against a
44
+ fleet-authored rubric file (`criterion: <weight>` labeled lines; criteria: severity, economy,
45
+ confidence, breadth — an unknown criterion is refused quoting the line) and emit deterministic,
46
+ read-only `proposal/1` records carrying score, the fired rubric lines, and an implications block
47
+ (projects, files, gates, reversibility) extracted from finding evidence. Guarded entries are
48
+ excluded from the output entire. `--manifest` (fresh read-only sweep) or `--from <fleet.json>`
49
+ (stored sweep), `--json`. Decision record: docs/decisions/012.
50
+
3
51
  ## 0.15.0
4
52
 
5
53
  ### Minor Changes
@@ -422,18 +470,18 @@ so a pack number cited in a baseline always means exactly one pack.
422
470
 
423
471
  ### Minor Changes
424
472
 
425
- - e265713: `fleet add --profile corp` now records the alias-to-directory mapping too, not just the entry.
473
+ - e265713: `fleet add --profile guarded` now records the alias-to-directory mapping too, not just the entry.
426
474
 
427
- A corp entry in the tracked manifest is deliberately alias-only — no path, no remote — which is
428
- what keeps employer names out of a file that gets pushed. It also means the entry resolves to
429
- nothing on its own. Registering wrote only that half, so every corp registration ended as a
475
+ A guarded entry in the tracked manifest is deliberately alias-only — no path, no remote — which is
476
+ what keeps guarded-side names out of a file that gets pushed. It also means the entry resolves to
477
+ nothing on its own. Registering wrote only that half, so every guarded registration ended as a
430
478
  dangling entry that `fleet check` reported immediately and the user had to fix by hand, in the
431
479
  one file the tool otherwise never asks anyone to hand-edit.
432
480
 
433
481
  The mapping is written `~`-relative, so the local manifest stays portable between machines, and
434
482
  merged into the existing document so hand-maintained entries survive.
435
483
 
436
- Two refusals guard it, because this file is the one place real employer directory names are
484
+ Two refusals guard it, because this file is the one place real guarded-side directory names are
437
485
  written down. If the local manifest is not gitignored, registration refuses rather than creating
438
486
  a file git would track — a leak the tool creates is worse than a registration it declines to
439
487
  finish. If the file exists but is not valid JSON, it refuses rather than overwriting mappings
@@ -452,7 +500,7 @@ so a pack number cited in a baseline always means exactly one pack.
452
500
  It is also the field that turned a mis-profiled entry into a real disclosure. A raw remote URL
453
501
  carries the host and the internal group path; `path` carries a bare directory name. Removing a
454
502
  field no consumer reads retires that class outright, with no host-matching heuristic, and unlike
455
- the corp-host guard it keeps working on a machine that has no local manifest to read corp hosts
503
+ the guarded-host guard it keeps working on a machine that has no local manifest to read guarded hosts
456
504
  from. The remote stays derivable from the checkout at any time, which is where it came from.
457
505
 
458
506
  Existing entries that already carry a remote are left alone — nothing reads them, and rewriting
@@ -462,20 +510,20 @@ so a pack number cited in a baseline always means exactly one pack.
462
510
 
463
511
  ### Patch Changes
464
512
 
465
- - 88492b5: Fix: `fleet add --profile corp` was silently ignored, registering employer repos as personal.
513
+ - 88492b5: Fix: `fleet add --profile guarded` was silently ignored, registering guarded repos as personal.
466
514
 
467
515
  `fleet` declares its own `--profile` (the sweep filter), and commander hands a parent-declared
468
- option the value even when it is typed after the subcommand. So `fleet add <dir> --profile corp`
516
+ option the value even when it is typed after the subcommand. So `fleet add <dir> --profile guarded`
469
517
  left the subcommand's own option undefined, the placement fell back to personal, and the
470
- personal branch records `path` and the RAW `remote` — writing the employer host and its internal
518
+ personal branch records `path` and the RAW `remote` — writing the guarded host and its internal
471
519
  group structure into a manifest that is tracked and pushed. That is precisely the disclosure the
472
520
  alias convention exists to prevent. The same shadowing class was already fixed for `--manifest`
473
521
  and `--json`; `--profile` was missed.
474
522
 
475
523
  Two changes, because the flag alone is not enough. The CLI now reads the merged option view, so
476
- the flag works. And `fleet add` refuses outright when the target's remote matches a corp host
477
- declared in the local manifest while the profile is not corp — the manifest already knows which
478
- hosts are the employer's and the remote has just been read, so the tool has every fact needed to
524
+ the flag works. And `fleet add` refuses outright when the target's remote matches a guarded host
525
+ declared in the local manifest while the profile is not guarded — the manifest already knows which
526
+ hosts are the guarded side's and the remote has just been read, so the tool has every fact needed to
479
527
  prevent this without anyone remembering a flag. It refuses rather than auto-correcting:
480
528
  placement is the user's decision, but it may not be made by omission.
481
529
 
@@ -612,7 +660,7 @@ so a pack number cited in a baseline always means exactly one pack.
612
660
  value it explains changes, because a stale reason misleads exactly where it meant to inform.
613
661
 
614
662
  **`etymd fleet add`** registers a project, prompting for what no scan can derive and refusing to
615
- write an incomplete entry. Fleet manifests gain a mandatory `trust` level on non-corp entries —
663
+ write an incomplete entry. Fleet manifests gain a mandatory `trust` level on non-guarded entries —
616
664
  absence is a finding, never a silent default — a manifest-level `orientation.root` replacing
617
665
  per-entry links, and a gate-drift check that reports a repository missing a gate its siblings
618
666
  install.
@@ -672,9 +720,9 @@ so a pack number cited in a baseline always means exactly one pack.
672
720
 
673
721
  - New `state-freshness` truth lens: state/decisions artifacts dated by git committer dates only (never mtime); staleness is relative, so a dormant repo's old state is current; state char budget against the ~10k session-hook truncation; marker-gated decisions format checks (`Scope:`, duplicate/out-of-order `D-NNN` ids, past `Revisit:` dates as due review debt); ADR conventions (`docs/adr/`, `docs/decisions/`, `NNNN-*.md`) recognized natively.
674
722
  - New `etymd fleet` command family. The sweep runs a read-only audit per registered repo (`--manifest` required unless the cwd holds `registry.json` — no env var, no global pointer) and renders one line per project with a delta against `last.fleet.json`; detail only for new or risk findings. `fleet check` validates the manifest pair alone (dangling mappings, duplicate names, privacy leaks, machine paths). `fleet dismiss`/`fleet accept` resolve a project's finding from any cwd.
675
- - The manifest loader (`src/core/fleet.ts`) resolves both the fleet registry pair (`registry.json` + gitignored `registry.local.json`) and the legacy corpus pair (`sources.json` + `sources.local.json`); corp entries are opaque aliases resolved only through the local file, and every resolution failure is disclosed, never silently skipped.
676
- - Persistence invariants, pinned by tests: the sweep never creates `.etymd` anywhere; `--persist-ledgers` only persists into personal repos that already opted in; corp worktrees take zero writes under every flag combination — corp findings persist (and stay dismissible) at `<manifestDir>/corp/<name>/.etymd/`; zero corp-resolved content under the manifest repo's tracked paths.
677
- - Fleet-scope wall findings (lens id `fleet-manifest`): corp contract files inside a corp worktree, unregistered corp-remote checkouts under the fleet root, tracked `/Users/` paths in the manifest's own repo, private needles inside `trust: "public-repo"` entries, corp-host commit emails on personal entries.
723
+ - The manifest loader (`src/core/fleet.ts`) resolves both the fleet registry pair (`registry.json` + gitignored `registry.local.json`) and the legacy corpus pair (`sources.json` + `sources.local.json`); guarded entries are opaque aliases resolved only through the local file, and every resolution failure is disclosed, never silently skipped.
724
+ - Persistence invariants, pinned by tests: the sweep never creates `.etymd` anywhere; `--persist-ledgers` only persists into personal repos that already opted in; guarded worktrees take zero writes under every flag combination — guarded findings persist (and stay dismissible) at `<manifestDir>/guarded/<name>/.etymd/`; zero guarded-resolved content under the manifest repo's tracked paths.
725
+ - Fleet-scope wall findings (lens id `fleet-manifest`): guarded contract files inside a guarded worktree, unregistered guarded-remote checkouts under the fleet root, tracked `/Users/` paths in the manifest's own repo, private needles inside `trust: "public-repo"` entries, guarded-host commit emails on personal entries.
678
726
  - Fork-aware freshness: entries with `upstream` are dated on fork-authored commits only (`HEAD --not --remotes=<upstream>`), with a disclosed fallback when the remote is absent.
679
727
 
680
728
  ## 0.1.0 — 2026-07-31
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Etymd
2
2
 
3
3
  <div align="center">
4
- <img src="https://raw.githubusercontent.com/triartleet/etymd/main/media/etymd-logo.png" width="520" alt="Etymd — a papyrus of written instructions, each line checked against the repository it describes">
4
+ <img src="https://raw.githubusercontent.com/fleetorders/etymd/main/media/etymd-logo.png" width="520" alt="Etymd — a papyrus of written instructions, each line checked against the repository it describes">
5
5
  <p>
6
6
  <a href="https://www.npmjs.com/package/etymd"><img src="https://img.shields.io/npm/v/etymd.svg?label=npm&color=cb3837" alt="npm version"></a>
7
- <a href="https://github.com/triartleet/etymd/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/triartleet/etymd/ci.yml?branch=main&label=CI" alt="CI"></a>
8
- <a href="https://github.com/triartleet/etymd/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="MIT license"></a>
7
+ <a href="https://github.com/fleetorders/etymd/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/fleetorders/etymd/ci.yml?branch=main&label=CI" alt="CI"></a>
8
+ <a href="https://github.com/fleetorders/etymd/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="MIT license"></a>
9
9
  </p>
10
10
  </div>
11
11
 
@@ -167,29 +167,31 @@ append race is a defect in the file's own convention, not a format opinion.
167
167
 
168
168
  **`fleet-manifest`** (via `etymd fleet`) — one truth guard across every repo you registered:
169
169
  per-repo audits plus checks on the fleet manifest itself and on the placement wall between
170
- personal and employer repos. See [the fleet manifest](#the-fleet-manifest-experimental) below.
170
+ personal and guarded repos. See [the fleet manifest](#the-fleet-manifest-experimental) below.
171
171
 
172
172
  ## Commands
173
173
 
174
- | Command | What it does |
175
- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
176
- | `etymd audit` | Verify every claim; ranked findings (risk → gap → polish) + ledger diff. `--lens`, `--truth`, `--json`, `--no-ledger`, `--fail-on <tier>`. |
177
- | `etymd init` | Onboard: approve the committed baseline; scaffold a minimal AGENTS.md **only if missing**. Never overwrites. |
178
- | `etymd doctor` | Alias for `audit --truth`. |
179
- | `etymd context` | The economy view: per-file always-loaded footprint + extraction candidates. |
180
- | `etymd gates` | Install local git-hook gates (pre-commit / commit-msg / pre-push, plus a publish screen where something ships) built from your own check scripts — and from the repo's shell surface, where it has one. |
181
- | `etymd screen` | Content screen: find text that must never be published. Four scopes — `--staged`, `--message`, `--tree`, `--dir`. Bring your own patterns; etymd ships none. |
182
- | `etymd scan` | The deterministic reckoning behind everything. `--json`. |
183
- | `etymd brief` | A grounded briefing your in-repo agent completes to author the semantic layer. |
184
- | `etymd premise` | `premise "<task>"` — is this the right task? What it names, verified against the repo; a brief for what only the agent can verify. `--file` (`-` = stdin), `--json`, `--no-brief`, `--fail-on <tier>`. No ledger. |
185
- | `etymd approve` | Refresh the committed baseline non-interactively after intentional structural changes. |
186
- | `etymd ledger` | The findings memory: every tracked finding with status and history. |
187
- | `etymd dismiss` | `dismiss <id> --reason <text>` — a dismissed finding never resurfaces without regressing. |
188
- | `etymd accept` | `accept <id>` — record a finding as accepted reality; visible in the ledger, out of the report. |
189
- | `etymd fleet` | Sweep every project in a fleet manifest: read-only per-repo audits + manifest/wall checks. `--manifest`, `--only`, `--profile`, `--truth`, `--persist-ledgers`, `--json`, `--fail-on`. |
190
- | `etymd fleet check` | Validate the manifest pair alone (no lenses): dangling mappings, duplicate names, privacy leaks, undeclared trust, machine paths. Non-zero exit on any finding. |
191
- | `etymd fleet add` | `add <dir>` — register a project: scans it, asks for what no scan can derive, and refuses to write an entry missing a mandatory field. `--name`, `--kind`, `--profile`, `--trust`, `-y`. |
192
- | `etymd fleet dismiss` / `accept` | `<name> <id>` — resolve a project's finding from any cwd; corp findings persist beside the manifest, never in the corp worktree. |
174
+ | Command | What it does |
175
+ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
176
+ | `etymd audit` | Verify every claim; ranked findings (risk → gap → polish) + ledger diff. `--lens`, `--truth`, `--json`, `--no-ledger`, `--fail-on <tier>`. |
177
+ | `etymd init` | Onboard: approve the committed baseline; scaffold a minimal AGENTS.md **only if missing**. Never overwrites. |
178
+ | `etymd doctor` | Alias for `audit --truth`. |
179
+ | `etymd context` | The economy view: per-file always-loaded footprint + extraction candidates. |
180
+ | `etymd gates` | Install local git-hook gates (pre-commit / commit-msg / pre-push, plus a publish screen where something ships) built from your own check scripts — and from the repo's shell surface, where it has one. |
181
+ | `etymd screen` | Content screen: find text that must never be published. Four scopes — `--staged`, `--message`, `--tree`, `--dir`. Bring your own patterns; etymd ships none. |
182
+ | `etymd scan` | The deterministic reckoning behind everything. `--json`. |
183
+ | `etymd brief` | A grounded briefing your in-repo agent completes to author the semantic layer. |
184
+ | `etymd premise` | `premise "<task>"` — is this the right task? What it names, verified against the repo; a brief for what only the agent can verify. `--file` (`-` = stdin), `--json`, `--no-brief`, `--fail-on <tier>`. No ledger. |
185
+ | `etymd approve` | Refresh the committed baseline non-interactively after intentional structural changes. |
186
+ | `etymd ledger` | The findings memory: every tracked finding with status and history. |
187
+ | `etymd dismiss` | `dismiss <id> --reason <text>` — a dismissed finding never resurfaces without regressing. |
188
+ | `etymd accept` | `accept <id>` — record a finding as accepted reality; visible in the ledger, out of the report. |
189
+ | `etymd fleet` | Sweep every project in a fleet manifest: read-only per-repo audits + manifest/wall checks. `--manifest`, `--only`, `--profile`, `--truth`, `--persist-ledgers`, `--json`, `--fail-on`. |
190
+ | `etymd fleet check` | Validate the manifest pair alone (no lenses): dangling mappings, duplicate names, privacy leaks, undeclared trust, machine paths. Non-zero exit on any finding. |
191
+ | `etymd fleet add` | `add <dir>` — register a project: scans it, asks for what no scan can derive, and refuses to write an entry missing a mandatory field. `--name`, `--kind`, `--profile`, `--trust`, `-y`. |
192
+ | `etymd fleet board` | Render the fleet board: every project's `MILESTONES.md` (contract key `milestones`, shape-checked by the sweep) plus a ranked initiatives table on one page. `--initiatives <file>`, `--out <file>`, `--json`. |
193
+ | `etymd propose` | Score the sweep's improvement findings + recurring classes against a fleet-authored rubric — stable `proposal/1` records, read-only, deterministic, guarded entries excluded. `--rubric <file>` (required), `--manifest <file>` or `--from <fleet.json>`, `--json`. |
194
+ | `etymd fleet dismiss` / `accept` | `<name> <id>` — resolve a project's finding from any cwd; guarded findings persist beside the manifest, never in the guarded worktree. |
193
195
 
194
196
  `--cwd <dir>` targets another directory. Read-only probing of any repo leaves **zero trace**
195
197
  (`audit --no-ledger` writes nothing).
@@ -324,7 +326,7 @@ the generated pre-push now carries a `shellcheck` step. Three properties are del
324
326
  ### The content screen (`etymd screen`)
325
327
 
326
328
  A separate question from "are the instructions true?": **does this repo carry text that must
327
- never be published?** Absolute home paths, an employer's name, an internal hostname, an account
329
+ never be published?** Absolute home paths, an organisation's name, an internal hostname, an account
328
330
  identifier — permanent the moment they are committed, because publishing exposes all history,
329
331
  not the current tree.
330
332
 
@@ -358,7 +360,7 @@ inline marker would not survive into the artifact. Those live in `.etymd-screen-
358
360
  repo root — one labeled line per field, so the pattern is never delimited:
359
361
 
360
362
  ```
361
- pattern ^AcmeCorp|BetaCorp$
363
+ pattern ^AcmeInc|BetaInc$
362
364
  reason fixture proving the detector fires on either name
363
365
  date 2026-08-15
364
366
  author someone
@@ -373,6 +375,24 @@ missing a field is reported and does not apply: an exemption is a hole in the ga
373
375
  nobody signed cannot be audited later. The file is read from the repo being screened, never a
374
376
  shared location, and it screens itself out (it necessarily contains every string it exempts).
375
377
 
378
+ The file carries a second record kind, for paths rather than lines:
379
+
380
+ ```
381
+ generated ^src/data/corpus\.json$
382
+ reason public-domain corpus, regenerated by scripts/corpus each January
383
+ date 2026-09-04
384
+ author someone
385
+ ```
386
+
387
+ A `generated` path is data a pipeline rebuilds — a public-domain corpus, generated fixtures —
388
+ and period English trips credential vocabulary (`passkey`, `secret`) on a schedule, training
389
+ exactly the `--no-verify` habit the screen exists to prevent. The file is still read and still
390
+ screened: only vocabulary-class patterns drop for it, while secret-class patterns and the
391
+ machine-path check stay — a generated file is precisely where a real credential would be least
392
+ visible. Provenance is unconditional (a path is never the repo naming itself), and the paths
393
+ that took the exemption are named in the output, so a clean run never reads as fully screened
394
+ where it was screened narrower.
395
+
376
396
  ### The commit subject, if you ask for it
377
397
 
378
398
  Off unless you turn it on:
@@ -418,10 +438,10 @@ steps:
418
438
  `etymd fleet` extends the one objective across every repository you work in — your fleet of
419
439
  **repositories**, not a fleet of agents. The manifest, `registry.json`, is itself an
420
440
  agent-context file: claims about your fleet (what exists, where, under which **profile** — the
421
- side of the wall an entry belongs to, `personal` or `corp`; the **wall** is the placement
422
- boundary between personal and employer content that the sweep polices). It rots like any
441
+ side of the wall an entry belongs to, `personal` or `guarded`; the **wall** is the placement
442
+ boundary between personal and guarded content that the sweep polices). It rots like any
423
443
  AGENTS.md does, and `etymd fleet` keeps it true. Decision record:
424
- [`docs/decisions/004-fleet-truth-guard.md`](https://github.com/triartleet/etymd/blob/main/docs/decisions/004-fleet-truth-guard.md). Both the
444
+ [`docs/decisions/004-fleet-truth-guard.md`](https://github.com/fleetorders/etymd/blob/main/docs/decisions/004-fleet-truth-guard.md). Both the
425
445
  registry schema and the fleet `--json` schema are **experimental through 0.2.x**.
426
446
 
427
447
  Two files beside each other — the split is the privacy model:
@@ -446,7 +466,7 @@ Two files beside each other — the split is the privacy model:
446
466
  "kind": "docs",
447
467
  "profile": "personal",
448
468
  "path": "notes",
449
- "trust": "private", // mandatory on every non-corp entry — see below
469
+ "trust": "private", // mandatory on every non-guarded entry — see below
450
470
  "staleAfterDays": 45, // per-entry freshness window
451
471
  "contract": { "state": "STATUS.md" }, // native conventions register, never migrate
452
472
  },
@@ -458,8 +478,14 @@ Two files beside each other — the split is the privacy model:
458
478
  "upstream": "origin", // freshness measured on fork-authored commits only
459
479
  "trust": "public-repo", // hygiene needles apply (see below)
460
480
  },
461
- // Corp entries: opaque alias, private, NO path — real dirs live only in the local file.
462
- { "name": "c-one", "kind": "repo", "profile": "corp", "private": true, "staleAfterDays": 45 },
481
+ // Guarded entries: opaque alias, private, NO path — real dirs live only in the local file.
482
+ {
483
+ "name": "c-one",
484
+ "kind": "repo",
485
+ "profile": "guarded",
486
+ "private": true,
487
+ "staleAfterDays": 45,
488
+ },
463
489
  ],
464
490
  }
465
491
  ```
@@ -468,23 +494,23 @@ Two files beside each other — the split is the privacy model:
468
494
 
469
495
  ```jsonc
470
496
  {
471
- "machineProfile": "corp", // which profile this machine resolves; "personal" resolves corp entries disclosed-absent
497
+ "machineProfile": "guarded", // which profile this machine resolves; "personal" resolves guarded entries disclosed-absent
472
498
  "root": "~/projects", // optional per-machine root override
473
- "dirs": { "c-one": "~/projects/real-corp-dir" },
474
- "labels": { "c-one": "real-corp-dir" },
475
- "corpHosts": ["git.example-corp.com"],
499
+ "dirs": { "c-one": "~/projects/real-guarded-dir" },
500
+ "labels": { "c-one": "real-guarded-dir" },
501
+ "guardedHosts": ["git.example-guarded.com"],
476
502
  }
477
503
  ```
478
504
 
479
505
  Two fields the scan can never derive, so the manifest must declare them:
480
506
 
481
- - **`trust` — mandatory on every non-corp entry** (`public-repo` | `public-bound` | `private`).
507
+ - **`trust` — mandatory on every non-guarded entry** (`public-repo` | `public-bound` | `private`).
482
508
  It is a _safety predicate_, not a label: it decides whether content screening applies, so an
483
509
  absent value is reported (`fleet check` flags it), never read as a silent `private`.
484
510
  `public-bound` means private today, plausibly public later — screened exactly as hard as
485
511
  public, because publishing exposes _all_ history: the scrub has to precede the first commit,
486
512
  not the visibility flip. A value outside the vocabulary is flagged rather than coerced, so a
487
- typo can never quietly disable screening. Corp entries omit it — `profile: "corp"` already
513
+ typo can never quietly disable screening. Guarded entries omit it — `profile: "guarded"` already
488
514
  implies the answer.
489
515
  - **`orientation.root` — optional, declared once.** Names the one entry every other entry is
490
516
  guided by. Declared at the manifest level rather than repeated per entry, because a per-entry
@@ -499,10 +525,10 @@ CI) must pass every mandatory value as a flag — there is deliberately no defau
499
525
  How the sweep behaves:
500
526
 
501
527
  - **Read-only by default, everywhere.** The sweep never creates `.etymd` anywhere.
502
- `--persist-ledgers` persists only into personal repos that already opted in, and a **corp
528
+ `--persist-ledgers` persists only into personal repos that already opted in, and a **guarded
503
529
  worktree is never written** — regardless of flags, even if a stray `.etymd` exists inside it
504
- (pinned by test). Corp findings stay dismissible: their ledger lives at
505
- `<manifest-dir>/corp/<name>/.etymd/`, beside the manifest.
530
+ (pinned by test). Guarded findings stay dismissible: their ledger lives at
531
+ `<manifest-dir>/guarded/<name>/.etymd/`, beside the manifest.
506
532
  - **Deltas.** Each sweep compares against `last.fleet.json` stored beside the manifest and
507
533
  renders `Δ +new −resolved` per project. Add `*.fleet.json` to the manifest repo's
508
534
  `.gitignore` — sweep output is local-only and never tracked.
@@ -514,17 +540,64 @@ How the sweep behaves:
514
540
  that instruction files are legitimately absent in that project — the sweep drops its
515
541
  missing-contract finding instead of re-reporting a decision every run. Absence disclosed on
516
542
  purpose is a state, not a gap.
517
- - **Wall checks.** Corp contract files found inside a corp worktree, unregistered checkouts
518
- under the fleet root whose remotes match `corpHosts`, tracked `/Users/` paths in the manifest
543
+ - **Wall checks.** Guarded contract files found inside a guarded worktree, unregistered checkouts
544
+ under the fleet root whose remotes match `guardedHosts`, tracked `/Users/` paths in the manifest
519
545
  repo, private **needles** — the identifiers the local file holds (labels, dir names, hosts) —
520
- inside `trust: "public-repo"` entries, and corp-host commit emails on personal entries — each
546
+ inside `trust: "public-repo"` entries, and guarded-host commit emails on personal entries — each
521
547
  a risk finding; each check that cannot run is disclosed.
522
548
  - **No global pointer.** `--manifest` is required unless the cwd holds `registry.json` — there
523
549
  is deliberately no env var and no home-directory pointer.
550
+ - **Milestones and the fleet board.** A project declares its plan in one file — `MILESTONES.md`,
551
+ registered as `"contract": { "milestones": "MILESTONES.md" }` (`fleet add` registers it when the
552
+ file is present; `"none"` declares a project deliberately carries no plan). The file has a fixed
553
+ shape so the fleet can read every plan without an agent: a `# Milestones` heading, then a table
554
+ `| id | milestone | goal | status | next | effort | depends-on |` — `id` is `M<n>`, `goal` is 1, 2
555
+ or 3 (your fleet's own ordered goals, declared outside this tool), `status` is planned | active |
556
+ blocked | done, `next` is the one concrete next step, `effort` the S | M | L remaining,
557
+ `depends-on` a list of ids or `—`. Prose after the table is free. The sweep files a gap when a
558
+ declared file is absent or off-shape. `etymd fleet board --initiatives <file> --out <file>`
559
+ renders every project's rows, a ranked initiatives table (`| rank | id | initiative | goal |
560
+ status | next | effort | projects | depends-on |`, the one hand-edited fleet-level surface), and
561
+ totals; guarded entries never appear on it. Day-precision stamp, deterministic output, exit code 1
562
+ when any project is missing or invalid — a board with holes still renders, and says so.
524
563
 
525
564
  Formatter interop for the `.etymd` state the sweep resolves: same rule as everywhere — see
526
565
  [the files Etymd keeps](#the-files-etymd-keeps).
527
566
 
567
+ ### Rubric-scored proposals (`etymd propose`)
568
+
569
+ The sweep already gives every improvement finding an action, an effort and a confidence, and
570
+ names the classes open in two or more projects. `etymd propose` adds the scoring step — against
571
+ a rubric **your fleet authors**, because what is worth doing is your call, not the tool's:
572
+
573
+ ```
574
+ severity: 2 # risk=3 · gap=2 · polish=1
575
+ economy: 3 # S=3 · M=2 · L=1
576
+ confidence: 1 # high=3 · medium=2 · low=1
577
+ breadth: 4 # projects carrying it, capped at 3
578
+ ```
579
+
580
+ One criterion per line (`#` comments and blanks ignored). Those four criteria are the whole
581
+ vocabulary — each is computed from finding facts, so a score is arithmetic, not an opinion; a
582
+ line naming anything else is refused quoting the line. `score` is Σ weight × value; a line
583
+ **fires** (listed in `matched`) when the subject reads at/above the criterion's midpoint.
584
+
585
+ ```bash
586
+ etymd propose --manifest registry.json --rubric opportunity.rubric --json
587
+ # or, without re-sweeping: --from <fleet.json> (a stored `etymd fleet --json` output)
588
+ ```
589
+
590
+ Subjects are every `kind: improvement` finding from personal projects plus every recurring
591
+ class, recomputed over personal projects only — **guarded entries are excluded from the output
592
+ entire, by name**. A class is scored conservatively (worst tier, dearest effort, weakest
593
+ confidence). Each `proposal/1` record carries id, class, projects, action, effort, confidence,
594
+ score, the fired rubric lines, and an `implications` block (projects, files, gates,
595
+ reversibility) extracted from the findings' evidence — files are path-shaped evidence tokens,
596
+ and `undetermined` reversibility says so rather than guessing. Read-only and deterministic: no
597
+ timestamps, nothing written, identical input → identical bytes, so a filed proposal can be
598
+ re-derived and compared. Decision record:
599
+ [`docs/decisions/012-propose-rubric-scored-proposals.md`](https://github.com/fleetorders/etymd/blob/main/docs/decisions/012-propose-rubric-scored-proposals.md).
600
+
528
601
  ## Programmatic use
529
602
 
530
603
  ```ts
@@ -537,7 +610,7 @@ console.log(audit.findings) // one schema: claim · evidence · why · action ·
537
610
  ## The corpus (how this is validated)
538
611
 
539
612
  Etymd is developed against a corpus of real sibling repos rather than fixtures alone —
540
- [`sources.json`](https://github.com/triartleet/etymd/blob/main/sources.json) lists them by shape. Every heuristic here exists because a real
613
+ [`sources.json`](https://github.com/fleetorders/etymd/blob/main/sources.json) lists them by shape. Every heuristic here exists because a real
541
614
  repo proved the previous one wrong, and each skip class in the truth lens is a false positive that
542
615
  a corpus run caught.
543
616
 
@@ -555,11 +628,12 @@ Without it — on a fresh clone or in CI — those suites skip cleanly and the r
555
628
 
556
629
  ## Decision record & roadmap
557
630
 
558
- [`docs/decisions/`](https://github.com/triartleet/etymd/tree/main/docs/decisions) — 001 founding · 002 foundation re-lock · **003 the truth-guard
631
+ [`docs/decisions/`](https://github.com/fleetorders/etymd/tree/main/docs/decisions) — 001 founding · 002 foundation re-lock · **003 the truth-guard
559
632
  pivot** (the current identity; includes the state-of-the-field investigation it rests on) ·
560
633
  **004 fleet mode** (the truth guard across your repositories) · 005 declared rules (design only) ·
561
- **006 local gate provenance** (what the tool may read, and what it may rewrite).
562
- [`ROADMAP.md`](https://github.com/triartleet/etymd/blob/main/ROADMAP.md) — what's now / next / later, and the accepted heuristic
634
+ **006 local gate provenance** (what the tool may read, and what it may rewrite) · 011 milestones
635
+ & the fleet board · 012 `etymd propose` (rubric-scored proposals).
636
+ [`ROADMAP.md`](https://github.com/fleetorders/etymd/blob/main/ROADMAP.md) — what's now / next / later, and the accepted heuristic
563
637
  trade-offs.
564
638
 
565
639
  ## License
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { section, theme, print, renderBaselineDrift, glyph } from './chunk-HI7NWPRA.js';
3
- import { scanProject } from './chunk-Y6RZRED3.js';
4
- import { VERSION } from './chunk-DBWDMIYO.js';
3
+ import { scanProject } from './chunk-446EZNAQ.js';
4
+ import { VERSION } from './chunk-BUMOGHQA.js';
5
5
  import { readBaseline, summarizeBaselineDrift, isDriftEmpty, writeBaseline, deriveProfile } from './chunk-P6ATKV2R.js';
6
6
  import { PACK_VERSION } from './chunk-YQZDYDAK.js';
7
7
  import './chunk-4VPBP6K6.js';
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- export { run } from './chunk-CEO3BXQB.js';
3
- import './chunk-2RNQ6OLV.js';
4
- import './chunk-IWG77WV3.js';
2
+ export { run } from './chunk-ANFNXFRI.js';
3
+ import './chunk-WKP7M2B3.js';
4
+ import './chunk-FNA4R5KT.js';
5
5
  import './chunk-3E2IPCRY.js';
6
6
  import './chunk-HI7NWPRA.js';
7
- import './chunk-Y6RZRED3.js';
8
- import './chunk-DBWDMIYO.js';
7
+ import './chunk-446EZNAQ.js';
8
+ import './chunk-BUMOGHQA.js';
9
9
  import './chunk-P6ATKV2R.js';
10
10
  import './chunk-YQZDYDAK.js';
11
11
  import './chunk-4VPBP6K6.js';
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { print, theme } from './chunk-HI7NWPRA.js';
3
- import { scanProject } from './chunk-Y6RZRED3.js';
4
- import './chunk-DBWDMIYO.js';
3
+ import { scanProject } from './chunk-446EZNAQ.js';
4
+ import './chunk-BUMOGHQA.js';
5
5
  import { ETYMD_DIR } from './chunk-P6ATKV2R.js';
6
6
  import './chunk-YQZDYDAK.js';
7
7
  import './chunk-4VPBP6K6.js';
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { VERSION } from './chunk-DBWDMIYO.js';
2
+ import { VERSION } from './chunk-BUMOGHQA.js';
3
3
  import { PACK_VERSION } from './chunk-YQZDYDAK.js';
4
4
  import { normalizeRelPath, isDirectory, readJson, git, matchesAnyGlob, pathExists, readText } from './chunk-4VPBP6K6.js';
5
5
  import path from 'path';
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { runAudit } from './chunk-2RNQ6OLV.js';
3
- import { parseFailOnTier, meetsFailOn } from './chunk-IWG77WV3.js';
2
+ import { runAudit } from './chunk-WKP7M2B3.js';
3
+ import { parseFailOnTier, meetsFailOn } from './chunk-FNA4R5KT.js';
4
4
  import { print, section, theme, renderLensCoverage, renderFindings, renderLedgerDiff } from './chunk-HI7NWPRA.js';
5
5
 
6
6
  // src/commands/audit.ts
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // package.json
3
3
  var package_default = {
4
- version: "0.15.0"};
4
+ version: "0.17.0"};
5
5
 
6
6
  // src/version.ts
7
7
  var VERSION = package_default.version;
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { expandFileGlobs } from './chunk-Y6RZRED3.js';
2
+ import { expandFileGlobs } from './chunk-446EZNAQ.js';
3
3
  import { DEFAULT_CONFIG, CONFIG_FILE } from './chunk-P6ATKV2R.js';
4
4
  import { readText, isDirectory, matchesAnyGlob, normalizeRelPath, readJson, pathExists, git } from './chunk-4VPBP6K6.js';
5
5
  import path3 from 'path';
@@ -448,6 +448,13 @@ var PLACEHOLDER_PREFIX_RE = /^(?:my|your)-/i;
448
448
  function isPlaceholderClaim(token) {
449
449
  return token.split("/").some((seg) => PLACEHOLDER_PREFIX_RE.test(seg) || PLACEHOLDER_SEGMENTS.has(seg.toLowerCase()));
450
450
  }
451
+ var NAMESPACE_IDENT = "[A-Za-z][A-Za-z0-9-]{0,15}";
452
+ var NAMESPACE_STOP = new Set(
453
+ "note notes see warning caution caveat caveats example examples ex eg ie nb ps re per file files path paths hint tip tips todo fixme step steps rule rules ref refs fact facts output outputs input inputs result results summary status overview next prev then thus hence plus goal goals spec specs context".split(" ")
454
+ );
455
+ function isNamespace(ident) {
456
+ return Boolean(ident) && !NAMESPACE_STOP.has(ident.toLowerCase());
457
+ }
451
458
  function claimContext(text, index) {
452
459
  const start = text.lastIndexOf("\n", index) + 1;
453
460
  const endRaw = text.indexOf("\n", index);
@@ -467,8 +474,9 @@ ${line}`;
467
474
  }
468
475
  return line;
469
476
  }
470
- function extractPathClaims(text) {
477
+ function extractPathClaims(text, opts = {}) {
471
478
  const prospectiveOnly = /* @__PURE__ */ new Map();
479
+ const namespacedOnly = /* @__PURE__ */ new Map();
472
480
  const placeholder = /* @__PURE__ */ new Set();
473
481
  for (const m of text.matchAll(/`([^`\n]+)`/g)) {
474
482
  const token = m[1].trim();
@@ -490,14 +498,21 @@ function extractPathClaims(text) {
490
498
  }
491
499
  const prospective2 = CREATION_CONTEXT_RE.test(claimContext(text, m.index ?? 0));
492
500
  prospectiveOnly.set(claim, (prospectiveOnly.get(claim) ?? true) && prospective2);
501
+ if (opts.namespaces) {
502
+ const nsLead = new RegExp(`(${NAMESPACE_IDENT}):[ \\t]*$`).exec(text.slice(0, m.index ?? 0));
503
+ const prefixed = isNamespace(nsLead?.[1]);
504
+ namespacedOnly.set(claim, (namespacedOnly.get(claim) ?? true) && prefixed);
505
+ }
493
506
  }
494
507
  const paths = [];
495
508
  const prospective = [];
509
+ const namespaced = [];
496
510
  for (const [claim, only] of prospectiveOnly) {
497
511
  if (only) prospective.push(claim);
512
+ else if (namespacedOnly.get(claim)) namespaced.push(claim);
498
513
  else paths.push(claim);
499
514
  }
500
- return { paths, prospective, placeholder: [...placeholder] };
515
+ return { paths, prospective, namespaced, placeholder: [...placeholder] };
501
516
  }
502
517
  var LOCAL_REF_LEADINS = new Set(
503
518
  "decision decisions entry entries ruling rulings record records ledger id ids item items see per in of on at by to as is was are were the a an and or but not with under over from via vs than after before since between through against latest newest earliest only also still now supersedes superseded superseding amends amended extends extended cites cited citing adds added adding wrote written writes locked locks closed closes opened opens resolves resolved reopened recorded number numbers".split(" ")
@@ -604,7 +619,9 @@ function emptyCounters() {
604
619
  prospectiveSkipped: 0,
605
620
  placeholderSkipped: 0,
606
621
  qualifiedRefsSkipped: 0,
607
- unresolvableRefs: 0
622
+ unresolvableRefs: 0,
623
+ namespacedSkipped: 0,
624
+ outsideRepoSkipped: 0
608
625
  };
609
626
  }
610
627
  async function checkTextClaims(env, file, opts, counters) {
@@ -642,9 +659,12 @@ async function checkTextClaims(env, file, opts, counters) {
642
659
  confidence: "high"
643
660
  });
644
661
  }
645
- const { paths, prospective, placeholder } = extractPathClaims(file.text);
662
+ const { paths, prospective, placeholder, namespaced } = extractPathClaims(file.text, {
663
+ namespaces: opts.treatNamespacedPrefixes
664
+ });
646
665
  counters.prospectiveSkipped += prospective.length;
647
666
  counters.placeholderSkipped += placeholder.length;
667
+ counters.namespacedSkipped += namespaced.length;
648
668
  const missing = [];
649
669
  for (const claim of paths) {
650
670
  if (await env.pathResolves(claim)) examined.push({ kind: "path", value: claim, exists: true });
@@ -654,6 +674,11 @@ async function checkTextClaims(env, file, opts, counters) {
654
674
  const gitignored = new Set((ignoredOut ?? "").split("\n").filter(Boolean));
655
675
  let pathFindings = 0;
656
676
  for (const claim of missing) {
677
+ if (opts.rootedFirstSegments && !opts.rootedFirstSegments.has(claim.split("/")[0] ?? claim)) {
678
+ counters.outsideRepoSkipped += 1;
679
+ examined.push({ kind: "path", value: claim, exists: null });
680
+ continue;
681
+ }
657
682
  if (gitignored.has(claim)) {
658
683
  counters.gitignoredSkipped += 1;
659
684
  examined.push({ kind: "path", value: claim, exists: null });
@@ -755,4 +780,4 @@ function checkDecisionRefs(file, ledger, opts, counters) {
755
780
  return { findings, examined };
756
781
  }
757
782
 
758
- export { KNOWN_EXTENSIONS, PATH_TOKEN_RE, buildTruthEnv, checkDecisionRefs, checkDocRefs, checkTextClaims, emptyCounters, listInstructionFiles, listStateDocuments, loadDecisionLedger, meetsFailOn, packageManagerUsage, parseFailOnTier, rankFindings, stateFreshnessLens };
783
+ export { KNOWN_EXTENSIONS, NAMESPACE_IDENT, NAMESPACE_STOP, PATH_TOKEN_RE, buildTruthEnv, checkDecisionRefs, checkDocRefs, checkTextClaims, emptyCounters, listInstructionFiles, listStateDocuments, loadDecisionLedger, meetsFailOn, packageManagerUsage, parseFailOnTier, rankFindings, stateFreshnessLens };