crewforth 3.2.0-rc.2 → 3.2.0-rc.3
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 +100 -3
- package/README.md +2 -0
- package/VERSION +1 -1
- package/adopt.sh +10 -1
- package/bin/cli.js +3 -2
- package/kit/CLAUDE.md +3 -3
- package/kit/agents/crew-review-agent.md +2 -0
- package/kit/eval/doctor.sh +13 -2
- package/kit/eval/gate-report.sh +2 -0
- package/kit/eval/lib/crew-map/narrative.mjs +42 -8
- package/kit/eval/lib/crew-map/project.mjs +3 -2
- package/kit/eval/lib/crew-map/rules/tech.json +35 -0
- package/kit/eval/lib/crew-map/scan.mjs +56 -4
- package/kit/eval/lib/crew-map/stacks.mjs +114 -8
- package/kit/eval/lib/crew-map/surface.mjs +62 -1
- package/kit/eval/lib/gate-log.sh +52 -0
- package/kit/eval/lib/json-string.awk +27 -0
- package/kit/eval/lib/preset.sh +109 -0
- package/kit/eval/lib/presets/default.json +7 -0
- package/kit/eval/lib/presets/solo.json +7 -0
- package/kit/eval/lib/presets/strict.json +11 -0
- package/kit/eval/lib/preview.sh +150 -0
- package/kit/eval/lib/review-bench/bench.mjs +273 -0
- package/kit/eval/lib/review-prepass/prepass.mjs +203 -0
- package/kit/eval/lib/review-prepass/prepass.sh +23 -0
- package/kit/eval/lib/settings-json.awk +4 -0
- package/kit/eval/lib/statusline.sh +1 -1
- package/kit/eval/lib/verify-cmd.sh +17 -0
- package/kit/eval/review-bench.sh +26 -0
- package/kit/eval/smoke-test.sh +142 -20
- package/kit/hooks/board-sync.sh +2 -23
- package/kit/hooks/guard-agent-model.sh +32 -23
- package/kit/hooks/guard-bash.sh +36 -21
- package/kit/hooks/guard-write.sh +3 -7
- package/kit/hooks/precompact-snapshot.sh +75 -0
- package/kit/hooks/session-rehydrate.sh +25 -2
- package/kit/settings.json +12 -0
- package/kit/skills/crew-code-review/SKILL.md +16 -0
- package/kit/skills/crew-map/SKILL.md +16 -5
- package/kit/skills/crew-review/SKILL.md +2 -1
- package/kit/studio/README.md +21 -10
- package/kit/studio/server/index.js +74 -22
- package/kit/studio/server/lib/graph.js +9 -1
- package/kit/studio/server/lib/kit-telemetry.js +30 -5
- package/kit/studio/server/lib/lan.js +87 -0
- package/kit/studio/server/lib/redact.js +60 -0
- package/kit/studio/server/lib/remote.js +104 -18
- package/kit/studio/server/lib/session.js +1 -1
- package/kit/studio/server/lib/terminal.js +51 -13
- package/kit/studio/server/lib/transcript.js +32 -0
- package/kit/studio/web/app.js +69 -7
- package/kit/studio/web/canvas.js +14 -2
- package/kit/studio/web/chat.js +32 -7
- package/kit/studio/web/graph-plan.js +3 -2
- package/kit/studio/web/map-plan.js +72 -3
- package/kit/studio/web/map.js +103 -10
- package/kit/studio/web/qr.js +200 -0
- package/kit/studio/web/style.css +34 -0
- package/kit/studio/web/tokens.css +6 -0
- package/package.json +1 -1
- package/start.sh +14 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,81 @@ versioning follows [SemVer](https://semver.org/).
|
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
+
- **Watch Studio from a phone on the same Wi-Fi.** A viewer is a second token that only reads: the Machines tab makes
|
|
13
|
+
one, lists each with its last request, and revokes it. **Add viewer** shows its link as a QR code; a phone on
|
|
14
|
+
the same network scans it and the panel opens there, with no ssh and no tunnel. While a viewer is live the server
|
|
15
|
+
also listens on the machine's private Wi-Fi address, for viewer tokens only (the panel's own token gets `401` from
|
|
16
|
+
the network); with none, nothing listens beyond `127.0.0.1`. A viewer lasts eight hours, then the phone shows
|
|
17
|
+
Session expired; revoking it closes its stream at once, and the last one closes the Wi-Fi listener. Transcripts
|
|
18
|
+
are hidden by default: the server sends such a link no transcript text at all (an allow-list of fields, applied to
|
|
19
|
+
every answer and to the event stream), and shows them only for a link added with them ticked. Every viewer
|
|
20
|
+
connection and refusal is logged. Measured with an Android phone; the firewall asks once (macOS: allow `node`;
|
|
21
|
+
Windows: Private networks).
|
|
22
|
+
|
|
23
|
+
- **The advisor, measured.** `review-bench --advisor <model>` runs the agent arm with an advisor and reports each
|
|
24
|
+
run's cost by model and its advisor calls; a case of `kind: "task"` is an implementation graded by a check the
|
|
25
|
+
session never saw. Against Opus alone on five review cases and a task (24 runs), Sonnet with an Opus advisor
|
|
26
|
+
never called it: 6 of 6 defects caught against 5 of 6, for $1.06 against $2.23. No rule is added; the runs and
|
|
27
|
+
their numbers are in `packaging/advisor-bench/`.
|
|
28
|
+
|
|
29
|
+
- **An organisation preset.** `--preset <name|file>` on the installers (and `preset.sh --apply` later) writes an
|
|
30
|
+
organisation's critical paths, model floors, verify commands and language as one marked block at the top of
|
|
31
|
+
`.claude/crew-model-rules`; the project's own lines stay below it and win where the rules let them. Three ship:
|
|
32
|
+
`default`, `strict`, `solo`. An invalid preset is refused before anything is installed; `doctor` names the one
|
|
33
|
+
in force. It covers the surfaces the gates read today; which agents a project gets is not part of it.
|
|
34
|
+
|
|
35
|
+
- **Continue here says what it will do before it is pressed.** Studio continues a session it did not start as
|
|
36
|
+
that same session when nothing else holds it open, and starts a copy when something does; the read-only foot now
|
|
37
|
+
says which it will be before the press, from the check it acts on, and offers the command that opens the session
|
|
38
|
+
in the desktop app again (`claude --desktop --resume <id>`) to copy. Measured with a real session: its id stayed
|
|
39
|
+
the same from a terminal, through Studio, and back to a terminal, in one transcript.
|
|
40
|
+
On Windows the desktop command is a PowerShell line, and the terminal Studio opens runs `claude --resume <id>`
|
|
41
|
+
without single quotes, which cmd.exe handed to the program as part of the id. An id that is not a session id
|
|
42
|
+
builds no command at all.
|
|
43
|
+
The terminal Studio opens runs `claude --resume <id>` and nothing else: the route no longer takes a `command`
|
|
44
|
+
from the request, which it ran unchecked and the panel never sent.
|
|
45
|
+
|
|
46
|
+
- **A stopped command says what it would have touched.** When the shell gate stops `git reset --hard`,
|
|
47
|
+
`git clean -f`, `git push --force` or `rm -r`, or asks about a commit or a push, one line says what it would
|
|
48
|
+
have done: the uncommitted changes and commits it discards, the paths a dry run lists, the remote commits it
|
|
49
|
+
overwrites, the files under a target inside the project, what a commit takes and a push sends. Read only, after
|
|
50
|
+
the decision and in a subshell of its own, so a failing preview never changes the decision. The push question
|
|
51
|
+
became cheaper too: the branch is read from HEAD without a process.
|
|
52
|
+
|
|
53
|
+
- **What a compaction keeps is written down first.** A PreCompact hook writes the state of the work from the files
|
|
54
|
+
and the gates (branch, uncommitted files, the last gate refusal, the findings on record, a handover if there is
|
|
55
|
+
one) inside the repository's git directory, and SessionStart gives it back after the compaction, to the same
|
|
56
|
+
session only. It never blocks the compaction. Studio's session card adds how much of the context since the last
|
|
57
|
+
compaction is tool output, and how many times the session was compacted.
|
|
58
|
+
|
|
59
|
+
- **The Map reads like a project, not a graph.** Studio's Map draws modules in the layers `/crew-map` wrote (UI,
|
|
60
|
+
API, Service, Data, Infrastructure, Library, Tools), each card naming the module in full with its domain, up to
|
|
61
|
+
three of what it is built with and the outside services it calls folded into one line under it. A search dims
|
|
62
|
+
what it does not name; the tour `/crew-map` wrote steps through the map over the bottom of the pane; the
|
|
63
|
+
inspector shows the layer, the domain and what is inside a module (its entry point, its folders and their file
|
|
64
|
+
counts). A map with no layers is drawn in the bands the scan gives, as before.
|
|
65
|
+
|
|
66
|
+
- **The Crew Map says where each module sits, and how to read the project.** `/crew-map` now has the model add,
|
|
67
|
+
to each module, its layer (`ui`, `api`, `service`, `data`, `infra`, `tool`, `library`) and the part of the
|
|
68
|
+
business it serves, and to the map a tour of up to eight stops a newcomer reads in order. Each rests on lines
|
|
69
|
+
the scan read; `--validate` refuses a layer not on the list, a domain written as a sentence, a tour stop at no
|
|
70
|
+
node, citing another node's lines or visiting a node twice, and a scan again drops what no longer holds.
|
|
71
|
+
A module's README is read too: its first line of prose is a line a description may rest on (measured on a real
|
|
72
|
+
project, four of ten modules had nothing else to describe them by).
|
|
73
|
+
|
|
74
|
+
- **How right the review is, in numbers.** `eval/review-bench.sh` runs the review on labelled cases and reports
|
|
75
|
+
recall, precision and every false alarm by name; the pre-pass arm costs nothing, the agent arm runs Claude Code
|
|
76
|
+
headless as the review agent under a per-case and a per-run dollar cap, isolated from the user's own settings.
|
|
77
|
+
Crewforth's 27 cases come from its own history (a fix undone, its explanation left out) and 8 are held out; the
|
|
78
|
+
pre-pass's floor on them is a CI gate. Measured with Sonnet: 18 of 20 defects found, 34 of 49 findings about the defect, $1.62.
|
|
79
|
+
|
|
80
|
+
- **The review starts from a deterministic pre-pass.** `eval/lib/review-prepass/prepass.sh` reads the lines a
|
|
81
|
+
change adds (staged, or `--base <ref>` for a branch) and writes `.crewforth/review-prepass.json` in the findings
|
|
82
|
+
shape: a secret pattern (cited by hash), a debug call or a TODO comment left in code, shellcheck on shell files, a lock
|
|
83
|
+
file that moved without its manifest, a very large change. The review agent carries those over and writes its
|
|
84
|
+
own findings to `.crewforth/review.json`, blockers through the refuter. What it could not check (no shellcheck,
|
|
85
|
+
a file edited after staging, no Node) is said, never read as clean.
|
|
86
|
+
|
|
12
87
|
- **In `auto`, approve a commit or a push with one click.** The refusal now also offers a fixed question, asked with
|
|
13
88
|
AskUserQuestion: Commit only, Commit + push, Push only, No. A new gate, `guard-question.sh`, answers `ask` on it,
|
|
14
89
|
so a hook that fills in the answer cannot answer it for you (measured in 34 runs and by hand in the terminal), and
|
|
@@ -60,9 +135,6 @@ versioning follows [SemVer](https://semver.org/).
|
|
|
60
135
|
- **Studio's Models view marks the classes whose agents consulted the advisor.** The cost cell carries an advisor tag
|
|
61
136
|
whose hover says how many calls and how much of the cost they were, already included; a class whose agents consulted
|
|
62
137
|
none has no tag.
|
|
63
|
-
- **Read-only remote viewers for Studio.** The Machines tab makes a viewer link that only reads, lists each viewer
|
|
64
|
-
with its last request, and revokes it. The server still binds to `127.0.0.1`; watching from another device goes
|
|
65
|
-
through a tunnel you run (`ssh -L`, Tailscale).
|
|
66
138
|
- **A visible way to the repository.** `/crew-doctor` now shows the star line once per version under its own marker,
|
|
67
139
|
so a line already shown at install no longer silences it, and a plugin install shows it too, with the plugin's
|
|
68
140
|
version. `/crew-update` keeps it to one line per update. The READMEs and the site's home page have a Star on GitHub
|
|
@@ -125,6 +197,31 @@ versioning follows [SemVer](https://semver.org/).
|
|
|
125
197
|
|
|
126
198
|
### Fixed
|
|
127
199
|
|
|
200
|
+
- **The Crew Map says what a project is made of.** A Flutter project read as a pile of Gradle modules and its
|
|
201
|
+
server as an external API (seen on a real project). The scanner now reads Dart: one module per `pubspec.yaml`,
|
|
202
|
+
its packages and `path:` references, and a Flutter app's `android/` and `ios/` folders as platforms of the app.
|
|
203
|
+
Each module says what it is built with (Hono, Cloudflare Workers, Flutter, Riverpod…, from `rules/tech.json`)
|
|
204
|
+
and what is inside it (folders with file counts, tests apart). A module served at a host its wrangler routes or
|
|
205
|
+
`fly.toml` name is called, module to module, by any module that writes that host; a Dart call to that host or a
|
|
206
|
+
Dio call's relative path is matched to the endpoints provided. A `.dart` file no `pubspec.yaml` owns is listed
|
|
207
|
+
as not measured, as is Dart package usage: Flutter declares packages nothing imports.
|
|
208
|
+
|
|
209
|
+
- **A commit message and a reworded paragraph no longer go to the expensive model.** Two floors of the model gate
|
|
210
|
+
raised prose for no gain (seen on a real project): with no verify command a commit message was held to sonnet,
|
|
211
|
+
though the commit hook checks it, and a two-sentence fix under a critical path such as `legal/` was held to opus.
|
|
212
|
+
A text change now runs on haiku without a verify command, the commit agent too; on a critical path a text change
|
|
213
|
+
whose every file is prose (`.md`, `.txt`, `.rst`, `.adoc`) is written on sonnet, and the audit of it stays on opus.
|
|
214
|
+
One code file on the card keeps it on opus, and an agent on sonnet still writes no code there. A project that
|
|
215
|
+
wants opus for such text says so in `.claude/crew-model-rules` (`floor * text critical opus`).
|
|
216
|
+
- **The gate log is written in every project, and Studio shows a refused agent call.** A project that tracks
|
|
217
|
+
`.claude/` had no gate log at all: no record of a refusal, no 🛡 on the status line, no note of a model lowered to
|
|
218
|
+
its card's floor (seen on a real project). The log now goes inside the repository's own `.git` there
|
|
219
|
+
(`crewforth-gate-log.tsv`, never committed); one rule (`eval/lib/gate-log.sh`) serves every gate and reader. A
|
|
220
|
+
call the model gate refused starts no agent, so the canvas showed nothing while the session said "2 failed"; the
|
|
221
|
+
session's card now says how many calls were refused and not made again, with the gate's words on hover.
|
|
222
|
+
- **`/crew-doctor` offers a verify rule that works, in every mode.** For `cd sub && ./check.sh` it offered the line
|
|
223
|
+
with the `cd` in it, which allows nothing (the rule names the command alone), and in default mode it offered
|
|
224
|
+
nothing, because that refusal is worded differently. Measured on Windows.
|
|
128
225
|
- **Studio keeps answering with many conversations open.** Each conversation pane held its own stream, and a
|
|
129
226
|
browser opens six connections to one host: at five panes a message sent and every request the page made waited
|
|
130
227
|
for a pane to close (on Windows a send did not arrive in 35 s; on macOS the page's own request timed out at 4 s).
|
package/README.md
CHANGED
|
@@ -33,6 +33,8 @@ Then open Claude Code and run `/crew-doctor` to confirm the setup. Requires bash
|
|
|
33
33
|
- **40 skills** holding the method: testing, migrations, API contracts, observability, accessibility, deployment.
|
|
34
34
|
- **13 commands**, each started with its `/crew-…` name: `/crew-plan`, `/crew-review`, `/crew-ship`, `/crew-handoff`, `/crew-brainstorm`, `/crew-update`, `/crew-doctor`, `/crew-gates`, `/crew-skill`, `/crew-studio`, `/crew-approve`, `/crew-loosen`, `/crew-map`.
|
|
35
35
|
- In `auto` mode a commit or a push waits for you: type `/crew-approve` and what was just refused is approved, tied to what is staged.
|
|
36
|
+
- A stopped destructive command says what it would have touched; a review starts from a deterministic pre-pass, and `crew-refuter` tries to refute each finding that blocks.
|
|
37
|
+
- **Organisation presets.** `npx crewforth init --preset strict` (or `solo`, or your own JSON file) carries critical paths, model floors and verify commands to every project.
|
|
36
38
|
- **Safe adoption.** `adopt` lands everything on a branch, staged and uncommitted; `main` is never touched.
|
|
37
39
|
|
|
38
40
|
## Updating
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.2.0-rc.
|
|
1
|
+
3.2.0-rc.3
|
package/adopt.sh
CHANGED
|
@@ -68,18 +68,24 @@ SRC="$HERE/kit"
|
|
|
68
68
|
# --- flags (B5 — where a refresh lands) --- --here: the current branch · --new-branch: a fresh review branch.
|
|
69
69
|
# Left empty, Stage 2 picks a smart default (first adopt -> new; update + untracked .claude -> here; update +
|
|
70
70
|
# tracked -> ask). Unknown flags are ignored here (start.sh owns --backend/--frontend/… ; adopt auto-detects shape).
|
|
71
|
-
BRANCH_MODE=""; ASSUME_YES=0
|
|
71
|
+
BRANCH_MODE=""; ASSUME_YES=0; PRESET=""; _preset_take=0
|
|
72
72
|
_lang_flag=""; _lang_take=0
|
|
73
73
|
for _a in "$@"; do
|
|
74
74
|
# `--lang` with no value must not swallow the next flag: `--lang --yes` is --yes with no language given.
|
|
75
75
|
if [ "$_lang_take" = 1 ]; then _lang_take=0; case "$_a" in -*) ;; *) _lang_flag="$_a"; continue ;; esac; fi
|
|
76
|
+
if [ "$_preset_take" = 1 ]; then _preset_take=0; PRESET="$_a"; continue; fi
|
|
76
77
|
case "$_a" in
|
|
78
|
+
--preset=*) PRESET="${_a#--preset=}" ;;
|
|
79
|
+
--preset) _preset_take=1 ;;
|
|
77
80
|
--here) BRANCH_MODE=here ;;
|
|
78
81
|
--new-branch) BRANCH_MODE=new ;;
|
|
79
82
|
--yes|-y) ASSUME_YES=1 ;; # assume "yes" at every gate — for agent-driven / CI updates (no TTY to prompt)
|
|
80
83
|
--lang=*) _lang_flag="${_a#--lang=}" ;;
|
|
81
84
|
--lang) _lang_take=1 ;;
|
|
82
85
|
esac; done
|
|
86
|
+
[ "$_preset_take" = 1 ] && { echo "ERROR: --preset needs a name or a file (default, strict, solo, or ./org.json)."; exit 1; }
|
|
87
|
+
# A preset is checked before anything is changed: one that is not valid stops the update here.
|
|
88
|
+
if [ -n "$PRESET" ]; then bash "$SRC/eval/lib/preset.sh" --check "$PRESET" || exit 1; fi
|
|
83
89
|
|
|
84
90
|
# ---- CREW-I18N (the twin of start.sh's; see the long note there for why the English string is the key) ----
|
|
85
91
|
# Short version, because the reasoning belongs in one place: `m 'text'` prints the translation of that text
|
|
@@ -1574,6 +1580,9 @@ fi
|
|
|
1574
1580
|
# set up in Turkish to English from its first unattended update on.
|
|
1575
1581
|
[ "$_LANG_CHOSEN" = 1 ] && echo "lang=$CREW_LANG"
|
|
1576
1582
|
} > .claude/kit.conf
|
|
1583
|
+
# The organisation's preset (checked before anything was changed): a block at the top of crew-model-rules. Without
|
|
1584
|
+
# --preset an update leaves the block that is there, with the project's own lines, as they were.
|
|
1585
|
+
if [ -n "$PRESET" ]; then bash .claude/eval/lib/preset.sh --apply "$PRESET" || exit 1; fi
|
|
1577
1586
|
# What changed, for /crew-update to report: the package's own CHANGELOG sections newer than the version this
|
|
1578
1587
|
# project had, and no newer than the one being installed — read from the package on disk, never from the network.
|
|
1579
1588
|
# A fresh adopt has no "before", so it gets no file (and a stale one from an earlier update is removed).
|
package/bin/cli.js
CHANGED
|
@@ -18,12 +18,13 @@ if (sub === '--help' || sub === '-h' || sub === 'help') {
|
|
|
18
18
|
console.log(`Crewforth
|
|
19
19
|
|
|
20
20
|
Usage:
|
|
21
|
-
npx crewforth [init] [--private|--shared] [--yes]
|
|
21
|
+
npx crewforth [init] [--private|--shared] [--yes] [--preset <name|file.json>]
|
|
22
22
|
Set up Crewforth in a fresh project (start.sh wizard). Every install ships everything and
|
|
23
23
|
is stack-agnostic: the stack is recorded per project in CLAUDE.md ## Stack.
|
|
24
24
|
(--dotnet was removed in 3.0; it is still accepted, warns, and installs the same setup.)
|
|
25
|
-
npx crewforth adopt
|
|
25
|
+
npx crewforth adopt [--preset <name|file.json>]
|
|
26
26
|
Hand Crewforth over onto an existing project (adopt.sh).
|
|
27
|
+
--preset applies an organisation preset (default, strict, solo, or your own JSON file).
|
|
27
28
|
npx crewforth@latest update
|
|
28
29
|
Refresh a project that already has Crewforth. Alias of 'adopt': it reads .claude/kit.conf,
|
|
29
30
|
migrates a pre-3.0 .NET install (its pattern skill stays, as a project skill), and
|
package/kit/CLAUDE.md
CHANGED
|
@@ -59,9 +59,9 @@ sequence. All are installed: "no owner" is an `ls`.
|
|
|
59
59
|
|
|
60
60
|
| `model` (`guard-agent-model.sh`: floor = default; `above: <why>` for more) | the card |
|
|
61
61
|
|---|---|
|
|
62
|
-
| `haiku` | text, test-run, mechanical work
|
|
63
|
-
| `sonnet` | feature, fix-known, refactor, test-write, audit, review;
|
|
64
|
-
| `opus` | **critical**: migration, security, architecture, fix-unknown, or a critical-path file (auth, payments,
|
|
62
|
+
| `haiku` | text, commit agent; test-run, mechanical work with `verify:` |
|
|
63
|
+
| `sonnet` | feature, fix-known, refactor, test-write, audit, review; other `verify: none`; critical-path prose |
|
|
64
|
+
| `opus` | **critical**: migration, security, architecture, fix-unknown, or a critical-path file (auth, payments, legal…), and its referees |
|
|
65
65
|
|
|
66
66
|
The referee follows the RISK, not the author's model. Verify fails → the agent fixes once; again → re-run ONCE one
|
|
67
67
|
model up; again → AskUserQuestion. The same for a report that ends `confidence: low`.
|
|
@@ -30,6 +30,8 @@ Read-only; the trigger for the `crew-code-review` skill.
|
|
|
30
30
|
Before a work package closes (pre-commit), on the changed diff.
|
|
31
31
|
|
|
32
32
|
## How (applies the `crew-code-review` skill)
|
|
33
|
+
- First the deterministic pre-pass (`eval/lib/review-prepass/prepass.sh`), carried over as it is; your own findings
|
|
34
|
+
go to `.crewforth/review.json` in the same findings shape, blockers through crew-refuter (skill §0).
|
|
33
35
|
- Simplicity: flag when 200 lines could be 50.
|
|
34
36
|
- Surgical: catch out-of-scope touches.
|
|
35
37
|
- Readability: naming, dead code, comment traps (S125 — commented-out code-like prose).
|
package/kit/eval/doctor.sh
CHANGED
|
@@ -8,7 +8,8 @@ set -uo pipefail
|
|
|
8
8
|
# The 2.x names of the variables a user can set still work (one helper: eval/lib/crew-env.sh).
|
|
9
9
|
_crew_d="${BASH_SOURCE%/*}"; [ "$_crew_d" = "${BASH_SOURCE}" ] && _crew_d=.
|
|
10
10
|
[ -f "$_crew_d/lib/crew-env.sh" ] && . "$_crew_d/lib/crew-env.sh"
|
|
11
|
-
[ -f "$_crew_d/lib/version.sh" ] && . "$_crew_d/lib/version.sh"
|
|
11
|
+
[ -f "$_crew_d/lib/version.sh" ] && . "$_crew_d/lib/version.sh" # an rc orders before its final
|
|
12
|
+
_DOC_LIB="$(cd "$_crew_d/lib" 2>/dev/null && pwd)"; unset _crew_d # absolute: the doctor cd's into ROOT below
|
|
12
13
|
ROOT="${1:-.}"
|
|
13
14
|
cd "$ROOT" 2>/dev/null || { echo "doctor: cannot enter '$ROOT'"; exit 2; }
|
|
14
15
|
|
|
@@ -107,6 +108,7 @@ _mt() {
|
|
|
107
108
|
'advisor: off (CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1)') s='advisor: kapalı (CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1)' ;;
|
|
108
109
|
"status line: Crewforth's (from %s)") s="durum satırı: Crewforth'unki (%s içinden)" ;;
|
|
109
110
|
"status line: your own (from %s); Crewforth's is not used") s="durum satırı: seninki (%s içinden); Crewforth'unki kullanılmıyor" ;;
|
|
111
|
+
"organisation preset: %s") s="kurum preset'i: %s" ;;
|
|
110
112
|
"verify commands never run, %s time(s), because nothing allows them; to let the hook run one, add its line to .claude/crew-model-rules: %s") s="doğrulama komutları %s kez hiç koşmadı, çünkü hiçbir şey izin vermiyor; hook'un koşturması için satırını .claude/crew-model-rules dosyasına ekle: %s" ;;
|
|
111
113
|
"status line: none; Crewforth's: bash .claude/eval/lib/statusline.sh --install (Bash tool, not PowerShell)") s="durum satırı: yok; Crewforth'unki: bash .claude/eval/lib/statusline.sh --install (Bash tool, not PowerShell)" ;;
|
|
112
114
|
"advisor: off (no advisorModel set; Crewforth never turns it on)") s='advisor: kapalı (advisorModel ayarlı değil; Crewforth onu hiç açmaz)' ;;
|
|
@@ -776,6 +778,7 @@ esac
|
|
|
776
778
|
# 9e) What model routing costs. Advisory. A verify command the hook was never allowed to run cannot vouch for
|
|
777
779
|
# cheaper work: the ones refused for that reason are listed with the line that allows them (measured on one
|
|
778
780
|
# project: 23 of 71 refusals).
|
|
781
|
+
[ -f .claude/eval/lib/verify-cmd.sh ] && . .claude/eval/lib/verify-cmd.sh # the rule names the command a cd prefix leads to
|
|
779
782
|
_vbn=0; _vbl=""
|
|
780
783
|
for _f in .claude/state/crew-model/results/*; do
|
|
781
784
|
[ -f "$_f" ] || continue
|
|
@@ -784,7 +787,12 @@ for _f in .claude/state/crew-model/results/*; do
|
|
|
784
787
|
case "$_l" in verify=blocked) _vb=1 ;; whynot=*) _vw="${_l#whynot=}" ;; cmd=*) _vc="${_l#cmd=}" ;; esac
|
|
785
788
|
done < "$_f"
|
|
786
789
|
[ "$_vb" = 1 ] || continue
|
|
787
|
-
|
|
790
|
+
# Both ways of saying "nothing allows it": in auto/bypassPermissions (not on the runner list) and in the other
|
|
791
|
+
# modes (Claude Code itself would not run it unallowed). Measured on Windows: the second was not listed at all.
|
|
792
|
+
case "$_vw" in *"not a test or build command on this hook's list"*|*"does not run a command the user has not allowed"*) ;; *) continue ;; esac
|
|
793
|
+
# A rule names the command alone: `cd sub && ./check.sh` is allowed by `verify ./check.sh` (measured on Windows:
|
|
794
|
+
# the line with the cd in it did not allow it).
|
|
795
|
+
declare -F crew_verify_split >/dev/null && { crew_verify_split "$_vc" .; _vc="$_AMVC"; }
|
|
788
796
|
_vc="${_vc:0:80}"; [ -n "$_vc" ] || continue
|
|
789
797
|
_vbn=$((_vbn+1))
|
|
790
798
|
case "$_vc" in *'('*|*' + '*) continue ;; esac # a sentence, not a command: counted, not offered as a rule
|
|
@@ -794,6 +802,9 @@ if [ "$_vbn" -gt 0 ]; then
|
|
|
794
802
|
_vsep=' || '; _vdot=' · '; _vbs="${_vbl//"$_vsep"/$_vdot}"; _vbs="${_vbs#| }"; _vbs="${_vbs% |}"; _vbs="${_vbs:0:300}"
|
|
795
803
|
warn "verify commands never run, %s time(s), because nothing allows them; to let the hook run one, add its line to .claude/crew-model-rules: %s" "$_vbn" "$_vbs"
|
|
796
804
|
fi
|
|
805
|
+
# The organisation preset in force: the block preset.sh wrote at the top of crew-model-rules, asked of preset.sh itself.
|
|
806
|
+
_pre=""; [ -n "$_DOC_LIB" ] && [ -f "$_DOC_LIB/preset.sh" ] && _pre="$(bash "$_DOC_LIB/preset.sh" --show 2>/dev/null)"
|
|
807
|
+
ok "organisation preset: %s" "${_pre:-none}"
|
|
797
808
|
|
|
798
809
|
# 10) Gate activity. The suite proves the gates CAN fire; this reports whether anything actually tripped them.
|
|
799
810
|
# Recording is on by default (rule names only, never the command), so "no log" here means no gate has
|
package/kit/eval/gate-report.sh
CHANGED
|
@@ -29,6 +29,8 @@ HOOKS="./.claude/hooks"; [ -d "$HOOKS" ] || HOOKS="./hooks"
|
|
|
29
29
|
[ -f "$HOOKS/guard-bash.sh" ] || { echo "gate-report: guard-bash.sh not found (run from the project root)" >&2; exit 4; }
|
|
30
30
|
|
|
31
31
|
# Default log location, so `--log` is not required once logging is on.
|
|
32
|
+
# The same rule the gates write by (eval/lib/gate-log.sh): .claude/gate-log.tsv, or the repository's .git.
|
|
33
|
+
if [ -z "$LOG" ] && [ -f ./.claude/eval/lib/gate-log.sh ]; then . ./.claude/eval/lib/gate-log.sh; crew_gate_log_read; LOG="$_CREW_GL"; fi
|
|
32
34
|
[ -z "$LOG" ] && [ -f "./.claude/gate-log.tsv" ] && LOG="./.claude/gate-log.tsv"
|
|
33
35
|
|
|
34
36
|
TMP="${TMPDIR:-/tmp}/crew-gate-$$"; mkdir -p "$TMP"; trap 'rm -rf "$TMP"' EXIT
|
|
@@ -5,18 +5,27 @@
|
|
|
5
5
|
// model fill them, and nothing else:
|
|
6
6
|
// node.narrative { text, cites } what the module is for, in at most two sentences; `cites` are "file:line"
|
|
7
7
|
// strings, each one a line the scanner itself cited for THAT node (its evidence)
|
|
8
|
+
// and, optionally, `layer` (one of LAYERS: where the module sits) and `domain` (what part of the
|
|
9
|
+
// business it serves, a short phrase), both resting on the same cites
|
|
8
10
|
// doc.overview { text, cites } a short orientation for someone new to the project; its cites may be any
|
|
9
11
|
// node's evidence
|
|
12
|
+
// doc.tour [{ node, text, cites }] one to eight stops a newcomer reads in order: each names a node of the
|
|
13
|
+
// map, says in at most two sentences why to look there, and cites that node's own lines
|
|
10
14
|
// A narrative with no citation, or one citing a line the scan did not read, is refused: the model may describe
|
|
11
15
|
// what the scan found, not add to it. A scanner field changed by hand is refused too (validate).
|
|
12
16
|
|
|
13
17
|
const MAX_NODE = 400;
|
|
14
18
|
const MAX_OVERVIEW = 2000;
|
|
19
|
+
const MAX_STOP = 300;
|
|
20
|
+
const MAX_STOPS = 8;
|
|
21
|
+
const MAX_DOMAIN = 60;
|
|
22
|
+
export const LAYERS = ['ui', 'api', 'service', 'data', 'infra', 'tool', 'library'];
|
|
15
23
|
|
|
16
|
-
/** The "file:line" a
|
|
24
|
+
/** The "file:line" a narrative of this node may cite. */
|
|
17
25
|
export function citesOf(node) {
|
|
18
26
|
const out = new Set();
|
|
19
|
-
|
|
27
|
+
// Its evidence, its entry point and what its README says about it: every line the scan read for this node.
|
|
28
|
+
for (const e of [node?.evidence, node?.entryPoint?.evidence, node?.about?.evidence].flatMap((x) => (Array.isArray(x) ? x : []))) {
|
|
20
29
|
if (e && typeof e.file === 'string' && Number.isInteger(e.line)) out.add(`${e.file}:${e.line}`);
|
|
21
30
|
}
|
|
22
31
|
return out;
|
|
@@ -35,11 +44,14 @@ const sentences = (t) => (String(t).trim().match(/[.!?](\s|$)/g) ?? []).length;
|
|
|
35
44
|
* Why a narrative is not acceptable, or null when it is (null itself is: not written yet).
|
|
36
45
|
* @param allowed the set of "file:line" it may cite
|
|
37
46
|
*/
|
|
38
|
-
export function problemOf(n, allowed, { max = MAX_NODE, twoSentences = true } = {}) {
|
|
47
|
+
export function problemOf(n, allowed, { max = MAX_NODE, twoSentences = true, module = false } = {}) {
|
|
39
48
|
if (n === null || n === undefined) return null;
|
|
40
49
|
if (typeof n !== 'object' || Array.isArray(n)) return 'is not { text, cites }';
|
|
41
|
-
const
|
|
42
|
-
|
|
50
|
+
const may = module ? ['cites', 'domain', 'layer', 'text'] : ['cites', 'text'];
|
|
51
|
+
const odd = Object.keys(n).filter((k) => !may.includes(k));
|
|
52
|
+
if (odd.length || !('text' in n) || !('cites' in n)) return `has fields other than ${may.join(', ')} (${Object.keys(n).sort().join()})`;
|
|
53
|
+
if ('layer' in n && !LAYERS.includes(n.layer)) return `names the layer ${JSON.stringify(n.layer)}, not one of ${LAYERS.join(', ')}`;
|
|
54
|
+
if ('domain' in n && (typeof n.domain !== 'string' || !n.domain.trim() || n.domain.length > MAX_DOMAIN || /[.!?]/.test(n.domain))) return `has a domain that is not a short phrase (at most ${MAX_DOMAIN} characters, no sentence)`;
|
|
43
55
|
if (typeof n.text !== 'string' || !n.text.trim()) return 'has no text';
|
|
44
56
|
if (n.text.length > max) return `is ${n.text.length} characters, more than ${max}`;
|
|
45
57
|
if (twoSentences && sentences(n.text) > 2) return 'is more than two sentences';
|
|
@@ -52,7 +64,24 @@ export function problemOf(n, allowed, { max = MAX_NODE, twoSentences = true } =
|
|
|
52
64
|
/** The document without what the model may add: the scanner's fields alone. */
|
|
53
65
|
export function strip(doc) {
|
|
54
66
|
if (!doc || typeof doc !== 'object') return doc;
|
|
55
|
-
return { ...doc, nodes: (doc.nodes ?? []).map((n) => ({ ...n, narrative: null })), overview: null };
|
|
67
|
+
return { ...doc, nodes: (doc.nodes ?? []).map((n) => ({ ...n, narrative: null })), overview: null, tour: null };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Why a tour is not acceptable, or null when it is (null itself is: not written yet). */
|
|
71
|
+
export function tourProblem(tour, doc) {
|
|
72
|
+
if (tour === null || tour === undefined) return null;
|
|
73
|
+
if (!Array.isArray(tour) || !tour.length || tour.length > MAX_STOPS) return `is not a list of 1 to ${MAX_STOPS} stops`;
|
|
74
|
+
const byId = new Map((doc?.nodes ?? []).map((n) => [n.id, n]));
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
for (const [i, s] of tour.entries()) {
|
|
77
|
+
if (!s || typeof s !== 'object' || Array.isArray(s) || Object.keys(s).sort().join() !== 'cites,node,text') return `stop ${i + 1} is not { node, text, cites }`;
|
|
78
|
+
if (!byId.has(s.node)) return `stop ${i + 1} names ${JSON.stringify(s.node)}, which is not a node of this map`;
|
|
79
|
+
if (seen.has(s.node)) return `stop ${i + 1} names ${s.node} again: a tour stops at a node once`;
|
|
80
|
+
seen.add(s.node);
|
|
81
|
+
const p = problemOf({ text: s.text, cites: s.cites }, citesOf(byId.get(s.node)), { max: MAX_STOP });
|
|
82
|
+
if (p) return `stop ${i + 1} (${s.node}) ${p}`;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
56
85
|
}
|
|
57
86
|
|
|
58
87
|
/**
|
|
@@ -65,7 +94,7 @@ export function carry(old, fresh) {
|
|
|
65
94
|
const nodes = fresh.nodes.map((n) => {
|
|
66
95
|
const nar = was.get(n.id);
|
|
67
96
|
if (nar === null || nar === undefined) return n;
|
|
68
|
-
if (problemOf(nar, citesOf(n)) === null) { kept += 1; return { ...n, narrative: nar }; }
|
|
97
|
+
if (problemOf(nar, citesOf(n), { module: n.kind === 'module' }) === null) { kept += 1; return { ...n, narrative: nar }; }
|
|
69
98
|
dropped += 1; return n;
|
|
70
99
|
});
|
|
71
100
|
const doc = { ...fresh, nodes };
|
|
@@ -73,6 +102,9 @@ export function carry(old, fresh) {
|
|
|
73
102
|
if (problemOf(old.overview, allCites(doc), { max: MAX_OVERVIEW, twoSentences: false }) === null) { doc.overview = old.overview; kept += 1; }
|
|
74
103
|
else dropped += 1;
|
|
75
104
|
}
|
|
105
|
+
if (old?.tour != null) {
|
|
106
|
+
if (tourProblem(old.tour, doc) === null) { doc.tour = old.tour; kept += 1; } else dropped += 1;
|
|
107
|
+
}
|
|
76
108
|
return { doc, kept, dropped };
|
|
77
109
|
}
|
|
78
110
|
|
|
@@ -89,10 +121,12 @@ export function validate(file, fresh, serialize) {
|
|
|
89
121
|
}
|
|
90
122
|
const byId = new Map((fresh.nodes ?? []).map((n) => [n.id, n]));
|
|
91
123
|
for (const n of file.nodes ?? []) {
|
|
92
|
-
const p = problemOf(n.narrative, citesOf(byId.get(n.id) ?? n));
|
|
124
|
+
const p = problemOf(n.narrative, citesOf(byId.get(n.id) ?? n), { module: (byId.get(n.id) ?? n).kind === 'module' });
|
|
93
125
|
if (p) out.push({ at: n.id, message: `the narrative ${p}` });
|
|
94
126
|
}
|
|
95
127
|
const p = problemOf(file.overview, allCites(fresh), { max: MAX_OVERVIEW, twoSentences: false });
|
|
96
128
|
if (p) out.push({ at: 'overview', message: `the overview ${p}` });
|
|
129
|
+
const t = tourProblem(file.tour, fresh);
|
|
130
|
+
if (t) out.push({ at: 'tour', message: `the tour ${t}` });
|
|
97
131
|
return out;
|
|
98
132
|
}
|
|
@@ -130,14 +130,15 @@ export function risky(text, patterns) {
|
|
|
130
130
|
// A host is the only part of a connection string or URL the map keeps. Credentials are cut off first; then a host
|
|
131
131
|
// that looks like a secret by Crewforth's own scanner patterns is replaced. Without those patterns every host is
|
|
132
132
|
// replaced: a map with fewer names is a smaller loss than one that published a token.
|
|
133
|
-
|
|
133
|
+
// skip: pattern lines left out, as written (a project's .secret-allowlist.txt names the exact line it allows).
|
|
134
|
+
export function loadSecretPatterns(fromDir, skip = new Set()) {
|
|
134
135
|
for (const rel of ['../../../hooks/secret-blocklist.txt', '../../../../hooks/secret-blocklist.txt']) {
|
|
135
136
|
const f = path.join(fromDir, rel);
|
|
136
137
|
let text; try { text = fs.readFileSync(f, 'utf8'); } catch { continue; }
|
|
137
138
|
const out = [];
|
|
138
139
|
for (const raw of text.split('\n')) {
|
|
139
140
|
const l = raw.replace(/\r$/, '');
|
|
140
|
-
if (!l || l.startsWith('#')) continue;
|
|
141
|
+
if (!l || l.startsWith('#') || skip.has(l)) continue;
|
|
141
142
|
const js = l.replace(/\[\[:alnum:\]\]/g, '[A-Za-z0-9]').replace(/\[:alnum:\]/g, 'A-Za-z0-9')
|
|
142
143
|
.replace(/\[:space:\]/g, '\\s').replace(/\[:alpha:\]/g, 'A-Za-z').replace(/\[:digit:\]/g, '0-9');
|
|
143
144
|
try { out.push(new RegExp(js)); } catch { /* a pattern JS cannot read is skipped; the rest still apply */ }
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"about": "What a module is built with, in the words a newcomer reads: a package a manifest declares, or a file the module holds, gives a label. One list for every stack; a label is shown once per module, frameworks before tools, in the order of this file. Versions are not part of a label: the manifest line the label was read from is the module's evidence.",
|
|
3
|
+
"packages": {
|
|
4
|
+
"npm": [
|
|
5
|
+
["next", "Next.js"], ["react", "React"], ["vue", "Vue"], ["svelte", "Svelte"], ["@angular/core", "Angular"],
|
|
6
|
+
["astro", "Astro"], ["react-native", "React Native"], ["expo", "Expo"], ["electron", "Electron"],
|
|
7
|
+
["hono", "Hono"], ["express", "Express"], ["fastify", "Fastify"], ["@nestjs/core", "NestJS"], ["koa", "Koa"],
|
|
8
|
+
["graphql", "GraphQL"], ["socket.io", "Socket.IO"], ["wrangler", "Cloudflare Workers"],
|
|
9
|
+
["@prisma/client", "Prisma"], ["prisma", "Prisma"], ["drizzle-orm", "Drizzle"], ["typeorm", "TypeORM"],
|
|
10
|
+
["mongoose", "Mongoose"], ["tailwindcss", "Tailwind CSS"], ["vite", "Vite"], ["typescript", "TypeScript"],
|
|
11
|
+
["vitest", "Vitest"], ["jest", "Jest"], ["@playwright/test", "Playwright"]
|
|
12
|
+
],
|
|
13
|
+
"pypi": [
|
|
14
|
+
["django", "Django"], ["fastapi", "FastAPI"], ["flask", "Flask"], ["sqlalchemy", "SQLAlchemy"],
|
|
15
|
+
["celery", "Celery"], ["pydantic", "Pydantic"], ["langchain", "LangChain"], ["torch", "PyTorch"],
|
|
16
|
+
["transformers", "Transformers"], ["spacy", "spaCy"], ["uvicorn", "Uvicorn"], ["pytest", "pytest"]
|
|
17
|
+
],
|
|
18
|
+
"pub": [
|
|
19
|
+
["flutter_riverpod", "Riverpod"], ["hooks_riverpod", "Riverpod"], ["riverpod", "Riverpod"],
|
|
20
|
+
["flutter_bloc", "Bloc"], ["bloc", "Bloc"], ["provider", "Provider"], ["get", "GetX"], ["go_router", "go_router"],
|
|
21
|
+
["dio", "Dio"], ["http", "http"], ["drift", "Drift"], ["sqflite", "sqflite"], ["hive", "Hive"], ["isar", "Isar"],
|
|
22
|
+
["firebase_core", "Firebase"], ["freezed", "Freezed"], ["google_mobile_ads", "Google Mobile Ads"],
|
|
23
|
+
["in_app_purchase", "In-app purchase"], ["flutter_test", "flutter_test"]
|
|
24
|
+
],
|
|
25
|
+
"go": [
|
|
26
|
+
["github.com/gin-gonic/gin", "Gin"], ["github.com/labstack/echo/v4", "Echo"], ["github.com/go-chi/chi/v5", "chi"],
|
|
27
|
+
["gorm.io/gorm", "GORM"]
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
["wrangler.toml", "Cloudflare Workers"], ["wrangler.jsonc", "Cloudflare Workers"], ["tsconfig.json", "TypeScript"],
|
|
32
|
+
["Dockerfile", "Docker"], ["vercel.json", "Vercel"], ["netlify.toml", "Netlify"], ["fly.toml", "Fly.io"],
|
|
33
|
+
["firebase.json", "Firebase"]
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -24,15 +24,15 @@
|
|
|
24
24
|
import fs from 'node:fs';
|
|
25
25
|
import path from 'node:path';
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
|
-
import { openProject, loadSecretPatterns, makeHostFilter, sortEv, bearsValues, risky, spawned, isTestPath } from './project.mjs';
|
|
28
|
-
import { scanStacks } from './stacks.mjs';
|
|
29
|
-
import { externals, configuration, codeUrls, endpoints, containers } from './surface.mjs';
|
|
27
|
+
import { openProject, loadSecretPatterns, makeHostFilter, sortEv, bearsValues, risky, spawned, isTestPath, ev } from './project.mjs';
|
|
28
|
+
import { scanStacks, applyTech } from './stacks.mjs';
|
|
29
|
+
import { externals, configuration, codeUrls, servedCalls, endpoints, containers } from './surface.mjs';
|
|
30
30
|
import { detect } from './detect.mjs';
|
|
31
31
|
import { stamp, write, serialize, validate, OUT_DIR } from '../findings/findings.mjs';
|
|
32
32
|
import { carry, strip, validate as validateNarrative } from './narrative.mjs';
|
|
33
33
|
|
|
34
34
|
// Languages with source the scanner has no reader for, by extension.
|
|
35
|
-
const UNREAD = {
|
|
35
|
+
const UNREAD = { swift: 'Swift', rb: 'Ruby', php: 'PHP', rs: 'Rust', scala: 'Scala', ex: 'Elixir', exs: 'Elixir',
|
|
36
36
|
c: 'C', cc: 'C++', cpp: 'C++', m: 'Objective-C', mm: 'Objective-C', lua: 'Lua', clj: 'Clojure', hs: 'Haskell', erl: 'Erlang' };
|
|
37
37
|
|
|
38
38
|
const SELF = fileURLToPath(import.meta.url);
|
|
@@ -43,6 +43,50 @@ const MAX_EV = 5;
|
|
|
43
43
|
const load = (f) => JSON.parse(fs.readFileSync(path.join(HERE, 'rules', f), 'utf8'));
|
|
44
44
|
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* What is inside a module, for a reader who has not opened it: its folders and how many source files each holds.
|
|
48
|
+
* A folder holding nearly everything (lib/ of a Flutter app, src/ of a package) is opened one level, at most twice,
|
|
49
|
+
* so the rows say lib/data and lib/features and not just lib. Tests are counted apart. Twelve rows at most.
|
|
50
|
+
*/
|
|
51
|
+
const MAX_ROWS = 12;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* What a module says about itself: the first line of prose in its own README (a heading, a badge, an image, an
|
|
55
|
+
* HTML tag or a code fence is not prose). Kept apart from the evidence, which is cut at five lines, so a
|
|
56
|
+
* description always has a line to rest on that says what the module is for. Measured on a real project: four of
|
|
57
|
+
* ten modules were left undescribed because the only line the scan cited for them was the manifest's name.
|
|
58
|
+
*/
|
|
59
|
+
function aboutOf(pj, m) {
|
|
60
|
+
const base = m.dir === '.' ? '' : `${m.dir}/`;
|
|
61
|
+
const f = pj.files.find((x) => x.startsWith(base) && /^readme(\.(md|markdown|txt|rst))?$/i.test(x.slice(base.length)));
|
|
62
|
+
const src = f && pj.read(f); if (!src) return null;
|
|
63
|
+
let fence = false;
|
|
64
|
+
for (const [i, raw] of src.lines.entries()) {
|
|
65
|
+
const l = raw.trim();
|
|
66
|
+
if (/^(```|~~~)/.test(l)) { fence = !fence; continue; }
|
|
67
|
+
if (fence || !l || /^(#|=+$|-+$|\[!|!\[|<|\||>\s*\[!)/.test(l)) continue;
|
|
68
|
+
return { evidence: [ev(f, i + 1, l.slice(0, 60))] };
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function structure(m) {
|
|
73
|
+
const base = m.dir === '.' ? '' : `${m.dir}/`;
|
|
74
|
+
let rel = m.files.filter((f) => !isTestPath(f)).map((f) => f.slice(base.length));
|
|
75
|
+
const tests = m.files.length - rel.length;
|
|
76
|
+
let at = '';
|
|
77
|
+
const group = (list) => { const g = new Map(); for (const r of list) { const k = r.includes('/') ? r.split('/')[0] : ''; g.set(k, (g.get(k) ?? 0) + 1); } return g; };
|
|
78
|
+
for (let d = 0; d < 2 && rel.length; d++) {
|
|
79
|
+
const [top, n] = [...group(rel)].sort((a, b) => b[1] - a[1] || cmp(a[0], b[0]))[0];
|
|
80
|
+
if (!top || n / rel.length < 0.8 || !rel.some((r) => r.startsWith(`${top}/`) && r.split('/').length > 2)) break;
|
|
81
|
+
at += `${top}/`; rel = rel.filter((r) => r.startsWith(`${top}/`)).map((r) => r.slice(top.length + 1));
|
|
82
|
+
}
|
|
83
|
+
const rows = [...group(rel)].map(([k, n]) => ({ folder: k ? `${at}${k}` : (at.replace(/\/$/, '') || '.'), files: n }))
|
|
84
|
+
.sort((a, b) => b.files - a.files || cmp(a.folder, b.folder));
|
|
85
|
+
const shown = rows.slice(0, MAX_ROWS);
|
|
86
|
+
const rest = rows.slice(MAX_ROWS).reduce((s, r) => s + r.files, 0);
|
|
87
|
+
return { folders: shown, ...(rest ? { otherFiles: rest } : {}), testFiles: tests };
|
|
88
|
+
}
|
|
89
|
+
|
|
46
90
|
// opts.hostFilter replaces the masking: ONLY the gate's must-fail twin uses it. No command-line flag reaches it.
|
|
47
91
|
export function scan(root = '.', opts = {}) {
|
|
48
92
|
const pj = openProject(root);
|
|
@@ -51,10 +95,12 @@ export function scan(root = '.', opts = {}) {
|
|
|
51
95
|
const rules = load('packages.json'); const names = load('names.json');
|
|
52
96
|
const stdlib = new Set(load('python-stdlib.json').modules);
|
|
53
97
|
const st = scanStacks(pj);
|
|
98
|
+
applyTech(st, pj, load('tech.json'));
|
|
54
99
|
const out = { edges: [], notMeasured: st.notMeasured, configUrls: [], codeHosts: new Set(), patterns };
|
|
55
100
|
const ex = externals(pj, st, rules, hostOf, out);
|
|
56
101
|
const cfg = configuration(pj, st, rules, hostOf, ex, out);
|
|
57
102
|
codeUrls(pj, st, hostOf, ex, out);
|
|
103
|
+
servedCalls(pj, st, ex, out);
|
|
58
104
|
const eps = endpoints(pj, st, out);
|
|
59
105
|
const services = containers(pj, st, rules, hostOf, ex, cfg, out);
|
|
60
106
|
const findings = detect({ st, cfg, eps, names, stdlib, notMeasured: out.notMeasured, configUrls: out.configUrls, codeHosts: out.codeHosts });
|
|
@@ -73,6 +119,9 @@ export function scan(root = '.', opts = {}) {
|
|
|
73
119
|
const lang = UNREAD[/\.([a-z]+)$/i.exec(f)?.[1]?.toLowerCase()];
|
|
74
120
|
if (lang && !isTestPath(f)) unread.set(lang, (unread.get(lang) ?? 0) + 1);
|
|
75
121
|
}
|
|
122
|
+
// Dart is read through its pubspec.yaml: a .dart file no package owns has no module to be drawn in, so it is said.
|
|
123
|
+
const loose = pj.files.filter((f) => /\.dart$/.test(f) && !isTestPath(f) && !st.modules.some((m) => m.stack === 'dart' && m.files.includes(f))).length;
|
|
124
|
+
if (loose) out.notMeasured.push({ what: `Dart source outside any pubspec.yaml (${loose} file${loose === 1 ? '' : 's'})`, reason: 'no package owns it: it is not on the map' });
|
|
76
125
|
for (const [lang, n] of unread) out.notMeasured.push({ what: `${lang} source (${n} file${n === 1 ? '' : 's'})`, reason: 'no reader for this language: its modules, dependencies and endpoints are not on the map' });
|
|
77
126
|
|
|
78
127
|
// ---- one edge per (from, to, kind, confidence), its evidence merged ----
|
|
@@ -94,6 +143,8 @@ export function scan(root = '.', opts = {}) {
|
|
|
94
143
|
// line to cite: the entry is then the file, and it says so.
|
|
95
144
|
...(m.entry ? { entryPoint: m.entryEv ? { evidence: [m.entryEv] } : { file: m.entryFile, evidence: [] } } : {}),
|
|
96
145
|
dependencies: m.deps.length,
|
|
146
|
+
structure: structure(m),
|
|
147
|
+
...((() => { const a = aboutOf(pj, m); return a ? { about: a } : {}; })()),
|
|
97
148
|
...capped(m.evidence), narrative: null })),
|
|
98
149
|
...services.map((s) => ({ id: s.id, kind: 'service', name: s.name, ...(s.image ? { image: s.image } : {}), ...capped(s.evidence), narrative: null })),
|
|
99
150
|
...[...ex.ext.values()].map((n) => ({ id: n.id, kind: 'external', resource: n.resource, label: n.label, hosts: [...n.hosts].sort(), ...capped(n.evidence), narrative: null })),
|
|
@@ -122,6 +173,7 @@ export function scan(root = '.', opts = {}) {
|
|
|
122
173
|
findings,
|
|
123
174
|
notMeasured: [...nm.values()].sort((a, b) => cmp(`${a.what}\u0000${a.reason}`, `${b.what}\u0000${b.reason}`)),
|
|
124
175
|
overview: null,
|
|
176
|
+
tour: null,
|
|
125
177
|
};
|
|
126
178
|
// The last net, whatever cited it: a quote is cut before anything that looks like a credential, and a line that
|
|
127
179
|
// carries values (by its file, or by what is on it) is never hashed.
|