forgedock 1.12.1 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine/phases.mjs +54 -12
- package/bin/engine/runlog.mjs +12 -0
- package/bin/engine-cli.mjs +3 -1
- package/bin/engine.mjs +64 -16
- package/bin/hooks/interactive-engine.mjs +135 -212
- package/bin/hooks/pre-tool-use.mjs +16 -4
- package/bin/labels.json +5 -0
- package/bin/opencode-adapter.mjs +1 -1
- package/bin/runner.mjs +234 -21
- package/commands/autopilot.md +6 -6
- package/commands/cleanup.md +7 -4
- package/commands/orchestrate/phase-1-resolve.md +1 -1
- package/commands/orchestrate/phase-3-dependency.md +14 -19
- package/commands/orchestrate/phase-4-execution.md +50 -38
- package/commands/orchestrate/phase-6-report.md +6 -0
- package/commands/orchestrate/safety.md +1 -1
- package/commands/pipeline-health/phase-2-metrics.md +39 -0
- package/commands/pipeline-health/phase-5-report.md +1 -0
- package/commands/pipeline-resume.md +1 -1
- package/commands/pipeline-status.md +2 -2
- package/commands/review-pr-agents/api.md +1 -1
- package/commands/review-pr-agents/auth.md +1 -1
- package/commands/review-pr-agents/infra.md +3 -3
- package/commands/review-pr-agents/protocols.md +14 -5
- package/commands/review-pr-agents/scraper.md +1 -1
- package/commands/review-pr-agents/security.md +5 -5
- package/commands/review-pr-agents.md +1 -1
- package/commands/review-pr-staging.md +226 -8
- package/commands/review-pr.md +239 -29
- package/commands/work-on/build/architect.md +36 -0
- package/commands/work-on/build/validate.md +88 -4
- package/commands/work-on/build.md +95 -3
- package/commands/work-on/remediate.md +115 -13
- package/commands/work-on.md +4 -4
- package/docs/CONFIG.md +29 -0
- package/docs/RELEASE-NOTES.md +45 -0
- package/docs/spec/forge-protocol-v1.md +22 -1
- package/docs/spec/review-protocol.md +19 -4
- package/package.json +2 -1
- package/scripts/check-blast-radius.sh +164 -0
- package/scripts/check-blast-radius.test.sh +183 -0
- package/scripts/check-contract-scope.sh +137 -0
- package/scripts/check-contract-scope.test.sh +83 -0
- package/scripts/check-spec-bash.sh +60 -3
- package/scripts/check-spec-bash.test.sh +56 -0
- package/scripts/check-spec-markers.sh +29 -0
- package/scripts/check-test-wiring.sh +57 -0
- package/scripts/check-test-wiring.test.sh +70 -0
- package/scripts/classify-finding.sh +72 -8
- package/scripts/classify-finding.test.sh +77 -8
- package/scripts/close-dossier-no-main-commit.test.sh +12 -12
- package/scripts/diff-size.test.sh +3 -2
- package/scripts/doctor-pipeline-state.sh +35 -0
- package/scripts/forge-annotation.sh +3 -0
- package/scripts/forge-root.test.sh +70 -1
- package/scripts/forge-run.sh +2 -2
- package/scripts/recovery-claim-live.test.sh +10 -7
- package/scripts/remediate-basesync.test.sh +103 -0
- package/scripts/review-delta.sh +206 -0
- package/scripts/review-delta.test.sh +213 -0
- package/scripts/review-pr-gate2.test.sh +84 -0
- package/scripts/review-pr-staging-scope.test.sh +99 -0
- package/scripts/run-shell-tests.sh +115 -0
- package/scripts/select-fix-targets.sh +17 -5
package/docs/CONFIG.md
CHANGED
|
@@ -457,6 +457,35 @@ pipeline:
|
|
|
457
457
|
|
|
458
458
|
---
|
|
459
459
|
|
|
460
|
+
## `engine` (OPTIONAL)
|
|
461
|
+
|
|
462
|
+
Per-command `--model` and `--max-turns` for the CLI backend (`forgedock run-issue` phases and `forgedock run <command>`). Opt-in: with no keys set, the `claude` invocation is unchanged and uses the CLI's own default model with no turn bound. `forge.yaml` is parsed one level deep, so settings are **flat** keys under `engine:`, named `<key>_model` and `<key>_max_turns`, where `<key>` is the command name with every non-alphanumeric character replaced by `_`.
|
|
463
|
+
|
|
464
|
+
```yaml
|
|
465
|
+
engine:
|
|
466
|
+
# work-on/review -> work_on_review (the review phase is the most expensive).
|
|
467
|
+
work_on_review_model: sonnet
|
|
468
|
+
work_on_review_max_turns: 120
|
|
469
|
+
|
|
470
|
+
# review-pr-staging -> review_pr_staging (non-work-on commands work the same way).
|
|
471
|
+
review_pr_staging_max_turns: 80
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
| Field | Type | Required | Description |
|
|
475
|
+
|-------|------|----------|-------------|
|
|
476
|
+
| `<key>_model` | string | No | Model alias (`sonnet`, `opus`, `haiku`) or full model id passed as `--model`. Invalid values are ignored. |
|
|
477
|
+
| `<key>_max_turns` | integer | No | Positive integer passed as `--max-turns`. Invalid values are ignored. |
|
|
478
|
+
|
|
479
|
+
**Precedence** (model): `engine.<key>_model`, then the run-level model the caller passed explicitly, then omitted. `max_turns` comes only from this section. No defaults ship.
|
|
480
|
+
|
|
481
|
+
**Version gate**: the flags are added only when the installed `claude --version` supports them (`--max-turns` is hidden from `claude --help`, so help text is never scraped); an unparseable version omits both flags.
|
|
482
|
+
|
|
483
|
+
**Turn exhaustion**: when `--max-turns` is exhausted the run ends `engine-error` (detail `max-turns`) and the phase is never committed, so `forgedock run-issue --retry` re-enters the same phase. Raise `<key>_max_turns` before retrying. Phase runlog events record `model` and `turns` next to `usage` so a limit can be sized from measured turn counts.
|
|
484
|
+
|
|
485
|
+
**Commands that use this section**: CLI backend (`runCommand` in `bin/runner.mjs`)
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
460
489
|
## `build` (OPTIONAL)
|
|
461
490
|
|
|
462
491
|
Tuning for the `/work-on` build phase. Today it holds the diff-size gate: after the implement phase stages its changes and before validation commits them, the build measures the changed lines (added plus deleted) of the index and any earlier commits against `origin/{base}`, using `scripts/diff-size.sh`. Review findings per PR rise steeply with diff size, so an oversized build is split before it is validated.
|
package/docs/RELEASE-NOTES.md
CHANGED
|
@@ -2,6 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
Operator-facing notes for behavior changes that need action or awareness. Newest first.
|
|
4
4
|
|
|
5
|
+
## Review damper: LOW needs keyword + CONFIRMED; a missing classifier blocks the review (#3639)
|
|
6
|
+
|
|
7
|
+
`scripts/classify-finding.sh` is stricter for LOW findings. A safety keyword now files a LOW finding only when it is also CONFIRMED (LOW LIKELY/POSSIBLE with a keyword is a NOTE), and a dedicated domain agent (Auth, Billing, Concurrency, Database) rescues MEDIUM findings only, no longer LOW CONFIRMED. MEDIUM behaviour is unchanged.
|
|
8
|
+
|
|
9
|
+
`/review-pr` §6B.5 and `/review-pr-staging` §7B.5 no longer let the model hand-classify (`classifier=manual`) when the script cannot be resolved. They return `REVIEW_RESULT: status: BLOCKED` with blocker `classifier unavailable (classify-finding.sh not resolvable through the trusted resolver)`, file nothing and do not merge or approve a deploy. The staging per-finding runtime-failure fallback is renamed `ISSUE classifier-error`. The `classifier=` marker stays and is always `script`.
|
|
10
|
+
|
|
11
|
+
Action: if reviews now report `classifier unavailable`, fix the ForgeDock install so the trusted resolver finds `scripts/classify-finding.sh`, then re-run the review.
|
|
12
|
+
|
|
13
|
+
## Introduced-line gate no longer demotes CRITICAL/HIGH (#3597, #3598)
|
|
14
|
+
|
|
15
|
+
`/review-pr` §6B.4 gate 2 never routes a CRITICAL/HIGH finding (any confidence) as `pre-existing`: a new caller of an unchanged unsafe helper is an interaction defect (`protocols.md` §5), so it stays a blocking `review-finding` under §7B criterion 2, including with `--auto-merge`. The pre-existing route now covers MEDIUM and lower only. A file with no `patch` in the PR files API (large or truncated diff) is treated as UNKNOWN and its findings are kept, never routed as pre-existing. Expect slightly more blocking findings on PRs that touch unsafe code through unchanged helpers.
|
|
16
|
+
|
|
17
|
+
## Contract out-of-scope items now typed; review demotes matching findings (#3447)
|
|
18
|
+
|
|
19
|
+
The `### Out of Scope` section of `FORGE:CONTRACT` is no longer free text. It is `None.` or bullets that each name a backticked path and one disposition: `deferred → #N`, `not-affected: <evidence>` or `accepted-risk: <reason>`. A contract that keeps a known-flawed design must list it as `deferred → #N`.
|
|
20
|
+
|
|
21
|
+
- **Build** (B2.0) files a follow-up issue for each `deferred` item before the contract is posted (idempotent through a `FORGE:DEFERRED_FROM: #N` body marker) and validates the section with the new `scripts/check-contract-scope.sh`. An untyped item blocks the build after one retry.
|
|
22
|
+
- **Review** (`/review-pr` §6B.5) reads the latest trusted contract and passes it to `scripts/classify-finding.sh --contract-scope`. A finding on a `deferred` path (follow-up still open) or an `accepted-risk` path becomes a note instead of a new `review-finding`; deferred matches are commented on the existing issue. `not-affected` items, HIGH/CRITICAL findings, safety-exempt `accepted-risk` findings and files the PR itself changed are never demoted. `FORGE:NOTE_DISPOSITION` gains `contract_demoted=N`.
|
|
23
|
+
- The review scope-creep check now reads only the contract's `### Deliverables`, so backticked out-of-scope paths no longer count as in-scope files.
|
|
24
|
+
|
|
25
|
+
Action: none. Older contracts with free-text Out of Scope sections are not parsed and demote nothing.
|
|
26
|
+
|
|
27
|
+
## Review provenance gates and noise share (#3452)
|
|
28
|
+
|
|
29
|
+
`/review-pr` §6B.4 now checks each finding before it can become a `review-finding` issue: the cited line must exist at the reviewed head SHA (else dropped as stale), must be added or changed by the PR diff (else routed as `pre-existing`), and must carry a concrete `**Failure scenario**` (else demoted to a note; CRITICAL/HIGH, any confidence, are exempt). Pre-existing defects are never counted as this PR's findings or in the amplification ratio; only MEDIUM or lower ones are routed this way, and the safety-domain ones are filed once with the new `pre-existing` label. The `FORGE:NOTE_DISPOSITION` record gains `findings_dropped_stale`, `findings_preexisting`, `notes_demoted_no_scenario`, and `/pipeline-health` reports a noise share (2E.5).
|
|
30
|
+
|
|
31
|
+
Action: run `npx forgedock labels setup` to create the `pre-existing` label (the review also creates it on demand).
|
|
32
|
+
|
|
33
|
+
## Diff-size gate in `/work-on` build (#3450)
|
|
34
|
+
|
|
35
|
+
`/work-on` build now measures the changed lines (added plus deleted) after implement stages its changes and before validate commits them. The gate is **on by default at 1000 lines**. Review findings per PR rise steeply with diff size, so an oversized build is split instead of validated.
|
|
36
|
+
|
|
37
|
+
- **Over the threshold**: the build posts a `FORGE:DIFF_SIZE` comment with a split proposal and exits `NEEDS_DECOMPOSE`; the router then runs `work-on:decompose`.
|
|
38
|
+
- **Tune or disable**: set `build.diff_size.threshold` in `forge.yaml` (`0` disables the gate) and add exclusions with `build.diff_size.exclude_globs`. Lockfiles, `*.min.*`, `*.snap` and vendored/generated directories are excluded by default.
|
|
39
|
+
- **Per-issue bypass**: post a `FORGE:SIZE_OVERRIDE` comment with a non-empty justification on the line after the marker. Only comments from trusted authors count.
|
|
40
|
+
- **No re-decomposition**: an issue that is already a decomposed child, or has `FORGE:DECOMPOSED`, blocks until an override is posted instead of being split again.
|
|
41
|
+
|
|
42
|
+
Action: if your builds routinely exceed 1000 changed lines, raise `build.diff_size.threshold` or set it to `0`. See the `build` section of [CONFIG.md](CONFIG.md).
|
|
43
|
+
|
|
44
|
+
## Stricter `review-pr` / `review-pr-staging` argument rejection (#3466)
|
|
45
|
+
|
|
46
|
+
`/review-pr` and `/review-pr-staging` now reject an argument string as a whole if it contains a quote, backtick, `$`, backslash, newline or tab. Nothing is parsed from a rejected string, so no PR number, repo, merge value or flag survives, and `--auto-merge` is not honoured. `/review-pr-staging` stops with nothing reviewed and nothing posted.
|
|
47
|
+
|
|
48
|
+
Action: callers must pass `--gh-flag -R owner/repo` unquoted, and must not forward untrusted text (issue titles, comment bodies) into the argument string.
|
|
49
|
+
|
|
5
50
|
## Spec bash no longer corrupted by Claude Code argument substitution
|
|
6
51
|
|
|
7
52
|
Claude Code replaces `$0`..`$9` in a skill body with the invocation's arguments (0-based), so spec bash loaded via `Skill(...)` with args was silently rewritten: `awk '{print $2}'` became `awk '{print --issue}'`, `local AGENT="$1"` became `local AGENT="--auto-merge"`, which broke `/review-pr` agent selection and the CI/deploy comparisons with no error. `${N}`, `$(N)`, `$NF`, `$@`, `$#` and `$10`+ are not substituted.
|
|
@@ -251,9 +251,30 @@ findings and what will actually be built.
|
|
|
251
251
|
{auth model, new configuration, data-safety, security surface, migration risk}
|
|
252
252
|
|
|
253
253
|
### Out of Scope
|
|
254
|
-
|
|
254
|
+
None.
|
|
255
255
|
```
|
|
256
256
|
|
|
257
|
+
`### Out of Scope` is either the single line `None.` or a list of typed bullets. Each bullet names at
|
|
258
|
+
least one backticked path or symbol followed by exactly one disposition:
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
- `path` — deferred → #N: <why>
|
|
262
|
+
- `path` — not-affected: <evidence>
|
|
263
|
+
- `path` — accepted-risk: <reason>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
- `deferred → #N`: a follow-up issue `#N` was filed before the contract was posted (`->` is accepted
|
|
267
|
+
for `→`). A contract that keeps a known-flawed design must list the flaw as a `deferred` item.
|
|
268
|
+
- `not-affected: <evidence>`: the path was checked and is not reached by the change.
|
|
269
|
+
- `accepted-risk: <reason>`: a known gap that is knowingly kept.
|
|
270
|
+
- Free-text items, a `deferred` item without an issue number, or a missing/empty section are invalid.
|
|
271
|
+
`scripts/check-contract-scope.sh validate|list` is the reference parser (TSV
|
|
272
|
+
`disposition<TAB>path<TAB>issue`).
|
|
273
|
+
- Review demotion: a finding on a `deferred` path (follow-up issue still open) or an `accepted-risk`
|
|
274
|
+
path becomes a note instead of a new `review-finding`; `not-affected` items, HIGH/CRITICAL findings,
|
|
275
|
+
safety-exempt `accepted-risk` findings and files the PR changed are never demoted. Only a trusted
|
|
276
|
+
author's contract is read. The count is recorded as `contract_demoted` in `FORGE:NOTE_DISPOSITION`.
|
|
277
|
+
|
|
257
278
|
#### `FORGE:CONTEXT`
|
|
258
279
|
|
|
259
280
|
- **Written by**: a context-gathering agent
|
|
@@ -25,6 +25,9 @@ Pipeline agents MUST NOT use `find` (unbounded or filesystem-wide) to locate pro
|
|
|
25
25
|
Every agent MUST follow this protocol:
|
|
26
26
|
|
|
27
27
|
### 1. Start From the PR Diff
|
|
28
|
+
|
|
29
|
+
**Input scoping**: You have been given a pre-computed diff slice containing only the files relevant to your domain (`[DOMAIN_DIFF_SLICE]`). Do NOT re-fetch `gh pr diff [PR_NUMBER]` in full — use the slice provided. This is capped at ~100K chars, mirroring the runner's tool-result limit.
|
|
30
|
+
|
|
28
31
|
```bash
|
|
29
32
|
# Verify review is still current before reading diff
|
|
30
33
|
CURRENT_SHA=$(gh pr view [PR_NUMBER] --json headRefOid --jq '.headRefOid')
|
|
@@ -34,10 +37,19 @@ if [ "$CURRENT_SHA" != "[REVIEW_SHA]" ]; then
|
|
|
34
37
|
echo "Current HEAD: $(echo $CURRENT_SHA | cut -c1-7)"
|
|
35
38
|
fi
|
|
36
39
|
|
|
40
|
+
# List files in your domain slice
|
|
37
41
|
gh pr diff [PR_NUMBER] --name-only
|
|
38
|
-
|
|
42
|
+
|
|
43
|
+
# Materialize the pre-computed domain diff slice supplied by the orchestrator.
|
|
44
|
+
# Persona greps below read "$SLICE_FILE" -- they never re-fetch the full diff.
|
|
45
|
+
SLICE_FILE=$(mktemp)
|
|
46
|
+
cat > "$SLICE_FILE" <<'FORGE_SLICE_EOF'
|
|
47
|
+
[DOMAIN_DIFF_SLICE]
|
|
48
|
+
FORGE_SLICE_EOF
|
|
39
49
|
```
|
|
40
50
|
|
|
51
|
+
**Empty or truncated slice**: if the slice is empty or ends mid-hunk, read the specific changed files with bounded reads (`sed -n 'X,Yp'`, `head -N`) — never fall back to fetching the full PR diff.
|
|
52
|
+
|
|
41
53
|
**Hot-spot prior**:
|
|
42
54
|
[CHURN_CONTEXT]
|
|
43
55
|
|
|
@@ -82,7 +94,7 @@ If a file you are reviewing is listed above as a hot-spot, apply deeper scrutiny
|
|
|
82
94
|
3. Will this cause degraded performance? → **MEDIUM**
|
|
83
95
|
4. Is it genuinely cosmetic with no runtime impact after tracing all consumers? → **LOW**
|
|
84
96
|
|
|
85
|
-
|
|
97
|
+
**Precision first.** Report defects that are likely to be real in the code this PR changes and the code it reaches. If you cannot verify a runtime impact after tracing the path, report the finding at **POSSIBLE** confidence — do not inflate its severity or confidence to be safe. A false positive costs reviewer trust and triggers a cascade of follow-up issues and re-reviews; an unverified suspicion belongs at POSSIBLE, where it stays a non-blocking note, not at an inflated severity.
|
|
86
98
|
|
|
87
99
|
### 5. INTERACTION ANALYSIS — "Pre-existing" Is Not "Safe"
|
|
88
100
|
|
|
@@ -95,6 +107,8 @@ A redundant import, an unused variable, or a duplicated constant may be harmless
|
|
|
95
107
|
2. For each reference, ask: "Does the pre-existing construct cause this new line to fail at runtime?"
|
|
96
108
|
3. If yes → CONFIRMED finding, not a dismissal
|
|
97
109
|
|
|
110
|
+
**Severity rule**: CRITICAL/HIGH findings, any confidence, are never routed as pre-existing by the `/review-pr` introduced-line gate (§6B.4): they stay blocking `review-finding` issues. The `**Scope**` tag does not gate routing.
|
|
111
|
+
|
|
98
112
|
### 6. FALSE POSITIVE PREVENTION
|
|
99
113
|
|
|
100
114
|
**Before claiming variable scope issues:** Read the FULL function, count indentation levels, check if/else structure.
|
|
@@ -120,7 +134,7 @@ Every finding must include:
|
|
|
120
134
|
|
|
121
135
|
## Structured Findings Protocol
|
|
122
136
|
|
|
123
|
-
**All review agents MUST include a machine-readable findings block at the end of their PR comment.** This is NON-OPTIONAL. Without structured findings, the review system cannot
|
|
137
|
+
**All review agents MUST include a machine-readable findings block at the end of their PR comment.** This is NON-OPTIONAL. Without structured findings, the review system cannot classify, file, or track what you found. The orchestrator (`/review-pr` §6B.4/§6B.5) decides disposition: CONFIRMED/LIKELY findings that pass its gates become issues, while LOW severity and POSSIBLE confidence findings are non-blocking NOTEs that are not filed as standalone issues.
|
|
124
138
|
|
|
125
139
|
### Persist Before Post
|
|
126
140
|
|
|
@@ -138,7 +152,7 @@ Append this block at the very end of your comment (after the `---` footer line,
|
|
|
138
152
|
|
|
139
153
|
### Rules
|
|
140
154
|
|
|
141
|
-
1. **Include
|
|
155
|
+
1. **Include every finding you can support at CONFIRMED, LIKELY, or POSSIBLE confidence in the block** — the orchestrator needs them all to classify. Inclusion does not mean a GitHub issue is filed: `/review-pr` §6B.4/§6B.5 decide filing. **POSSIBLE confidence and LOW severity findings are non-blocking NOTEs** — reported in the review, never filed as standalone issues, never block merge. CONFIRMED and LIKELY findings are blocking at P1/P2 respectively. Do not pad the block with unverified or speculative items; precision over recall.
|
|
142
156
|
2. **One line per finding** — sequential numbering (PREFIX-1, PREFIX-2, ...)
|
|
143
157
|
3. **Confidence**: `CONFIRMED`, `LIKELY`, or `POSSIBLE`
|
|
144
158
|
4. **Severity**: `CRITICAL`, `HIGH`, `MEDIUM`, or `LOW`
|
|
@@ -148,6 +162,7 @@ Append this block at the very end of your comment (after the `---` footer line,
|
|
|
148
162
|
8. **HTML comments**: The block is invisible in rendered markdown but parseable by the review system
|
|
149
163
|
9. **Agent marker**: Include exactly one `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker in the persisted body, where `{domain}` is the marker domain the orchestrator passes you (the canonical value from the roster-to-marker table in `review-pr.md`, e.g. `infra` and `scraper`; never a lowercased display name such as `infrastructure` or `scraping`).
|
|
150
164
|
10. **Reviewed SHA**: Include exactly one line `Reviewed-SHA: [REVIEW_SHA]` (the full 40-hex head you were pinned to) in the persisted body, outside the marker. `/review-pr` scopes its completion wait and idempotent re-entry to this line, so a comment without the current head does not count as a completed review. Do NOT put the SHA inside the `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker.
|
|
165
|
+
11. **Failure scenario and scope (provenance inputs)**: every finding's body section (the prose above the findings block) MUST carry a `**Failure scenario**` line — concrete inputs leading to a wrong output or behaviour — and a `**Scope**: changed` or `**Scope**: pre-existing` line (`changed` = the cited line is added or modified by this PR; `pre-existing` = it was already there). `/review-pr` §6B.4 reads these: a finding without a failure scenario is demoted to a note (unless CRITICAL/HIGH, any confidence), and a MEDIUM or lower finding whose cited line is not in the PR diff is routed as pre-existing (CRITICAL/HIGH never are; see §5). The `**Scope**` tag is informational and never gates routing. Speculative hardening with no failure scenario is a note, not a finding. The one-line `<!-- FINDING:... -->` grammar above is unchanged. <!-- Added: forge#3452 -->
|
|
151
166
|
|
|
152
167
|
### Domain Prefixes
|
|
153
168
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forgedock",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "Autonomous development pipeline for coding agents. GitHub as institutional memory, with native Claude Code and OpenCode entrypoints.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"forgedock": "bin/forgedock.mjs"
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
],
|
|
49
49
|
"scripts": {
|
|
50
50
|
"test": "node --test \"bin/tests/**/*.test.mjs\" \"bin/engine/**/*.test.mjs\" \"packages/protocol/test/**/*.test.mjs\"",
|
|
51
|
+
"test:sh": "bash scripts/run-shell-tests.sh",
|
|
51
52
|
"docs:dev": "vitepress dev docs/site",
|
|
52
53
|
"docs:build": "vitepress build docs/site",
|
|
53
54
|
"docs:preview": "vitepress preview docs/site"
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# check-blast-radius.sh — Enforce the FORGE:ARCHITECT blast-radius manifest against the final tree.
|
|
6
|
+
#
|
|
7
|
+
# Usage: check-blast-radius.sh --manifest <file> --base <ref> [--repo <path>]
|
|
8
|
+
#
|
|
9
|
+
# <file> is either the extracted manifest block or a whole FORGE:ARCHITECT comment body. Only the text between
|
|
10
|
+
# <!-- FORGE:BLAST_RADIUS:BEGIN --> and <!-- FORGE:BLAST_RADIUS:END --> is parsed (all such regions, concatenated).
|
|
11
|
+
# Grammar, one record per line (blank lines and markdown code-fence lines are the only other lines allowed):
|
|
12
|
+
# SYMBOL: id=<sN> kind=function|field|flag|schema|contract name=<symbol> query="<fixed-string>"
|
|
13
|
+
# HIT: symbol=<sN> file=<path> role=producer|consumer|sibling disposition=change
|
|
14
|
+
# HIT: symbol=<sN> file=<path> role=producer|consumer|sibling disposition=verified-unaffected reason=<one-token>
|
|
15
|
+
# Values contain no spaces (use hyphens in reason). query must match ^[A-Za-z0-9_.:/-]{3,80}$ and not start with "-".
|
|
16
|
+
#
|
|
17
|
+
# For every SYMBOL the query is re-run as a fixed-string search over the final tree (working tree, tracked plus
|
|
18
|
+
# untracked files, so it works before and after the commit). A file that matches must be in the change set
|
|
19
|
+
# (base...HEAD plus anything staged, modified or untracked) or carry a verified-unaffected row.
|
|
20
|
+
# A "change" row whose file is not in the change set is a planned-but-not-done item.
|
|
21
|
+
#
|
|
22
|
+
# Exit codes:
|
|
23
|
+
# 0 covered, or no manifest ("SKIP: no manifest")
|
|
24
|
+
# 1 UNLISTED: <symbol> <file> and/or NOT_DONE: <symbol> <file>
|
|
25
|
+
# 2 malformed manifest, invalid query, unresolvable base/repo, or a git failure — NEVER a pass; callers report DEGRADED
|
|
26
|
+
#
|
|
27
|
+
# Offline: no network, no gh. Portable to bash 3.2: no mapfile, no associative arrays, no sort -V.
|
|
28
|
+
|
|
29
|
+
set -uo pipefail
|
|
30
|
+
|
|
31
|
+
MAX_HITS=200
|
|
32
|
+
MANIFEST=""; BASE=""; REPO="."
|
|
33
|
+
die2() { echo "check-blast-radius: $*" >&2; exit 2; }
|
|
34
|
+
|
|
35
|
+
while [ "$#" -gt 0 ]; do
|
|
36
|
+
case "$1" in
|
|
37
|
+
--manifest) [ "$#" -ge 2 ] || die2 "--manifest needs a value"; MANIFEST="$2"; shift 2 ;;
|
|
38
|
+
--base) [ "$#" -ge 2 ] || die2 "--base needs a value"; BASE="$2"; shift 2 ;;
|
|
39
|
+
--repo) [ "$#" -ge 2 ] || die2 "--repo needs a value"; REPO="$2"; shift 2 ;;
|
|
40
|
+
*) die2 "usage: check-blast-radius.sh --manifest <file> --base <ref> [--repo <path>]" ;;
|
|
41
|
+
esac
|
|
42
|
+
done
|
|
43
|
+
[ -n "$MANIFEST" ] || die2 "usage: check-blast-radius.sh --manifest <file> --base <ref> [--repo <path>]"
|
|
44
|
+
[ -f "$MANIFEST" ] || die2 "manifest file not found: $MANIFEST"
|
|
45
|
+
|
|
46
|
+
TMP=$(mktemp -d "${TMPDIR:-/tmp}/check-blast-radius.XXXXXX") || die2 "mktemp failed"
|
|
47
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
48
|
+
|
|
49
|
+
# ---- extract BEGIN..END regions -------------------------------------------------------------------------
|
|
50
|
+
awk -v out="$TMP/region" '
|
|
51
|
+
index($0, "<!-- FORGE:BLAST_RADIUS:BEGIN -->") { if (inb) { bad = 1 } inb = 1; seen = 1; next }
|
|
52
|
+
index($0, "<!-- FORGE:BLAST_RADIUS:END -->") { if (!inb) { bad = 1 } inb = 0; next }
|
|
53
|
+
inb { print > out }
|
|
54
|
+
END { if (inb || bad) exit 3; if (!seen) exit 4 }
|
|
55
|
+
' "$MANIFEST"
|
|
56
|
+
case $? in
|
|
57
|
+
0) ;;
|
|
58
|
+
4) echo "SKIP: no manifest"; exit 0 ;;
|
|
59
|
+
*) die2 "unbalanced FORGE:BLAST_RADIUS:BEGIN/END markers" ;;
|
|
60
|
+
esac
|
|
61
|
+
[ -f "$TMP/region" ] || { echo "SKIP: no manifest"; exit 0; }
|
|
62
|
+
|
|
63
|
+
# has_symbol <id>: awk over the file (no grep -q in a pipeline: SIGPIPE under pipefail misreports the status)
|
|
64
|
+
has_symbol() { awk -F'\t' -v s="$1" '$1==s {ok=1} END{exit ok?0:1}' "$TMP/symbols"; }
|
|
65
|
+
|
|
66
|
+
# ---- parse ----------------------------------------------------------------------------------------------
|
|
67
|
+
: > "$TMP/symbols" # id<TAB>name<TAB>query
|
|
68
|
+
: > "$TMP/hits" # symbol<TAB>file<TAB>disposition
|
|
69
|
+
QRE='^[A-Za-z0-9_.:/-]{3,80}$'
|
|
70
|
+
FRE='^[A-Za-z0-9_./@+=:,-]+$'
|
|
71
|
+
LN=0
|
|
72
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
73
|
+
LN=$((LN+1))
|
|
74
|
+
line="${line%$'\r'}"
|
|
75
|
+
trimmed="${line#"${line%%[![:space:]]*}"}"
|
|
76
|
+
case "$trimmed" in
|
|
77
|
+
""|'```'*) continue ;;
|
|
78
|
+
"SYMBOL: "*) kind_rec=SYMBOL; rest="${trimmed#SYMBOL: }" ;;
|
|
79
|
+
"HIT: "*) kind_rec=HIT; rest="${trimmed#HIT: }" ;;
|
|
80
|
+
*) die2 "line $LN: unrecognised record: $trimmed" ;;
|
|
81
|
+
esac
|
|
82
|
+
id=""; kind=""; name=""; query=""; symbol=""; file=""; role=""; disp=""; reason=""
|
|
83
|
+
set -f
|
|
84
|
+
for tok in $rest; do
|
|
85
|
+
case "$tok" in
|
|
86
|
+
*=*) k="${tok%%=*}"; v="${tok#*=}" ;;
|
|
87
|
+
*) set +f; die2 "line $LN: token without key=value: $tok" ;;
|
|
88
|
+
esac
|
|
89
|
+
case "$k" in
|
|
90
|
+
id) id="$v" ;; kind) kind="$v" ;; name) name="$v" ;;
|
|
91
|
+
query) v="${v#\"}"; v="${v%\"}"; query="$v" ;;
|
|
92
|
+
symbol) symbol="$v" ;; file) file="$v" ;; role) role="$v" ;;
|
|
93
|
+
disposition) disp="$v" ;; reason) reason="$v" ;;
|
|
94
|
+
*) set +f; die2 "line $LN: unknown key: $k" ;;
|
|
95
|
+
esac
|
|
96
|
+
done
|
|
97
|
+
set +f
|
|
98
|
+
if [ "$kind_rec" = SYMBOL ]; then
|
|
99
|
+
case "$id" in s[0-9]*) ;; *) die2 "line $LN: SYMBOL id must look like s1: '$id'" ;; esac
|
|
100
|
+
case "$id" in *[!A-Za-z0-9]*) die2 "line $LN: invalid SYMBOL id: '$id'" ;; esac
|
|
101
|
+
case "$kind" in function|field|flag|schema|contract) ;; *) die2 "line $LN: invalid kind: '$kind'" ;; esac
|
|
102
|
+
[ -n "$name" ] || die2 "line $LN: SYMBOL without name"
|
|
103
|
+
[ -n "$query" ] || die2 "line $LN: empty query"
|
|
104
|
+
case "$query" in -*) die2 "line $LN: query must not start with '-'" ;; esac
|
|
105
|
+
[[ "$query" =~ $QRE ]] || die2 "line $LN: invalid query (want $QRE): $query"
|
|
106
|
+
has_symbol "$id" && die2 "line $LN: duplicate SYMBOL id: $id"
|
|
107
|
+
printf '%s\t%s\t%s\n' "$id" "$name" "$query" >> "$TMP/symbols"
|
|
108
|
+
else
|
|
109
|
+
[ -n "$symbol" ] || die2 "line $LN: HIT without symbol"
|
|
110
|
+
has_symbol "$symbol" || die2 "line $LN: HIT references unknown symbol: $symbol"
|
|
111
|
+
[ -n "$file" ] || die2 "line $LN: HIT without file"
|
|
112
|
+
case "$file" in -*) die2 "line $LN: file must not start with '-'" ;; esac
|
|
113
|
+
[[ "$file" =~ $FRE ]] || die2 "line $LN: invalid file path: $file"
|
|
114
|
+
case "$role" in producer|consumer|sibling) ;; *) die2 "line $LN: invalid role: '$role'" ;; esac
|
|
115
|
+
case "$disp" in
|
|
116
|
+
change) ;;
|
|
117
|
+
verified-unaffected) [ -n "$reason" ] || die2 "line $LN: verified-unaffected requires reason=<token>" ;;
|
|
118
|
+
*) die2 "line $LN: invalid disposition: '$disp'" ;;
|
|
119
|
+
esac
|
|
120
|
+
printf '%s\t%s\t%s\n' "$symbol" "$file" "$disp" >> "$TMP/hits"
|
|
121
|
+
fi
|
|
122
|
+
done < "$TMP/region"
|
|
123
|
+
|
|
124
|
+
if [ ! -s "$TMP/symbols" ]; then
|
|
125
|
+
if [ -s "$TMP/hits" ]; then die2 "HIT records without any SYMBOL"; fi
|
|
126
|
+
echo "SKIP: no manifest"; exit 0
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# ---- repo, base, change set -----------------------------------------------------------------------------
|
|
130
|
+
case "$BASE" in ""|-*) die2 "invalid --base: '$BASE'" ;; esac
|
|
131
|
+
git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1 || die2 "not a git work tree: $REPO"
|
|
132
|
+
git -C "$REPO" rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null 2>&1 || die2 "cannot resolve base ref '$BASE'"
|
|
133
|
+
|
|
134
|
+
: > "$TMP/changed"
|
|
135
|
+
git -C "$REPO" diff -z --name-only "${BASE}...HEAD" -- > "$TMP/c1" 2>/dev/null || die2 "git diff ${BASE}...HEAD failed"
|
|
136
|
+
git -C "$REPO" diff -z --name-only HEAD -- > "$TMP/c2" 2>/dev/null || die2 "git diff HEAD failed"
|
|
137
|
+
git -C "$REPO" ls-files -z --others --exclude-standard > "$TMP/c3" 2>/dev/null || die2 "git ls-files failed"
|
|
138
|
+
cat "$TMP/c1" "$TMP/c2" "$TMP/c3" | tr '\0' '\n' | sed '/^$/d' | sort -u > "$TMP/changed"
|
|
139
|
+
|
|
140
|
+
# ---- check ----------------------------------------------------------------------------------------------
|
|
141
|
+
RC=0
|
|
142
|
+
while IFS="$(printf '\t')" read -r sid sname squery; do
|
|
143
|
+
git -C "$REPO" grep -z --untracked -l -F -e "$squery" -- > "$TMP/g" 2>"$TMP/gerr"
|
|
144
|
+
grc=$?
|
|
145
|
+
if [ "$grc" -gt 1 ]; then die2 "git grep failed for symbol $sid (rc=$grc): $(head -n 1 "$TMP/gerr")"; fi
|
|
146
|
+
tr '\0' '\n' < "$TMP/g" | sed '/^$/d' | sort -u > "$TMP/found"
|
|
147
|
+
n=$(wc -l < "$TMP/found" | tr -d ' ')
|
|
148
|
+
[ "$n" -le "$MAX_HITS" ] || die2 "query too broad for symbol $sid ($n files > $MAX_HITS): use a more distinctive query"
|
|
149
|
+
while IFS= read -r f; do
|
|
150
|
+
[ -n "$f" ] || continue
|
|
151
|
+
if grep -Fxq -- "$f" "$TMP/changed"; then continue; fi
|
|
152
|
+
if awk -F'\t' -v s="$sid" -v f="$f" '$1==s && $2==f && $3=="verified-unaffected" {ok=1} END{exit ok?0:1}' "$TMP/hits"; then continue; fi
|
|
153
|
+
echo "UNLISTED: $sid $f"; RC=1
|
|
154
|
+
done < "$TMP/found"
|
|
155
|
+
# planned-but-not-done: change rows for this symbol whose file is not in the change set
|
|
156
|
+
while IFS="$(printf '\t')" read -r hs hf hd; do
|
|
157
|
+
[ "$hs" = "$sid" ] && [ "$hd" = change ] || continue
|
|
158
|
+
grep -Fxq -- "$hf" "$TMP/changed" || { echo "NOT_DONE: $sid $hf"; RC=1; }
|
|
159
|
+
done < "$TMP/hits"
|
|
160
|
+
done < "$TMP/symbols"
|
|
161
|
+
|
|
162
|
+
# HIT rows whose symbol exists were validated at parse time; a manifest that covers every hit passes.
|
|
163
|
+
[ "$RC" -eq 0 ] && echo "OK: blast-radius manifest covered ($(wc -l < "$TMP/symbols" | tr -d ' ') symbol(s))"
|
|
164
|
+
exit "$RC"
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# Fixtures for scripts/check-blast-radius.sh. Run: bash scripts/check-blast-radius.test.sh (bash 3.2 compatible)
|
|
6
|
+
|
|
7
|
+
set -u
|
|
8
|
+
DIR=$(cd "$(dirname "$0")" && pwd)
|
|
9
|
+
S="$DIR/check-blast-radius.sh"
|
|
10
|
+
T=$(mktemp -d "${TMPDIR:-/tmp}/check-blast-radius-test.XXXXXX")
|
|
11
|
+
trap 'rm -rf "$T"' EXIT
|
|
12
|
+
pass=0; fail=0
|
|
13
|
+
OUT=""; RC=0
|
|
14
|
+
|
|
15
|
+
g() { git -C "$T/r" -c user.name=t -c user.email=t@example.com -c commit.gpgsign=false "$@" >/dev/null 2>&1; }
|
|
16
|
+
run() { OUT=$(bash "$S" --manifest "$T/m" --base staging --repo "$T/r" 2>&1); RC=$?; }
|
|
17
|
+
manifest() { { echo '<!-- FORGE:BLAST_RADIUS:BEGIN -->'; cat; echo '<!-- FORGE:BLAST_RADIUS:END -->'; } > "$T/m"; }
|
|
18
|
+
expect() { # expect <want_rc> <desc> [want_substring]
|
|
19
|
+
if [ "$RC" = "$1" ] && { [ -z "${3:-}" ] || case "$OUT" in *"$3"*) true ;; *) false ;; esac; }; then
|
|
20
|
+
pass=$((pass+1))
|
|
21
|
+
else
|
|
22
|
+
fail=$((fail+1)); echo "FAIL: $2 (rc=$RC, want $1)"; printf '%s\n' "$OUT" | sed 's/^/ /'
|
|
23
|
+
fi
|
|
24
|
+
}
|
|
25
|
+
expect_absent() { # expect_absent <desc> <substring>
|
|
26
|
+
case "$OUT" in *"$2"*) fail=$((fail+1)); echo "FAIL: $1 (unexpected '$2')"; printf '%s\n' "$OUT" | sed 's/^/ /' ;; *) pass=$((pass+1)) ;; esac
|
|
27
|
+
}
|
|
28
|
+
reset_branch() { g checkout -q -f staging; g branch -q -D feat; g checkout -q -b feat; }
|
|
29
|
+
|
|
30
|
+
mkdir "$T/r"; git -C "$T/r" init -q -b staging >/dev/null 2>&1 || { git -C "$T/r" init -q; g checkout -b staging; }
|
|
31
|
+
mkdir -p "$T/r/routers" "$T/r/docs"
|
|
32
|
+
for f in a b c; do printf 'def handler():\n use(billing_principal)\n' > "$T/r/routers/$f.py"; done
|
|
33
|
+
echo 'notes about nothing' > "$T/r/docs/readme.md"
|
|
34
|
+
g add -A; g commit -m base
|
|
35
|
+
g checkout -q -b feat
|
|
36
|
+
|
|
37
|
+
SYM='SYMBOL: id=s1 kind=field name=billing_principal query="billing_principal"'
|
|
38
|
+
|
|
39
|
+
# (a) 3 sibling callers, 1 changed, 2 unlisted -> FAIL naming both unlisted files
|
|
40
|
+
echo 'use(billing_principal, new)' >> "$T/r/routers/a.py"; g add -A; g commit -m "change a"
|
|
41
|
+
manifest <<M
|
|
42
|
+
$SYM
|
|
43
|
+
HIT: symbol=s1 file=routers/a.py role=consumer disposition=change
|
|
44
|
+
M
|
|
45
|
+
run; expect 1 "3 siblings, 1 changed, 2 unlisted fails" "UNLISTED: s1 routers/b.py"
|
|
46
|
+
case "$OUT" in *"UNLISTED: s1 routers/c.py"*) pass=$((pass+1)) ;; *) fail=$((fail+1)); echo "FAIL: second unlisted sibling not named"; echo "$OUT" ;; esac
|
|
47
|
+
expect_absent "changed file is not unlisted" "UNLISTED: s1 routers/a.py"
|
|
48
|
+
|
|
49
|
+
# (b) all 3 changed -> PASS
|
|
50
|
+
echo 'use(billing_principal, new)' >> "$T/r/routers/b.py"; echo 'use(billing_principal, new)' >> "$T/r/routers/c.py"
|
|
51
|
+
g add -A; g commit -m "change b c"
|
|
52
|
+
manifest <<M
|
|
53
|
+
$SYM
|
|
54
|
+
HIT: symbol=s1 file=routers/a.py role=consumer disposition=change
|
|
55
|
+
M
|
|
56
|
+
run; expect 0 "all siblings changed passes (changed files need no row)" "OK:"
|
|
57
|
+
|
|
58
|
+
# (c) 1 changed + 2 verified-unaffected -> PASS
|
|
59
|
+
reset_branch; echo 'use(billing_principal, new)' >> "$T/r/routers/a.py"; g add -A; g commit -m "change a"
|
|
60
|
+
manifest <<M
|
|
61
|
+
$SYM
|
|
62
|
+
HIT: symbol=s1 file=routers/a.py role=consumer disposition=change
|
|
63
|
+
HIT: symbol=s1 file=routers/b.py role=sibling disposition=verified-unaffected reason=reads-only-never-writes
|
|
64
|
+
HIT: symbol=s1 file=routers/c.py role=sibling disposition=verified-unaffected reason=dead-code-path
|
|
65
|
+
M
|
|
66
|
+
run; expect 0 "verified-unaffected siblings pass" "OK:"
|
|
67
|
+
|
|
68
|
+
# (c2) staged but uncommitted and untracked changes count (validate runs before the commit)
|
|
69
|
+
reset_branch; echo 'use(billing_principal, new)' >> "$T/r/routers/a.py"; echo 'use(billing_principal, new)' >> "$T/r/routers/b.py"
|
|
70
|
+
g add routers/a.py; echo 'billing_principal = 1' > "$T/r/routers/d.py"
|
|
71
|
+
manifest <<M
|
|
72
|
+
$SYM
|
|
73
|
+
M
|
|
74
|
+
run; expect 1 "uncommitted: c.py still unlisted" "UNLISTED: s1 routers/c.py"
|
|
75
|
+
expect_absent "staged change counts" "UNLISTED: s1 routers/a.py"
|
|
76
|
+
expect_absent "unstaged tracked change counts" "UNLISTED: s1 routers/b.py"
|
|
77
|
+
expect_absent "untracked new file counts" "UNLISTED: s1 routers/d.py"
|
|
78
|
+
g checkout -q -f staging; rm -f "$T/r/routers/d.py"; g checkout -q -f feat; rm -f "$T/r/routers/d.py"; g reset -q --hard
|
|
79
|
+
|
|
80
|
+
# (d) change row whose file is not in the diff -> FAIL (planned but not done)
|
|
81
|
+
reset_branch; echo 'use(billing_principal, new)' >> "$T/r/routers/a.py"; g add -A; g commit -m "change a"
|
|
82
|
+
manifest <<M
|
|
83
|
+
$SYM
|
|
84
|
+
HIT: symbol=s1 file=routers/a.py role=consumer disposition=change
|
|
85
|
+
HIT: symbol=s1 file=docs/readme.md role=consumer disposition=change
|
|
86
|
+
HIT: symbol=s1 file=routers/b.py role=sibling disposition=verified-unaffected reason=x-reason
|
|
87
|
+
HIT: symbol=s1 file=routers/c.py role=sibling disposition=verified-unaffected reason=x-reason
|
|
88
|
+
M
|
|
89
|
+
run; expect 1 "planned-but-not-done change row fails" "NOT_DONE: s1 docs/readme.md"
|
|
90
|
+
|
|
91
|
+
# (e) rc 2 cases
|
|
92
|
+
manifest <<M
|
|
93
|
+
SYMBOL: id=s1 kind=field name=x query="-rf"
|
|
94
|
+
M
|
|
95
|
+
run; expect 2 "leading dash query rejected"
|
|
96
|
+
manifest <<M
|
|
97
|
+
SYMBOL: id=s1 kind=field name=x query=""
|
|
98
|
+
M
|
|
99
|
+
run; expect 2 "empty query rejected"
|
|
100
|
+
manifest <<M
|
|
101
|
+
SYMBOL: id=s1 kind=field name=x query="ab"
|
|
102
|
+
M
|
|
103
|
+
run; expect 2 "too-short query rejected"
|
|
104
|
+
manifest <<M
|
|
105
|
+
SYMBOL: id=s1 kind=field name=x query="foo bar baz"
|
|
106
|
+
M
|
|
107
|
+
run; expect 2 "query with spaces rejected"
|
|
108
|
+
manifest <<M
|
|
109
|
+
SYMBOL: id=s1 kind=field name=x query="a;rm -rf"
|
|
110
|
+
M
|
|
111
|
+
run; expect 2 "query with shell metacharacters rejected"
|
|
112
|
+
manifest <<M
|
|
113
|
+
$SYM
|
|
114
|
+
this is not a record
|
|
115
|
+
M
|
|
116
|
+
run; expect 2 "malformed line rejected"
|
|
117
|
+
manifest <<M
|
|
118
|
+
$SYM
|
|
119
|
+
HIT: symbol=s1 file=routers/b.py role=sibling disposition=verified-unaffected
|
|
120
|
+
M
|
|
121
|
+
run; expect 2 "verified-unaffected without reason rejected"
|
|
122
|
+
manifest <<M
|
|
123
|
+
$SYM
|
|
124
|
+
HIT: symbol=s9 file=routers/b.py role=sibling disposition=change
|
|
125
|
+
M
|
|
126
|
+
run; expect 2 "HIT with unknown symbol rejected"
|
|
127
|
+
manifest <<M
|
|
128
|
+
$SYM
|
|
129
|
+
HIT: symbol=s1 file=routers/b.py role=sibling disposition=maybe
|
|
130
|
+
M
|
|
131
|
+
run; expect 2 "invalid disposition rejected"
|
|
132
|
+
manifest <<M
|
|
133
|
+
$SYM
|
|
134
|
+
$SYM
|
|
135
|
+
M
|
|
136
|
+
run; expect 2 "duplicate symbol id rejected"
|
|
137
|
+
printf '<!-- FORGE:BLAST_RADIUS:BEGIN -->\n%s\n' "$SYM" > "$T/m"
|
|
138
|
+
run; expect 2 "unterminated BEGIN rejected"
|
|
139
|
+
manifest <<M
|
|
140
|
+
$SYM
|
|
141
|
+
M
|
|
142
|
+
OUT=$(bash "$S" --manifest "$T/m" --base no-such-ref --repo "$T/r" 2>&1); RC=$?; expect 2 "bad base ref is rc 2, not a pass"
|
|
143
|
+
mkdir "$T/notgit"
|
|
144
|
+
OUT=$(bash "$S" --manifest "$T/m" --base staging --repo "$T/notgit" 2>&1); RC=$?; expect 2 "non-git repo is rc 2, not a pass"
|
|
145
|
+
OUT=$(bash "$S" --manifest "$T/missing" --base staging --repo "$T/r" 2>&1); RC=$?; expect 2 "missing manifest file is rc 2"
|
|
146
|
+
OUT=$(bash "$S" --base staging 2>&1); RC=$?; expect 2 "missing --manifest is rc 2"
|
|
147
|
+
|
|
148
|
+
# (f) absent / empty manifest -> SKIP rc 0
|
|
149
|
+
: > "$T/m"; run; expect 0 "empty manifest skips" "SKIP: no manifest"
|
|
150
|
+
printf '## Implementation Plan\nno block here\n' > "$T/m"; run; expect 0 "no BEGIN/END block skips" "SKIP: no manifest"
|
|
151
|
+
printf '## Plan\nprose\n<!-- FORGE:BLAST_RADIUS:BEGIN -->\n%s\nHIT: symbol=s1 file=routers/a.py role=consumer disposition=change\n<!-- FORGE:BLAST_RADIUS:END -->\ntrailing prose\n<!-- FORGE:ARCHITECT:COMPLETE -->\n' "$SYM" > "$T/m"
|
|
152
|
+
run; expect 1 "whole comment body is accepted, prose outside the block ignored" "UNLISTED: s1 routers/b.py"
|
|
153
|
+
|
|
154
|
+
# fixed-string, not regex: a query with metacharacter-like text must match literally
|
|
155
|
+
g checkout -q -f staging; echo 'a.b(c)' > "$T/r/docs/regex.txt"; echo 'aXb(c)' > "$T/r/docs/other.txt"; g add -A; g commit -m regex
|
|
156
|
+
g branch -q -D feat; g checkout -q -b feat
|
|
157
|
+
manifest <<M
|
|
158
|
+
SYMBOL: id=s1 kind=function name=dot query="a.b"
|
|
159
|
+
M
|
|
160
|
+
run; expect 1 "fixed-string match lists the literal hit" "UNLISTED"
|
|
161
|
+
expect_absent "regex-style wildcard does not match aXb" "other.txt"
|
|
162
|
+
HIT_OK='HIT: symbol=s1 file=docs/regex.txt role=producer disposition=verified-unaffected reason=literal-only'
|
|
163
|
+
manifest <<M
|
|
164
|
+
SYMBOL: id=s1 kind=function name=dot query="a.b"
|
|
165
|
+
$HIT_OK
|
|
166
|
+
M
|
|
167
|
+
run; expect 0 "literal hit covered by verified-unaffected" "OK:"
|
|
168
|
+
|
|
169
|
+
# zero-hit symbol passes
|
|
170
|
+
manifest <<M
|
|
171
|
+
SYMBOL: id=s1 kind=flag name=nothing query="zz_never_present_zz"
|
|
172
|
+
M
|
|
173
|
+
run; expect 0 "zero-hit symbol passes" "OK:"
|
|
174
|
+
|
|
175
|
+
# query too broad -> rc 2
|
|
176
|
+
reset_branch; mkdir -p "$T/r/many"; i=0; while [ "$i" -lt 205 ]; do echo broadtoken > "$T/r/many/f$i.txt"; i=$((i+1)); done; g add -A; g commit -m many
|
|
177
|
+
manifest <<M
|
|
178
|
+
SYMBOL: id=s1 kind=flag name=broad query="broadtoken"
|
|
179
|
+
M
|
|
180
|
+
run; expect 2 "over-broad query is rc 2" "too broad"
|
|
181
|
+
|
|
182
|
+
echo "check-blast-radius.test.sh: $pass passed, $fail failed"
|
|
183
|
+
[ "$fail" -eq 0 ]
|