etymd 0.17.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/README.md +10 -5
- package/dist/{approve-VN4EDBR5.js → approve-J6JU7MBX.js} +3 -3
- package/dist/audit-5N2TU2X7.js +11 -0
- package/dist/{brief-7MWP35UI.js → brief-NHHILXIH.js} +3 -3
- package/dist/{chunk-YQZDYDAK.js → chunk-2DRA7S67.js} +1 -1
- package/dist/{chunk-446EZNAQ.js → chunk-6DJMKZGK.js} +83 -10
- package/dist/chunk-7N5JDXQG.js +109 -0
- package/dist/{chunk-ANFNXFRI.js → chunk-GFUDKTC5.js} +2 -2
- package/dist/{chunk-BUMOGHQA.js → chunk-KQFEKOON.js} +1 -1
- package/dist/{chunk-LVR2DPR7.js → chunk-M7XDP6FT.js} +30 -3
- package/dist/{chunk-PIJZUDDQ.js → chunk-PQISPOB2.js} +191 -130
- package/dist/{chunk-WKP7M2B3.js → chunk-UMKC3OWQ.js} +11 -6
- package/dist/{chunk-FNA4R5KT.js → chunk-WKP72Z3J.js} +59 -11
- package/dist/cli.js +16 -16
- package/dist/{doctor-R4QDCAN5.js → doctor-264Q5PLY.js} +6 -6
- package/dist/{fleet-WPYEVHVL.js → fleet-VT625Q2J.js} +22 -6
- package/dist/{gates-3HRGYCYY.js → gates-YUMSTKUG.js} +5 -4
- package/dist/{generate-FSJQFQPV.js → generate-62GQSCKH.js} +3 -2
- package/dist/index.d.ts +21 -3
- package/dist/index.js +379 -58
- package/dist/{init-6RULX5JZ.js → init-G4XXFXVD.js} +5 -4
- package/dist/{premise-GFWHSR3K.js → premise-RNMAKMVA.js} +9 -5
- package/dist/{propose-IYNOIWAO.js → propose-35JXMEPH.js} +6 -6
- package/dist/scan-F6OW4ANK.js +5 -0
- package/dist/{scan-LLWW4YOU.js → scan-PJH32H7C.js} +3 -3
- package/package.json +2 -2
- package/dist/audit-PHAK5HEW.js +0 -11
- package/dist/scan-5SC47FSM.js +0 -5
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { PACK_VERSION } from './chunk-YQZDYDAK.js';
|
|
4
|
-
import { readText, pathExists } from './chunk-4VPBP6K6.js';
|
|
5
|
-
import path from 'path';
|
|
2
|
+
import { PACK_VERSION } from './chunk-2DRA7S67.js';
|
|
6
3
|
import { createHash } from 'crypto';
|
|
7
4
|
|
|
8
5
|
var GENERATION_MARKER_RE = /^(?:# |<!-- )etymd:generated pack-v\S+ ([0-9a-f]{16})(?: -->)?$/;
|
|
@@ -128,6 +125,19 @@ ${[
|
|
|
128
125
|
].filter(Boolean).join("\n") || "# add the project's key commands"}
|
|
129
126
|
\`\`\`
|
|
130
127
|
|
|
128
|
+
`,
|
|
129
|
+
"md"
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
function generateClaudePointerMd() {
|
|
133
|
+
return stampGenerated(
|
|
134
|
+
`# CLAUDE.md
|
|
135
|
+
|
|
136
|
+
Single source of truth for agent instructions lives in \`AGENTS.md\`. This pointer keeps Claude
|
|
137
|
+
Code aligned with every other agent \u2014 edit \`AGENTS.md\`, not this file.
|
|
138
|
+
|
|
139
|
+
@AGENTS.md
|
|
140
|
+
|
|
131
141
|
`,
|
|
132
142
|
"md"
|
|
133
143
|
);
|
|
@@ -151,15 +161,21 @@ function contentScreenCall(opts) {
|
|
|
151
161
|
`${indent}fi`
|
|
152
162
|
].join("\n");
|
|
153
163
|
}
|
|
154
|
-
function localHookCall(hook) {
|
|
164
|
+
function localHookCall(hook, feedRefs = false) {
|
|
165
|
+
const refsCapture = feedRefs ? `# git hands the pushed refs to pre-push ONCE, on stdin \u2014 one line per ref:
|
|
166
|
+
# "<local ref> <local sha> <remote ref> <remote sha>". The shell gate below reads them too, so
|
|
167
|
+
# they are captured here and fed to each.
|
|
168
|
+
refs=$(cat)
|
|
169
|
+
` : "";
|
|
170
|
+
const call = feedRefs ? `printf '%s\\n' "$refs" | "$LOCAL" "$@" || exit 1` : `"$LOCAL" "$@" || exit 1`;
|
|
155
171
|
return `# Repo-owned checks. This file is generated and will be overwritten; \`.githooks/${hook}.local\`
|
|
156
172
|
# is yours \u2014 etymd never reads, writes, or regenerates it. Put project-specific guards there.
|
|
157
173
|
# A guard running tests that build fixture repositories should scrub git's exported GIT_* names
|
|
158
174
|
# first \u2014 a child git inherits them and ignores its cwd, so the suite would hit the real repo:
|
|
159
175
|
# env $(env | grep -o '^GIT_[A-Za-z0-9_]*' | sed 's/^/-u /') <your command>
|
|
160
176
|
LOCAL="$(dirname "$0")/${hook}.local"
|
|
161
|
-
if [ -x "$LOCAL" ]; then
|
|
162
|
-
|
|
177
|
+
${refsCapture}if [ -x "$LOCAL" ]; then
|
|
178
|
+
${call}
|
|
163
179
|
fi`;
|
|
164
180
|
}
|
|
165
181
|
function generatePreCommitHook(selfBuild = false) {
|
|
@@ -168,10 +184,10 @@ function generatePreCommitHook(selfBuild = false) {
|
|
|
168
184
|
|
|
169
185
|
${localHookCall("pre-commit")}
|
|
170
186
|
|
|
171
|
-
# Content screen \u2014 staged file bytes. Refuses to commit environment,
|
|
172
|
-
#
|
|
173
|
-
# are machine-local by design, so this is a NO-OP wherever no checker is installed:
|
|
174
|
-
# commit anywhere, active only where you opted in.
|
|
187
|
+
# Content screen \u2014 staged file bytes. Refuses to commit detail about your environment, work
|
|
188
|
+
# or identity into a repo whose history is (or could become) public. The checker and its
|
|
189
|
+
# patterns are machine-local by design, so this is a NO-OP wherever no checker is installed:
|
|
190
|
+
# safe to commit anywhere, active only where you opted in.
|
|
175
191
|
#
|
|
176
192
|
# Bypass, with a reason: git commit --no-verify
|
|
177
193
|
${contentGateResolution(selfBuild)}
|
|
@@ -239,42 +255,181 @@ ${localHookCall("commit-msg")}
|
|
|
239
255
|
exit 0
|
|
240
256
|
`);
|
|
241
257
|
}
|
|
258
|
+
function generateShellDiscoveryScript() {
|
|
259
|
+
return stampGenerated(`#!/usr/bin/env sh
|
|
260
|
+
# etymd: shell script discovery for the pre-push shellcheck step. Arguments: the scratch
|
|
261
|
+
# directory the hook created, then the tracked paths to classify (NUL-delimited on the hook's
|
|
262
|
+
# side, positional here). Verdicts land in the scratch: scripts (NUL-delimited matches) and one
|
|
263
|
+
# dot per decision into count / zsh-count / skip-count, tallied by the hook after the pipeline.
|
|
264
|
+
#
|
|
265
|
+
# The hook runs it from inside a commit materialised from git's object store, so paths resolve
|
|
266
|
+
# against that tree, never the working tree. A path with nothing readable behind it \u2014 a
|
|
267
|
+
# submodule entry, a dangling symlink \u2014 cannot lie about its contents, so it is a disclosed
|
|
268
|
+
# skip, never a block. A regular file
|
|
269
|
+
# that EXISTS but cannot be read is the other branch \u2014 coverage would silently shrink, so it
|
|
270
|
+
# fails, naming the path.
|
|
271
|
+
#
|
|
272
|
+
# The two \`[ "$?" -eq 1 ]\` guards are the match/error protocol: grep reports "no match" as 1
|
|
273
|
+
# and a failure as 2 or more, and only the first is a verdict. Dropping the guard would let a
|
|
274
|
+
# failing matcher pass as "not a shell script" \u2014 the exact silent coverage-shrink the
|
|
275
|
+
# fail-closed rules exist to prevent. The checker does not associate \`$?\` with the enclosing
|
|
276
|
+
# if-condition, which is why this shape survives the pass it serves; keep it that way.
|
|
277
|
+
work=$1
|
|
278
|
+
shift
|
|
279
|
+
for file do
|
|
280
|
+
if [ ! -f "./$file" ]; then
|
|
281
|
+
printf . >> "$work/skip-count" || exit 1
|
|
282
|
+
continue
|
|
283
|
+
fi
|
|
284
|
+
# 4096 bytes bound the read \u2014 a binary with no newline would otherwise be copied whole
|
|
285
|
+
# into the scratch on every push. The second head restores line-1-only semantics, so a
|
|
286
|
+
# shebang embedded on a LATER line of a document cannot match the patterns below.
|
|
287
|
+
head -c 4096 "./$file" > "$work/head-bytes" || {
|
|
288
|
+
echo "etymd: cannot read tracked file for shellcheck: $file" >&2
|
|
289
|
+
exit 1
|
|
290
|
+
}
|
|
291
|
+
head -n 1 "$work/head-bytes" > "$work/first-line" || exit 1
|
|
292
|
+
if grep -qE "^#!.*[/ ](ba|da)?sh( |$)" "$work/first-line"; then
|
|
293
|
+
printf "./%s\\0" "$file" >> "$work/scripts" || exit 1
|
|
294
|
+
printf . >> "$work/count" || exit 1
|
|
295
|
+
else
|
|
296
|
+
[ "$?" -eq 1 ] || exit 1
|
|
297
|
+
if grep -qE "^#!.*[/ ]zsh( |$)" "$work/first-line"; then
|
|
298
|
+
printf . >> "$work/zsh-count" || exit 1
|
|
299
|
+
else
|
|
300
|
+
[ "$?" -eq 1 ] || exit 1
|
|
301
|
+
fi
|
|
302
|
+
fi
|
|
303
|
+
done
|
|
304
|
+
`);
|
|
305
|
+
}
|
|
242
306
|
function shellcheckStep() {
|
|
243
307
|
return `
|
|
244
308
|
# Shell correctness. Scripts are discovered by shebang over TRACKED files at push time, so a
|
|
245
|
-
# script added later is covered without regenerating this hook.
|
|
309
|
+
# script added later is covered without regenerating this hook. The commits BEING PUSHED are
|
|
310
|
+
# the bytes that ship, so each one is materialised from git's object store and checked there \u2014
|
|
311
|
+
# never the working tree (wrong in both directions: a fixed tree let an unfixed commit ship, and
|
|
312
|
+
# a dirty tree shared by several sessions blocked an unrelated push) and never the tip alone (a
|
|
313
|
+
# bad commit under a clean tip shipped while the gate read only the tip's fix). The classifier
|
|
314
|
+
# is discover-shell-scripts.sh beside this hook \u2014 tracked and shebanged like what it classifies,
|
|
315
|
+
# so the scan it implements finds and checks it too. zsh is NOT in the checked set:
|
|
246
316
|
# the checker cannot parse it (SC1071 is a parser-level error no inline directive can silence),
|
|
247
317
|
# so checking it would fail every push on the parser, not on the script. Excluded \u2014 and said so
|
|
248
318
|
# at run time below, because a coverage hole that is silent is indistinguishable from coverage.
|
|
319
|
+
# The same honesty splits the unreadable: a regular file that exists but cannot be read blocks
|
|
320
|
+
# the push (coverage would otherwise silently shrink), while a tracked path with nothing readable
|
|
321
|
+
# behind it \u2014 a submodule entry, a dangling symlink \u2014 is counted and said so below as skipped,
|
|
322
|
+
# never a block.
|
|
249
323
|
#
|
|
250
324
|
# "the checker", not its name, on purpose: a comment whose first word is that name is read as
|
|
251
325
|
# a DIRECTIVE, and an unparseable directive is itself an error (SC1072/SC1073). A hook that
|
|
252
326
|
# explains why it skips a shell dialect must not break the checker while doing it.
|
|
253
327
|
if command -v shellcheck >/dev/null 2>&1; then
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
328
|
+
(
|
|
329
|
+
# POSIX pipelines report only the LAST command's status, so discovery tallies progress in
|
|
330
|
+
# files \u2014 one dot per decision, counted with wc -c after the pipeline \u2014 rather than
|
|
331
|
+
# streaming through it: a pipeline that dies halfway cannot then pass as complete coverage,
|
|
332
|
+
# and the same tallies carry the counts to the reporting below. The classifier writes the
|
|
333
|
+
# tallies; this half only reads them.
|
|
334
|
+
# NUL delimiters preserve filenames; positional arguments avoid xargs -I size limits and
|
|
335
|
+
# interpreting filenames as shell code (never -I{}). The subshell confines cleanup to
|
|
336
|
+
# this step. The classifier itself is the tracked, shebanged helper beside this hook, so
|
|
337
|
+
# the discovery it performs finds and checks it too \u2014 the gate covers its own classifier.
|
|
338
|
+
shellcheck_tmp=$(mktemp -d) || exit 1
|
|
339
|
+
trap 'rm -rf "$shellcheck_tmp"' 0
|
|
340
|
+
trap 'exit 1' 1 2 3 15
|
|
341
|
+
# Every commit in each pushed range, never the tip alone: pushing two commits \u2014 a bad
|
|
342
|
+
# script, then its fix \u2014 passed a tip-only read while the bad commit landed on the remote.
|
|
343
|
+
# An all-zero local sha is a delete (nothing to check). An all-zero REMOTE sha is a new
|
|
344
|
+
# branch: everything no remote already has is being pushed, so the range is the local sha
|
|
345
|
+
# minus every remote-tracking ref \u2014 commits a remote already received were gated when they
|
|
346
|
+
# landed there, and the residue is exactly this push's new commits. A remote sha this clone
|
|
347
|
+
# has never seen (the remote moved on since the last fetch) cannot bound a range, so it takes
|
|
348
|
+
# the same new-branch rule instead of refusing a push the gate could have read. Enumeration
|
|
349
|
+
# failure refuses the push: a range the gate could not list is a range it did not read.
|
|
350
|
+
# (pattern) with both parens: bash 3.2 (macOS /bin/sh) cannot parse an unbalanced )
|
|
351
|
+
# in a case pattern.
|
|
352
|
+
: > "$shellcheck_tmp/shas" || exit 1
|
|
353
|
+
printf '%s\\n' "$refs" | while read -r _lref lsha _rref rsha; do
|
|
354
|
+
case "$lsha" in
|
|
355
|
+
(*[!0]*) ;;
|
|
356
|
+
(*) continue ;;
|
|
357
|
+
esac
|
|
358
|
+
case "$rsha" in
|
|
359
|
+
(*[!0]*)
|
|
360
|
+
if git cat-file -e "\${rsha}^{commit}" 2>/dev/null; then
|
|
361
|
+
git rev-list "$rsha..$lsha"
|
|
362
|
+
else
|
|
363
|
+
git rev-list "$lsha" --not --remotes
|
|
364
|
+
fi ;;
|
|
365
|
+
(*) git rev-list "$lsha" --not --remotes ;;
|
|
366
|
+
esac >> "$shellcheck_tmp/shas" || exit 1
|
|
367
|
+
done || {
|
|
368
|
+
echo "etymd: could not enumerate the commits being pushed for shellcheck" >&2
|
|
267
369
|
exit 1
|
|
268
370
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
371
|
+
shas=$(sort -u "$shellcheck_tmp/shas") || exit 1
|
|
372
|
+
[ -n "$shas" ] || echo "\u203A shellcheck: no commit in the pushed refs (deletes only, or nothing on stdin) \u2014 nothing to check"
|
|
373
|
+
# Resolved once, before any cd: the classifier runs inside each materialised tree, and a
|
|
374
|
+
# relative hook path would no longer point at it from there.
|
|
375
|
+
discover="$(cd "$(dirname "$0")" && pwd)/discover-shell-scripts.sh" || exit 1
|
|
376
|
+
for sha in $shas; do
|
|
377
|
+
# A fresh directory per commit, removed once that commit is checked so a long push does
|
|
378
|
+
# not pile up one full tree per commit; the subshell trap above still cleans the root on
|
|
379
|
+
# every early exit.
|
|
380
|
+
tree=$(mktemp -d "$shellcheck_tmp/commit.XXXXXX") || exit 1
|
|
381
|
+
# Checked out through a scratch index, never \`git archive\`: archive honours the
|
|
382
|
+
# export-ignore and export-subst attributes, so a script marked export-ignore would leave
|
|
383
|
+
# the checked set without a word. read-tree + checkout-index writes every tracked blob.
|
|
384
|
+
# Large-file pointers stay pointers \u2014 they are never shell, and a push must not need the
|
|
385
|
+
# network to be checked.
|
|
386
|
+
if ! git cat-file -e "\${sha}^{commit}" 2>/dev/null \\
|
|
387
|
+
|| ! GIT_INDEX_FILE="$shellcheck_tmp/index" git read-tree "$sha" 2>/dev/null \\
|
|
388
|
+
|| ! GIT_INDEX_FILE="$shellcheck_tmp/index" GIT_LFS_SKIP_SMUDGE=1 \\
|
|
389
|
+
git checkout-index -a -f --prefix="$tree/" 2>/dev/null; then
|
|
390
|
+
echo "\u2717 shellcheck: could not materialise $(git rev-parse --short "$sha" 2>/dev/null || echo "$sha") \u2014 refusing the push rather than certifying bytes this gate did not read" >&2
|
|
391
|
+
exit 1
|
|
392
|
+
fi
|
|
393
|
+
if [ -n "$(git ls-tree -r --name-only "$sha")" ] && [ -z "$(ls -A "$tree")" ]; then
|
|
394
|
+
echo "\u2717 shellcheck: $(git rev-parse --short "$sha" 2>/dev/null || echo "$sha") extracted to an empty tree \u2014 refusing the push" >&2
|
|
395
|
+
exit 1
|
|
396
|
+
fi
|
|
397
|
+
: > "$shellcheck_tmp/scripts" && : > "$shellcheck_tmp/count" && : > "$shellcheck_tmp/zsh-count" && : > "$shellcheck_tmp/skip-count" || exit 1
|
|
398
|
+
git ls-tree -r -z --name-only "$sha" > "$shellcheck_tmp/tracked" || {
|
|
399
|
+
echo "etymd: cannot enumerate the tree of $(git rev-parse --short "$sha" 2>/dev/null || echo "$sha") for shellcheck" >&2
|
|
400
|
+
exit 1
|
|
401
|
+
}
|
|
402
|
+
( cd "$tree" && xargs -0 "$discover" "$shellcheck_tmp" ) < "$shellcheck_tmp/tracked" || {
|
|
403
|
+
echo "etymd: shell script discovery failed; shellcheck coverage is incomplete" >&2
|
|
404
|
+
exit 1
|
|
405
|
+
}
|
|
406
|
+
count=$(wc -c < "$shellcheck_tmp/count") || exit 1
|
|
407
|
+
zsh_count=$(wc -c < "$shellcheck_tmp/zsh-count") || exit 1
|
|
408
|
+
skip_count=$(wc -c < "$shellcheck_tmp/skip-count") || exit 1
|
|
409
|
+
if [ "$skip_count" -gt 0 ]; then
|
|
410
|
+
echo "\u203A shellcheck: $((skip_count)) tracked path(s) with nothing readable behind them (submodule or dangling symlink) \u2014 not checked, not failed"
|
|
411
|
+
fi
|
|
412
|
+
if [ "$zsh_count" -gt 0 ]; then
|
|
413
|
+
echo "\u203A shellcheck: $((zsh_count)) zsh script(s) excluded \u2014 shellcheck cannot parse zsh (SC1071); not checked, not failed"
|
|
414
|
+
fi
|
|
415
|
+
if [ "$count" -gt 0 ]; then
|
|
416
|
+
echo "\u203A shellcheck ($((count)) scripts in $(git rev-parse --short "$sha" 2>/dev/null || echo "$sha"), blocking at severity=warning)"
|
|
417
|
+
( cd "$tree" && xargs -0 shellcheck -S warning -- < "$shellcheck_tmp/scripts" ) || {
|
|
418
|
+
echo " fix, or justify inline with '# shellcheck disable=SCxxxx # why'"
|
|
419
|
+
exit 1
|
|
420
|
+
}
|
|
421
|
+
# Everything below the blocking bar, shown once the push is already cleared. Never affects
|
|
422
|
+
# the exit code \u2014 advice that can fail a push is not advice.
|
|
423
|
+
advice=$( ( cd "$tree" && xargs -0 shellcheck -S style -f gcc -- < "$shellcheck_tmp/scripts" 2>/dev/null ) \\
|
|
424
|
+
| grep -v ': warning:\\|: error:' || true)
|
|
425
|
+
if [ -n "$advice" ]; then
|
|
426
|
+
echo " \xB7 style/info (not blocking):"
|
|
427
|
+
printf '%s\\n' "$advice" | sed 's/^/ /'
|
|
428
|
+
fi
|
|
429
|
+
fi
|
|
430
|
+
rm -rf "$tree" || exit 1
|
|
431
|
+
done
|
|
432
|
+
) || exit 1
|
|
278
433
|
else
|
|
279
434
|
echo "\u203A shellcheck skipped (not on PATH) \u2014 install it to gate this repo's shell scripts"
|
|
280
435
|
fi`;
|
|
@@ -315,7 +470,7 @@ fi`;
|
|
|
315
470
|
return stampGenerated(`#!/usr/bin/env sh
|
|
316
471
|
# etymd: correctness gate. Mirrors CI cheapest-first; blocks the push on any failure.
|
|
317
472
|
|
|
318
|
-
${localHookCall("pre-push")}${runner}
|
|
473
|
+
${localHookCall("pre-push", true)}${runner}
|
|
319
474
|
${body}${shellStep}
|
|
320
475
|
${auditStep}
|
|
321
476
|
|
|
@@ -359,98 +514,4 @@ exit 0
|
|
|
359
514
|
`);
|
|
360
515
|
}
|
|
361
516
|
|
|
362
|
-
|
|
363
|
-
function derivedCommands(facts, existingHook) {
|
|
364
|
-
const c = facts.commands;
|
|
365
|
-
const base = [c.formatCheck, c.typecheck, c.lint].filter(
|
|
366
|
-
(k) => Boolean(k) && isSafeGateCommand(c.raw[k])
|
|
367
|
-
);
|
|
368
|
-
if (c.test && existingHook && new RegExp(`\\b${c.test}\\b`).test(existingHook)) {
|
|
369
|
-
base.push(c.test);
|
|
370
|
-
}
|
|
371
|
-
return base;
|
|
372
|
-
}
|
|
373
|
-
function riskReachability(facts) {
|
|
374
|
-
const reasons = [];
|
|
375
|
-
if (facts.publishRoute !== "none") {
|
|
376
|
-
reasons.push("an instruction file can name a package script that no longer exists");
|
|
377
|
-
}
|
|
378
|
-
if (facts.artifacts.some((a) => a.kind === "state" && a.exists)) {
|
|
379
|
-
reasons.push("a state doc can fall far enough behind the repo to escalate");
|
|
380
|
-
}
|
|
381
|
-
return reasons;
|
|
382
|
-
}
|
|
383
|
-
function deriveFailOn(facts, recorded) {
|
|
384
|
-
const reachable = riskReachability(facts);
|
|
385
|
-
if (recorded.explicit) return { failOn: recorded.failOn, source: "config", reachable };
|
|
386
|
-
if (recorded.failOn === "risk" && reachable.length === 0) {
|
|
387
|
-
return { failOn: "gap", source: "derived", reachable };
|
|
388
|
-
}
|
|
389
|
-
return { failOn: recorded.failOn, source: "default", reachable };
|
|
390
|
-
}
|
|
391
|
-
async function planWorkflow(root, facts, opts) {
|
|
392
|
-
const out = [];
|
|
393
|
-
const add = async (rel, contents, label, executable = false) => {
|
|
394
|
-
const abs = path.join(root, rel);
|
|
395
|
-
const exists = await pathExists(abs);
|
|
396
|
-
const existing = exists ? await readText(abs) : null;
|
|
397
|
-
const differs = exists ? existing !== contents : void 0;
|
|
398
|
-
const origin = differs && existing !== null ? fileOrigin(existing) : void 0;
|
|
399
|
-
out.push({
|
|
400
|
-
path: rel,
|
|
401
|
-
contents,
|
|
402
|
-
exists,
|
|
403
|
-
differs,
|
|
404
|
-
drift: origin === "pack" ? "stale" : origin,
|
|
405
|
-
executable,
|
|
406
|
-
label
|
|
407
|
-
});
|
|
408
|
-
};
|
|
409
|
-
if (opts.agents) {
|
|
410
|
-
await add("AGENTS.md", generateAgentsMd(facts), "Minimal operating contract (scaffold)");
|
|
411
|
-
}
|
|
412
|
-
if (opts.gates) {
|
|
413
|
-
const existingPrePush = await readText(path.join(root, ".githooks", "pre-push"));
|
|
414
|
-
const selfBuild = isSelfBuildRepo(facts);
|
|
415
|
-
await add(
|
|
416
|
-
".githooks/pre-commit",
|
|
417
|
-
generatePreCommitHook(selfBuild),
|
|
418
|
-
"Process gate (pre-commit)",
|
|
419
|
-
true
|
|
420
|
-
);
|
|
421
|
-
await add(
|
|
422
|
-
".githooks/commit-msg",
|
|
423
|
-
generateCommitMsgHook(opts.gateConfig),
|
|
424
|
-
"Message gate (content screen, format)",
|
|
425
|
-
true
|
|
426
|
-
);
|
|
427
|
-
const gateConfig = opts.gateConfig?.commands?.length ? opts.gateConfig : {
|
|
428
|
-
commands: derivedCommands(facts, existingPrePush ?? void 0),
|
|
429
|
-
failOn: opts.gateConfig?.failOn ?? DEFAULT_CONFIG.gates.failOn,
|
|
430
|
-
publishGate: opts.gateConfig?.publishGate,
|
|
431
|
-
commitFormat: opts.gateConfig?.commitFormat,
|
|
432
|
-
allowWriting: opts.gateConfig?.allowWriting ?? []
|
|
433
|
-
};
|
|
434
|
-
const tier = deriveFailOn(facts, {
|
|
435
|
-
failOn: gateConfig.failOn,
|
|
436
|
-
explicit: opts.gateFailOnPinned ?? false
|
|
437
|
-
});
|
|
438
|
-
await add(
|
|
439
|
-
".githooks/pre-push",
|
|
440
|
-
generatePrePushHook(facts, { ...gateConfig, failOn: tier.failOn }, selfBuild),
|
|
441
|
-
"Correctness gate (pre-push)",
|
|
442
|
-
true
|
|
443
|
-
);
|
|
444
|
-
if (opts.gateConfig?.publishGate ?? opts.publishGate ?? facts.publishable) {
|
|
445
|
-
await add(
|
|
446
|
-
"scripts/artifact-check.sh",
|
|
447
|
-
generateArtifactCheckScript(selfBuild),
|
|
448
|
-
"Content screen (published artifact)",
|
|
449
|
-
true
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
return out;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
export { deriveFailOn, derivedCommands, isSafeGateCommand, planWorkflow, riskReachability, runPrefix };
|
|
517
|
+
export { fileOrigin, generateAgentsMd, generateArtifactCheckScript, generateClaudePointerMd, generateCommitMsgHook, generatePreCommitHook, generatePrePushHook, generateShellDiscoveryScript, isSafeGateCommand, isSelfBuildRepo, runPrefix };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { stateFreshnessLens, rankFindings, listInstructionFiles, buildTruthEnv, emptyCounters, packageManagerUsage, checkDocRefs, listStateDocuments, loadDecisionLedger, checkDecisionRefs, checkTextClaims } from './chunk-
|
|
2
|
+
import { stateFreshnessLens, rankFindings, listInstructionFiles, buildTruthEnv, emptyCounters, packageManagerUsage, checkDocRefs, listStateDocuments, loadDecisionLedger, checkDecisionRefs, checkTextClaims } from './chunk-WKP72Z3J.js';
|
|
3
3
|
import { readLedger, reconcileLedger, writeLedger, visibleFindings } from './chunk-3E2IPCRY.js';
|
|
4
4
|
import { measureContext, contextFileLabel } from './chunk-HI7NWPRA.js';
|
|
5
|
-
import { scanProject } from './chunk-
|
|
5
|
+
import { scanProject } from './chunk-6DJMKZGK.js';
|
|
6
6
|
import { DEFAULT_CONFIG, ETYMD_DIR, writeCachedFacts, readBaseline, readConfig, deriveProfile, baselineCarriesMachinePath, BASELINE_FILE, CONFIG_FILE } from './chunk-P6ATKV2R.js';
|
|
7
|
-
import { PACK_VERSION } from './chunk-
|
|
7
|
+
import { PACK_VERSION } from './chunk-2DRA7S67.js';
|
|
8
8
|
import { pathExists, readText, readJson, isCiEnvironment, isExecutable } from './chunk-4VPBP6K6.js';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import YAML from 'yaml';
|
|
@@ -316,7 +316,7 @@ async function localHookTools(root, facts, scripts) {
|
|
|
316
316
|
const inertCompanions = [];
|
|
317
317
|
const readHook = async (name) => {
|
|
318
318
|
if (!hooks.dir) return empty;
|
|
319
|
-
const text = await readText(path.
|
|
319
|
+
const text = await readText(path.resolve(root, hooks.dir, name));
|
|
320
320
|
if (!text) return empty;
|
|
321
321
|
const tools = new Set(matchTools(text, scripts));
|
|
322
322
|
const companion = await companionOf(root, hooks.dir, name, text, scripts);
|
|
@@ -479,7 +479,7 @@ async function probeScreener(root, facts) {
|
|
|
479
479
|
const doors = [];
|
|
480
480
|
let devBuildArm = false;
|
|
481
481
|
for (const name of HOOK_FILES) {
|
|
482
|
-
const text = await readText(path.
|
|
482
|
+
const text = await readText(path.resolve(root, dir, name));
|
|
483
483
|
if (!text || !SCREEN_CALL_RE.test(text)) continue;
|
|
484
484
|
doors.push(`${dir}/${name}`);
|
|
485
485
|
if (text.includes(DEV_BUILD_ARM)) devBuildArm = true;
|
|
@@ -923,6 +923,11 @@ var instructionTruthLens = {
|
|
|
923
923
|
`${counters.prospectiveSkipped} path claim(s) sit in create-this prose (the file instructs generating them) \u2014 forward-looking, not stale; skipped, not flagged.`
|
|
924
924
|
);
|
|
925
925
|
}
|
|
926
|
+
if (counters.fetchedSkipped) {
|
|
927
|
+
disclosures.push(
|
|
928
|
+
`${counters.fetchedSkipped} path claim(s) sit beside the URL they are fetched from \u2014 another tree's files, unverifiable here; skipped, not flagged.`
|
|
929
|
+
);
|
|
930
|
+
}
|
|
926
931
|
if (counters.placeholderSkipped) {
|
|
927
932
|
disclosures.push(
|
|
928
933
|
`${counters.placeholderSkipped} path claim(s) are naming stand-ins (e.g. \`my-custom-skill\`) rather than real references; skipped, not flagged.`
|
|
@@ -960,7 +965,7 @@ var instructionTruthLens = {
|
|
|
960
965
|
);
|
|
961
966
|
}
|
|
962
967
|
disclosures.push(
|
|
963
|
-
`Checked ${files.length} instruction file(s); commands resolved against root + ${facts.packages.length} workspace manifest(s) plus installed binaries; paths matched against root and
|
|
968
|
+
`Checked ${files.length} instruction file(s); commands resolved against root + ${facts.packages.length} workspace manifest(s) plus installed binaries; paths matched against root, package roots, the claiming file's directory, and directories the same file names; existence judged on the working tree (gitignored-but-present is true, gitignored-and-absent unverifiable). Heuristics: workspace-filtered commands skipped (${counters.filteredSkipped}); tokens without a recognized extension treated as prose (a dir claim needs a trailing slash); schemeless hosts read as URLs; gitignored claims unverifiable; create-this, fetched-from-URL, and stand-in path claims skipped; absolute/globbed/placeholder tokens skipped; doc mentions inside \`~/\` home paths skipped; framework-pattern staleness not checked.`
|
|
964
969
|
);
|
|
965
970
|
return {
|
|
966
971
|
lens: LENS_ID3,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { expandFileGlobs } from './chunk-
|
|
2
|
+
import { expandFileGlobs } from './chunk-6DJMKZGK.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';
|
|
@@ -439,10 +439,13 @@ function extractCommandClaims(text) {
|
|
|
439
439
|
return { scripts, filteredSkipped };
|
|
440
440
|
}
|
|
441
441
|
var PATH_TOKEN_RE = /^[A-Za-z0-9_.-]+(\/[A-Za-z0-9_.$-]+)+\/?$/;
|
|
442
|
+
var HOSTNAME_RE = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*\.([a-z]{2,})$/i;
|
|
442
443
|
var KNOWN_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
443
444
|
..."ts tsx cts mts js jsx cjs mjs json jsonc json5 md mdx mdc yml yaml toml ini cfg conf env sh bash zsh fish ps1 bat cmd css scss sass less html htm xml svg sql prisma graphql gql proto py rb rs go java kt kts swift c h cc cpp hpp cs php vue svelte astro txt log lock csv tsv png jpg jpeg gif webp ico avif woff woff2 ttf otf wasm map pem key crt tf tfvars example sample local snap ejs hbs pug".split(" ")
|
|
444
445
|
]);
|
|
445
446
|
var CREATION_CONTEXT_RE = /\b(?:creat(?:e|es|ed|ing)|generat(?:e|es|ed|ing)|scaffold(?:s|ed|ing)?|quarantin(?:e|es|ed|ing)|(?:writ(?:e|es|ten|ing)|output(?:s|ted)?|emit(?:s|ted|ting)?|sav(?:e|es|ed|ing)|mov(?:e|es|ed|ing)|copy|copi(?:es|ed))\s+(?:it\s+|them\s+)?(?:to|into)|new\s+(?:file|directory|folder)|will\s+(?:be\s+)?(?:created|generated|written)|add(?:s|ed|ing)?\s+(?:a|the)\s+new)\b/i;
|
|
447
|
+
var FETCH_CONTEXT_RE = /\b(?:fetch(?:es|ed|ing)?|pull(?:s|ed|ing)?|clone(?:s|d)?|download(?:s|ed|ing)?)\b/i;
|
|
448
|
+
var EXTERNAL_URL_RE = /https?:\/\/|\bwww\./i;
|
|
446
449
|
var PLACEHOLDER_SEGMENTS = /* @__PURE__ */ new Set(["placeholder", "foo", "bar", "baz", "qux"]);
|
|
447
450
|
var PLACEHOLDER_PREFIX_RE = /^(?:my|your)-/i;
|
|
448
451
|
function isPlaceholderClaim(token) {
|
|
@@ -476,14 +479,21 @@ ${line}`;
|
|
|
476
479
|
}
|
|
477
480
|
function extractPathClaims(text, opts = {}) {
|
|
478
481
|
const prospectiveOnly = /* @__PURE__ */ new Map();
|
|
482
|
+
const fetchedOnly = /* @__PURE__ */ new Map();
|
|
479
483
|
const namespacedOnly = /* @__PURE__ */ new Map();
|
|
480
484
|
const placeholder = /* @__PURE__ */ new Set();
|
|
485
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
481
486
|
for (const m of text.matchAll(/`([^`\n]+)`/g)) {
|
|
482
487
|
const token = m[1].trim();
|
|
483
488
|
if (token.includes(" ") || token.length > 120) continue;
|
|
484
489
|
if (token.startsWith("/") || token.startsWith("~") || token.startsWith("@") || token.startsWith("$"))
|
|
485
490
|
continue;
|
|
486
491
|
if (token.includes("://") || token.startsWith("www.")) continue;
|
|
492
|
+
if (token.includes("/")) {
|
|
493
|
+
const firstSegment = token.split("/")[0] ?? token;
|
|
494
|
+
const hostSuffix = HOSTNAME_RE.exec(firstSegment)?.[1]?.toLowerCase();
|
|
495
|
+
if (hostSuffix && !KNOWN_EXTENSIONS.has(hostSuffix)) continue;
|
|
496
|
+
}
|
|
487
497
|
if (/[*?{}<>|]/.test(token)) continue;
|
|
488
498
|
if (token.includes("@")) continue;
|
|
489
499
|
if (!PATH_TOKEN_RE.test(token)) continue;
|
|
@@ -496,8 +506,12 @@ function extractPathClaims(text, opts = {}) {
|
|
|
496
506
|
placeholder.add(claim);
|
|
497
507
|
continue;
|
|
498
508
|
}
|
|
499
|
-
|
|
509
|
+
if (isDirClaim) dirs.add(claim);
|
|
510
|
+
const context = claimContext(text, m.index ?? 0);
|
|
511
|
+
const prospective2 = CREATION_CONTEXT_RE.test(context);
|
|
500
512
|
prospectiveOnly.set(claim, (prospectiveOnly.get(claim) ?? true) && prospective2);
|
|
513
|
+
const fetched2 = FETCH_CONTEXT_RE.test(context) && EXTERNAL_URL_RE.test(context);
|
|
514
|
+
fetchedOnly.set(claim, (fetchedOnly.get(claim) ?? true) && fetched2);
|
|
501
515
|
if (opts.namespaces) {
|
|
502
516
|
const nsLead = new RegExp(`(${NAMESPACE_IDENT}):[ \\t]*$`).exec(text.slice(0, m.index ?? 0));
|
|
503
517
|
const prefixed = isNamespace(nsLead?.[1]);
|
|
@@ -506,13 +520,15 @@ function extractPathClaims(text, opts = {}) {
|
|
|
506
520
|
}
|
|
507
521
|
const paths = [];
|
|
508
522
|
const prospective = [];
|
|
523
|
+
const fetched = [];
|
|
509
524
|
const namespaced = [];
|
|
510
525
|
for (const [claim, only] of prospectiveOnly) {
|
|
511
|
-
if (
|
|
526
|
+
if (fetchedOnly.get(claim)) fetched.push(claim);
|
|
527
|
+
else if (only) prospective.push(claim);
|
|
512
528
|
else if (namespacedOnly.get(claim)) namespaced.push(claim);
|
|
513
529
|
else paths.push(claim);
|
|
514
530
|
}
|
|
515
|
-
return { paths, prospective, namespaced, placeholder: [...placeholder] };
|
|
531
|
+
return { paths, dirs: [...dirs], prospective, fetched, namespaced, placeholder: [...placeholder] };
|
|
516
532
|
}
|
|
517
533
|
var LOCAL_REF_LEADINS = new Set(
|
|
518
534
|
"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(" ")
|
|
@@ -582,12 +598,14 @@ async function buildTruthEnv(root, facts) {
|
|
|
582
598
|
for (const key of Object.keys(pkgJson?.scripts ?? {})) knownScripts.add(key);
|
|
583
599
|
}
|
|
584
600
|
const bases = [root, ...facts.packages.map((p) => path3.join(root, p.dir))];
|
|
585
|
-
const pathResolves = async (claim) => {
|
|
601
|
+
const pathResolves = async (claim, fromDir) => {
|
|
586
602
|
for (const base of bases) {
|
|
587
603
|
if (await pathExists(path3.join(base, claim))) return true;
|
|
588
604
|
if (await pathExists(path3.join(base, "src", claim))) return true;
|
|
589
605
|
if (await pathExists(path3.join(base, "scripts", claim))) return true;
|
|
590
606
|
}
|
|
607
|
+
if (fromDir && fromDir !== "." && await pathExists(path3.join(root, fromDir, claim)))
|
|
608
|
+
return true;
|
|
591
609
|
return false;
|
|
592
610
|
};
|
|
593
611
|
const binResolves = async (name) => {
|
|
@@ -617,6 +635,7 @@ function emptyCounters() {
|
|
|
617
635
|
unverifiableCommands: 0,
|
|
618
636
|
gitignoredSkipped: 0,
|
|
619
637
|
prospectiveSkipped: 0,
|
|
638
|
+
fetchedSkipped: 0,
|
|
620
639
|
placeholderSkipped: 0,
|
|
621
640
|
qualifiedRefsSkipped: 0,
|
|
622
641
|
unresolvableRefs: 0,
|
|
@@ -659,19 +678,48 @@ async function checkTextClaims(env, file, opts, counters) {
|
|
|
659
678
|
confidence: "high"
|
|
660
679
|
});
|
|
661
680
|
}
|
|
662
|
-
const { paths, prospective, placeholder, namespaced } = extractPathClaims(
|
|
663
|
-
|
|
664
|
-
|
|
681
|
+
const { paths, dirs, prospective, fetched, placeholder, namespaced } = extractPathClaims(
|
|
682
|
+
file.text,
|
|
683
|
+
{ namespaces: opts.treatNamespacedPrefixes }
|
|
684
|
+
);
|
|
665
685
|
counters.prospectiveSkipped += prospective.length;
|
|
686
|
+
counters.fetchedSkipped += fetched.length;
|
|
666
687
|
counters.placeholderSkipped += placeholder.length;
|
|
667
688
|
counters.namespacedSkipped += namespaced.length;
|
|
668
|
-
const
|
|
689
|
+
const fromDir = path3.dirname(file.path);
|
|
690
|
+
const unresolved = [];
|
|
669
691
|
for (const claim of paths) {
|
|
670
|
-
if (await env.pathResolves(claim
|
|
692
|
+
if (await env.pathResolves(claim, fromDir))
|
|
693
|
+
examined.push({ kind: "path", value: claim, exists: true });
|
|
694
|
+
else unresolved.push(claim);
|
|
695
|
+
}
|
|
696
|
+
const proseBases = /* @__PURE__ */ new Set();
|
|
697
|
+
for (const dirClaim of dirs) {
|
|
698
|
+
if (await env.pathResolves(dirClaim, fromDir)) {
|
|
699
|
+
proseBases.add(dirClaim);
|
|
700
|
+
proseBases.add(path3.dirname(dirClaim));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
const missing = [];
|
|
704
|
+
for (const claim of unresolved) {
|
|
705
|
+
let anchored = false;
|
|
706
|
+
for (const base of proseBases) {
|
|
707
|
+
if (await pathExists(path3.join(env.root, base, claim))) anchored = true;
|
|
708
|
+
else if (fromDir !== "." && await pathExists(path3.join(env.root, fromDir, base, claim)))
|
|
709
|
+
anchored = true;
|
|
710
|
+
if (anchored) break;
|
|
711
|
+
}
|
|
712
|
+
if (anchored) examined.push({ kind: "path", value: claim, exists: true });
|
|
671
713
|
else missing.push(claim);
|
|
672
714
|
}
|
|
673
715
|
const ignoredOut = missing.length ? await git(env.root, ["check-ignore", ...missing]) : null;
|
|
674
716
|
const gitignored = new Set((ignoredOut ?? "").split("\n").filter(Boolean));
|
|
717
|
+
const unproven = missing.filter((claim) => !gitignored.has(claim));
|
|
718
|
+
if (unproven.length) {
|
|
719
|
+
const dirOut = await git(env.root, ["check-ignore", ...unproven.map((claim) => `${claim}/`)]);
|
|
720
|
+
for (const line of (dirOut ?? "").split("\n").filter(Boolean))
|
|
721
|
+
gitignored.add(line.replace(/\/$/, ""));
|
|
722
|
+
}
|
|
675
723
|
let pathFindings = 0;
|
|
676
724
|
for (const claim of missing) {
|
|
677
725
|
if (opts.rootedFirstSegments && !opts.rootedFirstSegments.has(claim.split("/")[0] ?? claim)) {
|
|
@@ -780,4 +828,4 @@ function checkDecisionRefs(file, ledger, opts, counters) {
|
|
|
780
828
|
return { findings, examined };
|
|
781
829
|
}
|
|
782
830
|
|
|
783
|
-
export { KNOWN_EXTENSIONS, NAMESPACE_IDENT, NAMESPACE_STOP, PATH_TOKEN_RE, buildTruthEnv, checkDecisionRefs, checkDocRefs, checkTextClaims, emptyCounters, listInstructionFiles, listStateDocuments, loadDecisionLedger, meetsFailOn, packageManagerUsage, parseFailOnTier, rankFindings, stateFreshnessLens };
|
|
831
|
+
export { HOSTNAME_RE, KNOWN_EXTENSIONS, NAMESPACE_IDENT, NAMESPACE_STOP, PATH_TOKEN_RE, buildTruthEnv, checkDecisionRefs, checkDocRefs, checkTextClaims, emptyCounters, listInstructionFiles, listStateDocuments, loadDecisionLedger, meetsFailOn, packageManagerUsage, parseFailOnTier, rankFindings, stateFreshnessLens };
|