@mmerterden/multi-agent-pipeline 20.8.0 → 20.8.2
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 +48 -0
- package/docs/facts.json +3 -3
- package/index.js +1 -0
- package/install/_codex-agents.mjs +1 -1
- package/install/_common.mjs +486 -53
- package/install/_mcp-register.mjs +173 -117
- package/install/claude.mjs +281 -220
- package/install/codex.mjs +7 -7
- package/install/copilot.mjs +13 -11
- package/install/index.mjs +92 -27
- package/install/templates/claude-hooks.json +9 -9
- package/manifest.json +112 -114
- package/package.json +5 -2
- package/pipeline/commands/multi-agent/update/SKILL.md +28 -17
- package/pipeline/lib/confusables.json +79 -33
- package/pipeline/lib/extract-conventions.sh +3 -3
- package/pipeline/lib/json-file-lock.mjs +27 -7
- package/pipeline/lib/normalize-text.mjs +86 -17
- package/pipeline/lib/outbound-gate.mjs +13 -4
- package/pipeline/lib/redact.mjs +87 -13
- package/pipeline/multi-agent-refs/analysis/evidence.md +1 -1
- package/pipeline/multi-agent-refs/analysis/synthesis.md +1 -1
- package/pipeline/multi-agent-refs/component-dispatch.md +1 -1
- package/pipeline/multi-agent-refs/conventions-defaults.md +1 -1
- package/pipeline/multi-agent-refs/features/unattended-security.md +2 -2
- package/pipeline/scripts/agent-guard.py +150 -25
- package/pipeline/scripts/audit-log.sh +3 -4
- package/pipeline/scripts/autopilot-runner.mjs +14 -5
- package/pipeline/scripts/doctor.mjs +8 -2
- package/pipeline/scripts/gen-skills-index.mjs +13 -1
- package/pipeline/scripts/log-metric.sh +9 -3
- package/pipeline/scripts/migrate-prefs.mjs +18 -4
- package/pipeline/scripts/pre-commit-check.sh +119 -27
- package/pipeline/scripts/scan-agent-config.sh +9 -9
- package/pipeline/scripts/unattended_policy.py +12 -3
- package/pipeline/scripts/uninstall.mjs +88 -1
- package/pipeline/scripts/usage-identity.mjs +1 -1
- package/pipeline/scripts/usage-register.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +30 -30
- package/pipeline/skills/shared/README.md +64 -64
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +2 -2
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +2 -2
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +6 -0
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +3 -0
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +3 -0
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +3 -4
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +5 -3
- package/pipeline/skills/shared/external/authentication/SKILL.md +29 -17
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +3 -1
- package/pipeline/skills/shared/external/background-processing/SKILL.md +10 -8
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +7 -7
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +6 -3
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +43 -0
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +4 -2
- package/pipeline/skills/shared/external/core-data/SKILL.md +12 -2
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +31 -0
- package/pipeline/skills/shared/external/coreml/SKILL.md +1 -1
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +1 -1
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +11 -5
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +2 -2
- package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +1 -1
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +6 -6
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +3 -2
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +20 -18
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +4 -4
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +12 -9
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +201 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +64 -30
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +14 -16
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +20 -12
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +7 -7
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +26 -12
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +4 -3
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +51 -24
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +19 -9
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +26 -19
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +39 -46
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +37 -63
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +4 -2
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +5 -4
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +3 -2
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +2 -2
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +4 -4
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +15 -6
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +2 -1
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +8 -4
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +1 -1
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +25 -6
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/skill-creator/template.md +7 -1
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +6 -1
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +3 -2
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +1 -1
- package/pipeline/skills/shared/external/swift-security/SKILL.md +10 -8
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +8 -5
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +13 -9
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +7 -6
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +8 -5
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +3 -2
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +5 -3
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +17 -9
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +4 -2
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +13 -6
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +3 -1
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +8 -5
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +15 -7
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +8 -6
- package/pipeline/scripts/gen-ref-toc.mjs +0 -279
- package/pipeline/scripts/make-manifest.mjs +0 -199
- package/pipeline/scripts/scorecard-snapshot.mjs +0 -178
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmerterden/multi-agent-pipeline",
|
|
3
|
-
"version": "20.8.
|
|
3
|
+
"version": "20.8.2",
|
|
4
4
|
"description": "6-phase AI development pipeline with full orchestration on Claude Code, Copilot CLI and Codex CLI. Analysis, planning, TDD, CLI-aware parallel review with consensus surfacing + Fable triage, default-FAIL evidence gates, secret + intent guards, per-phase cost ledger, persistent learnings memory, wiki generation, commit automation. Token-preserving uninstall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"start": "node index.js",
|
|
23
|
-
"test": "npm run format:check && npm run lint && node --test test/*.test.mjs && node pipeline/scripts/run-smokes.mjs && node pipeline/scripts/lint-skills.mjs && node pipeline/scripts/lint-personas.mjs && node pipeline/scripts/lint-mcp-refs.mjs && node pipeline/scripts/eval-triage.mjs && node pipeline/scripts/eval-golden-tasks.mjs && node pipeline/scripts/eval-intent.mjs && node pipeline/scripts/eval-recall.mjs && node pipeline/scripts/eval-guard-robustness.mjs && node pipeline/scripts/validate-schemas.mjs && node pipeline/scripts/validate-prefs.mjs && node pipeline/scripts/scorecard.mjs",
|
|
23
|
+
"test": "npm run format:check && npm run lint && node --test test/*.test.mjs && node pipeline/scripts/run-smokes.mjs && node pipeline/scripts/lint-skills.mjs && node pipeline/scripts/gen-skills-index.mjs --check && node pipeline/scripts/lint-personas.mjs && node pipeline/scripts/lint-mcp-refs.mjs && node pipeline/scripts/eval-triage.mjs && node pipeline/scripts/eval-golden-tasks.mjs && node pipeline/scripts/eval-intent.mjs && node pipeline/scripts/eval-recall.mjs && node pipeline/scripts/eval-guard-robustness.mjs && node pipeline/scripts/validate-schemas.mjs && node pipeline/scripts/validate-prefs.mjs && node pipeline/scripts/scorecard.mjs",
|
|
24
24
|
"test:unit": "node --test test/*.test.mjs",
|
|
25
25
|
"test:smoke": "node pipeline/scripts/run-smokes.mjs",
|
|
26
26
|
"lint:skills": "node pipeline/scripts/lint-skills.mjs",
|
|
@@ -94,6 +94,9 @@
|
|
|
94
94
|
"!pipeline/scripts/run-smokes.mjs",
|
|
95
95
|
"!pipeline/scripts/eval-*.mjs",
|
|
96
96
|
"!pipeline/scripts/check-rewrite-overlap.mjs",
|
|
97
|
+
"!pipeline/scripts/gen-ref-toc.mjs",
|
|
98
|
+
"!pipeline/scripts/make-manifest.mjs",
|
|
99
|
+
"!pipeline/scripts/scorecard-snapshot.mjs",
|
|
97
100
|
"!pipeline/scripts/scorecard.mjs",
|
|
98
101
|
"!pipeline/scripts/lint-skills.mjs",
|
|
99
102
|
"!pipeline/scripts/lint-personas.mjs",
|
|
@@ -28,10 +28,16 @@ A git clone of the pipeline repo is a maintainer workspace, kept in sync by `/mu
|
|
|
28
28
|
|
|
29
29
|
## Steps
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Every fenced block below runs in a fresh shell, so no variable survives from one
|
|
32
|
+
block to the next. Step 1 writes the values the later steps need to a state file,
|
|
33
|
+
and every later block that needs them starts by sourcing it.
|
|
34
|
+
|
|
35
|
+
1. **Resolve the package and both versions**:
|
|
32
36
|
```bash
|
|
37
|
+
STATE="${TMPDIR:-/tmp}/multi-agent-update.env"
|
|
33
38
|
PKG="@{npm-scope}/multi-agent-pipeline"
|
|
34
39
|
REG="https://registry.npmjs.org"
|
|
40
|
+
UPD_DIR="${TMPDIR:-/tmp}/multi-agent-update"
|
|
35
41
|
CUR=$(tr -d '[:space:]' < "$HOME/.claude/.pipeline-version" 2>/dev/null)
|
|
36
42
|
[ -n "$CUR" ] || CUR="unknown"
|
|
37
43
|
# Read the registry directly - `npm view` can answer from a stale local cache.
|
|
@@ -45,23 +51,25 @@ A git clone of the pipeline repo is a maintainer workspace, kept in sync by `/mu
|
|
|
45
51
|
REQUIRED=$(curl -fsS -H 'Accept: application/vnd.npm.install-v1+json' \
|
|
46
52
|
"$REG/$(printf '%s' "$PKG" | sed 's|/|%2F|')" \
|
|
47
53
|
| node -pe 'JSON.parse(require("fs").readFileSync(0,"utf8"))["dist-tags"].required || ""' 2>/dev/null)
|
|
54
|
+
printf 'PKG=%q\nREG=%q\nUPD_DIR=%q\nCUR=%q\nLATEST=%q\n' \
|
|
55
|
+
"$PKG" "$REG" "$UPD_DIR" "$CUR" "$LATEST" > "$STATE"
|
|
48
56
|
echo "Current: v$CUR Latest: v$LATEST${REQUIRED:+ Required floor: v$REQUIRED}"
|
|
49
57
|
```
|
|
50
58
|
|
|
51
59
|
This command is never blocked by the floor - it is the remedy for it. The
|
|
52
|
-
line above is where the floor is reported, so the user sees why a run halted
|
|
53
|
-
`$REQUIRED` does not survive into the later shell blocks.
|
|
60
|
+
line above is where the floor is reported, so the user sees why a run halted.
|
|
54
61
|
|
|
55
62
|
2. **Stop early when already current.** When the line below prints, skip
|
|
56
63
|
step 3 (no download, no reinstall) and continue at step 4b, so the stack
|
|
57
64
|
plugins are still refreshed:
|
|
58
65
|
```bash
|
|
66
|
+
. "${TMPDIR:-/tmp}/multi-agent-update.env" || { echo "Run step 1 first."; exit 1; }
|
|
59
67
|
[ "$CUR" = "$LATEST" ] && echo "✓ Already up to date: v$LATEST"
|
|
60
68
|
```
|
|
61
69
|
|
|
62
70
|
3. **Download the release and install**:
|
|
63
71
|
```bash
|
|
64
|
-
|
|
72
|
+
. "${TMPDIR:-/tmp}/multi-agent-update.env" || { echo "Run step 1 first."; exit 1; }
|
|
65
73
|
rm -rf "$UPD_DIR" && mkdir -p "$UPD_DIR"
|
|
66
74
|
# `--registry` does NOT override a scope mapping. A user-level .npmrc line
|
|
67
75
|
# like `@scope:registry=https://npm.pkg.github.com` wins over it, so the
|
|
@@ -80,10 +88,15 @@ A git clone of the pipeline repo is a maintainer workspace, kept in sync by `/mu
|
|
|
80
88
|
TGZ="$UPD_DIR/$(printf '%s' "$PACK_OUT" | tail -1 | tr -d '[:space:]')"
|
|
81
89
|
[ -f "$TGZ" ] || { echo "npm pack reported no tarball; aborting."; exit 1; }
|
|
82
90
|
tar -xzf "$TGZ" -C "$UPD_DIR"
|
|
83
|
-
node "$UPD_DIR/package/install.js" --
|
|
91
|
+
node "$UPD_DIR/package/install.js" --installed
|
|
84
92
|
```
|
|
85
93
|
|
|
86
|
-
|
|
94
|
+
`--installed` refreshes exactly the CLI targets that already carry a version
|
|
95
|
+
stamp (`$HOME/.claude/.pipeline-version`, `$HOME/.copilot/.pipeline-version`,
|
|
96
|
+
`$HOME/.codex/.pipeline-version`) and adds none: an update never installs
|
|
97
|
+
Copilot CLI or Codex CLI on a machine that only had Claude Code. With no stamp
|
|
98
|
+
at all it installs Claude Code. Each refreshed target gets the new version
|
|
99
|
+
stamp.
|
|
87
100
|
|
|
88
101
|
4b. **Update the stack plugins** (catalog refresh, then every installed plugin in its own scope):
|
|
89
102
|
```bash
|
|
@@ -98,10 +111,8 @@ A git clone of the pipeline repo is a maintainer workspace, kept in sync by `/mu
|
|
|
98
111
|
echo " -> no global adapter files to prune"
|
|
99
112
|
```
|
|
100
113
|
|
|
101
|
-
> **Do NOT delete `$HOME/.codex/prompts/multi-agent.md`.**
|
|
102
|
-
>
|
|
103
|
-
> supported target again as of v13.0.0 and the installer writes that file, so
|
|
104
|
-
> deleting it silently breaks the `/multi-agent` slash command on Codex.
|
|
114
|
+
> **Do NOT delete `$HOME/.codex/prompts/multi-agent.md`.** The Codex installer
|
|
115
|
+
> writes it and it backs the `/multi-agent` slash command on Codex CLI.
|
|
105
116
|
|
|
106
117
|
5. **Migrate preferences** (if there is an old schema):
|
|
107
118
|
```bash
|
|
@@ -110,11 +121,9 @@ A git clone of the pipeline repo is a maintainer workspace, kept in sync by `/mu
|
|
|
110
121
|
fi
|
|
111
122
|
```
|
|
112
123
|
|
|
113
|
-
5b. **Auto-configure operational reporting.** One call,
|
|
114
|
-
`/multi-agent:setup` and a first run make
|
|
115
|
-
|
|
116
|
-
update was never registered and never reported, which reads in the panel
|
|
117
|
-
exactly like nobody using the pipeline.
|
|
124
|
+
5b. **Auto-configure operational reporting.** One call, the same one
|
|
125
|
+
`/multi-agent:setup` and a first run make, so a machine is registered by
|
|
126
|
+
whichever of the three runs first.
|
|
118
127
|
|
|
119
128
|
```bash
|
|
120
129
|
node "$HOME/.claude/scripts/usage-register.mjs"
|
|
@@ -128,6 +137,7 @@ A git clone of the pipeline repo is a maintainer workspace, kept in sync by `/mu
|
|
|
128
137
|
|
|
129
138
|
6. **Show the new version and its changes** (from the packaged CHANGELOG - there is no git history on this channel):
|
|
130
139
|
```bash
|
|
140
|
+
. "${TMPDIR:-/tmp}/multi-agent-update.env" || { echo "Run step 1 first."; exit 1; }
|
|
131
141
|
NEW=$(tr -d '[:space:]' < "$HOME/.claude/.pipeline-version" 2>/dev/null)
|
|
132
142
|
echo ""
|
|
133
143
|
echo "✓ Updated: v$CUR → v$NEW"
|
|
@@ -140,11 +150,12 @@ A git clone of the pipeline repo is a maintainer workspace, kept in sync by `/mu
|
|
|
140
150
|
|
|
141
151
|
7. **Smoke test (optional)** - the release tarball ships exactly two smokes for this purpose:
|
|
142
152
|
```bash
|
|
153
|
+
. "${TMPDIR:-/tmp}/multi-agent-update.env" || { echo "Run step 1 first."; exit 1; }
|
|
143
154
|
echo ""
|
|
144
155
|
echo "Verification:"
|
|
145
156
|
bash "$UPD_DIR/package/pipeline/scripts/smoke-schema-validation.sh" 2>&1 | tail -1
|
|
146
157
|
bash "$UPD_DIR/package/pipeline/scripts/smoke-cross-cli-behavior.sh" 2>&1 | tail -1
|
|
147
|
-
rm -rf "$UPD_DIR"
|
|
158
|
+
rm -rf "$UPD_DIR" "${TMPDIR:-/tmp}/multi-agent-update.env"
|
|
148
159
|
```
|
|
149
160
|
|
|
150
161
|
## Output
|
|
@@ -152,7 +163,7 @@ A git clone of the pipeline repo is a maintainer workspace, kept in sync by `/mu
|
|
|
152
163
|
```
|
|
153
164
|
Current: v15.6.0 Latest: v15.6.1
|
|
154
165
|
-> npm pack @{npm-scope}/multi-agent-pipeline@15.6.1
|
|
155
|
-
-> node install.js --
|
|
166
|
+
-> node install.js --installed (Claude Code; no Copilot CLI or Codex CLI stamp, not installed)
|
|
156
167
|
-> migrate-prefs.mjs (0 changes - already v2.6.0)
|
|
157
168
|
|
|
158
169
|
✓ Updated: v15.6.0 → v15.6.1
|
|
@@ -1,36 +1,23 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_readme": "Characters a guard folds before matching, applied after NFKC. `
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"\u202e",
|
|
22
|
-
"\u2060",
|
|
23
|
-
"\u2061",
|
|
24
|
-
"\u2062",
|
|
25
|
-
"\u2063",
|
|
26
|
-
"\u2064",
|
|
27
|
-
"\u2066",
|
|
28
|
-
"\u2067",
|
|
29
|
-
"\u2068",
|
|
30
|
-
"\u2069",
|
|
31
|
-
"\u3164",
|
|
32
|
-
"\ufeff",
|
|
33
|
-
"\uffa0"
|
|
2
|
+
"_readme": "Characters a guard folds before matching, applied after NFKC. `stripRanges` lists the Default_Ignorable_Code_Point set as hex code point ranges (a single value or `FIRST-LAST`); every character in them is removed. `map` sends a character that renders like an ASCII one to that ASCII character: the dash family, the Cyrillic and Greek letters whose glyphs match Latin ones, and the compatibility letters and digits added after Unicode 13 whose NFKC form is ASCII, so a Python whose unicodedata predates them folds them the way Node does. Chosen from the Unicode confusables data for the letters and punctuation the guard rules use. Read by lib/normalize-text.mjs and scripts/agent-guard.py.",
|
|
3
|
+
"stripRanges": [
|
|
4
|
+
"00AD",
|
|
5
|
+
"034F",
|
|
6
|
+
"061C",
|
|
7
|
+
"115F-1160",
|
|
8
|
+
"17B4-17B5",
|
|
9
|
+
"180B-180F",
|
|
10
|
+
"200B-200F",
|
|
11
|
+
"202A-202E",
|
|
12
|
+
"2060-206F",
|
|
13
|
+
"3164",
|
|
14
|
+
"FE00-FE0F",
|
|
15
|
+
"FEFF",
|
|
16
|
+
"FFA0",
|
|
17
|
+
"FFF0-FFF8",
|
|
18
|
+
"1BCA0-1BCA3",
|
|
19
|
+
"1D173-1D17A",
|
|
20
|
+
"E0000-E0FFF"
|
|
34
21
|
],
|
|
35
22
|
"map": {
|
|
36
23
|
"\u2010": "-",
|
|
@@ -95,6 +82,65 @@
|
|
|
95
82
|
"\u03b1": "a",
|
|
96
83
|
"\u03bd": "v",
|
|
97
84
|
"\u03c1": "p",
|
|
98
|
-
"\u0131": "i"
|
|
85
|
+
"\u0131": "i",
|
|
86
|
+
"\ua7f1": "S",
|
|
87
|
+
"\ua7f2": "C",
|
|
88
|
+
"\ua7f3": "F",
|
|
89
|
+
"\ua7f4": "Q",
|
|
90
|
+
"\ud801\udf81": ":",
|
|
91
|
+
"\ud801\udfa5": "q",
|
|
92
|
+
"\ud833\udcd6": "A",
|
|
93
|
+
"\ud833\udcd7": "B",
|
|
94
|
+
"\ud833\udcd8": "C",
|
|
95
|
+
"\ud833\udcd9": "D",
|
|
96
|
+
"\ud833\udcda": "E",
|
|
97
|
+
"\ud833\udcdb": "F",
|
|
98
|
+
"\ud833\udcdc": "G",
|
|
99
|
+
"\ud833\udcdd": "H",
|
|
100
|
+
"\ud833\udcde": "I",
|
|
101
|
+
"\ud833\udcdf": "J",
|
|
102
|
+
"\ud833\udce0": "K",
|
|
103
|
+
"\ud833\udce1": "L",
|
|
104
|
+
"\ud833\udce2": "M",
|
|
105
|
+
"\ud833\udce3": "N",
|
|
106
|
+
"\ud833\udce4": "O",
|
|
107
|
+
"\ud833\udce5": "P",
|
|
108
|
+
"\ud833\udce6": "Q",
|
|
109
|
+
"\ud833\udce7": "R",
|
|
110
|
+
"\ud833\udce8": "S",
|
|
111
|
+
"\ud833\udce9": "T",
|
|
112
|
+
"\ud833\udcea": "U",
|
|
113
|
+
"\ud833\udceb": "V",
|
|
114
|
+
"\ud833\udcec": "W",
|
|
115
|
+
"\ud833\udced": "X",
|
|
116
|
+
"\ud833\udcee": "Y",
|
|
117
|
+
"\ud833\udcef": "Z",
|
|
118
|
+
"\ud833\udcf0": "0",
|
|
119
|
+
"\ud833\udcf1": "1",
|
|
120
|
+
"\ud833\udcf2": "2",
|
|
121
|
+
"\ud833\udcf3": "3",
|
|
122
|
+
"\ud833\udcf4": "4",
|
|
123
|
+
"\ud833\udcf5": "5",
|
|
124
|
+
"\ud833\udcf6": "6",
|
|
125
|
+
"\ud833\udcf7": "7",
|
|
126
|
+
"\ud833\udcf8": "8",
|
|
127
|
+
"\ud833\udcf9": "9",
|
|
128
|
+
"\ud838\udc30": "a",
|
|
129
|
+
"\ud838\udc35": "e",
|
|
130
|
+
"\ud838\udc3c": "o",
|
|
131
|
+
"\ud838\udc3e": "p",
|
|
132
|
+
"\ud838\udc3f": "c",
|
|
133
|
+
"\ud838\udc41": "y",
|
|
134
|
+
"\ud838\udc43": "x",
|
|
135
|
+
"\ud838\udc4c": "i",
|
|
136
|
+
"\ud838\udc4d": "j",
|
|
137
|
+
"\ud838\udc51": "a",
|
|
138
|
+
"\ud838\udc56": "e",
|
|
139
|
+
"\ud838\udc5c": "o",
|
|
140
|
+
"\ud838\udc5e": "c",
|
|
141
|
+
"\ud838\udc5f": "y",
|
|
142
|
+
"\ud838\udc61": "x",
|
|
143
|
+
"\ud838\udc68": "i",
|
|
144
|
+
"\ud838\udc69": "s"
|
|
99
145
|
}
|
|
100
146
|
}
|
|
@@ -927,11 +927,11 @@ bucket_di_registration() {
|
|
|
927
927
|
case "$PLATFORM" in
|
|
928
928
|
ios)
|
|
929
929
|
local configurators
|
|
930
|
-
configurators=$(run_find -type f \( -name '*DependencyConfigurator.swift' -o -name '*Assembly.swift' -o -name '*Module.swift' \) 2>/dev/null | head -20 || true)
|
|
930
|
+
configurators=$(run_find -type f \( -name '*DependencyConfigurator.swift' -o -name '*DependencyRegistrar.swift' -o -name '*Assembly.swift' -o -name '*Module.swift' \) 2>/dev/null | head -20 || true)
|
|
931
931
|
local resolver_count factory_count manual_count swinject_count
|
|
932
932
|
manual_count=0; resolver_count=0; factory_count=0; swinject_count=0
|
|
933
933
|
if [ -n "$configurators" ]; then
|
|
934
|
-
manual_count=$(printf '%s\n' "$configurators" | count_matches '
|
|
934
|
+
manual_count=$(printf '%s\n' "$configurators" | count_matches -E 'Dependency(Configurator|Registrar)')
|
|
935
935
|
fi
|
|
936
936
|
files=$(run_find -type f -name '*.swift' 2>/dev/null || true)
|
|
937
937
|
if [ -n "$files" ]; then
|
|
@@ -943,7 +943,7 @@ bucket_di_registration() {
|
|
|
943
943
|
|
|
944
944
|
sample=$(printf '%s\n' "$configurators" | head -3 | sed "s|^$REPO_PATH/||")
|
|
945
945
|
if [ "$manual_count" -ge "$resolver_count" ] && [ "$manual_count" -ge "$factory_count" ] && [ "$manual_count" -ge "$swinject_count" ] && [ "$manual_count" -gt 0 ]; then
|
|
946
|
-
emit_bucket "manual
|
|
946
|
+
emit_bucket "manual per-module registrar" "FooDependencyRegistrar.swift" "$(confidence_for "$manual_count")" "$(files_to_json "$sample")" '["Resolver","Factory","Swinject"]'
|
|
947
947
|
elif [ "$resolver_count" -ge "$factory_count" ] && [ "$resolver_count" -ge "$swinject_count" ] && [ "$resolver_count" -gt 0 ]; then
|
|
948
948
|
emit_bucket "Resolver" "Resolver.register { FooViewModel() }" "$(confidence_for "$resolver_count")" "[]" '["manual-configurator","Factory","Swinject"]'
|
|
949
949
|
elif [ "$factory_count" -ge "$swinject_count" ] && [ "$factory_count" -gt 0 ]; then
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
import {
|
|
20
20
|
chmodSync,
|
|
21
21
|
linkSync,
|
|
22
|
+
lstatSync,
|
|
22
23
|
readFileSync,
|
|
24
|
+
realpathSync,
|
|
23
25
|
renameSync,
|
|
24
26
|
rmSync,
|
|
25
27
|
statSync,
|
|
@@ -125,17 +127,31 @@ export function withFileLockSync(
|
|
|
125
127
|
/**
|
|
126
128
|
* Replace `path` with `data` as JSON, atomically, at `mode`.
|
|
127
129
|
*
|
|
130
|
+
* `followSymlink` is for a file the user owns and may keep in a dotfiles repo
|
|
131
|
+
* (the preferences file): when `path` is a symlink, the rename lands on its
|
|
132
|
+
* target, so the link and the dotfiles copy stay in step. Without it a
|
|
133
|
+
* symlink at `path` is replaced by a regular file, which is the safe default
|
|
134
|
+
* for a path an unattended run could plant a link at.
|
|
135
|
+
*
|
|
128
136
|
* @param {string} path
|
|
129
137
|
* @param {unknown} data
|
|
130
|
-
* @param {{mode?: number}} [opts]
|
|
138
|
+
* @param {{mode?: number, followSymlink?: boolean}} [opts]
|
|
131
139
|
*/
|
|
132
|
-
export function writeJsonAtomicSync(path, data, { mode = 0o600 } = {}) {
|
|
133
|
-
|
|
140
|
+
export function writeJsonAtomicSync(path, data, { mode = 0o600, followSymlink = false } = {}) {
|
|
141
|
+
let target = path;
|
|
142
|
+
if (followSymlink) {
|
|
143
|
+
try {
|
|
144
|
+
if (lstatSync(path).isSymbolicLink()) target = realpathSync(path);
|
|
145
|
+
} catch {
|
|
146
|
+
// Missing file: write the path as given.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const tmp = join(dirname(target), `.${basename(target)}.${process.pid}.tmp`);
|
|
134
150
|
rmSync(tmp, { force: true });
|
|
135
151
|
try {
|
|
136
152
|
writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, { mode, flag: "wx" });
|
|
137
153
|
chmodSync(tmp, mode);
|
|
138
|
-
renameSync(tmp,
|
|
154
|
+
renameSync(tmp, target);
|
|
139
155
|
} catch (err) {
|
|
140
156
|
rmSync(tmp, { force: true });
|
|
141
157
|
throw err;
|
|
@@ -149,16 +165,20 @@ export function writeJsonAtomicSync(path, data, { mode = 0o600 } = {}) {
|
|
|
149
165
|
*
|
|
150
166
|
* @param {string} path
|
|
151
167
|
* @param {(current: any) => any} mutate
|
|
152
|
-
* @param {{fallback?: any, mode?: number, timeoutMs?: number, staleMs?: number}} [opts]
|
|
168
|
+
* @param {{fallback?: any, mode?: number, followSymlink?: boolean, timeoutMs?: number, staleMs?: number}} [opts]
|
|
153
169
|
* @returns {any} the document written, or undefined when nothing was
|
|
154
170
|
*/
|
|
155
|
-
export function updateJsonFileSync(
|
|
171
|
+
export function updateJsonFileSync(
|
|
172
|
+
path,
|
|
173
|
+
mutate,
|
|
174
|
+
{ fallback = null, mode = 0o600, followSymlink = false, ...lock } = {},
|
|
175
|
+
) {
|
|
156
176
|
return withFileLockSync(
|
|
157
177
|
path,
|
|
158
178
|
() => {
|
|
159
179
|
const next = mutate(readRegularJson(path, fallback));
|
|
160
180
|
if (next === undefined) return undefined;
|
|
161
|
-
writeJsonAtomicSync(path, next, { mode });
|
|
181
|
+
writeJsonAtomicSync(path, next, { mode, followSymlink });
|
|
162
182
|
return next;
|
|
163
183
|
},
|
|
164
184
|
lock,
|
|
@@ -2,13 +2,20 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* normalize-text.mjs - fold look-alike text to the form a guard matches on.
|
|
4
4
|
*
|
|
5
|
-
* NFKC, then the
|
|
6
|
-
* look-alike letters and punctuation mapped to ASCII. A guard
|
|
7
|
-
* raw text and on this folded copy, so a trailer or a token
|
|
8
|
-
* zero-width space or a Cyrillic letter is still recognised. The
|
|
9
|
-
* only ever matched against; nothing written or shown uses it.
|
|
5
|
+
* NFKC, then the Default_Ignorable_Code_Point ranges in confusables.json
|
|
6
|
+
* removed and its look-alike letters and punctuation mapped to ASCII. A guard
|
|
7
|
+
* matches on the raw text and on this folded copy, so a trailer or a token
|
|
8
|
+
* written with a zero-width space or a Cyrillic letter is still recognised. The
|
|
9
|
+
* folded copy is only ever matched against; nothing written or shown uses it.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* The table is loaded fail-soft: when confusables.json cannot be read the fold
|
|
12
|
+
* is NFKC alone, `TABLE_LOADED` is false and one notice goes to stderr. The log
|
|
13
|
+
* redactor keeps working on that reduced fold; the outbound gate checks
|
|
14
|
+
* `TABLE_LOADED` and refuses to scan.
|
|
15
|
+
*
|
|
16
|
+
* As a CLI it reads stdin and writes the folded text to stdout. With `--nul`
|
|
17
|
+
* it reads NUL-separated records and writes each folded record followed by a
|
|
18
|
+
* NUL, so a caller folds many texts in one process.
|
|
12
19
|
*
|
|
13
20
|
* @module pipeline/lib/normalize-text
|
|
14
21
|
*/
|
|
@@ -17,32 +24,94 @@ import { readFileSync } from "node:fs";
|
|
|
17
24
|
import { join } from "node:path";
|
|
18
25
|
import { invokedDirectly } from "./invoked-directly.mjs";
|
|
19
26
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
27
|
+
function loadTable() {
|
|
28
|
+
try {
|
|
29
|
+
const table = JSON.parse(readFileSync(join(import.meta.dirname, "confusables.json"), "utf8"));
|
|
30
|
+
return { table, loaded: true };
|
|
31
|
+
} catch (err) {
|
|
32
|
+
process.stderr.write(
|
|
33
|
+
`normalize-text: confusables.json unreadable (${err.code || err.name}) - folding with NFKC only\n`,
|
|
34
|
+
);
|
|
35
|
+
return { table: {}, loaded: false };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { table: TABLE, loaded } = loadTable();
|
|
40
|
+
|
|
41
|
+
/** False when confusables.json could not be read and the fold is NFKC only. */
|
|
42
|
+
export const TABLE_LOADED = loaded;
|
|
43
|
+
|
|
44
|
+
function stripSet(table) {
|
|
45
|
+
const set = new Set();
|
|
46
|
+
for (const range of table.stripRanges || []) {
|
|
47
|
+
const [first, last = first] = String(range).split("-");
|
|
48
|
+
const lo = parseInt(first, 16);
|
|
49
|
+
const hi = parseInt(last, 16);
|
|
50
|
+
if (Number.isNaN(lo) || Number.isNaN(hi)) continue;
|
|
51
|
+
for (let cp = lo; cp <= hi; cp++) set.add(cp);
|
|
52
|
+
}
|
|
53
|
+
return set;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const STRIP = stripSet(TABLE);
|
|
57
|
+
const MAP = new Map(Object.entries(TABLE.map || {}));
|
|
23
58
|
|
|
24
59
|
/** @param {string} text */
|
|
25
60
|
export function normalizeForMatch(text) {
|
|
26
61
|
if (typeof text !== "string" || !text) return text;
|
|
27
62
|
let out = "";
|
|
28
63
|
for (const ch of text.normalize("NFKC")) {
|
|
29
|
-
if (STRIP.has(ch)) continue;
|
|
64
|
+
if (STRIP.has(ch.codePointAt(0))) continue;
|
|
30
65
|
out += MAP.get(ch) ?? ch;
|
|
31
66
|
}
|
|
32
67
|
return out;
|
|
33
68
|
}
|
|
34
69
|
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
70
|
+
/**
|
|
71
|
+
* The fold of `text` one code point at a time, with the span of the original
|
|
72
|
+
* each folded UTF-16 unit came from: folded unit `j` came from
|
|
73
|
+
* `text.slice(start[j], end[j])`. A match on `folded` from `a` to `b` covers
|
|
74
|
+
* `text.slice(start[a], end[b - 1])`. Folding per code point rather than over
|
|
75
|
+
* the whole string differs from normalizeForMatch only where NFKC would
|
|
76
|
+
* compose a base letter with a following combining mark, which no credential
|
|
77
|
+
* shape contains.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} text
|
|
80
|
+
* @returns {{ folded: string, start: number[], end: number[] }}
|
|
81
|
+
*/
|
|
82
|
+
export function foldWithIndex(text) {
|
|
83
|
+
const start = [];
|
|
84
|
+
const end = [];
|
|
85
|
+
let folded = "";
|
|
86
|
+
let i = 0;
|
|
87
|
+
for (const ch of text) {
|
|
88
|
+
const next = i + ch.length;
|
|
89
|
+
for (const part of ch.normalize("NFKC")) {
|
|
90
|
+
if (STRIP.has(part.codePointAt(0))) continue;
|
|
91
|
+
const mapped = MAP.get(part) ?? part;
|
|
92
|
+
for (let k = 0; k < mapped.length; k++) {
|
|
93
|
+
start.push(i);
|
|
94
|
+
end.push(next);
|
|
95
|
+
}
|
|
96
|
+
folded += mapped;
|
|
97
|
+
}
|
|
98
|
+
i = next;
|
|
99
|
+
}
|
|
100
|
+
return { folded, start, end };
|
|
41
101
|
}
|
|
42
102
|
|
|
43
103
|
if (invokedDirectly(import.meta.url)) {
|
|
104
|
+
const nul = process.argv.includes("--nul");
|
|
44
105
|
let input = "";
|
|
45
106
|
process.stdin.setEncoding("utf8");
|
|
46
107
|
process.stdin.on("data", (d) => (input += d));
|
|
47
|
-
process.stdin.on("end", () =>
|
|
108
|
+
process.stdin.on("end", () => {
|
|
109
|
+
if (!nul) {
|
|
110
|
+
process.stdout.write(normalizeForMatch(input));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const records = input.split("\0");
|
|
114
|
+
if (records.at(-1) === "") records.pop();
|
|
115
|
+
process.stdout.write(records.map((r) => `${normalizeForMatch(r)}\0`).join(""));
|
|
116
|
+
});
|
|
48
117
|
}
|
|
@@ -42,14 +42,15 @@
|
|
|
42
42
|
* cat body.md | outbound-gate.mjs --warn # report, never block
|
|
43
43
|
*
|
|
44
44
|
* Exit codes:
|
|
45
|
-
* 0 - nothing found
|
|
45
|
+
* 0 - nothing found, or an empty body
|
|
46
46
|
* 7 - at least one finding; the caller must not publish
|
|
47
|
-
* 2 - usage error (
|
|
47
|
+
* 2 - usage error (--file without a path)
|
|
48
|
+
* or a stdin that cannot be read at all
|
|
48
49
|
*/
|
|
49
50
|
|
|
50
51
|
import { readFileSync } from "node:fs";
|
|
51
52
|
import { invokedDirectly } from "./invoked-directly.mjs";
|
|
52
|
-
import { normalizeForMatch } from "./normalize-text.mjs";
|
|
53
|
+
import { TABLE_LOADED, normalizeForMatch } from "./normalize-text.mjs";
|
|
53
54
|
|
|
54
55
|
/**
|
|
55
56
|
* Each rule is a SHAPE, not a dictionary. A list of known token prefixes goes
|
|
@@ -148,17 +149,25 @@ export const SURFACES = [
|
|
|
148
149
|
},
|
|
149
150
|
];
|
|
150
151
|
|
|
152
|
+
// Printable ASCII and tab fold to themselves; any other line is folded.
|
|
153
|
+
const NEEDS_FOLD = /[^\t -~]/;
|
|
154
|
+
|
|
151
155
|
/**
|
|
152
156
|
* @param {string} text
|
|
153
157
|
* @returns {{rule: string, certain: boolean, line: number}[]}
|
|
154
158
|
*/
|
|
155
159
|
export function scan(text) {
|
|
160
|
+
// Without the fold table a disguised credential would pass as clean text,
|
|
161
|
+
// so the gate refuses to scan rather than report nothing.
|
|
162
|
+
if (!TABLE_LOADED) {
|
|
163
|
+
throw new Error("outbound-gate: confusables.json unreadable - refusing to scan");
|
|
164
|
+
}
|
|
156
165
|
const findings = [];
|
|
157
166
|
const lines = String(text).split("\n");
|
|
158
167
|
for (let i = 0; i < lines.length; i++) {
|
|
159
168
|
// A credential written with an invisible character or a look-alike letter
|
|
160
169
|
// still matches: the certain rules also run on the folded line.
|
|
161
|
-
const folded = normalizeForMatch(lines[i]);
|
|
170
|
+
const folded = NEEDS_FOLD.test(lines[i]) ? normalizeForMatch(lines[i]) : lines[i];
|
|
162
171
|
for (const rule of RULES) {
|
|
163
172
|
// `lastIndex` survives between calls on a /g regex and would make every
|
|
164
173
|
// second line skip its start. Reset per line.
|