forgedock 1.0.16 → 1.0.18

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 (190) hide show
  1. package/README.md +54 -3
  2. package/bin/cross-publish.mjs +344 -0
  3. package/bin/forgedock.mjs +2205 -541
  4. package/bin/github-app-manifest.json.license +2 -0
  5. package/bin/hooks/session-start.mjs +4 -5
  6. package/bin/registry.mjs +20 -0
  7. package/bin/tui.mjs +172 -96
  8. package/commands/adopt.md +433 -0
  9. package/commands/analytics.md +2 -0
  10. package/commands/audit-agents.md +2 -0
  11. package/commands/audit.md +2 -0
  12. package/commands/autopilot.md +77 -0
  13. package/commands/changelog.md +354 -0
  14. package/commands/cleanup.md +2 -0
  15. package/commands/compat-audit.md +330 -0
  16. package/commands/deploy-info.md +6 -5
  17. package/commands/design-bench.md +176 -0
  18. package/commands/design-render-critique-loop.md +103 -0
  19. package/commands/design.md +259 -0
  20. package/commands/diagnose.md +483 -0
  21. package/commands/explain.md +349 -0
  22. package/commands/forge-stats.md +2 -0
  23. package/commands/forgedock-init.md +2 -0
  24. package/commands/geo-audit.md +2 -0
  25. package/commands/incident-response.md +2 -0
  26. package/commands/issue.md +9 -3
  27. package/commands/milestone.md +2 -0
  28. package/commands/optimize.md +411 -0
  29. package/commands/orchestrate.md +239 -0
  30. package/commands/pipeline-health.md +60 -1
  31. package/commands/qa-sweep.md +39 -2
  32. package/commands/quality-gate.md +67 -0
  33. package/commands/replay.md +263 -0
  34. package/commands/resume.md +264 -0
  35. package/commands/review-pr-agents.md +5 -1
  36. package/commands/review-pr-staging.md +160 -2
  37. package/commands/review-pr.md +85 -3
  38. package/commands/rollback.md +2 -0
  39. package/commands/scope.md +280 -0
  40. package/commands/security-audit.md +2 -0
  41. package/commands/status.md +262 -0
  42. package/commands/sync-ecosystem.md +2 -0
  43. package/commands/test-gate.md +929 -0
  44. package/commands/validate.md +2 -0
  45. package/commands/work-on/build/architect.md +128 -18
  46. package/commands/work-on/build/context.md +130 -17
  47. package/commands/work-on/build/implement.md +7 -3
  48. package/commands/work-on/build/validate.md +2 -0
  49. package/commands/work-on/build.md +32 -7
  50. package/commands/work-on/close.md +85 -0
  51. package/commands/work-on/decompose.md +2 -0
  52. package/commands/work-on/investigate.md +30 -3
  53. package/commands/work-on/review.md +13 -1
  54. package/commands/work-on-monolithic.md +2 -0
  55. package/commands/work-on.md +676 -38
  56. package/docs/BRAND.md +51 -0
  57. package/docs/CI.md +177 -0
  58. package/docs/CONFIG.md +94 -0
  59. package/docs/FORGE-PROTOCOL.md +324 -0
  60. package/docs/articles/b-vs-c-baseline.md +210 -0
  61. package/docs/articles/full-abc-benchmark.md +205 -0
  62. package/docs/articles/per-repo-adaptive-scripts-token-savings.md +174 -0
  63. package/docs/articles/selective-spec-loading-token-savings.md +140 -0
  64. package/docs/claude-breakpoints.json +47 -0
  65. package/docs/design/abc-benchmark.md +195 -0
  66. package/docs/design/design-architect-rationale.md +123 -0
  67. package/docs/design/design-memory.md +61 -0
  68. package/docs/design/design-spec-schema.md +248 -0
  69. package/docs/design/design-system-lint.md +150 -0
  70. package/docs/design/divergent-generation.md +106 -0
  71. package/docs/design/effects-appropriateness.md +133 -0
  72. package/docs/design/fixtures/briefs/README.md +47 -0
  73. package/docs/design/fixtures/briefs/cadence.md +39 -0
  74. package/docs/design/fixtures/briefs/plume.md +39 -0
  75. package/docs/design/fixtures/briefs/slipstream.md +39 -0
  76. package/docs/design/fixtures/briefs/tender.md +39 -0
  77. package/docs/design/fixtures/briefs/voltage.md +39 -0
  78. package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-1.html +364 -0
  79. package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-2.html +420 -0
  80. package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-3.html +506 -0
  81. package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-1.html +611 -0
  82. package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-2.html +594 -0
  83. package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-3.html +817 -0
  84. package/docs/design/fixtures/runs/b-vs-c-baseline/runs.json +175 -0
  85. package/docs/design/fixtures/runs/b-vs-c-baseline/scorecard.json +138 -0
  86. package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-1.html +373 -0
  87. package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-2.html +398 -0
  88. package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-3.html +595 -0
  89. package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-1.html +531 -0
  90. package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-2.html +565 -0
  91. package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-3.html +674 -0
  92. package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-1.html +388 -0
  93. package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-2.html +463 -0
  94. package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-3.html +568 -0
  95. package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-1.html +562 -0
  96. package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-2.html +618 -0
  97. package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-3.html +618 -0
  98. package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-1.html +364 -0
  99. package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-2.html +420 -0
  100. package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-3.html +506 -0
  101. package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-1.html +527 -0
  102. package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-2.html +445 -0
  103. package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-3.html +483 -0
  104. package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-1.html +611 -0
  105. package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-2.html +594 -0
  106. package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-3.html +817 -0
  107. package/docs/design/fixtures/runs/full-abc/runs.json +190 -0
  108. package/docs/design/fixtures/runs/full-abc/scorecard.json +144 -0
  109. package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-1.html +496 -0
  110. package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-2.html +447 -0
  111. package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-3.html +564 -0
  112. package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-1.html +373 -0
  113. package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-2.html +398 -0
  114. package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-3.html +595 -0
  115. package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-1.html +622 -0
  116. package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-2.html +475 -0
  117. package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-3.html +660 -0
  118. package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-1.html +531 -0
  119. package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-2.html +565 -0
  120. package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-3.html +674 -0
  121. package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-1.html +525 -0
  122. package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-2.html +446 -0
  123. package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-3.html +463 -0
  124. package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-1.html +388 -0
  125. package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-2.html +463 -0
  126. package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-3.html +568 -0
  127. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-1.html +1068 -0
  128. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-2.html +1423 -0
  129. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-3.html +1245 -0
  130. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-1.html +305 -0
  131. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-2.html +327 -0
  132. package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-3.html +428 -0
  133. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-1.html +1270 -0
  134. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-2.html +1172 -0
  135. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-3.html +1532 -0
  136. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-1.html +315 -0
  137. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-2.html +342 -0
  138. package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-3.html +353 -0
  139. package/docs/design/fixtures/runs/full-abc-opus/runs.json +190 -0
  140. package/docs/design/fixtures/runs/full-abc-opus/scorecard.json +144 -0
  141. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-1.html +345 -0
  142. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-2.html +396 -0
  143. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-3.html +502 -0
  144. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-1.html +408 -0
  145. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-2.html +446 -0
  146. package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-3.html +496 -0
  147. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-1.html +813 -0
  148. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-2.html +711 -0
  149. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-3.html +721 -0
  150. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-1.html +280 -0
  151. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-2.html +291 -0
  152. package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-3.html +366 -0
  153. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-1.html +1010 -0
  154. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-2.html +836 -0
  155. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-3.html +930 -0
  156. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-1.html +407 -0
  157. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-2.html +396 -0
  158. package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-3.html +529 -0
  159. package/docs/design/foundations/brutalist.css +294 -0
  160. package/docs/design/foundations/editorial.css +300 -0
  161. package/docs/design/foundations/minimal-luxury.css +295 -0
  162. package/docs/design/foundations/technical.css +293 -0
  163. package/docs/design/foundations/warm-photo.css +299 -0
  164. package/docs/design/reference-corpus.md +1726 -0
  165. package/docs/design/render-critique-loop.md +57 -0
  166. package/docs/site/getting-started.md +21 -0
  167. package/docs/spec-graph-schema.md +397 -0
  168. package/package.json +3 -3
  169. package/scripts/bench-scorecard.mjs +239 -0
  170. package/scripts/build-spec-graph.mjs +509 -0
  171. package/scripts/classify-lane.sh +138 -0
  172. package/scripts/design-system-lint.mjs +993 -0
  173. package/scripts/graph-query.sh +486 -0
  174. package/scripts/transition-label.sh +169 -0
  175. package/scripts/validate-pr-target.sh +30 -0
  176. package/scripts/validate-spec-graph.sh +423 -0
  177. package/templates/devdocs/README.md +72 -0
  178. package/templates/devdocs/agent/using-forgedock.md +3 -0
  179. package/templates/devdocs/agent/using-github.md +3 -0
  180. package/templates/devdocs/index.yaml +162 -0
  181. package/templates/devdocs/project/architecture.md +3 -0
  182. package/templates/devdocs/project/conventions.md +3 -0
  183. package/templates/devdocs/project/custom-instructions.md +3 -0
  184. package/templates/devdocs/project/glossary.md +3 -0
  185. package/templates/devdocs/project/stack.md +3 -0
  186. package/templates/devdocs/templates/commit-convention.md +3 -0
  187. package/templates/devdocs/templates/issue.md +3 -0
  188. package/templates/devdocs/templates/pull-request.md +3 -0
  189. package/templates/devdocs/templates/review-finding.md +3 -0
  190. package/templates/workflows/forgedock-review.yml +96 -0
