etymd 0.3.1 → 0.4.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 +49 -0
- package/README.md +7 -7
- package/dist/{approve-3B2UL7LG.js → approve-Y6TX4LTN.js} +3 -3
- package/dist/{audit-4YB3K62W.js → audit-AU4R45TP.js} +5 -5
- package/dist/{brief-6EEXGQUW.js → brief-TIKPGPFV.js} +3 -3
- package/dist/{chunk-MEJ4MBVL.js → chunk-7K3BDNLW.js} +1 -1
- package/dist/{chunk-V6NZEIHF.js → chunk-ECY7BLBT.js} +8 -8
- package/dist/{chunk-CRJHIXJH.js → chunk-HGSLHNNT.js} +1 -1
- package/dist/{chunk-VFUYQLW7.js → chunk-JTHZVEON.js} +28 -3
- package/dist/{chunk-DKC7MLWG.js → chunk-QYHROOMK.js} +1 -1
- package/dist/{chunk-HOLFW5QY.js → chunk-SXCR6RRM.js} +31 -2
- package/dist/cli.js +13 -13
- package/dist/{doctor-T76ZNL4S.js → doctor-OE5E36DX.js} +5 -5
- package/dist/{fleet-WOPKM2Z3.js → fleet-LSN5XZ5Q.js} +6 -6
- package/dist/{gates-WIGDS5VF.js → gates-IJNAQ7DC.js} +7 -5
- package/dist/{generate-I75IEXXF.js → generate-JEREMVVO.js} +2 -2
- package/dist/index.d.ts +25 -1
- package/dist/index.js +65 -11
- package/dist/{init-NSYVVMF2.js → init-BYZJOCS6.js} +4 -4
- package/dist/{scan-COEPZJBO.js → scan-DPEOA3KZ.js} +3 -3
- package/dist/scan-O3SM2ODH.js +5 -0
- package/package.json +1 -1
- package/dist/scan-BAD2UJAL.js +0 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# etymd
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 69d1aad: Gate the shell surface: `etymd gates` now installs a shellcheck step in repos that have one.
|
|
8
|
+
|
|
9
|
+
Package scripts are not the only executable surface a repo has, and in some repos they are not
|
|
10
|
+
the main one. A tools or infra repo can be entirely `bootstrap/*.sh` plus `.githooks/*` with no
|
|
11
|
+
`package.json` at all — and those repos were told "no correctness commands detected", which reads
|
|
12
|
+
as "nothing to check" when it means "nothing was looked at".
|
|
13
|
+
|
|
14
|
+
The scan now counts tracked files a shell executes (`facts.shell.scripts`), by extension and by
|
|
15
|
+
shebang, and the generated pre-push gains a shellcheck step when that count is non-zero. Three
|
|
16
|
+
properties, each one a lesson from a gate that failed:
|
|
17
|
+
|
|
18
|
+
- Scripts are re-discovered inside the hook at push time, by shebang over tracked files, never
|
|
19
|
+
baked in as a list. A generated list is correct the day it is written and silently wrong the
|
|
20
|
+
first time someone adds a script.
|
|
21
|
+
- A missing shellcheck binary is a loud skip that names the install command, never a quiet pass.
|
|
22
|
+
A check that goes silent when its tool is absent looks installed on every machine and is
|
|
23
|
+
installed on one.
|
|
24
|
+
- The blocking bar is severity `warning`; style and info print as advice. A gate with a high
|
|
25
|
+
false-positive rate does not make a repo careful, it teaches everyone the bypass flag — and
|
|
26
|
+
that flag is shared with the content screen, which must never be bypassed.
|
|
27
|
+
|
|
28
|
+
The install summary lists the step too, so accepting the plan shows what will actually run.
|
|
29
|
+
Pack version 4.
|
|
30
|
+
|
|
31
|
+
## 0.3.2
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- A truth finding from any lens now reaches a `doctor` run.
|
|
36
|
+
|
|
37
|
+
`doctor` filters to truth-only, and it used to filter at the **lens** level — so `gate-integrity`
|
|
38
|
+
(an improvement lens) was skipped entirely, and its `hooks-not-wired` finding (tier: risk —
|
|
39
|
+
"tracked hooks exist but never run") was invisible in the one view meant to catch it. The lens
|
|
40
|
+
label was never the right granularity: a lens can emit both kinds, and "hooks exist and are
|
|
41
|
+
unwired" is as objective as anything the truth lens reports.
|
|
42
|
+
|
|
43
|
+
`kind` now lives on the Finding, defaulting to the lens kind when a finding does not set its
|
|
44
|
+
own, and the `--truth` filter operates at the finding level. Every lens still runs under
|
|
45
|
+
`--truth`; findings are filtered afterward, so nothing objective is invisible.
|
|
46
|
+
|
|
47
|
+
Also in this release: CI proves the publish tarball's exact manifest on every push (packagers
|
|
48
|
+
ignore `.gitignore`, so a stray local file can ship while every git-scoped check stays green),
|
|
49
|
+
and the generated hook companions (`.githooks/<hook>.local`) ship in this repo as a worked
|
|
50
|
+
example of the resolve-by-name convention.
|
|
51
|
+
|
|
3
52
|
## 0.3.1
|
|
4
53
|
|
|
5
54
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Etymd
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
|
-
<img src="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/triartleet/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
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="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="MIT license"></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>
|
|
9
9
|
</p>
|
|
10
10
|
</div>
|
|
11
11
|
|
|
@@ -335,7 +335,7 @@ agent-context file: claims about your fleet (what exists, where, under which **p
|
|
|
335
335
|
side of the wall an entry belongs to, `personal` or `corp`; the **wall** is the placement
|
|
336
336
|
boundary between personal and employer content that the sweep polices). It rots like any
|
|
337
337
|
AGENTS.md does, and `etymd fleet` keeps it true. Decision record:
|
|
338
|
-
[`docs/decisions/004-fleet-truth-guard.md`](docs/decisions/004-fleet-truth-guard.md). Both the
|
|
338
|
+
[`docs/decisions/004-fleet-truth-guard.md`](https://github.com/triartleet/etymd/blob/main/docs/decisions/004-fleet-truth-guard.md). Both the
|
|
339
339
|
registry schema and the fleet `--json` schema are **experimental through 0.2.x**.
|
|
340
340
|
|
|
341
341
|
Two files beside each other — the split is the privacy model:
|
|
@@ -346,7 +346,7 @@ Two files beside each other — the split is the privacy model:
|
|
|
346
346
|
{
|
|
347
347
|
"registryVersion": 1,
|
|
348
348
|
"root": "~/projects", // ~ expands on the consumer side — never a machine home
|
|
349
|
-
"orientation": { "root": "
|
|
349
|
+
"orientation": { "root": "notes" }, // optional: the entry every other entry is guided by
|
|
350
350
|
"projects": [
|
|
351
351
|
{
|
|
352
352
|
"name": "web-app",
|
|
@@ -451,7 +451,7 @@ console.log(audit.findings) // one schema: claim · evidence · why · action ·
|
|
|
451
451
|
## The corpus (how this is validated)
|
|
452
452
|
|
|
453
453
|
Etymd is developed against a corpus of real sibling repos rather than fixtures alone —
|
|
454
|
-
[`sources.json`](sources.json) lists them by shape. Every heuristic here exists because a real
|
|
454
|
+
[`sources.json`](https://github.com/triartleet/etymd/blob/main/sources.json) lists them by shape. Every heuristic here exists because a real
|
|
455
455
|
repo proved the previous one wrong, and each skip class in the truth lens is a false positive that
|
|
456
456
|
a corpus run caught.
|
|
457
457
|
|
|
@@ -469,10 +469,10 @@ Without it — on a fresh clone or in CI — those suites skip cleanly and the r
|
|
|
469
469
|
|
|
470
470
|
## Decision record & roadmap
|
|
471
471
|
|
|
472
|
-
[`docs/decisions/`](docs/decisions
|
|
472
|
+
[`docs/decisions/`](https://github.com/triartleet/etymd/tree/main/docs/decisions) — 001 founding · 002 foundation re-lock · **003 the truth-guard
|
|
473
473
|
pivot** (the current identity; includes the state-of-the-field investigation it rests on) ·
|
|
474
474
|
**004 fleet mode** (the truth guard across your repositories).
|
|
475
|
-
[`ROADMAP.md`](ROADMAP.md) — what's now / next / later, the pre-publish checklist, and the
|
|
475
|
+
[`ROADMAP.md`](https://github.com/triartleet/etymd/blob/main/ROADMAP.md) — what's now / next / later, the pre-publish checklist, and the
|
|
476
476
|
accepted heuristic trade-offs.
|
|
477
477
|
|
|
478
478
|
## License
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { section, theme, print, renderBaselineDrift, glyph } from './chunk-TADKOW6P.js';
|
|
3
|
-
import { scanProject } from './chunk-
|
|
4
|
-
import { VERSION } from './chunk-
|
|
3
|
+
import { scanProject } from './chunk-SXCR6RRM.js';
|
|
4
|
+
import { VERSION } from './chunk-QYHROOMK.js';
|
|
5
5
|
import { readBaseline, summarizeBaselineDrift, isDriftEmpty, writeBaseline, deriveProfile } from './chunk-R74SJ7HS.js';
|
|
6
|
-
import { PACK_VERSION } from './chunk-
|
|
6
|
+
import { PACK_VERSION } from './chunk-HGSLHNNT.js';
|
|
7
7
|
import './chunk-YLNQQZ5F.js';
|
|
8
8
|
|
|
9
9
|
// src/commands/approve.ts
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export { run } from './chunk-
|
|
3
|
-
import './chunk-
|
|
2
|
+
export { run } from './chunk-7K3BDNLW.js';
|
|
3
|
+
import './chunk-ECY7BLBT.js';
|
|
4
4
|
import './chunk-LT67FU2Y.js';
|
|
5
5
|
import './chunk-WWBF4Y7K.js';
|
|
6
6
|
import './chunk-NMZ3RHWW.js';
|
|
7
7
|
import './chunk-TADKOW6P.js';
|
|
8
|
-
import './chunk-
|
|
9
|
-
import './chunk-
|
|
8
|
+
import './chunk-SXCR6RRM.js';
|
|
9
|
+
import './chunk-QYHROOMK.js';
|
|
10
10
|
import './chunk-R74SJ7HS.js';
|
|
11
|
-
import './chunk-
|
|
11
|
+
import './chunk-HGSLHNNT.js';
|
|
12
12
|
import './chunk-YLNQQZ5F.js';
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { print, theme } from './chunk-TADKOW6P.js';
|
|
3
|
-
import { scanProject } from './chunk-
|
|
4
|
-
import './chunk-
|
|
3
|
+
import { scanProject } from './chunk-SXCR6RRM.js';
|
|
4
|
+
import './chunk-QYHROOMK.js';
|
|
5
5
|
import { ETYMD_DIR } from './chunk-R74SJ7HS.js';
|
|
6
|
-
import './chunk-
|
|
6
|
+
import './chunk-HGSLHNNT.js';
|
|
7
7
|
import './chunk-YLNQQZ5F.js';
|
|
8
8
|
import { promises } from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { parseFailOnTier, runAudit, meetsFailOn } from './chunk-
|
|
2
|
+
import { parseFailOnTier, runAudit, meetsFailOn } from './chunk-ECY7BLBT.js';
|
|
3
3
|
import { print, section, theme, renderLensCoverage, renderFindings, renderLedgerDiff } from './chunk-TADKOW6P.js';
|
|
4
4
|
|
|
5
5
|
// src/commands/audit.ts
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { measureContext } from './chunk-LT67FU2Y.js';
|
|
3
3
|
import { readLedger, reconcileLedger, writeLedger, visibleFindings } from './chunk-WWBF4Y7K.js';
|
|
4
4
|
import { DEFAULT_CONFIG, readConfig, CONFIG_FILE } from './chunk-NMZ3RHWW.js';
|
|
5
|
-
import { scanProject, expandFileGlobs } from './chunk-
|
|
5
|
+
import { scanProject, expandFileGlobs } from './chunk-SXCR6RRM.js';
|
|
6
6
|
import { ETYMD_DIR, writeCachedFacts, readBaseline, deriveProfile, baselineCarriesMachinePath, BASELINE_FILE } from './chunk-R74SJ7HS.js';
|
|
7
|
-
import { PACK_VERSION } from './chunk-
|
|
7
|
+
import { PACK_VERSION } from './chunk-HGSLHNNT.js';
|
|
8
8
|
import { pathExists, readJson, git, readText, isDirectory, matchesAnyGlob, isCiEnvironment, normalizeRelPath } from './chunk-YLNQQZ5F.js';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import YAML from 'yaml';
|
|
@@ -474,6 +474,7 @@ function deriveGateFindings(inv, disclosures = []) {
|
|
|
474
474
|
finding({
|
|
475
475
|
id: `${LENS_ID2}/hooks-not-wired`,
|
|
476
476
|
tier: "risk",
|
|
477
|
+
kind: "truth",
|
|
477
478
|
claim: "Tracked .githooks/ exist but git core.hooksPath is unset \u2014 the hooks never run",
|
|
478
479
|
evidence: [".githooks/"],
|
|
479
480
|
why: "Every check in those hooks is silently skipped on this clone.",
|
|
@@ -1329,9 +1330,7 @@ async function runAudit(root, opts = {}) {
|
|
|
1329
1330
|
};
|
|
1330
1331
|
}
|
|
1331
1332
|
const profile = baseline?.profile ?? deriveProfile(facts);
|
|
1332
|
-
const selected = LENSES.filter(
|
|
1333
|
-
(lens) => (!opts.kind || lens.kind === opts.kind) && (!opts.lensIds || opts.lensIds.includes(lens.id))
|
|
1334
|
-
);
|
|
1333
|
+
const selected = LENSES.filter((lens) => !opts.lensIds || opts.lensIds.includes(lens.id));
|
|
1335
1334
|
const reports = [];
|
|
1336
1335
|
for (const lens of selected) {
|
|
1337
1336
|
try {
|
|
@@ -1349,11 +1348,12 @@ async function runAudit(root, opts = {}) {
|
|
|
1349
1348
|
});
|
|
1350
1349
|
}
|
|
1351
1350
|
}
|
|
1352
|
-
const all = reports.flatMap((r) => r.findings);
|
|
1351
|
+
const all = reports.flatMap((r) => r.findings.map((f) => ({ ...f, kind: f.kind ?? r.kind })));
|
|
1352
|
+
const visible = opts.kind ? all.filter((f) => f.kind === opts.kind) : all;
|
|
1353
1353
|
const previous = await readLedger(ledgerRoot);
|
|
1354
1354
|
const outOfScope = reports.flatMap((r) => r.outOfScope ?? []);
|
|
1355
1355
|
const { ledger, diff } = reconcileLedger(previous, all, void 0, outOfScope);
|
|
1356
|
-
const fullRun =
|
|
1356
|
+
const fullRun = !opts.kind && !opts.lensIds;
|
|
1357
1357
|
const ledgerWritable = !opts.readOnlyRoot || ledgerRoot !== root;
|
|
1358
1358
|
if ((opts.persistLedger ?? true) && fullRun && ledgerWritable) {
|
|
1359
1359
|
await writeLedger(ledgerRoot, ledger);
|
|
@@ -1364,7 +1364,7 @@ async function runAudit(root, opts = {}) {
|
|
|
1364
1364
|
baseline,
|
|
1365
1365
|
config,
|
|
1366
1366
|
reports,
|
|
1367
|
-
findings: rankFindings(visibleFindings(
|
|
1367
|
+
findings: rankFindings(visibleFindings(visible, previous)),
|
|
1368
1368
|
ledger,
|
|
1369
1369
|
diff
|
|
1370
1370
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { DEFAULT_CONFIG } from './chunk-NMZ3RHWW.js';
|
|
3
|
-
import { PACK_VERSION } from './chunk-
|
|
3
|
+
import { PACK_VERSION } from './chunk-HGSLHNNT.js';
|
|
4
4
|
import { readText, pathExists } from './chunk-YLNQQZ5F.js';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
|
|
@@ -158,6 +158,25 @@ ${localHookCall("commit-msg")}
|
|
|
158
158
|
exit 0
|
|
159
159
|
`;
|
|
160
160
|
}
|
|
161
|
+
function shellcheckStep() {
|
|
162
|
+
return `
|
|
163
|
+
# Shell correctness. Scripts are discovered by shebang over TRACKED files at push time, so a
|
|
164
|
+
# script added later is covered without regenerating this hook.
|
|
165
|
+
if command -v shellcheck >/dev/null 2>&1; then
|
|
166
|
+
scripts=$(git ls-files -z \\
|
|
167
|
+
| xargs -0 -I{} sh -c 'head -1 "{}" 2>/dev/null | grep -qE "^#!.*[/ ](ba|da|z)?sh( |$)" && echo "{}"' \\
|
|
168
|
+
| sort)
|
|
169
|
+
if [ -n "$scripts" ]; then
|
|
170
|
+
echo "\u203A shellcheck ($(printf '%s\\n' "$scripts" | wc -l | tr -d ' ') scripts, blocking at severity=warning)"
|
|
171
|
+
printf '%s\\n' "$scripts" | xargs shellcheck -S warning || {
|
|
172
|
+
echo " fix, or justify inline with '# shellcheck disable=SCxxxx # why'"
|
|
173
|
+
exit 1
|
|
174
|
+
}
|
|
175
|
+
fi
|
|
176
|
+
else
|
|
177
|
+
echo "\u203A shellcheck skipped (not on PATH) \u2014 install it to gate this repo's shell scripts"
|
|
178
|
+
fi`;
|
|
179
|
+
}
|
|
161
180
|
function generatePrePushHook(facts, gates) {
|
|
162
181
|
const run = runPrefix(facts.packageManager);
|
|
163
182
|
const c = facts.commands;
|
|
@@ -166,8 +185,14 @@ function generatePrePushHook(facts, gates) {
|
|
|
166
185
|
const steps = candidates.filter(
|
|
167
186
|
(key) => Boolean(key) && (allowed.has(key) || isSafeGateCommand(c.raw[key]))
|
|
168
187
|
).map((key) => `${run} ${key}`);
|
|
188
|
+
const shellStep = facts.shell?.scripts ? shellcheckStep() : "";
|
|
169
189
|
const body = steps.length ? steps.map((s) => `echo "\u203A ${s}"
|
|
170
|
-
${s} || exit 1`).join("\n") :
|
|
190
|
+
${s} || exit 1`).join("\n") : shellStep ? (
|
|
191
|
+
// A repo whose executable surface is shell HAS a correctness command — it just is not in
|
|
192
|
+
// package.json. Claiming "none detected" beside a step that is about to run would be the
|
|
193
|
+
// tool contradicting itself.
|
|
194
|
+
`echo "\u203A no package scripts \u2014 shell is this repo's checkable surface"`
|
|
195
|
+
) : 'echo "etymd: no correctness commands detected \u2014 add format:check / typecheck / lint"';
|
|
171
196
|
const failOn = gates?.failOn ?? "risk";
|
|
172
197
|
const auditStep = `
|
|
173
198
|
if command -v etymd >/dev/null 2>&1; then
|
|
@@ -180,7 +205,7 @@ fi`;
|
|
|
180
205
|
# etymd: correctness gate. Mirrors CI cheapest-first; blocks the push on any failure.
|
|
181
206
|
|
|
182
207
|
${localHookCall("pre-push")}
|
|
183
|
-
${body}
|
|
208
|
+
${body}${shellStep}
|
|
184
209
|
${auditStep}
|
|
185
210
|
|
|
186
211
|
# Content screen, second pass \u2014 the WHOLE TREE rather than one diff. Catches anything committed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { VERSION } from './chunk-
|
|
3
|
-
import { PACK_VERSION } from './chunk-
|
|
2
|
+
import { VERSION } from './chunk-QYHROOMK.js';
|
|
3
|
+
import { PACK_VERSION } from './chunk-HGSLHNNT.js';
|
|
4
4
|
import { normalizeRelPath, isDirectory, readJson, git, matchesAnyGlob, pathExists, readText } from './chunk-YLNQQZ5F.js';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { promises } from 'node:fs';
|
|
@@ -390,6 +390,33 @@ var ARTIFACT_SPECS = [
|
|
|
390
390
|
kind: "map"
|
|
391
391
|
}
|
|
392
392
|
];
|
|
393
|
+
async function firstLine(file) {
|
|
394
|
+
let handle;
|
|
395
|
+
try {
|
|
396
|
+
handle = await promises.open(file, "r");
|
|
397
|
+
const { buffer, bytesRead } = await handle.read(Buffer.alloc(128), 0, 128, 0);
|
|
398
|
+
return buffer.subarray(0, bytesRead).toString("utf8").split("\n", 1)[0] ?? "";
|
|
399
|
+
} catch {
|
|
400
|
+
return "";
|
|
401
|
+
} finally {
|
|
402
|
+
await handle?.close();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
var SHELL_SHEBANG = /^#!\s*\/\S*\b(?:ba|da|z)?sh\b|^#!\s*\/usr\/bin\/env\s+(?:ba|da|z)?sh\b/;
|
|
406
|
+
var SHELL_EXT = /\.(?:sh|bash|zsh)$/;
|
|
407
|
+
async function detectShellSurface(root, isRepo) {
|
|
408
|
+
if (!isRepo) return { scripts: 0 };
|
|
409
|
+
const listed = await git(root, ["ls-files", "-z"]);
|
|
410
|
+
if (!listed) return { scripts: 0 };
|
|
411
|
+
const files = listed.split("\0").filter(Boolean);
|
|
412
|
+
const byExtension = files.filter((f) => SHELL_EXT.test(f));
|
|
413
|
+
const candidates = files.filter((f) => !path.basename(f).includes("."));
|
|
414
|
+
const SHEBANG_PROBE_CAP = 400;
|
|
415
|
+
const probed = await Promise.all(
|
|
416
|
+
candidates.slice(0, SHEBANG_PROBE_CAP).map(async (f) => SHELL_SHEBANG.test(await firstLine(path.join(root, f))))
|
|
417
|
+
);
|
|
418
|
+
return { scripts: byExtension.length + probed.filter(Boolean).length };
|
|
419
|
+
}
|
|
393
420
|
async function detectArtifacts(root) {
|
|
394
421
|
const artifacts = await Promise.all(
|
|
395
422
|
ARTIFACT_SPECS.map(async (spec) => ({
|
|
@@ -537,6 +564,7 @@ async function scanProject(root, opts = {}) {
|
|
|
537
564
|
]) : [null, null, null, null, null];
|
|
538
565
|
const hooksPath = hooksPathRaw ?? void 0;
|
|
539
566
|
const hooks = await detectHooks(abs, hooksPath, rootPkg);
|
|
567
|
+
const shell = await detectShellSurface(abs, isRepo);
|
|
540
568
|
const freshness = await collectFreshness(abs, isRepo, artifacts, opts.upstreamRemote);
|
|
541
569
|
const packages = workspace.packageGlobs.length ? await listWorkspacePackages(abs, workspace.packageGlobs) : [];
|
|
542
570
|
const recentAuthors = authorsRaw ? new Set(authorsRaw.split("\n").filter(Boolean)).size : void 0;
|
|
@@ -568,6 +596,7 @@ async function scanProject(root, opts = {}) {
|
|
|
568
596
|
// vsce — which never runs prepublishOnly.
|
|
569
597
|
publishRoute: !rootPkg ? "none" : rootPkg.engines?.vscode ? "vscode" : "npm",
|
|
570
598
|
artifacts,
|
|
599
|
+
shell,
|
|
571
600
|
freshness,
|
|
572
601
|
tree
|
|
573
602
|
};
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { VERSION } from './chunk-
|
|
2
|
+
import { VERSION } from './chunk-QYHROOMK.js';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
5
|
import pc from 'picocolors';
|
|
@@ -31,7 +31,7 @@ program.command("audit").description("Verify every instruction claim against the
|
|
|
31
31
|
"exit non-zero when findings at/above this tier exist (risk|gap|polish)"
|
|
32
32
|
).action(
|
|
33
33
|
(opts, cmd) => action(async () => {
|
|
34
|
-
const { run } = await import('./audit-
|
|
34
|
+
const { run } = await import('./audit-AU4R45TP.js');
|
|
35
35
|
await run({
|
|
36
36
|
cwd: resolveCwd(cmd),
|
|
37
37
|
json: opts.json,
|
|
@@ -44,7 +44,7 @@ program.command("audit").description("Verify every instruction claim against the
|
|
|
44
44
|
);
|
|
45
45
|
program.command("init").description("Onboard the truth guard: approve the baseline; scaffold AGENTS.md only if missing").option("-y, --yes", "accept defaults without prompting (never overwrites)").action(
|
|
46
46
|
(opts, cmd) => action(async () => {
|
|
47
|
-
const { run } = await import('./init-
|
|
47
|
+
const { run } = await import('./init-BYZJOCS6.js');
|
|
48
48
|
await run({ cwd: resolveCwd(cmd), yes: opts.yes });
|
|
49
49
|
})
|
|
50
50
|
);
|
|
@@ -52,19 +52,19 @@ program.command("approve").description(
|
|
|
52
52
|
"Re-approve the committed baseline after intentional structural changes (non-interactive)"
|
|
53
53
|
).action(
|
|
54
54
|
(_opts, cmd) => action(async () => {
|
|
55
|
-
const { run } = await import('./approve-
|
|
55
|
+
const { run } = await import('./approve-Y6TX4LTN.js');
|
|
56
56
|
await run({ cwd: resolveCwd(cmd) });
|
|
57
57
|
})
|
|
58
58
|
);
|
|
59
59
|
program.command("scan").description("Deterministically reckon the project into a facts index").option("--json", "print the raw facts as JSON").option("--no-save", "do not write the .etymd cache").action(
|
|
60
60
|
(opts, cmd) => action(async () => {
|
|
61
|
-
const { run } = await import('./scan-
|
|
61
|
+
const { run } = await import('./scan-DPEOA3KZ.js');
|
|
62
62
|
await run({ cwd: resolveCwd(cmd), json: opts.json, save: opts.save });
|
|
63
63
|
})
|
|
64
64
|
);
|
|
65
65
|
program.command("doctor").description('Alias for `audit --truth` \u2014 "are the recorded instructions still true?"').option("--json", "print findings as JSON").action(
|
|
66
66
|
(opts, cmd) => action(async () => {
|
|
67
|
-
const { run } = await import('./doctor-
|
|
67
|
+
const { run } = await import('./doctor-OE5E36DX.js');
|
|
68
68
|
await run({ cwd: resolveCwd(cmd), json: opts.json });
|
|
69
69
|
})
|
|
70
70
|
);
|
|
@@ -76,7 +76,7 @@ program.command("context").description("Measure the always-loaded context footpr
|
|
|
76
76
|
);
|
|
77
77
|
program.command("brief").description("Emit a grounded briefing for the in-repo agent to complete the semantic layer").option("--human", "write a human onboarding brief instead of the agent briefing").action(
|
|
78
78
|
(opts, cmd) => action(async () => {
|
|
79
|
-
const { run } = await import('./brief-
|
|
79
|
+
const { run } = await import('./brief-TIKPGPFV.js');
|
|
80
80
|
await run({ cwd: resolveCwd(cmd), human: opts.human });
|
|
81
81
|
})
|
|
82
82
|
);
|
|
@@ -111,7 +111,7 @@ var fleet = program.command("fleet").description(
|
|
|
111
111
|
"exit non-zero when findings at/above this tier exist (risk|gap|polish)"
|
|
112
112
|
).action(
|
|
113
113
|
(opts, cmd) => action(async () => {
|
|
114
|
-
const { sweep } = await import('./fleet-
|
|
114
|
+
const { sweep } = await import('./fleet-LSN5XZ5Q.js');
|
|
115
115
|
await sweep({
|
|
116
116
|
cwd: resolveCwd(cmd),
|
|
117
117
|
manifest: opts.manifest,
|
|
@@ -126,7 +126,7 @@ var fleet = program.command("fleet").description(
|
|
|
126
126
|
);
|
|
127
127
|
fleet.command("check").description("Validate the manifest pair only \u2014 no lenses: dangling mappings, duplicates, leaks").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").option("--json", "print the findings as JSON (EXPERIMENTAL through 0.2.x)").action(
|
|
128
128
|
(_opts, cmd) => action(async () => {
|
|
129
|
-
const { check } = await import('./fleet-
|
|
129
|
+
const { check } = await import('./fleet-LSN5XZ5Q.js');
|
|
130
130
|
const opts = cmd.optsWithGlobals();
|
|
131
131
|
await check({ cwd: resolveCwd(cmd), manifest: opts.manifest, json: opts.json });
|
|
132
132
|
})
|
|
@@ -136,7 +136,7 @@ fleet.command("add").argument("<dir>", "directory of the project to register").d
|
|
|
136
136
|
"public-repo | public-bound | private \u2014 mandatory for personal entries"
|
|
137
137
|
).option("-y, --yes", "skip prompts; every mandatory value must be passed as a flag").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").action(
|
|
138
138
|
(dir, opts, cmd) => action(async () => {
|
|
139
|
-
const { add } = await import('./fleet-
|
|
139
|
+
const { add } = await import('./fleet-LSN5XZ5Q.js');
|
|
140
140
|
const shared = cmd.optsWithGlobals();
|
|
141
141
|
await add({
|
|
142
142
|
cwd: resolveCwd(cmd),
|
|
@@ -154,7 +154,7 @@ fleet.command("dismiss").argument("<name>", "the registered project name").argum
|
|
|
154
154
|
"Dismiss a project's finding from any cwd \u2014 corp ledgers persist beside the manifest"
|
|
155
155
|
).requiredOption("--reason <text>", "why it is dismissed \u2014 recorded so the decision survives").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").action(
|
|
156
156
|
(name, id, _opts, cmd) => action(async () => {
|
|
157
|
-
const { dismiss } = await import('./fleet-
|
|
157
|
+
const { dismiss } = await import('./fleet-LSN5XZ5Q.js');
|
|
158
158
|
const opts = cmd.optsWithGlobals();
|
|
159
159
|
await dismiss({
|
|
160
160
|
cwd: resolveCwd(cmd),
|
|
@@ -169,7 +169,7 @@ fleet.command("accept").argument("<name>", "the registered project name").argume
|
|
|
169
169
|
"Accept a project's finding as a known trade-off \u2014 corp ledgers persist beside the manifest"
|
|
170
170
|
).option("--reason <text>", "optional note on why the trade-off is accepted").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").action(
|
|
171
171
|
(name, id, _opts, cmd) => action(async () => {
|
|
172
|
-
const { accept } = await import('./fleet-
|
|
172
|
+
const { accept } = await import('./fleet-LSN5XZ5Q.js');
|
|
173
173
|
const opts = cmd.optsWithGlobals();
|
|
174
174
|
await accept({ cwd: resolveCwd(cmd), manifest: opts.manifest, name, id, reason: opts.reason });
|
|
175
175
|
})
|
|
@@ -191,7 +191,7 @@ program.command("screen").description(
|
|
|
191
191
|
);
|
|
192
192
|
program.command("gates").description("Install the local git-hook gates (process \u2192 pre-commit, correctness \u2192 pre-push)").option("--ci", "note about the CI review gate (ships later; local gates install now)").option("-y, --yes", "skip prompts; never overwrites a hand-edited hook").action(
|
|
193
193
|
(opts, cmd) => action(async () => {
|
|
194
|
-
const { run } = await import('./gates-
|
|
194
|
+
const { run } = await import('./gates-IJNAQ7DC.js');
|
|
195
195
|
await run({ cwd: resolveCwd(cmd), ci: opts.ci, yes: opts.yes });
|
|
196
196
|
})
|
|
197
197
|
);
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { run } from './chunk-
|
|
3
|
-
import './chunk-
|
|
2
|
+
import { run } from './chunk-7K3BDNLW.js';
|
|
3
|
+
import './chunk-ECY7BLBT.js';
|
|
4
4
|
import './chunk-LT67FU2Y.js';
|
|
5
5
|
import './chunk-WWBF4Y7K.js';
|
|
6
6
|
import './chunk-NMZ3RHWW.js';
|
|
7
7
|
import './chunk-TADKOW6P.js';
|
|
8
|
-
import './chunk-
|
|
9
|
-
import './chunk-
|
|
8
|
+
import './chunk-SXCR6RRM.js';
|
|
9
|
+
import './chunk-QYHROOMK.js';
|
|
10
10
|
import './chunk-R74SJ7HS.js';
|
|
11
|
-
import './chunk-
|
|
11
|
+
import './chunk-HGSLHNNT.js';
|
|
12
12
|
import './chunk-YLNQQZ5F.js';
|
|
13
13
|
|
|
14
14
|
// src/commands/doctor.ts
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { dismiss, accept } from './chunk-MMPV67FW.js';
|
|
3
|
-
import { parseFailOnTier, meetsFailOn, runAudit } from './chunk-
|
|
3
|
+
import { parseFailOnTier, meetsFailOn, runAudit } from './chunk-ECY7BLBT.js';
|
|
4
4
|
import './chunk-LT67FU2Y.js';
|
|
5
5
|
import { readLedger } from './chunk-WWBF4Y7K.js';
|
|
6
6
|
import { DEFAULT_CONFIG } from './chunk-NMZ3RHWW.js';
|
|
7
7
|
import { print, theme, section, renderFleetRows, renderFleetNotes, renderFindings, TIER_BADGE, glyph } from './chunk-TADKOW6P.js';
|
|
8
|
-
import { scanProject } from './chunk-
|
|
9
|
-
import './chunk-
|
|
8
|
+
import { scanProject } from './chunk-SXCR6RRM.js';
|
|
9
|
+
import './chunk-QYHROOMK.js';
|
|
10
10
|
import { ETYMD_DIR } from './chunk-R74SJ7HS.js';
|
|
11
|
-
import './chunk-
|
|
11
|
+
import './chunk-HGSLHNNT.js';
|
|
12
12
|
import { readText, pathExists, git, isDirectory } from './chunk-YLNQQZ5F.js';
|
|
13
13
|
import { promises } from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
@@ -687,8 +687,8 @@ async function checkCorpEmails(manifest, findings, disclosures) {
|
|
|
687
687
|
}
|
|
688
688
|
}
|
|
689
689
|
async function checkGateDrift(manifest, findings, disclosures) {
|
|
690
|
-
const { planWorkflow } = await import('./generate-
|
|
691
|
-
const { scanProject: scanProject2 } = await import('./scan-
|
|
690
|
+
const { planWorkflow } = await import('./generate-JEREMVVO.js');
|
|
691
|
+
const { scanProject: scanProject2 } = await import('./scan-O3SM2ODH.js');
|
|
692
692
|
const { readConfig } = await import('./config-MX7OYI7F.js');
|
|
693
693
|
for (const entry of manifest.entries) {
|
|
694
694
|
const root = entry.resolvedRoot;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { applyFiles } from './chunk-EF6BPBG6.js';
|
|
3
|
-
import { derivedCommands, planWorkflow, runPrefix, isSafeGateCommand } from './chunk-
|
|
3
|
+
import { derivedCommands, planWorkflow, runPrefix, isSafeGateCommand } from './chunk-JTHZVEON.js';
|
|
4
4
|
import { readConfig, CONFIG_FILE, configPath } from './chunk-NMZ3RHWW.js';
|
|
5
5
|
import { section, print, glyph, theme, renderPlan } from './chunk-TADKOW6P.js';
|
|
6
|
-
import { scanProject } from './chunk-
|
|
7
|
-
import './chunk-
|
|
6
|
+
import { scanProject } from './chunk-SXCR6RRM.js';
|
|
7
|
+
import './chunk-QYHROOMK.js';
|
|
8
8
|
import './chunk-R74SJ7HS.js';
|
|
9
|
-
import './chunk-
|
|
9
|
+
import './chunk-HGSLHNNT.js';
|
|
10
10
|
import { readText, git } from './chunk-YLNQQZ5F.js';
|
|
11
11
|
import { promises } from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
@@ -57,7 +57,9 @@ function publishGateKey(route) {
|
|
|
57
57
|
}
|
|
58
58
|
function printGateSummary(gates, facts, source) {
|
|
59
59
|
const run2 = runPrefix(facts.packageManager);
|
|
60
|
-
const
|
|
60
|
+
const shellStep = facts.shell?.scripts ? `shellcheck (${facts.shell.scripts} script${facts.shell.scripts === 1 ? "" : "s"})` : "";
|
|
61
|
+
const listed = [...gates.commands.map((c) => `${run2} ${c}`), ...shellStep ? [shellStep] : []];
|
|
62
|
+
const cmds = listed.length ? listed.join(", ") : "none detected";
|
|
61
63
|
print(` ${glyph.bullet} ${theme.dim("pre-push runs")} ${theme.info(cmds)}`);
|
|
62
64
|
print(` ${glyph.bullet} ${theme.dim("audit fails on")} ${theme.info(gates.failOn)}`);
|
|
63
65
|
print(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export { derivedCommands, planWorkflow } from './chunk-
|
|
2
|
+
export { derivedCommands, planWorkflow } from './chunk-JTHZVEON.js';
|
|
3
3
|
import './chunk-NMZ3RHWW.js';
|
|
4
4
|
import './chunk-R74SJ7HS.js';
|
|
5
|
-
import './chunk-
|
|
5
|
+
import './chunk-HGSLHNNT.js';
|
|
6
6
|
import './chunk-YLNQQZ5F.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -121,6 +121,23 @@ interface ProjectFacts {
|
|
|
121
121
|
*/
|
|
122
122
|
publishRoute: "npm" | "vscode" | "none";
|
|
123
123
|
artifacts: DetectedArtifact[];
|
|
124
|
+
/**
|
|
125
|
+
* The repo's SHELL surface: tracked files that a shell executes.
|
|
126
|
+
*
|
|
127
|
+
* Package scripts are not the only executable surface a repo has, and in some repos they are
|
|
128
|
+
* not the main one — a tools or infra repo can be entirely `bootstrap/*.sh` plus `.githooks/*`
|
|
129
|
+
* with no `package.json` at all. Those are exactly the repos that report "no correctness
|
|
130
|
+
* commands detected", which reads as "nothing to check" when it means "nothing was looked at".
|
|
131
|
+
*
|
|
132
|
+
* Counted, not listed: the generated hook re-discovers scripts at push time by shebang, so it
|
|
133
|
+
* stays correct as the repo grows. This number only decides WHETHER the step is worth writing.
|
|
134
|
+
*
|
|
135
|
+
* Optional for the same reason as `freshness`: a facts file written by an older etymd predates
|
|
136
|
+
* the field, and absent must read as "not measured" rather than "measured as zero".
|
|
137
|
+
*/
|
|
138
|
+
shell?: {
|
|
139
|
+
scripts: number;
|
|
140
|
+
};
|
|
124
141
|
/** Optional: baselines approved by older versions predate this fact. */
|
|
125
142
|
freshness?: FreshnessFacts;
|
|
126
143
|
tree: {
|
|
@@ -279,6 +296,13 @@ interface Finding {
|
|
|
279
296
|
id: string;
|
|
280
297
|
lens: string;
|
|
281
298
|
tier: FindingTier;
|
|
299
|
+
/**
|
|
300
|
+
* Whether this finding is an objective truth (the repo is lying) or an improvement opinion
|
|
301
|
+
* (something worth doing). Lives ON THE FINDING, not the lens: a lens can emit both kinds —
|
|
302
|
+
* gate-integrity is an improvement lens, but "hooks-not-wired" is an objective truth a `doctor`
|
|
303
|
+
* run must not skip. Defaulted from the lens kind when a finding does not set its own.
|
|
304
|
+
*/
|
|
305
|
+
kind?: LensKind;
|
|
282
306
|
/** One sentence: what is wrong / missing. */
|
|
283
307
|
claim: string;
|
|
284
308
|
/** File paths, job names, or metrics that ground the claim. Never empty. */
|
|
@@ -745,7 +769,7 @@ declare const gateIntegrityLens: Lens;
|
|
|
745
769
|
* change meaning. Stamped into facts, baselines, and generated artifacts so drift against the
|
|
746
770
|
* pack is computable and `harvest` has something to diff.
|
|
747
771
|
*/
|
|
748
|
-
declare const PACK_VERSION = "
|
|
772
|
+
declare const PACK_VERSION = "4";
|
|
749
773
|
|
|
750
774
|
declare const VERSION: string;
|
|
751
775
|
declare const NAME: string;
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ var __export = (target, all) => {
|
|
|
19
19
|
var PACK_VERSION;
|
|
20
20
|
var init_version = __esm({
|
|
21
21
|
"src/pack/version.ts"() {
|
|
22
|
-
PACK_VERSION = "
|
|
22
|
+
PACK_VERSION = "4";
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -29,7 +29,7 @@ var init_package = __esm({
|
|
|
29
29
|
"package.json"() {
|
|
30
30
|
package_default = {
|
|
31
31
|
name: "etymd",
|
|
32
|
-
version: "0.
|
|
32
|
+
version: "0.4.0",
|
|
33
33
|
description: "Keep your agent instructions true \u2014 verify AGENTS.md, CLAUDE.md, rules & skills against the actual repo, with drift caught over time and a regression ledger.",
|
|
34
34
|
keywords: [
|
|
35
35
|
"cli",
|
|
@@ -438,6 +438,31 @@ async function detectHooks(root, hooksPath, pkg) {
|
|
|
438
438
|
}
|
|
439
439
|
return { source: "none", preCommit: false, prePush: false, commitMsg: false, lintStaged };
|
|
440
440
|
}
|
|
441
|
+
async function firstLine(file) {
|
|
442
|
+
let handle;
|
|
443
|
+
try {
|
|
444
|
+
handle = await promises.open(file, "r");
|
|
445
|
+
const { buffer, bytesRead } = await handle.read(Buffer.alloc(128), 0, 128, 0);
|
|
446
|
+
return buffer.subarray(0, bytesRead).toString("utf8").split("\n", 1)[0] ?? "";
|
|
447
|
+
} catch {
|
|
448
|
+
return "";
|
|
449
|
+
} finally {
|
|
450
|
+
await handle?.close();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
async function detectShellSurface(root, isRepo) {
|
|
454
|
+
if (!isRepo) return { scripts: 0 };
|
|
455
|
+
const listed = await git(root, ["ls-files", "-z"]);
|
|
456
|
+
if (!listed) return { scripts: 0 };
|
|
457
|
+
const files = listed.split("\0").filter(Boolean);
|
|
458
|
+
const byExtension = files.filter((f) => SHELL_EXT.test(f));
|
|
459
|
+
const candidates = files.filter((f) => !path.basename(f).includes("."));
|
|
460
|
+
const SHEBANG_PROBE_CAP = 400;
|
|
461
|
+
const probed = await Promise.all(
|
|
462
|
+
candidates.slice(0, SHEBANG_PROBE_CAP).map(async (f) => SHELL_SHEBANG.test(await firstLine(path.join(root, f))))
|
|
463
|
+
);
|
|
464
|
+
return { scripts: byExtension.length + probed.filter(Boolean).length };
|
|
465
|
+
}
|
|
441
466
|
async function detectArtifacts(root) {
|
|
442
467
|
const artifacts = await Promise.all(
|
|
443
468
|
ARTIFACT_SPECS.map(async (spec) => ({
|
|
@@ -504,7 +529,7 @@ async function walkTree(root) {
|
|
|
504
529
|
dirs.sort((a, b) => b.files - a.files);
|
|
505
530
|
return { dirs, truncated: state.truncated };
|
|
506
531
|
}
|
|
507
|
-
var IGNORED_DIRS, MEANINGFUL_DOT_DIRS, isMeta, ROLE_LADDERS, FRAMEWORK_MARKERS, ARTIFACT_SPECS;
|
|
532
|
+
var IGNORED_DIRS, MEANINGFUL_DOT_DIRS, isMeta, ROLE_LADDERS, FRAMEWORK_MARKERS, ARTIFACT_SPECS, SHELL_SHEBANG, SHELL_EXT;
|
|
508
533
|
var init_detect = __esm({
|
|
509
534
|
"src/core/detect.ts"() {
|
|
510
535
|
init_util();
|
|
@@ -677,6 +702,8 @@ var init_detect = __esm({
|
|
|
677
702
|
kind: "map"
|
|
678
703
|
}
|
|
679
704
|
];
|
|
705
|
+
SHELL_SHEBANG = /^#!\s*\/\S*\b(?:ba|da|z)?sh\b|^#!\s*\/usr\/bin\/env\s+(?:ba|da|z)?sh\b/;
|
|
706
|
+
SHELL_EXT = /\.(?:sh|bash|zsh)$/;
|
|
680
707
|
}
|
|
681
708
|
});
|
|
682
709
|
|
|
@@ -764,6 +791,7 @@ async function scanProject(root, opts = {}) {
|
|
|
764
791
|
]) : [null, null, null, null, null];
|
|
765
792
|
const hooksPath = hooksPathRaw ?? void 0;
|
|
766
793
|
const hooks = await detectHooks(abs, hooksPath, rootPkg);
|
|
794
|
+
const shell = await detectShellSurface(abs, isRepo);
|
|
767
795
|
const freshness = await collectFreshness(abs, isRepo, artifacts, opts.upstreamRemote);
|
|
768
796
|
const packages = workspace.packageGlobs.length ? await listWorkspacePackages(abs, workspace.packageGlobs) : [];
|
|
769
797
|
const recentAuthors = authorsRaw ? new Set(authorsRaw.split("\n").filter(Boolean)).size : void 0;
|
|
@@ -795,6 +823,7 @@ async function scanProject(root, opts = {}) {
|
|
|
795
823
|
// vsce — which never runs prepublishOnly.
|
|
796
824
|
publishRoute: !rootPkg ? "none" : rootPkg.engines?.vscode ? "vscode" : "npm",
|
|
797
825
|
artifacts,
|
|
826
|
+
shell,
|
|
798
827
|
freshness,
|
|
799
828
|
tree
|
|
800
829
|
};
|
|
@@ -1115,6 +1144,25 @@ ${localHookCall("commit-msg")}
|
|
|
1115
1144
|
exit 0
|
|
1116
1145
|
`;
|
|
1117
1146
|
}
|
|
1147
|
+
function shellcheckStep() {
|
|
1148
|
+
return `
|
|
1149
|
+
# Shell correctness. Scripts are discovered by shebang over TRACKED files at push time, so a
|
|
1150
|
+
# script added later is covered without regenerating this hook.
|
|
1151
|
+
if command -v shellcheck >/dev/null 2>&1; then
|
|
1152
|
+
scripts=$(git ls-files -z \\
|
|
1153
|
+
| xargs -0 -I{} sh -c 'head -1 "{}" 2>/dev/null | grep -qE "^#!.*[/ ](ba|da|z)?sh( |$)" && echo "{}"' \\
|
|
1154
|
+
| sort)
|
|
1155
|
+
if [ -n "$scripts" ]; then
|
|
1156
|
+
echo "\u203A shellcheck ($(printf '%s\\n' "$scripts" | wc -l | tr -d ' ') scripts, blocking at severity=warning)"
|
|
1157
|
+
printf '%s\\n' "$scripts" | xargs shellcheck -S warning || {
|
|
1158
|
+
echo " fix, or justify inline with '# shellcheck disable=SCxxxx # why'"
|
|
1159
|
+
exit 1
|
|
1160
|
+
}
|
|
1161
|
+
fi
|
|
1162
|
+
else
|
|
1163
|
+
echo "\u203A shellcheck skipped (not on PATH) \u2014 install it to gate this repo's shell scripts"
|
|
1164
|
+
fi`;
|
|
1165
|
+
}
|
|
1118
1166
|
function generatePrePushHook(facts, gates) {
|
|
1119
1167
|
const run = runPrefix(facts.packageManager);
|
|
1120
1168
|
const c = facts.commands;
|
|
@@ -1123,8 +1171,14 @@ function generatePrePushHook(facts, gates) {
|
|
|
1123
1171
|
const steps = candidates.filter(
|
|
1124
1172
|
(key) => Boolean(key) && (allowed.has(key) || isSafeGateCommand(c.raw[key]))
|
|
1125
1173
|
).map((key) => `${run} ${key}`);
|
|
1174
|
+
const shellStep = facts.shell?.scripts ? shellcheckStep() : "";
|
|
1126
1175
|
const body = steps.length ? steps.map((s) => `echo "\u203A ${s}"
|
|
1127
|
-
${s} || exit 1`).join("\n") :
|
|
1176
|
+
${s} || exit 1`).join("\n") : shellStep ? (
|
|
1177
|
+
// A repo whose executable surface is shell HAS a correctness command — it just is not in
|
|
1178
|
+
// package.json. Claiming "none detected" beside a step that is about to run would be the
|
|
1179
|
+
// tool contradicting itself.
|
|
1180
|
+
`echo "\u203A no package scripts \u2014 shell is this repo's checkable surface"`
|
|
1181
|
+
) : 'echo "etymd: no correctness commands detected \u2014 add format:check / typecheck / lint"';
|
|
1128
1182
|
const failOn = gates?.failOn ?? "risk";
|
|
1129
1183
|
const auditStep = `
|
|
1130
1184
|
if command -v etymd >/dev/null 2>&1; then
|
|
@@ -1137,7 +1191,7 @@ fi`;
|
|
|
1137
1191
|
# etymd: correctness gate. Mirrors CI cheapest-first; blocks the push on any failure.
|
|
1138
1192
|
|
|
1139
1193
|
${localHookCall("pre-push")}
|
|
1140
|
-
${body}
|
|
1194
|
+
${body}${shellStep}
|
|
1141
1195
|
${auditStep}
|
|
1142
1196
|
|
|
1143
1197
|
# Content screen, second pass \u2014 the WHOLE TREE rather than one diff. Catches anything committed
|
|
@@ -1806,6 +1860,7 @@ function deriveGateFindings(inv, disclosures = []) {
|
|
|
1806
1860
|
finding({
|
|
1807
1861
|
id: `${LENS_ID2}/hooks-not-wired`,
|
|
1808
1862
|
tier: "risk",
|
|
1863
|
+
kind: "truth",
|
|
1809
1864
|
claim: "Tracked .githooks/ exist but git core.hooksPath is unset \u2014 the hooks never run",
|
|
1810
1865
|
evidence: [".githooks/"],
|
|
1811
1866
|
why: "Every check in those hooks is silently skipped on this clone.",
|
|
@@ -2769,9 +2824,7 @@ async function runAudit(root, opts = {}) {
|
|
|
2769
2824
|
};
|
|
2770
2825
|
}
|
|
2771
2826
|
const profile = baseline?.profile ?? deriveProfile(facts);
|
|
2772
|
-
const selected = LENSES.filter(
|
|
2773
|
-
(lens) => (!opts.kind || lens.kind === opts.kind) && (!opts.lensIds || opts.lensIds.includes(lens.id))
|
|
2774
|
-
);
|
|
2827
|
+
const selected = LENSES.filter((lens) => !opts.lensIds || opts.lensIds.includes(lens.id));
|
|
2775
2828
|
const reports = [];
|
|
2776
2829
|
for (const lens of selected) {
|
|
2777
2830
|
try {
|
|
@@ -2789,11 +2842,12 @@ async function runAudit(root, opts = {}) {
|
|
|
2789
2842
|
});
|
|
2790
2843
|
}
|
|
2791
2844
|
}
|
|
2792
|
-
const all = reports.flatMap((r) => r.findings);
|
|
2845
|
+
const all = reports.flatMap((r) => r.findings.map((f) => ({ ...f, kind: f.kind ?? r.kind })));
|
|
2846
|
+
const visible = opts.kind ? all.filter((f) => f.kind === opts.kind) : all;
|
|
2793
2847
|
const previous = await readLedger(ledgerRoot);
|
|
2794
2848
|
const outOfScope = reports.flatMap((r) => r.outOfScope ?? []);
|
|
2795
2849
|
const { ledger, diff } = reconcileLedger(previous, all, void 0, outOfScope);
|
|
2796
|
-
const fullRun =
|
|
2850
|
+
const fullRun = !opts.kind && !opts.lensIds;
|
|
2797
2851
|
const ledgerWritable = !opts.readOnlyRoot || ledgerRoot !== root;
|
|
2798
2852
|
if ((opts.persistLedger ?? true) && fullRun && ledgerWritable) {
|
|
2799
2853
|
await writeLedger(ledgerRoot, ledger);
|
|
@@ -2804,7 +2858,7 @@ async function runAudit(root, opts = {}) {
|
|
|
2804
2858
|
baseline,
|
|
2805
2859
|
config,
|
|
2806
2860
|
reports,
|
|
2807
|
-
findings: rankFindings(visibleFindings(
|
|
2861
|
+
findings: rankFindings(visibleFindings(visible, previous)),
|
|
2808
2862
|
ledger,
|
|
2809
2863
|
diff
|
|
2810
2864
|
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { applyFiles } from './chunk-EF6BPBG6.js';
|
|
3
|
-
import { planWorkflow } from './chunk-
|
|
3
|
+
import { planWorkflow } from './chunk-JTHZVEON.js';
|
|
4
4
|
import './chunk-NMZ3RHWW.js';
|
|
5
5
|
import { theme, renderFacts, print, renderPlan, section, glyph } from './chunk-TADKOW6P.js';
|
|
6
|
-
import { scanProject } from './chunk-
|
|
7
|
-
import { VERSION } from './chunk-
|
|
6
|
+
import { scanProject } from './chunk-SXCR6RRM.js';
|
|
7
|
+
import { VERSION } from './chunk-QYHROOMK.js';
|
|
8
8
|
import { writeCachedFacts, deriveProfile, writeBaseline, CACHE_DIR } from './chunk-R74SJ7HS.js';
|
|
9
|
-
import { PACK_VERSION } from './chunk-
|
|
9
|
+
import { PACK_VERSION } from './chunk-HGSLHNNT.js';
|
|
10
10
|
import { git, readText } from './chunk-YLNQQZ5F.js';
|
|
11
11
|
import { promises } from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { print, renderFacts, theme } from './chunk-TADKOW6P.js';
|
|
3
|
-
import { scanProject } from './chunk-
|
|
4
|
-
import './chunk-
|
|
3
|
+
import { scanProject } from './chunk-SXCR6RRM.js';
|
|
4
|
+
import './chunk-QYHROOMK.js';
|
|
5
5
|
import { writeCachedFacts } from './chunk-R74SJ7HS.js';
|
|
6
|
-
import './chunk-
|
|
6
|
+
import './chunk-HGSLHNNT.js';
|
|
7
7
|
import './chunk-YLNQQZ5F.js';
|
|
8
8
|
|
|
9
9
|
// src/commands/scan.ts
|
package/package.json
CHANGED