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.
Files changed (134) hide show
  1. package/README.md +34 -1
  2. package/bin/cross-publish.mjs +344 -0
  3. package/bin/forgedock.mjs +229 -17
  4. package/commands/autopilot.md +77 -0
  5. package/commands/design-bench.md +176 -0
  6. package/commands/design-render-critique-loop.md +103 -0
  7. package/commands/design.md +259 -0
  8. package/commands/issue.md +7 -3
  9. package/commands/orchestrate.md +121 -0
  10. package/commands/{resume.md → pipeline-resume.md} +4 -4
  11. package/commands/{status.md → pipeline-status.md} +1 -1
  12. package/commands/qa-sweep.md +37 -2
  13. package/commands/quality-gate.md +57 -0
  14. package/commands/review-pr-staging.md +158 -2
  15. package/commands/test-gate.md +929 -0
  16. package/commands/work-on/build/implement.md +5 -3
  17. package/commands/work-on.md +53 -3
  18. package/docs/CI.md +177 -0
  19. package/docs/FORGE-PROTOCOL.md +324 -0
  20. package/docs/articles/b-vs-c-baseline.md +210 -0
  21. package/docs/articles/full-abc-benchmark.md +205 -0
  22. package/docs/articles/selective-spec-loading-token-savings.md +140 -0
  23. package/docs/design/abc-benchmark.md +195 -0
  24. package/docs/design/design-architect-rationale.md +123 -0
  25. package/docs/design/design-memory.md +61 -0
  26. package/docs/design/design-spec-schema.md +248 -0
  27. package/docs/design/design-system-lint.md +150 -0
  28. package/docs/design/divergent-generation.md +106 -0
  29. package/docs/design/effects-appropriateness.md +133 -0
  30. package/docs/design/fixtures/briefs/README.md +47 -0
  31. package/docs/design/fixtures/briefs/cadence.md +39 -0
  32. package/docs/design/fixtures/briefs/plume.md +39 -0
  33. package/docs/design/fixtures/briefs/slipstream.md +39 -0
  34. package/docs/design/fixtures/briefs/tender.md +39 -0
  35. package/docs/design/fixtures/briefs/voltage.md +39 -0
  36. package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-1.html +364 -0
  37. package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-2.html +420 -0
  38. package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-3.html +506 -0
  39. package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-1.html +611 -0
  40. package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-2.html +594 -0
  41. package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-3.html +817 -0
  42. package/docs/design/fixtures/runs/b-vs-c-baseline/runs.json +175 -0
  43. package/docs/design/fixtures/runs/b-vs-c-baseline/scorecard.json +138 -0
  44. package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-1.html +373 -0
  45. package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-2.html +398 -0
  46. package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-3.html +595 -0
  47. package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-1.html +531 -0
  48. package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-2.html +565 -0
  49. package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-3.html +674 -0
  50. package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-1.html +388 -0
  51. package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-2.html +463 -0
  52. package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-3.html +568 -0
  53. package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-1.html +562 -0
  54. package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-2.html +618 -0
  55. package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-3.html +618 -0
  56. package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-1.html +364 -0
  57. package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-2.html +420 -0
  58. package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-3.html +506 -0
  59. package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-1.html +527 -0
  60. package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-2.html +445 -0
  61. package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-3.html +483 -0
  62. package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-1.html +611 -0
  63. package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-2.html +594 -0
  64. package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-3.html +817 -0
  65. package/docs/design/fixtures/runs/full-abc/runs.json +190 -0
  66. package/docs/design/fixtures/runs/full-abc/scorecard.json +144 -0
  67. package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-1.html +496 -0
  68. package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-2.html +447 -0
  69. package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-3.html +564 -0
  70. package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-1.html +373 -0
  71. package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-2.html +398 -0
  72. package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-3.html +595 -0
  73. package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-1.html +622 -0
  74. package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-2.html +475 -0
  75. package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-3.html +660 -0
  76. package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-1.html +531 -0
  77. package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-2.html +565 -0
  78. package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-3.html +674 -0
  79. package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-1.html +525 -0
  80. package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-2.html +446 -0
  81. package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-3.html +463 -0
  82. package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-1.html +388 -0
  83. package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-2.html +463 -0
  84. package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-3.html +568 -0
  85. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-1.html +1068 -0
  86. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-2.html +1423 -0
  87. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-3.html +1245 -0
  88. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-1.html +305 -0
  89. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-2.html +327 -0
  90. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-3.html +428 -0
  91. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-1.html +1270 -0
  92. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-2.html +1172 -0
  93. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-3.html +1532 -0
  94. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-1.html +315 -0
  95. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-2.html +342 -0
  96. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-3.html +353 -0
  97. package/docs/design/fixtures/runs/full-abc-opus/runs.json +190 -0
  98. package/docs/design/fixtures/runs/full-abc-opus/scorecard.json +144 -0
  99. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-1.html +345 -0
  100. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-2.html +396 -0
  101. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-3.html +502 -0
  102. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-1.html +408 -0
  103. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-2.html +446 -0
  104. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-3.html +496 -0
  105. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-1.html +813 -0
  106. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-2.html +711 -0
  107. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-3.html +721 -0
  108. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-1.html +280 -0
  109. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-2.html +291 -0
  110. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-3.html +366 -0
  111. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-1.html +1010 -0
  112. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-2.html +836 -0
  113. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-3.html +930 -0
  114. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-1.html +407 -0
  115. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-2.html +396 -0
  116. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-3.html +529 -0
  117. package/docs/design/foundations/brutalist.css +294 -0
  118. package/docs/design/foundations/editorial.css +300 -0
  119. package/docs/design/foundations/minimal-luxury.css +295 -0
  120. package/docs/design/foundations/technical.css +293 -0
  121. package/docs/design/foundations/warm-photo.css +299 -0
  122. package/docs/design/reference-corpus.md +1726 -0
  123. package/docs/design/render-critique-loop.md +57 -0
  124. package/docs/spec-graph-schema.md +397 -0
  125. package/package.json +1 -1
  126. package/scripts/bench-scorecard.mjs +239 -0
  127. package/scripts/build-spec-graph.mjs +509 -0
  128. package/scripts/check-native-conflicts.sh +200 -0
  129. package/scripts/classify-lane.sh +0 -7
  130. package/scripts/design-system-lint.mjs +993 -0
  131. package/scripts/graph-query.sh +486 -0
  132. package/scripts/transition-label.sh +10 -2
  133. package/scripts/validate-spec-graph.sh +423 -0
  134. 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
+ # });