forgedock 1.0.17 → 1.0.19
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/README.md +34 -1
- package/bin/cross-publish.mjs +344 -0
- package/bin/forgedock.mjs +229 -17
- package/commands/autopilot.md +77 -0
- package/commands/design-bench.md +176 -0
- package/commands/design-render-critique-loop.md +103 -0
- package/commands/design.md +259 -0
- package/commands/issue.md +7 -3
- package/commands/orchestrate.md +121 -0
- package/commands/{resume.md → pipeline-resume.md} +4 -4
- package/commands/{status.md → pipeline-status.md} +1 -1
- package/commands/qa-sweep.md +37 -2
- package/commands/quality-gate.md +57 -0
- package/commands/review-pr-staging.md +158 -2
- package/commands/test-gate.md +929 -0
- package/commands/work-on/build/implement.md +5 -3
- package/commands/work-on.md +53 -3
- package/docs/CI.md +177 -0
- package/docs/FORGE-PROTOCOL.md +324 -0
- package/docs/articles/b-vs-c-baseline.md +210 -0
- package/docs/articles/full-abc-benchmark.md +205 -0
- package/docs/articles/selective-spec-loading-token-savings.md +140 -0
- package/docs/design/abc-benchmark.md +195 -0
- package/docs/design/design-architect-rationale.md +123 -0
- package/docs/design/design-memory.md +61 -0
- package/docs/design/design-spec-schema.md +248 -0
- package/docs/design/design-system-lint.md +150 -0
- package/docs/design/divergent-generation.md +106 -0
- package/docs/design/effects-appropriateness.md +133 -0
- package/docs/design/fixtures/briefs/README.md +47 -0
- package/docs/design/fixtures/briefs/cadence.md +39 -0
- package/docs/design/fixtures/briefs/plume.md +39 -0
- package/docs/design/fixtures/briefs/slipstream.md +39 -0
- package/docs/design/fixtures/briefs/tender.md +39 -0
- package/docs/design/fixtures/briefs/voltage.md +39 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-1.html +364 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-2.html +420 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-3.html +506 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-1.html +611 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-2.html +594 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-3.html +817 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/runs.json +175 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/scorecard.json +138 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-1.html +373 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-2.html +398 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-3.html +595 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-1.html +531 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-2.html +565 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-3.html +674 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-1.html +388 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-2.html +463 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-3.html +568 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-1.html +562 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-2.html +618 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-3.html +618 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-1.html +364 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-2.html +420 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-3.html +506 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-1.html +527 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-2.html +445 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-3.html +483 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-1.html +611 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-2.html +594 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-3.html +817 -0
- package/docs/design/fixtures/runs/full-abc/runs.json +190 -0
- package/docs/design/fixtures/runs/full-abc/scorecard.json +144 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-1.html +496 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-2.html +447 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-3.html +564 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-1.html +373 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-2.html +398 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-3.html +595 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-1.html +622 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-2.html +475 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-3.html +660 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-1.html +531 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-2.html +565 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-3.html +674 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-1.html +525 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-2.html +446 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-3.html +463 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-1.html +388 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-2.html +463 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-3.html +568 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-1.html +1068 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-2.html +1423 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-3.html +1245 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-1.html +305 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-2.html +327 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-3.html +428 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-1.html +1270 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-2.html +1172 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-3.html +1532 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-1.html +315 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-2.html +342 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-3.html +353 -0
- package/docs/design/fixtures/runs/full-abc-opus/runs.json +190 -0
- package/docs/design/fixtures/runs/full-abc-opus/scorecard.json +144 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-1.html +345 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-2.html +396 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-3.html +502 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-1.html +408 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-2.html +446 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-3.html +496 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-1.html +813 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-2.html +711 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-3.html +721 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-1.html +280 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-2.html +291 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-3.html +366 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-1.html +1010 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-2.html +836 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-3.html +930 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-1.html +407 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-2.html +396 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-3.html +529 -0
- package/docs/design/foundations/brutalist.css +294 -0
- package/docs/design/foundations/editorial.css +300 -0
- package/docs/design/foundations/minimal-luxury.css +295 -0
- package/docs/design/foundations/technical.css +293 -0
- package/docs/design/foundations/warm-photo.css +299 -0
- package/docs/design/reference-corpus.md +1726 -0
- package/docs/design/render-critique-loop.md +57 -0
- package/docs/spec-graph-schema.md +397 -0
- package/package.json +1 -1
- package/scripts/bench-scorecard.mjs +239 -0
- package/scripts/build-spec-graph.mjs +509 -0
- package/scripts/check-native-conflicts.sh +200 -0
- package/scripts/classify-lane.sh +0 -7
- package/scripts/design-system-lint.mjs +993 -0
- package/scripts/graph-query.sh +486 -0
- package/scripts/transition-label.sh +10 -2
- package/scripts/validate-spec-graph.sh +423 -0
- package/templates/workflows/forgedock-review.yml +96 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# validate-spec-graph.sh — Self-consistency validator over the Spec Knowledge Graph
|
|
6
|
+
#
|
|
7
|
+
# Consumes the spec knowledge graph produced by `build-spec-graph.mjs`
|
|
8
|
+
# (`.forgedock/graph/spec-graph.json`) and reports drift that is invisible in a
|
|
9
|
+
# ~1.2 MB prose spec corpus. Three classes of inconsistency are checked:
|
|
10
|
+
#
|
|
11
|
+
# 1. Orphan annotations (SOFT — warn)
|
|
12
|
+
# FORGE:* markers that are WRITTEN but never READ, or READ but never
|
|
13
|
+
# WRITTEN. These are usually intentional (a one-sided handshake) but
|
|
14
|
+
# worth a periodic triage. Isolated inline doc-scaffolding markers
|
|
15
|
+
# (e.g. FORGE:DISPATCHER, FORGE:PHASE_COMPLETE) are reported as INFO
|
|
16
|
+
# only — they are prose annotations, never posted via `gh comment`.
|
|
17
|
+
#
|
|
18
|
+
# 2. Dangling cross-refs (HARD — fail)
|
|
19
|
+
# A spec invokes `Skill(skill="X")` where X does not resolve to a real
|
|
20
|
+
# command/sub-phase node. The graph builder silently DROPS unresolved
|
|
21
|
+
# Skill/script/devdoc references (it only emits an edge when the target
|
|
22
|
+
# exists), so this check scans `commands/**/*.md` directly rather than
|
|
23
|
+
# querying graph edges.
|
|
24
|
+
#
|
|
25
|
+
# 3. Broken label transitions (HARD — fail)
|
|
26
|
+
# A `workflow:*` label node that is set by NO command (no incoming
|
|
27
|
+
# TRANSITIONS edge from any `--add-label`). Also surfaces (as INFO) the
|
|
28
|
+
# add/remove asymmetry where a label is `--remove-label`-d but never
|
|
29
|
+
# `--add-label`-ed, which signals a state-machine gap.
|
|
30
|
+
#
|
|
31
|
+
# Exit codes:
|
|
32
|
+
# 0 no HARD inconsistencies (SOFT warnings / INFO may still be printed),
|
|
33
|
+
# or --soft was passed (warn-only mode)
|
|
34
|
+
# 1 one or more HARD inconsistencies (dangling refs, broken transitions),
|
|
35
|
+
# or a usage / dependency error
|
|
36
|
+
#
|
|
37
|
+
# Usage:
|
|
38
|
+
# validate-spec-graph.sh [--soft] [--graph <path>] [--root <dir>] [--quiet] [--json] [-h|--help]
|
|
39
|
+
#
|
|
40
|
+
# Flags:
|
|
41
|
+
# --soft Warn-only mode: report HARD findings but exit 0 anyway.
|
|
42
|
+
# Used when first wiring into CI so the documented baseline
|
|
43
|
+
# orphans/refs do not break the build before triage.
|
|
44
|
+
# --graph <path> Validate a specific graph JSON (default:
|
|
45
|
+
# <repo>/.forgedock/graph/spec-graph.json). If the default is
|
|
46
|
+
# absent (it is gitignored), the graph is auto-built on the
|
|
47
|
+
# fly via build-spec-graph.mjs — no committed graph required.
|
|
48
|
+
# --root <dir> Repo root to scan for Skill() refs and to auto-build from
|
|
49
|
+
# (default: parent of this script's dir).
|
|
50
|
+
# --quiet Suppress the human-readable report; print only the summary
|
|
51
|
+
# line (and set the exit code).
|
|
52
|
+
# --json Emit findings as a single JSON object on stdout instead of
|
|
53
|
+
# the human-readable report.
|
|
54
|
+
# -h | --help Show this help.
|
|
55
|
+
#
|
|
56
|
+
# This is a UNIVERSAL-tier script (ships with the npm package). It is read-only:
|
|
57
|
+
# it never mutates the repo or the committed graph (it only writes a temp graph
|
|
58
|
+
# under $TMPDIR when it must auto-build, removed on exit). See
|
|
59
|
+
# devdocs/project/architecture.md for the scripts-layer precedence model and
|
|
60
|
+
# docs/spec-graph-schema.md for the graph schema.
|
|
61
|
+
|
|
62
|
+
set -euo pipefail
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# Paths & dependency checks
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
69
|
+
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
70
|
+
BUILDER="$SCRIPT_DIR/build-spec-graph.mjs"
|
|
71
|
+
DEFAULT_GRAPH="$REPO_ROOT/.forgedock/graph/spec-graph.json"
|
|
72
|
+
|
|
73
|
+
# Exported so per-repo adaptive scripts can delegate (see architecture.md).
|
|
74
|
+
export FORGEDOCK_SCRIPTS="$SCRIPT_DIR"
|
|
75
|
+
export FORGEDOCK_HOME="$REPO_ROOT"
|
|
76
|
+
|
|
77
|
+
# Inline doc-scaffolding annotations: real markers in the corpus that are used
|
|
78
|
+
# to annotate the spec prose itself (HTML comments inside the spec body), never
|
|
79
|
+
# posted via `gh ... comment --body`. They legitimately have neither WRITES nor
|
|
80
|
+
# READS edges, so they must NOT be flagged as orphans. Reported as INFO only.
|
|
81
|
+
#
|
|
82
|
+
# Keep this list sorted. Add a marker here only if it is a prose-scaffolding
|
|
83
|
+
# annotation by design (a `<!-- FORGE:X -->` that documents the spec, not a
|
|
84
|
+
# pipeline handshake posted to an issue/PR).
|
|
85
|
+
SCAFFOLDING_ANNOTATIONS=(
|
|
86
|
+
"FORGE:ANCESTRY_FAILED"
|
|
87
|
+
"FORGE:DISPATCHER"
|
|
88
|
+
"FORGE:GATE_FAILED"
|
|
89
|
+
"FORGE:JSONL_PARSER_UTIL"
|
|
90
|
+
"FORGE:MILESTONE_INDEX"
|
|
91
|
+
"FORGE:PHASE"
|
|
92
|
+
"FORGE:PHASE_COMPLETE"
|
|
93
|
+
"FORGE:PRIOR_GIST"
|
|
94
|
+
"FORGE:PUSH_BLOCKED"
|
|
95
|
+
"FORGE:PUSH_FAILED"
|
|
96
|
+
"FORGE:REVIEW_STARTED"
|
|
97
|
+
"FORGE:TYPE"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
print_help() {
|
|
101
|
+
# Echo the leading comment block (between the shebang and `set -euo`).
|
|
102
|
+
sed -n '3,/^set -euo pipefail/p' "${BASH_SOURCE[0]}" \
|
|
103
|
+
| sed '$d' \
|
|
104
|
+
| sed 's/^# \{0,1\}//'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
die() {
|
|
108
|
+
echo "ERROR: $*" >&2
|
|
109
|
+
exit 1
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
require_dep() {
|
|
113
|
+
command -v "$1" >/dev/null 2>&1 || die "required dependency '$1' not found on PATH"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
# Argument parsing
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
SOFT=0
|
|
121
|
+
QUIET=0
|
|
122
|
+
JSON=0
|
|
123
|
+
GRAPH_PATH=""
|
|
124
|
+
ROOT_OVERRIDE=""
|
|
125
|
+
|
|
126
|
+
while [ "$#" -gt 0 ]; do
|
|
127
|
+
case "$1" in
|
|
128
|
+
-h|--help)
|
|
129
|
+
print_help
|
|
130
|
+
exit 0
|
|
131
|
+
;;
|
|
132
|
+
--soft)
|
|
133
|
+
SOFT=1
|
|
134
|
+
shift
|
|
135
|
+
;;
|
|
136
|
+
--quiet)
|
|
137
|
+
QUIET=1
|
|
138
|
+
shift
|
|
139
|
+
;;
|
|
140
|
+
--json)
|
|
141
|
+
JSON=1
|
|
142
|
+
shift
|
|
143
|
+
;;
|
|
144
|
+
--graph)
|
|
145
|
+
[ "$#" -ge 2 ] || die "--graph requires a path"
|
|
146
|
+
GRAPH_PATH="$2"
|
|
147
|
+
shift 2
|
|
148
|
+
;;
|
|
149
|
+
--root)
|
|
150
|
+
[ "$#" -ge 2 ] || die "--root requires a path"
|
|
151
|
+
ROOT_OVERRIDE="$2"
|
|
152
|
+
shift 2
|
|
153
|
+
;;
|
|
154
|
+
-*)
|
|
155
|
+
die "unknown flag: $1 (try --help)"
|
|
156
|
+
;;
|
|
157
|
+
*)
|
|
158
|
+
die "unexpected argument: $1 (this validator takes no positionals; try --help)"
|
|
159
|
+
;;
|
|
160
|
+
esac
|
|
161
|
+
done
|
|
162
|
+
|
|
163
|
+
if [ -n "$ROOT_OVERRIDE" ]; then
|
|
164
|
+
REPO_ROOT="$ROOT_OVERRIDE"
|
|
165
|
+
DEFAULT_GRAPH="$REPO_ROOT/.forgedock/graph/spec-graph.json"
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
require_dep jq
|
|
169
|
+
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
# Resolve the graph JSON (auto-build if absent — the graph is gitignored)
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
GRAPH=""
|
|
175
|
+
TMP_GRAPH=""
|
|
176
|
+
|
|
177
|
+
cleanup() {
|
|
178
|
+
[ -n "$TMP_GRAPH" ] && [ -f "$TMP_GRAPH" ] && rm -f "$TMP_GRAPH" || true
|
|
179
|
+
}
|
|
180
|
+
trap cleanup EXIT
|
|
181
|
+
|
|
182
|
+
resolve_graph() {
|
|
183
|
+
if [ -n "$GRAPH_PATH" ]; then
|
|
184
|
+
[ -f "$GRAPH_PATH" ] || die "graph file not found: $GRAPH_PATH"
|
|
185
|
+
GRAPH="$GRAPH_PATH"
|
|
186
|
+
return
|
|
187
|
+
fi
|
|
188
|
+
if [ -f "$DEFAULT_GRAPH" ]; then
|
|
189
|
+
GRAPH="$DEFAULT_GRAPH"
|
|
190
|
+
return
|
|
191
|
+
fi
|
|
192
|
+
# Auto-build to a temp file (the default path is gitignored / may not exist).
|
|
193
|
+
require_dep node
|
|
194
|
+
[ -f "$BUILDER" ] || die "graph is absent and builder not found: $BUILDER"
|
|
195
|
+
TMP_GRAPH="$(mktemp "${TMPDIR:-/tmp}/spec-graph.XXXXXX.json")"
|
|
196
|
+
if ! node "$BUILDER" --root "$REPO_ROOT" --stdout --quiet >"$TMP_GRAPH" 2>/dev/null; then
|
|
197
|
+
die "failed to auto-build spec graph via $BUILDER"
|
|
198
|
+
fi
|
|
199
|
+
GRAPH="$TMP_GRAPH"
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
resolve_graph
|
|
203
|
+
|
|
204
|
+
# Validate the graph document shape early so jq filters can assume it.
|
|
205
|
+
jq -e '.graph.nodes and .graph.edges' "$GRAPH" >/dev/null 2>&1 \
|
|
206
|
+
|| die "not a valid spec-graph document (missing .graph.nodes/.graph.edges): $GRAPH"
|
|
207
|
+
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
# Findings accumulation
|
|
210
|
+
# Each finding is one line: "<SEVERITY>\t<CLASS>\t<message>"
|
|
211
|
+
# SEVERITY in {HARD, SOFT, INFO}. Collected then rendered/summarized.
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
FINDINGS=""
|
|
215
|
+
|
|
216
|
+
add_finding() {
|
|
217
|
+
# add_finding <severity> <class> <message>
|
|
218
|
+
FINDINGS="${FINDINGS}${1}"$'\t'"${2}"$'\t'"${3}"$'\n'
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
# Check 1: Orphan annotations
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
check_orphans() {
|
|
226
|
+
# written-never-read and read-never-written, computed from graph edges.
|
|
227
|
+
# Emits one finding per annotation. Scaffolding markers (no read AND no write)
|
|
228
|
+
# are downgraded to INFO via the allowlist; any non-allowlisted isolated
|
|
229
|
+
# marker is still surfaced (as SOFT) so new scaffolding gets curated.
|
|
230
|
+
local scaffold_json
|
|
231
|
+
scaffold_json="$(printf '%s\n' "${SCAFFOLDING_ANNOTATIONS[@]}" | jq -R . | jq -sc .)"
|
|
232
|
+
|
|
233
|
+
# Produce TSV rows: <kind>\t<ann-name> where kind is one of:
|
|
234
|
+
# WRITTEN_NEVER_READ | READ_NEVER_WRITTEN | ISOLATED_SCAFFOLD | ISOLATED_UNKNOWN
|
|
235
|
+
local rows
|
|
236
|
+
rows="$(jq -r --argjson scaffold "$scaffold_json" '
|
|
237
|
+
.graph as $g
|
|
238
|
+
| ([$g.edges[] | select(.type=="WRITES") | .to]) as $written
|
|
239
|
+
| ([$g.edges[] | select(.type=="READS") | .to]) as $read
|
|
240
|
+
| $g.nodes[]
|
|
241
|
+
| select(.type=="annotation")
|
|
242
|
+
| .id as $id | (.name) as $name
|
|
243
|
+
| ($written | index($id) != null) as $w
|
|
244
|
+
| ($read | index($id) != null) as $r
|
|
245
|
+
| if ($w and ($r|not)) then "WRITTEN_NEVER_READ\t\($name)"
|
|
246
|
+
elif (($w|not) and $r) then "READ_NEVER_WRITTEN\t\($name)"
|
|
247
|
+
elif (($w|not) and ($r|not)) then
|
|
248
|
+
(if ($scaffold | index($name)) then "ISOLATED_SCAFFOLD\t\($name)"
|
|
249
|
+
else "ISOLATED_UNKNOWN\t\($name)" end)
|
|
250
|
+
else empty end
|
|
251
|
+
' "$GRAPH")"
|
|
252
|
+
|
|
253
|
+
local kind name
|
|
254
|
+
while IFS=$'\t' read -r kind name; do
|
|
255
|
+
[ -n "$kind" ] || continue
|
|
256
|
+
case "$kind" in
|
|
257
|
+
WRITTEN_NEVER_READ)
|
|
258
|
+
add_finding SOFT orphan-annotation "$name is WRITTEN but never READ by any spec." ;;
|
|
259
|
+
READ_NEVER_WRITTEN)
|
|
260
|
+
add_finding SOFT orphan-annotation "$name is READ but never WRITTEN by any spec." ;;
|
|
261
|
+
ISOLATED_SCAFFOLD)
|
|
262
|
+
add_finding INFO scaffold-annotation "$name is an inline doc-scaffolding marker (no WRITES/READS edge — expected)." ;;
|
|
263
|
+
ISOLATED_UNKNOWN)
|
|
264
|
+
add_finding SOFT orphan-annotation "$name has neither a WRITES nor a READS edge and is not an allowlisted scaffolding marker — triage or add to the allowlist." ;;
|
|
265
|
+
esac
|
|
266
|
+
done <<< "$rows"
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
# Check 2: Dangling cross-refs (Skill() targets)
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
check_dangling() {
|
|
274
|
+
# The builder DROPS unresolved Skill()/script/devdoc refs, so we scan the spec
|
|
275
|
+
# corpus directly. We match every structured Skill() invocation form —
|
|
276
|
+
# positional Skill("X"), keyword Skill(skill="X"), and colon Skill(skill: "X")
|
|
277
|
+
# — with `/`-delimited sub-phases and uppercase/leading-digit targets, kept in
|
|
278
|
+
# sync with the builder's CONTAINS regex in build-spec-graph.mjs. The quoted
|
|
279
|
+
# target form keeps this off illustrative filename mentions in prose.
|
|
280
|
+
local commands_dir="$REPO_ROOT/commands"
|
|
281
|
+
[ -d "$commands_dir" ] || return 0
|
|
282
|
+
|
|
283
|
+
# Set of known command/sub-phase node names (e.g. "work-on", "work-on:build").
|
|
284
|
+
local known
|
|
285
|
+
known="$(jq -r '.graph.nodes[] | select(.type=="command" or .type=="sub-phase") | .name' "$GRAPH" | sort -u)"
|
|
286
|
+
|
|
287
|
+
# Extract every Skill(skill="X") / Skill(skill='X') target with file+line.
|
|
288
|
+
# grep -rn over the whole tree (no fixed -A/-m window that could truncate).
|
|
289
|
+
local hits
|
|
290
|
+
hits="$(grep -rnoE "Skill\(\s*(skill\s*[:=]\s*)?[\"'][A-Za-z0-9][A-Za-z0-9:_/-]*[\"']" "$commands_dir" 2>/dev/null || true)"
|
|
291
|
+
|
|
292
|
+
[ -n "$hits" ] || return 0
|
|
293
|
+
|
|
294
|
+
# Illustrative placeholder targets used in Skill() example snippets in prose —
|
|
295
|
+
# these are documentation stand-ins, never real command references.
|
|
296
|
+
local placeholder_targets=" X subcommand "
|
|
297
|
+
local line target relpath
|
|
298
|
+
while IFS= read -r line; do
|
|
299
|
+
[ -n "$line" ] || continue
|
|
300
|
+
# line form: <path>:<lineno>:Skill( ... "X" )
|
|
301
|
+
relpath="${line%%:*}"
|
|
302
|
+
relpath="${relpath#"$REPO_ROOT"/}"
|
|
303
|
+
# Pull the quoted target.
|
|
304
|
+
target="$(printf '%s' "$line" | grep -oE "[\"'][A-Za-z0-9][A-Za-z0-9:_/-]*[\"']" | head -1 | tr -d "\"'")"
|
|
305
|
+
[ -n "$target" ] || continue
|
|
306
|
+
# Normalize the `/`-delimited sub-phase form (work-on/review) to the graph's
|
|
307
|
+
# `:` form (work-on:review) before resolution — kept in sync with the builder.
|
|
308
|
+
target="${target//\//:}"
|
|
309
|
+
# Skip known illustrative placeholders.
|
|
310
|
+
case "$placeholder_targets" in *" $target "*) continue ;; esac
|
|
311
|
+
if ! printf '%s\n' "$known" | grep -qxF "$target"; then
|
|
312
|
+
add_finding HARD dangling-ref "$relpath references Skill(\"$target\") but no command/sub-phase node \"$target\" exists in the graph."
|
|
313
|
+
fi
|
|
314
|
+
done <<< "$hits"
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
# ---------------------------------------------------------------------------
|
|
318
|
+
# Check 3: Broken label transitions
|
|
319
|
+
# ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
check_transitions() {
|
|
322
|
+
# A label node set by no --add-label TRANSITIONS edge is a broken transition.
|
|
323
|
+
local orphan_labels
|
|
324
|
+
orphan_labels="$(jq -r '
|
|
325
|
+
.graph as $g
|
|
326
|
+
| ([$g.edges[] | select(.type=="TRANSITIONS") | .to]) as $set
|
|
327
|
+
| $g.nodes[] | select(.type=="label")
|
|
328
|
+
| select(.id as $id | ($set | index($id)) == null)
|
|
329
|
+
| .name
|
|
330
|
+
' "$GRAPH")"
|
|
331
|
+
|
|
332
|
+
local lbl
|
|
333
|
+
while IFS= read -r lbl; do
|
|
334
|
+
[ -n "$lbl" ] || continue
|
|
335
|
+
add_finding HARD broken-transition "Label $lbl is defined/referenced but set by NO command (no --add-label transition) — state-machine gap."
|
|
336
|
+
done <<< "$orphan_labels"
|
|
337
|
+
|
|
338
|
+
# Add/remove asymmetry: labels that are --remove-label-d but never
|
|
339
|
+
# --add-label-ed anywhere in the corpus. INFO — usually a defensive removal,
|
|
340
|
+
# but flags a potential dead state.
|
|
341
|
+
local commands_dir="$REPO_ROOT/commands"
|
|
342
|
+
[ -d "$commands_dir" ] || return 0
|
|
343
|
+
|
|
344
|
+
local added removed
|
|
345
|
+
added="$(jq -r '.graph.edges[] | select(.type=="TRANSITIONS") | .to | sub("^label:"; "")' "$GRAPH" | sort -u)"
|
|
346
|
+
removed="$(grep -rhoE -- "--remove-label[[:space:]]+[\"'][^\"']*[\"']" "$commands_dir" 2>/dev/null \
|
|
347
|
+
| grep -oE 'workflow:[a-z][a-z0-9-]*' | sort -u || true)"
|
|
348
|
+
|
|
349
|
+
local r
|
|
350
|
+
while IFS= read -r r; do
|
|
351
|
+
[ -n "$r" ] || continue
|
|
352
|
+
if ! printf '%s\n' "$added" | grep -qxF "$r"; then
|
|
353
|
+
add_finding INFO transition-asymmetry "Label $r is removed (--remove-label) but never added (--add-label) by any command."
|
|
354
|
+
fi
|
|
355
|
+
done <<< "$removed"
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
# ---------------------------------------------------------------------------
|
|
359
|
+
# Run checks
|
|
360
|
+
# ---------------------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
check_orphans
|
|
363
|
+
check_dangling
|
|
364
|
+
check_transitions
|
|
365
|
+
|
|
366
|
+
# Sort findings deterministically (severity rank, then class, then message).
|
|
367
|
+
sev_rank() {
|
|
368
|
+
case "$1" in
|
|
369
|
+
HARD) echo 0 ;;
|
|
370
|
+
SOFT) echo 1 ;;
|
|
371
|
+
INFO) echo 2 ;;
|
|
372
|
+
*) echo 9 ;;
|
|
373
|
+
esac
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
SORTED_FINDINGS="$(printf '%s' "$FINDINGS" \
|
|
377
|
+
| awk -F'\t' 'NF>=3 { r=($1=="HARD"?0:($1=="SOFT"?1:($1=="INFO"?2:9))); print r"\t"$0 }' \
|
|
378
|
+
| sort -t$'\t' -k1,1n -k3,3 -k4,4 \
|
|
379
|
+
| cut -f2-)"
|
|
380
|
+
|
|
381
|
+
HARD_COUNT="$(printf '%s\n' "$SORTED_FINDINGS" | awk -F'\t' 'NF>=3 && $1=="HARD"' | grep -c . || true)"
|
|
382
|
+
SOFT_COUNT="$(printf '%s\n' "$SORTED_FINDINGS" | awk -F'\t' 'NF>=3 && $1=="SOFT"' | grep -c . || true)"
|
|
383
|
+
INFO_COUNT="$(printf '%s\n' "$SORTED_FINDINGS" | awk -F'\t' 'NF>=3 && $1=="INFO"' | grep -c . || true)"
|
|
384
|
+
|
|
385
|
+
# ---------------------------------------------------------------------------
|
|
386
|
+
# Render
|
|
387
|
+
# ---------------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
if [ "$JSON" -eq 1 ]; then
|
|
390
|
+
printf '%s\n' "$SORTED_FINDINGS" \
|
|
391
|
+
| awk -F'\t' 'NF>=3 { print $1"\t"$2"\t"$3 }' \
|
|
392
|
+
| jq -R -s --argjson hard "$HARD_COUNT" --argjson soft "$SOFT_COUNT" --argjson info "$INFO_COUNT" '
|
|
393
|
+
split("\n")
|
|
394
|
+
| map(select(length>0) | split("\t") | {severity: .[0], class: .[1], message: .[2]})
|
|
395
|
+
| { summary: {hard: $hard, soft: $soft, info: $info}, findings: . }
|
|
396
|
+
'
|
|
397
|
+
elif [ "$QUIET" -eq 0 ]; then
|
|
398
|
+
echo "Spec graph self-consistency report"
|
|
399
|
+
echo " graph: $GRAPH"
|
|
400
|
+
echo ""
|
|
401
|
+
if [ -z "$SORTED_FINDINGS" ]; then
|
|
402
|
+
echo " (no findings — graph is internally consistent)"
|
|
403
|
+
else
|
|
404
|
+
printf '%s\n' "$SORTED_FINDINGS" \
|
|
405
|
+
| awk -F'\t' 'NF>=3 { printf " [%-4s] %-22s %s\n", $1, $2, $3 }'
|
|
406
|
+
fi
|
|
407
|
+
echo ""
|
|
408
|
+
fi
|
|
409
|
+
|
|
410
|
+
if [ "$QUIET" -eq 0 ] && [ "$JSON" -eq 0 ]; then
|
|
411
|
+
echo "Summary: ${HARD_COUNT} hard, ${SOFT_COUNT} soft, ${INFO_COUNT} info"
|
|
412
|
+
elif [ "$QUIET" -eq 1 ]; then
|
|
413
|
+
echo "spec-graph-validate: ${HARD_COUNT} hard, ${SOFT_COUNT} soft, ${INFO_COUNT} info"
|
|
414
|
+
fi
|
|
415
|
+
|
|
416
|
+
# ---------------------------------------------------------------------------
|
|
417
|
+
# Exit code
|
|
418
|
+
# ---------------------------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
if [ "$HARD_COUNT" -gt 0 ] && [ "$SOFT" -eq 0 ]; then
|
|
421
|
+
exit 1
|
|
422
|
+
fi
|
|
423
|
+
exit 0
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# ForgeDock CI Review — Workflow Template
|
|
2
|
+
#
|
|
3
|
+
# Copy this file to .github/workflows/forgedock-review.yml in your repository
|
|
4
|
+
# to enable automatic AI-powered PR review on every pull request.
|
|
5
|
+
#
|
|
6
|
+
# Prerequisites:
|
|
7
|
+
# 1. Add ANTHROPIC_API_KEY as a repository secret (Settings > Secrets > Actions)
|
|
8
|
+
# 2. Ensure Claude Code is installed: https://docs.anthropic.com/en/docs/claude-code
|
|
9
|
+
# 3. ForgeDock must be installed in the repo running the pipeline (npx forgedock)
|
|
10
|
+
#
|
|
11
|
+
# Security:
|
|
12
|
+
# - ANTHROPIC_API_KEY is ONLY passed via the env: block — never echoed or logged
|
|
13
|
+
# - GITHUB_TOKEN is scoped to this workflow run only
|
|
14
|
+
# - Review findings are posted as PR comments, not stored in workflow logs
|
|
15
|
+
#
|
|
16
|
+
# Fork PR limitation:
|
|
17
|
+
# - pull_request events from forks do NOT have access to repository secrets
|
|
18
|
+
# - External contributors' PRs will not trigger this workflow automatically
|
|
19
|
+
# - To review fork PRs, see the pull_request_target alternative below (with security caveats)
|
|
20
|
+
#
|
|
21
|
+
# See docs/CI.md for full setup guide, customization options, and troubleshooting.
|
|
22
|
+
|
|
23
|
+
name: ForgeDock PR Review
|
|
24
|
+
|
|
25
|
+
on:
|
|
26
|
+
pull_request:
|
|
27
|
+
types: [opened, synchronize, reopened]
|
|
28
|
+
|
|
29
|
+
# Minimum permissions required for ForgeDock review:
|
|
30
|
+
# contents: read — checkout code and read files
|
|
31
|
+
# pull-requests: write — post review findings as PR comments
|
|
32
|
+
permissions:
|
|
33
|
+
contents: read
|
|
34
|
+
pull-requests: write
|
|
35
|
+
|
|
36
|
+
jobs:
|
|
37
|
+
forgedock-review:
|
|
38
|
+
name: AI PR Review
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
|
|
41
|
+
# Skip review for bot-authored PRs (dependabot, release bots, etc.)
|
|
42
|
+
# Remove this condition if you want all PRs reviewed.
|
|
43
|
+
if: github.actor != 'dependabot[bot]' && github.actor != 'github-actions[bot]'
|
|
44
|
+
|
|
45
|
+
steps:
|
|
46
|
+
- name: Checkout
|
|
47
|
+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
48
|
+
with:
|
|
49
|
+
# Fetch full history so /review-pr can access git blame and log
|
|
50
|
+
fetch-depth: 0
|
|
51
|
+
|
|
52
|
+
- name: Set up Node.js
|
|
53
|
+
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
|
54
|
+
with:
|
|
55
|
+
node-version: '20'
|
|
56
|
+
|
|
57
|
+
- name: Install Claude Code
|
|
58
|
+
run: npm install -g @anthropic-ai/claude-code
|
|
59
|
+
# Installs claude CLI for non-interactive (headless) execution
|
|
60
|
+
|
|
61
|
+
- name: Install ForgeDock
|
|
62
|
+
run: npx forgedock install
|
|
63
|
+
# Symlinks ForgeDock commands into ~/.claude/commands/
|
|
64
|
+
# This makes /review-pr available to claude --print
|
|
65
|
+
|
|
66
|
+
- name: Run ForgeDock PR Review
|
|
67
|
+
env:
|
|
68
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
69
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
70
|
+
# Pass PR number via environment — do not echo secrets in run: scripts
|
|
71
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
72
|
+
run: |
|
|
73
|
+
claude --print "/review-pr $PR_NUMBER" \
|
|
74
|
+
--dangerously-skip-permissions
|
|
75
|
+
# --print runs Claude Code in non-interactive (headless) mode
|
|
76
|
+
# --dangerously-skip-permissions skips the interactive permission prompt
|
|
77
|
+
# required for unattended CI execution; safe here because the runner
|
|
78
|
+
# is ephemeral and permissions are constrained by the workflow above
|
|
79
|
+
|
|
80
|
+
# Optional: set a commit status based on review verdict
|
|
81
|
+
# Uncomment to enable pass/fail badges on PRs.
|
|
82
|
+
#
|
|
83
|
+
# - name: Set Commit Status
|
|
84
|
+
# if: always()
|
|
85
|
+
# uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
|
86
|
+
# with:
|
|
87
|
+
# script: |
|
|
88
|
+
# const state = '${{ job.status }}' === 'success' ? 'success' : 'failure';
|
|
89
|
+
# await github.rest.repos.createCommitStatus({
|
|
90
|
+
# owner: context.repo.owner,
|
|
91
|
+
# repo: context.repo.repo,
|
|
92
|
+
# sha: context.payload.pull_request.head.sha,
|
|
93
|
+
# state: state,
|
|
94
|
+
# context: 'ForgeDock Review',
|
|
95
|
+
# description: state === 'success' ? 'Review complete' : 'Review found issues',
|
|
96
|
+
# });
|