@@ -0,0 +1,486 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+ # graph-query.sh — Read-only query interface over the Spec Knowledge Graph
6
+ #
7
+ # Queries the spec knowledge graph produced by `build-spec-graph.mjs`
8
+ # (`.forgedock/graph/spec-graph.json`). The graph is a self-map of ForgeDock's
9
+ # own command specs: which command WRITES/READS which FORGE annotation, which
10
+ # scripts a command INVOKES, which devdocs it REQUIRES, etc. This script is the
11
+ # consumer-facing query API so agents (and humans) don't hand-write jq.
12
+ #
13
+ # Usage:
14
+ # graph-query.sh <subcommand> <arg> [--human] [--graph <path>]
15
+ #
16
+ # Subcommands:
17
+ # readers <annotation> — commands/specs that READ the annotation
18
+ # writers <annotation> — commands/specs that WRITE (post) the annotation
19
+ # impact <node> — blast radius: everything transitively affected if
20
+ # <node> changes (annotation | label | script | command/spec)
21
+ # deps <command> — what a command reads/invokes/requires/contains
22
+ # (its full input + output set), grouped by edge type
23
+ # load-set <command> — minimal spec set to read for a command: the command
24
+ # itself plus its transitively reachable sub-phases
25
+ # (CONTAINS) and required devdocs (REQUIRES), as a flat
26
+ # list of repo-relative file paths. This is the inverse
27
+ # of `impact` (forward instead of reverse reachability)
28
+ # and powers selective spec loading in the pipeline.
29
+ # search <term> — substring lookup over node ids / names / paths
30
+ #
31
+ # Arguments are normalized, so all of these are accepted:
32
+ # readers CONTRACT == readers FORGE:CONTRACT == readers ann:FORGE:CONTRACT
33
+ # deps work-on == deps cmd:work-on
34
+ # deps work-on:build:implement == deps cmd:work-on:build:implement
35
+ # load-set work-on == load-set cmd:work-on
36
+ # impact classify-lane.sh == impact script:classify-lane.sh
37
+ # impact workflow:merged == impact label:workflow:merged
38
+ #
39
+ # Staleness detection (no daemon):
40
+ # A persisted graph carries `builtFromHash`, a sha256 fingerprint of the spec
41
+ # corpus it was built from. On each query against a persisted graph, this
42
+ # script cheaply recomputes the current fingerprint (build-spec-graph.mjs
43
+ # --hash) and compares. If they differ, the persisted graph is stale: a
44
+ # `stale-graph` banner is printed to STDERR and the graph is transparently
45
+ # rebuilt for this query (stdout stays pure JSON). The no-change path does one
46
+ # cheap hash compare and no rebuild — no background process, no watcher.
47
+ #
48
+ # Flags:
49
+ # --human Render an aligned table instead of compact JSON.
50
+ # --graph <path> Use a specific graph JSON (default:
51
+ # <repo>/.forgedock/graph/spec-graph.json). If the default is
52
+ # absent (it is gitignored), the graph is auto-built on the fly
53
+ # via build-spec-graph.mjs — no committed graph is required.
54
+ # --strict-stale Do NOT auto-rebuild a stale persisted graph — error (exit 1)
55
+ # instead. Useful in CI to assert the committed graph is fresh.
56
+ # --no-stale-check Skip the staleness check entirely (query the persisted graph
57
+ # as-is, even if the spec corpus has changed).
58
+ # -h | --help Show this help.
59
+ #
60
+ # Output:
61
+ # Default is compact, agent-consumable JSON (a JSON array for
62
+ # readers/writers/impact/search/load-set; a JSON object keyed by edge type
63
+ # for deps). load-set returns a sorted, de-duplicated array of repo-relative
64
+ # file paths. With --human, an aligned text table is printed instead.
65
+ #
66
+ # Exit codes: 0 = success, 1 = error (bad args, missing deps, unknown node).
67
+ #
68
+ # This is a UNIVERSAL-tier script (ships with the npm package). It is read-only:
69
+ # it never mutates the repo or the graph (it only writes a temp cache under
70
+ # $TMPDIR when it must auto-build, removed on exit). See
71
+ # devdocs/project/architecture.md for the scripts-layer precedence model and
72
+ # docs/spec-graph-schema.md for the graph schema.
73
+
74
+ set -euo pipefail
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # Paths & dependency checks
78
+ # ---------------------------------------------------------------------------
79
+
80
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
81
+ REPO_ROOT="$(dirname "$SCRIPT_DIR")"
82
+ BUILDER="$SCRIPT_DIR/build-spec-graph.mjs"
83
+ DEFAULT_GRAPH="$REPO_ROOT/.forgedock/graph/spec-graph.json"
84
+
85
+ # Exported so per-repo adaptive scripts can delegate (see architecture.md).
86
+ export FORGEDOCK_SCRIPTS="$SCRIPT_DIR"
87
+ export FORGEDOCK_HOME="$REPO_ROOT"
88
+
89
+ print_help() {
90
+ # Echo the leading comment block (between the shebang and `set -euo`).
91
+ sed -n '3,/^set -euo pipefail/p' "${BASH_SOURCE[0]}" \
92
+ | sed '$d' \
93
+ | sed 's/^# \{0,1\}//'
94
+ }
95
+
96
+ die() {
97
+ echo "ERROR: $*" >&2
98
+ exit 1
99
+ }
100
+
101
+ require_dep() {
102
+ command -v "$1" >/dev/null 2>&1 || die "required dependency '$1' not found on PATH"
103
+ }
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # Argument parsing
107
+ # ---------------------------------------------------------------------------
108
+
109
+ SUBCMD=""
110
+ ARG=""
111
+ HUMAN=0
112
+ GRAPH_PATH=""
113
+ STRICT_STALE=0
114
+ STALE_CHECK=1
115
+
116
+ if [ "$#" -eq 0 ]; then
117
+ print_help
118
+ exit 1
119
+ fi
120
+
121
+ POSITIONAL=()
122
+ while [ "$#" -gt 0 ]; do
123
+ case "$1" in
124
+ -h|--help)
125
+ print_help
126
+ exit 0
127
+ ;;
128
+ --human)
129
+ HUMAN=1
130
+ shift
131
+ ;;
132
+ --graph)
133
+ [ "$#" -ge 2 ] || die "--graph requires a path"
134
+ GRAPH_PATH="$2"
135
+ shift 2
136
+ ;;
137
+ --strict-stale)
138
+ STRICT_STALE=1
139
+ shift
140
+ ;;
141
+ --no-stale-check)
142
+ STALE_CHECK=0
143
+ shift
144
+ ;;
145
+ --)
146
+ shift
147
+ while [ "$#" -gt 0 ]; do POSITIONAL+=("$1"); shift; done
148
+ ;;
149
+ -*)
150
+ die "unknown flag: $1"
151
+ ;;
152
+ *)
153
+ POSITIONAL+=("$1")
154
+ shift
155
+ ;;
156
+ esac
157
+ done
158
+
159
+ if [ "${#POSITIONAL[@]}" -ge 1 ]; then SUBCMD="${POSITIONAL[0]}"; fi
160
+ if [ "${#POSITIONAL[@]}" -ge 2 ]; then ARG="${POSITIONAL[1]}"; fi
161
+ if [ "${#POSITIONAL[@]}" -gt 2 ]; then
162
+ die "too many arguments: expected '<subcommand> <arg>', got ${#POSITIONAL[@]} positionals"
163
+ fi
164
+
165
+ [ -n "$SUBCMD" ] || die "no subcommand given (one of: readers writers impact deps load-set search)"
166
+
167
+ case "$SUBCMD" in
168
+ readers|writers|impact|deps|load-set|search) ;;
169
+ *) die "unknown subcommand '$SUBCMD' (one of: readers writers impact deps load-set search)" ;;
170
+ esac
171
+
172
+ [ -n "$ARG" ] || die "subcommand '$SUBCMD' requires an argument"
173
+
174
+ require_dep jq
175
+
176
+ # ---------------------------------------------------------------------------
177
+ # Resolve the graph JSON (auto-build if absent — the graph is gitignored)
178
+ # ---------------------------------------------------------------------------
179
+
180
+ GRAPH=""
181
+ TMP_GRAPH=""
182
+
183
+ cleanup() {
184
+ [ -n "$TMP_GRAPH" ] && [ -f "$TMP_GRAPH" ] && rm -f "$TMP_GRAPH" || true
185
+ }
186
+ trap cleanup EXIT
187
+
188
+ # Rebuild the graph to a temp file and point GRAPH at it. Shared by the
189
+ # absent-graph auto-build path and the stale-graph rebuild path.
190
+ build_graph_to_temp() {
191
+ require_dep node
192
+ [ -f "$BUILDER" ] || die "graph is absent and builder not found: $BUILDER"
193
+ TMP_GRAPH="$(mktemp "${TMPDIR:-/tmp}/spec-graph.XXXXXX.json")"
194
+ if ! node "$BUILDER" --root "$REPO_ROOT" --stdout --quiet >"$TMP_GRAPH" 2>/dev/null; then
195
+ die "failed to build spec graph via $BUILDER"
196
+ fi
197
+ GRAPH="$TMP_GRAPH"
198
+ }
199
+
200
+ # Staleness check for a PERSISTED graph (default path or --graph). Cheaply
201
+ # recompute the current spec-corpus fingerprint and compare to the graph's
202
+ # stored builtFromHash. On mismatch: warn (stderr) and rebuild, unless
203
+ # --strict-stale (error) is set. A graph with no builtFromHash (pre-staleness
204
+ # schema) is treated as stale. Diagnostics go to stderr only; stdout stays pure.
205
+ check_staleness() {
206
+ [ "$STALE_CHECK" -eq 1 ] || return 0
207
+ # `node` is required to recompute the hash; if it is unavailable, skip the
208
+ # check rather than failing an otherwise-valid query.
209
+ command -v node >/dev/null 2>&1 || return 0
210
+ [ -f "$BUILDER" ] || return 0
211
+
212
+ local stored current
213
+ stored="$(jq -r '.graph.builtFromHash // ""' "$GRAPH" 2>/dev/null || echo "")"
214
+ current="$(node "$BUILDER" --root "$REPO_ROOT" --hash --quiet 2>/dev/null || echo "")"
215
+
216
+ # If we could not compute the current hash, do not block the query.
217
+ [ -n "$current" ] || return 0
218
+ # Fresh: stored hash matches current corpus → use the persisted graph as-is.
219
+ [ "$stored" = "$current" ] && return 0
220
+
221
+ # Stale (mismatch, or missing builtFromHash on an older graph).
222
+ if [ "$STRICT_STALE" -eq 1 ]; then
223
+ die "stale-graph: persisted graph ($GRAPH) is out of date — spec corpus changed since it was built (run: node $BUILDER). --strict-stale set, refusing to auto-rebuild."
224
+ fi
225
+ echo "stale-graph: spec corpus changed since graph was built — rebuilding for this query" >&2
226
+ build_graph_to_temp
227
+ }
228
+
229
+ resolve_graph() {
230
+ if [ -n "$GRAPH_PATH" ]; then
231
+ [ -f "$GRAPH_PATH" ] || die "graph file not found: $GRAPH_PATH"
232
+ GRAPH="$GRAPH_PATH"
233
+ check_staleness
234
+ return
235
+ fi
236
+ if [ -f "$DEFAULT_GRAPH" ]; then
237
+ GRAPH="$DEFAULT_GRAPH"
238
+ check_staleness
239
+ return
240
+ fi
241
+ # Auto-build to a temp file (the default path is gitignored / may not exist).
242
+ # A freshly built graph is current by construction — no staleness check needed.
243
+ build_graph_to_temp
244
+ }
245
+
246
+ resolve_graph
247
+
248
+ # Validate the graph document shape early so jq filters can assume it.
249
+ jq -e '.graph.nodes and .graph.edges' "$GRAPH" >/dev/null 2>&1 \
250
+ || die "not a valid spec-graph document (missing .graph.nodes/.graph.edges): $GRAPH"
251
+
252
+ # ---------------------------------------------------------------------------
253
+ # Argument normalization to canonical node ids
254
+ # annotation -> ann:FORGE:<NAME>
255
+ # label -> label:workflow:<name>
256
+ # script -> script:<file>.sh
257
+ # command -> cmd:<name[:sub[:sub]]>
258
+ # ---------------------------------------------------------------------------
259
+
260
+ normalize_ann() {
261
+ # Accept: CONTRACT | FORGE:CONTRACT | ann:FORGE:CONTRACT
262
+ local a="$1"
263
+ case "$a" in
264
+ ann:FORGE:*) echo "$a" ;;
265
+ FORGE:*) echo "ann:$a" ;;
266
+ *) echo "ann:FORGE:$a" ;;
267
+ esac
268
+ }
269
+
270
+ normalize_cmd() {
271
+ # Accept: work-on | work-on:build:implement | cmd:work-on
272
+ local c="$1"
273
+ case "$c" in
274
+ cmd:*) echo "$c" ;;
275
+ *) echo "cmd:$c" ;;
276
+ esac
277
+ }
278
+
279
+ # For `impact`, the node may be any type. Resolve to a canonical id by trying,
280
+ # in order: exact id match in the graph, then type-specific normalizers.
281
+ normalize_node() {
282
+ local n="$1"
283
+ # Already a fully-qualified id present in the graph?
284
+ if node_exists "$n"; then echo "$n"; return; fi
285
+ case "$n" in
286
+ ann:*|label:*|script:*|cmd:*|devdoc:*)
287
+ echo "$n" ;; # explicit prefix, trust it
288
+ FORGE:*)
289
+ echo "ann:$n" ;;
290
+ workflow:*)
291
+ echo "label:$n" ;;
292
+ *.sh)
293
+ echo "script:$n" ;;
294
+ *)
295
+ # Bare token: prefer annotation, then command, then label — whichever
296
+ # exists in the graph. Fall back to annotation form.
297
+ if node_exists "ann:FORGE:$n"; then echo "ann:FORGE:$n"
298
+ elif node_exists "cmd:$n"; then echo "cmd:$n"
299
+ elif node_exists "label:workflow:$n"; then echo "label:workflow:$n"
300
+ elif node_exists "script:$n"; then echo "script:$n"
301
+ else echo "ann:FORGE:$n"
302
+ fi
303
+ ;;
304
+ esac
305
+ }
306
+
307
+ node_exists() {
308
+ jq -e --arg id "$1" 'any(.graph.nodes[]; .id == $id)' "$GRAPH" >/dev/null 2>&1
309
+ }
310
+
311
+ # ---------------------------------------------------------------------------
312
+ # Queries
313
+ # ---------------------------------------------------------------------------
314
+
315
+ # List .from of edges of a given type pointing at a target node. JSON array.
316
+ query_edge_from() {
317
+ local etype="$1" target="$2"
318
+ jq -c --arg t "$etype" --arg to "$target" \
319
+ '[.graph.edges[] | select(.type == $t and .to == $to) | .from] | unique' "$GRAPH"
320
+ }
321
+
322
+ # All outbound edges from a command, grouped by edge type. JSON object.
323
+ query_deps() {
324
+ local from="$1"
325
+ jq -c --arg from "$from" '
326
+ [.graph.edges[] | select(.from == $from)]
327
+ | reduce .[] as $e ({}; .[$e.type] += [$e.to])
328
+ | map_values(unique)
329
+ ' "$GRAPH"
330
+ }
331
+
332
+ # Transitive reverse-reachability: everything that (transitively) depends on the
333
+ # seed node. Walk edges where .to is in the frontier, collecting .from, to a
334
+ # fixpoint. Excludes the seed. JSON array of node ids.
335
+ query_impact() {
336
+ local seed="$1"
337
+ jq -c --arg seed "$seed" '
338
+ .graph.edges as $edges
339
+ | def step(set):
340
+ ( [ $edges[] | select(.to as $to | set | index($to)) | .from ] ) as $froms
341
+ | (set + $froms | unique) as $next
342
+ | if ($next | length) == (set | length) then set else step($next) end;
343
+ (step([$seed]) - [$seed]) | unique
344
+ ' "$GRAPH"
345
+ }
346
+
347
+ # Transitive FORWARD-reachability: the minimal spec set to read for a command.
348
+ # Seeds at the command node, then walks CONTAINS (sub-phases) and REQUIRES
349
+ # (devdocs) edges in the forward direction (.from in frontier -> collect .to) to
350
+ # a fixpoint. The resolved node ids are mapped to their repo-relative `.path`
351
+ # (nodes without a path — e.g. annotations — are dropped). Includes the seed
352
+ # command's own path. Output is a sorted, de-duplicated JSON array of paths.
353
+ #
354
+ # This is the inverse of query_impact (forward instead of reverse) and shares
355
+ # its cycle-safe length-equality termination, so a spurious self-CONTAINS edge
356
+ # cannot cause a non-terminating walk. Only CONTAINS/REQUIRES are followed —
357
+ # READS/WRITES/TRANSITIONS/INVOKES are NOT part of the spec-read set (annotations
358
+ # and labels have no spec file; scripts are resolved separately at invoke time).
359
+ query_load_set() {
360
+ local seed="$1"
361
+ jq -c --arg seed "$seed" '
362
+ [.graph.edges[] | select(.type == "CONTAINS" or .type == "REQUIRES")] as $edges
363
+ | (reduce .graph.nodes[] as $n ({}; .[$n.id] = ($n.path // null))) as $path
364
+ | def step(set):
365
+ ( [ $edges[] | select(.from as $f | set | index($f)) | .to ] ) as $tos
366
+ | (set + $tos | unique) as $next
367
+ | if ($next | length) == (set | length) then set else step($next) end;
368
+ step([$seed])
369
+ | map($path[.] // empty)
370
+ | unique
371
+ ' "$GRAPH"
372
+ }
373
+
374
+ # Substring search over node id / name / path. JSON array of {id,type,name}.
375
+ query_search() {
376
+ local term="$1"
377
+ jq -c --arg term "$term" '
378
+ ($term | ascii_downcase) as $t
379
+ | [ .graph.nodes[]
380
+ | select(
381
+ (.id | ascii_downcase | contains($t)) or
382
+ (.name | ascii_downcase | contains($t)) or
383
+ ((.path // "") | ascii_downcase | contains($t))
384
+ )
385
+ | {id, type, name} ]
386
+ | sort_by(.id)
387
+ ' "$GRAPH"
388
+ }
389
+
390
+ # ---------------------------------------------------------------------------
391
+ # Human-readable rendering
392
+ # ---------------------------------------------------------------------------
393
+
394
+ render_list_human() {
395
+ # stdin: JSON array of strings. Print one per line, or "(none)".
396
+ local title="$1"
397
+ local rows
398
+ rows="$(jq -r '.[]' 2>/dev/null || true)"
399
+ echo "$title"
400
+ if [ -z "$rows" ]; then
401
+ echo " (none)"
402
+ else
403
+ echo "$rows" | sed 's/^/ /'
404
+ fi
405
+ }
406
+
407
+ render_deps_human() {
408
+ # stdin: JSON object {EDGE_TYPE: [to, ...]}. Group by edge type.
409
+ local obj
410
+ obj="$(cat)"
411
+ if [ "$(echo "$obj" | jq 'length')" -eq 0 ]; then
412
+ echo "(no outbound edges)"
413
+ return
414
+ fi
415
+ echo "$obj" | jq -r 'to_entries[] | "\(.key):\n" + ([.value[] | " " + .] | join("\n"))'
416
+ }
417
+
418
+ render_search_human() {
419
+ # stdin: JSON array of {id,type,name}. Aligned 2-col table.
420
+ jq -r '.[] | "\(.type)\t\(.id)"' | column -t -s $'\t' 2>/dev/null \
421
+ || jq -r '.[] | "\(.type) \(.id)"'
422
+ }
423
+
424
+ # ---------------------------------------------------------------------------
425
+ # Dispatch
426
+ # ---------------------------------------------------------------------------
427
+
428
+ case "$SUBCMD" in
429
+ readers)
430
+ target="$(normalize_ann "$ARG")"
431
+ result="$(query_edge_from READS "$target")"
432
+ if [ "$HUMAN" -eq 1 ]; then
433
+ echo "$result" | render_list_human "Specs that READ $target:"
434
+ else
435
+ echo "$result"
436
+ fi
437
+ ;;
438
+ writers)
439
+ target="$(normalize_ann "$ARG")"
440
+ result="$(query_edge_from WRITES "$target")"
441
+ if [ "$HUMAN" -eq 1 ]; then
442
+ echo "$result" | render_list_human "Specs that WRITE $target:"
443
+ else
444
+ echo "$result"
445
+ fi
446
+ ;;
447
+ deps)
448
+ from="$(normalize_cmd "$ARG")"
449
+ node_exists "$from" || die "no command/spec node '$from' in the graph (try: graph-query.sh search ${ARG})"
450
+ result="$(query_deps "$from")"
451
+ if [ "$HUMAN" -eq 1 ]; then
452
+ echo "Dependencies of $from:"
453
+ echo "$result" | render_deps_human
454
+ else
455
+ echo "$result"
456
+ fi
457
+ ;;
458
+ load-set)
459
+ from="$(normalize_cmd "$ARG")"
460
+ node_exists "$from" || die "no command/spec node '$from' in the graph (try: graph-query.sh search ${ARG})"
461
+ result="$(query_load_set "$from")"
462
+ if [ "$HUMAN" -eq 1 ]; then
463
+ echo "$result" | render_list_human "Minimal spec set for $from:"
464
+ else
465
+ echo "$result"
466
+ fi
467
+ ;;
468
+ impact)
469
+ seed="$(normalize_node "$ARG")"
470
+ node_exists "$seed" || die "no node '$seed' in the graph (try: graph-query.sh search ${ARG})"
471
+ result="$(query_impact "$seed")"
472
+ if [ "$HUMAN" -eq 1 ]; then
473
+ echo "$result" | render_list_human "Impact (blast radius) of changing $seed:"
474
+ else
475
+ echo "$result"
476
+ fi
477
+ ;;
478
+ search)
479
+ result="$(query_search "$ARG")"
480
+ if [ "$HUMAN" -eq 1 ]; then
481
+ echo "$result" | render_search_human
482
+ else
483
+ echo "$result"
484
+ fi
485
+ ;;
486
+ esac
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env bash
2
+ # transition-label.sh — Workflow label state machine for ForgeDock pipeline
3
+ #
4
+ # Usage: transition-label.sh <ISSUE_NUMBER> <GH_FLAG...> <TARGET_STATE>
5
+ #
6
+ # ISSUE_NUMBER GitHub issue number (e.g. 674)
7
+ # GH_FLAG Repository flag passed to gh (e.g. -R RapierCraftStudios/forgedock)
8
+ # May be multiple tokens — pass before TARGET_STATE
9
+ # TARGET_STATE One of: investigating, ready-to-build, building, in-review,
10
+ # merged, invalid, decomposed
11
+ #
12
+ # Examples:
13
+ # transition-label.sh 674 -R RapierCraftStudios/forgedock investigating
14
+ # transition-label.sh 674 -R owner/repo ready-to-build
15
+ #
16
+ # Behavior:
17
+ # 1. Validates ISSUE_NUMBER and TARGET_STATE
18
+ # 2. Verifies the issue exists (exits 1 if not)
19
+ # 3. Adds workflow:{TARGET_STATE} label
20
+ # 4. Removes all other workflow:* labels atomically
21
+ #
22
+ # Exit codes: 0 = success, 1 = error (bad args, issue not found, gh failure)
23
+
24
+ set -euo pipefail
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Argument parsing
28
+ # Signature: <ISSUE_NUMBER> [GH_FLAG...] <TARGET_STATE>
29
+ # We consume ISSUE_NUMBER as $1, TARGET_STATE as the last arg, everything
30
+ # in between is GH_FLAG (e.g. -R RapierCraftStudios/forgedock).
31
+ # ---------------------------------------------------------------------------
32
+
33
+ if [ "$#" -lt 2 ]; then
34
+ echo "ERROR: Usage: transition-label.sh <ISSUE_NUMBER> [GH_FLAG...] <TARGET_STATE>" >&2
35
+ echo " Example: transition-label.sh 674 -R RapierCraftStudios/forgedock investigating" >&2
36
+ exit 1
37
+ fi
38
+
39
+ ISSUE_NUMBER="$1"
40
+ shift
41
+
42
+ # Last arg is TARGET_STATE; everything remaining before it is GH_FLAG
43
+ # Build an array of all remaining args, then split off the last one.
44
+ ALL_REMAINING=("$@")
45
+ LAST_INDEX=$(( ${#ALL_REMAINING[@]} - 1 ))
46
+ TARGET_STATE="${ALL_REMAINING[$LAST_INDEX]}"
47
+ GH_ARGS=("${ALL_REMAINING[@]:0:$LAST_INDEX}")
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Validate ISSUE_NUMBER
51
+ # ---------------------------------------------------------------------------
52
+ if ! [[ "$ISSUE_NUMBER" =~ ^[0-9]+$ ]]; then
53
+ echo "ERROR: ISSUE_NUMBER must be a positive integer, got: '$ISSUE_NUMBER'" >&2
54
+ exit 1
55
+ fi
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Valid workflow states (complete list — all workflow:* labels)
59
+ # ---------------------------------------------------------------------------
60
+ VALID_STATES=(
61
+ "investigating"
62
+ "ready-to-build"
63
+ "building"
64
+ "in-review"
65
+ "merged"
66
+ "invalid"
67
+ "decomposed"
68
+ )
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Validate TARGET_STATE
72
+ # ---------------------------------------------------------------------------
73
+ VALID=0
74
+ for state in "${VALID_STATES[@]}"; do
75
+ if [ "$state" = "$TARGET_STATE" ]; then
76
+ VALID=1
77
+ break
78
+ fi
79
+ done
80
+
81
+ if [ "$VALID" -eq 0 ]; then
82
+ echo "ERROR: Unknown target state: '$TARGET_STATE'" >&2
83
+ echo "Valid states:" >&2
84
+ for state in "${VALID_STATES[@]}"; do
85
+ echo " $state" >&2
86
+ done
87
+ exit 1
88
+ fi
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Verify issue exists before mutating labels
92
+ # ---------------------------------------------------------------------------
93
+ if ! gh issue view "$ISSUE_NUMBER" "${GH_ARGS[@]}" --json number >/dev/null 2>&1; then
94
+ echo "ERROR: Issue #$ISSUE_NUMBER not found (GH_FLAG: ${GH_ARGS[*]:-<none>})" >&2
95
+ exit 1
96
+ fi
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # Build remove list: all valid states except the target
100
+ # ---------------------------------------------------------------------------
101
+ REMOVE_LABELS=""
102
+ for state in "${VALID_STATES[@]}"; do
103
+ if [ "$state" != "$TARGET_STATE" ]; then
104
+ if [ -n "$REMOVE_LABELS" ]; then
105
+ REMOVE_LABELS="$REMOVE_LABELS,workflow:$state"
106
+ else
107
+ REMOVE_LABELS="workflow:$state"
108
+ fi
109
+ fi
110
+ done
111
+
112
+ # Export universal script environment so per-repo scripts can call back into universal scripts.
113
+ # Per-repo scripts (.forgedock/scripts/{operation}.sh) source these to delegate to universal ones.
114
+ export FORGEDOCK_SCRIPTS
115
+ FORGEDOCK_SCRIPTS="$(cd "$(dirname "$0")" && pwd)"
116
+ export FORGEDOCK_HOME
117
+ FORGEDOCK_HOME="$(cd "$(dirname "$0")/.." && pwd)"
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # FORGE_LABEL_MAP — optional env var (JSON object) from forge.yaml → learned.label_map
121
+ #
122
+ # If set and non-empty (not '{}'), look up "workflow:$TARGET_STATE" in the map.
123
+ # If a mapping exists, use the mapped label name instead of the canonical one.
124
+ # This allows repos with non-standard label naming to use ForgeDock without
125
+ # renaming their labels to match the canonical workflow:* format.
126
+ #
127
+ # Set by work-on.md Phase 0B.1: export FORGE_LABEL_MAP="$LEARNED_LABEL_MAP"
128
+ # Falls back to canonical label if: FORGE_LABEL_MAP is unset/empty/{}, jq
129
+ # is not available, or no mapping exists for the target state.
130
+ #
131
+ # Note: --remove-label always uses canonical workflow:* format — it clears all
132
+ # canonical labels regardless of mapping, which is correct cleanup behavior.
133
+ # ---------------------------------------------------------------------------
134
+ CANONICAL_LABEL="workflow:$TARGET_STATE"
135
+ EFFECTIVE_LABEL="$CANONICAL_LABEL"
136
+
137
+ if [ -n "${FORGE_LABEL_MAP:-}" ] && [ "${FORGE_LABEL_MAP:-}" != "{}" ]; then
138
+ if command -v jq >/dev/null 2>&1; then
139
+ MAPPED=$(echo "$FORGE_LABEL_MAP" | jq -r --arg key "$CANONICAL_LABEL" '.[$key] // empty' 2>/dev/null || true)
140
+ if [ -n "$MAPPED" ]; then
141
+ # Validate: reject any mapped value starting with '-' to prevent CLI flag injection.
142
+ # A forge.yaml learned.label_map entry like "workflow:investigating": "--json" would
143
+ # otherwise be passed directly to gh issue edit --add-label, interpreted as a flag.
144
+ if [[ "$MAPPED" == -* ]]; then
145
+ echo "ERROR: FORGE_LABEL_MAP value for '$CANONICAL_LABEL' is not a valid label name: '$MAPPED'" >&2
146
+ echo " Label names must not start with '-'. Using canonical label fallback: $CANONICAL_LABEL" >&2
147
+ else
148
+ EFFECTIVE_LABEL="$MAPPED"
149
+ echo "Label map override: $CANONICAL_LABEL → $EFFECTIVE_LABEL"
150
+ fi
151
+ fi
152
+ else
153
+ echo "WARNING: FORGE_LABEL_MAP is set but jq is not available — using canonical label ($CANONICAL_LABEL)" >&2
154
+ fi
155
+ fi
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # Add target label
159
+ # ---------------------------------------------------------------------------
160
+ echo "Adding $EFFECTIVE_LABEL to issue #$ISSUE_NUMBER..."
161
+ gh issue edit "$ISSUE_NUMBER" "${GH_ARGS[@]}" --add-label "$EFFECTIVE_LABEL"
162
+
163
+ # ---------------------------------------------------------------------------
164
+ # Remove all other workflow:* labels (best-effort — labels may not all exist)
165
+ # ---------------------------------------------------------------------------
166
+ echo "Removing stale workflow:* labels ($REMOVE_LABELS)..."
167
+ gh issue edit "$ISSUE_NUMBER" "${GH_ARGS[@]}" --remove-label "$REMOVE_LABELS" 2>/dev/null || true
168
+
169
+ echo "OK: $EFFECTIVE_LABEL set on issue #$ISSUE_NUMBER"
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env bash
2
+ # validate-pr-target.sh — Hard-fail if PR base branch doesn't match classified lane
3
+ #
4
+ # Usage: validate-pr-target.sh <actual_base> <classified_lane>
5
+ # actual_base: the branch the PR will target (e.g. staging, milestone/deterministic-pipeline-v2)
6
+ # classified_lane: the lane produced by classify-lane.sh (e.g. staging, milestone/deterministic-pipeline-v2)
7
+ #
8
+ # Output: Structured finding (prefixed with severity)
9
+ # BLOCKING: <message> — mismatch detected; pipeline must not continue
10
+ # OK: <message> — base matches classified lane
11
+ #
12
+ # Exit codes: 0 = match (PR target is correct), 1 = mismatch (hard-fail)
13
+
14
+ set -euo pipefail
15
+
16
+ if [ $# -ne 2 ]; then
17
+ echo "BLOCKING: validate-pr-target.sh requires exactly 2 arguments: <actual_base> <classified_lane>" >&2
18
+ exit 1
19
+ fi
20
+
21
+ ACTUAL_BASE="$1"
22
+ CLASSIFIED_LANE="$2"
23
+
24
+ if [ "$ACTUAL_BASE" = "$CLASSIFIED_LANE" ]; then
25
+ echo "OK: PR target '${ACTUAL_BASE}' matches classified lane"
26
+ exit 0
27
+ else
28
+ echo "BLOCKING: PR target '${ACTUAL_BASE}' does not match classified lane '${CLASSIFIED_LANE}'"
29
+ exit 1
30
+ fi