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,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
|
|
@@ -138,8 +138,16 @@ if [ -n "${FORGE_LABEL_MAP:-}" ] && [ "${FORGE_LABEL_MAP:-}" != "{}" ]; then
|
|
|
138
138
|
if command -v jq >/dev/null 2>&1; then
|
|
139
139
|
MAPPED=$(echo "$FORGE_LABEL_MAP" | jq -r --arg key "$CANONICAL_LABEL" '.[$key] // empty' 2>/dev/null || true)
|
|
140
140
|
if [ -n "$MAPPED" ]; then
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
143
151
|
fi
|
|
144
152
|
else
|
|
145
153
|
echo "WARNING: FORGE_LABEL_MAP is set but jq is not available — using canonical label ($CANONICAL_LABEL)" >&2
|