aw-spec-cli 0.1.23 → 0.1.24
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/assets/.appweaver/ORCH-DIALECT.md +3 -3
- package/assets/.appweaver/RULE-PARTITION.md +41 -28
- package/assets/.appweaver/orchestrator/orchestrator.py +92 -14
- package/assets/.appweaver/orchestrator/rev-templates/ai-rules.reference.md +3 -3
- package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +19 -12
- package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +25 -17
- package/assets/.appweaver/orchestrator/rev-templates/domain-requirements.skeleton.md +2 -2
- package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +10 -10
- package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +28 -17
- package/assets/.appweaver/orchestrator/rev-templates/technical-constitution.skeleton.md +22 -24
- package/assets/.appweaver/orchestrator/rev-templates/technical-rule-file.skeleton.md +1 -1
- package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +29 -19
- package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +27 -20
- package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +184 -37
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +18 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +19 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +18 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-inventory.orch.yaml +55 -29
- package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +650 -205
- package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +19 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-feature.orch.yaml +5 -5
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +2 -2
- package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +13 -3
- package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +40 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-workflow-model.orch.yaml +2 -1
- package/assets/.appweaver/scripts/bash/aw-rev-constitution.sh +1 -1
- package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +69 -7
- package/assets/.appweaver/scripts/powershell/aw-rev-constitution.ps1 +1 -1
- package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +58 -2
- package/assets/.appweaver/scripts/python/aw-inventory.py +1809 -156
- package/assets/.appweaver/scripts/python/aw-rev-constitution.py +106 -145
- package/assets/.appweaver/scripts/python/aw-stack.py +45 -1
- package/assets/.appweaver/scripts/python/aw-xref.py +73 -2
- package/assets/.appweaver/scripts/python/validate_okf.py +10 -5
- package/assets/.appweaver/templates/okf-concept-template.md +10 -2
- package/assets/.appweaver/templates/proposed-inventory-template.md +100 -48
- package/assets/.appweaver/templates/source-map-template.md +14 -6
- package/package.json +1 -1
|
@@ -8,52 +8,50 @@ reverse-engineers that tier from what the code itself evidences, so forward
|
|
|
8
8
|
development of the SAME codebase runs governed exactly like greenfield (Change
|
|
9
9
|
Requests through the CR delta loop).
|
|
10
10
|
|
|
11
|
-
CALLER
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
This core survived the retirement unchanged because it is the MODEL-FREE half: it
|
|
22
|
-
enumerates whatever slices ARE adopted and scaffolds the
|
|
23
|
-
`guardrails/projects/brf-<domain>/` project directory; the calling workflow's authoring
|
|
24
|
-
agents fill its rule files + the split constitution. Governance stance is AS-BUILT
|
|
25
|
-
(the constitution reflects what the code does, not an aspiration — a separate
|
|
26
|
-
modernization/migration workflow drains the debt later).
|
|
11
|
+
CALLER: `/appweaver onboard` is the only caller of this core (workflow `scripts:` ref,
|
|
12
|
+
both shells). The first context to onboard authors the project tier; every later one
|
|
13
|
+
amends it additively from its own code. The standalone
|
|
14
|
+
`/appweaver rev-constitution` workflow is retired, and adopt reconciles in its own
|
|
15
|
+
steps without calling this core.
|
|
16
|
+
|
|
17
|
+
This core is the MODEL-FREE half: it gathers the project framing the OKF bundle holds
|
|
18
|
+
and scaffolds the `guardrails/projects/brf-<domain>/` project directory; the calling
|
|
19
|
+
workflow's authoring agents fill its rule files + the split constitution. Governance
|
|
20
|
+
stance is AS-BUILT (the constitution reflects what the code does, not an aspiration).
|
|
27
21
|
|
|
28
22
|
Two subcommands, each printing a single JSON object as the LAST stdout line (so
|
|
29
23
|
the orchestrator's execute_script can bind it); human-readable summaries go to
|
|
30
24
|
STDERR so they never corrupt the JSON contract:
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
-> the
|
|
34
|
-
{
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
source-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
prompt (cite code paths, ignore the source-map's domain spec-ids / entity names).
|
|
26
|
+
project-context [--root <dir>] [--bundle <dir>]
|
|
27
|
+
-> the OKF bundle's project framing. Returns
|
|
28
|
+
{project_context[], technical_context[], project_name, overview, platform,
|
|
29
|
+
root}. The *_context arrays are workspace-relative paths ready for a
|
|
30
|
+
build_context `context_from:`. project_context is the domain side (overview +
|
|
31
|
+
aggregate index + platform note + bundle index); technical_context is the
|
|
32
|
+
domain-free side (overview + platform note). Neither carries a feature's
|
|
33
|
+
spec, model, plan or source-map: onboard's single oracle is the code, and a
|
|
34
|
+
feature adopted before the tier is authored must not steer project-wide
|
|
35
|
+
rules. `list-adopted` is the older name for the same subcommand and still
|
|
36
|
+
works; it answers with this framing and no longer lists features.
|
|
44
37
|
|
|
45
38
|
project-setup --name <domain> [--fallback <name>] [--root <dir>]
|
|
46
39
|
-> slugify + `brf-` prefix the domain, create guardrails/projects/<slug>/ with its
|
|
47
|
-
service/ frontend/ domain/ subfolders
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
pack_present
|
|
53
|
-
|
|
40
|
+
service/ frontend/ domain/ subfolders when the project does not exist yet.
|
|
41
|
+
Without --name, an ACTIVE `brf-` project (.appweaver/domain.json) that already
|
|
42
|
+
holds rule files wins over the fallback name, so every later context amends the
|
|
43
|
+
project the first one created even if the bundle's project name has drifted.
|
|
44
|
+
Returns {domain_slug, pack_dir, domain_rule_bindings, domain_constitution_source,
|
|
45
|
+
pack_present, domain_present, service_present, frontend_present,
|
|
46
|
+
domain_files[], service_files[], frontend_files[]}. Both maps sit at the project
|
|
47
|
+
root; every rule file, the domain charter included, sits in one of the three
|
|
48
|
+
subfolders. The *_present flags are STRINGS "true"/"false" (so a workflow
|
|
49
|
+
condition can compare them): pack_present = the project holds any .md file,
|
|
50
|
+
and each tier flag = that subfolder does. The *_files arrays are the tier's
|
|
51
|
+
rule files, workspace-relative, ready for a build_context `context_from:`.
|
|
54
52
|
`pack-setup` is the older name for the same subcommand and still works.
|
|
55
53
|
|
|
56
|
-
Usage: aw-rev-constitution.py <
|
|
54
|
+
Usage: aw-rev-constitution.py <project-context|project-setup> [options]
|
|
57
55
|
"""
|
|
58
56
|
|
|
59
57
|
import argparse
|
|
@@ -80,8 +78,7 @@ def _emit(obj):
|
|
|
80
78
|
|
|
81
79
|
|
|
82
80
|
def _uniq(seq):
|
|
83
|
-
"""Order-preserving de-dup (context
|
|
84
|
-
sources — the OKF overview belongs to several of them)."""
|
|
81
|
+
"""Order-preserving de-dup (a context array can name the same bundle file twice)."""
|
|
85
82
|
seen, out = set(), []
|
|
86
83
|
for x in seq:
|
|
87
84
|
if x and x not in seen:
|
|
@@ -109,52 +106,12 @@ def _project_name(overview_abs):
|
|
|
109
106
|
return b.group(1).strip() if b else ""
|
|
110
107
|
|
|
111
108
|
|
|
112
|
-
#
|
|
113
|
-
def
|
|
109
|
+
# ────────────────────────────── project-context ────────────────────────────
|
|
110
|
+
def cmd_project_context(args):
|
|
114
111
|
root = _root(args.root)
|
|
115
|
-
specs_dir = os.path.join(root, args.specs)
|
|
116
112
|
bundle_dir = os.path.join(root, args.bundle)
|
|
117
113
|
|
|
118
|
-
|
|
119
|
-
spec_context = []
|
|
120
|
-
design_context = []
|
|
121
|
-
source_maps = []
|
|
122
|
-
reverse_count = 0
|
|
123
|
-
|
|
124
|
-
if os.path.isdir(specs_dir):
|
|
125
|
-
for name in sorted(os.listdir(specs_dir)):
|
|
126
|
-
sdir = os.path.join(specs_dir, name)
|
|
127
|
-
if not os.path.isdir(sdir):
|
|
128
|
-
continue
|
|
129
|
-
spec = os.path.join(sdir, "spec.md")
|
|
130
|
-
design = os.path.join(sdir, "design.md")
|
|
131
|
-
# A slice is adoptable-governance input only when BOTH docs exist.
|
|
132
|
-
if not (os.path.isfile(spec) and os.path.isfile(design)):
|
|
133
|
-
continue
|
|
134
|
-
smap = os.path.join(sdir, "source-map.md")
|
|
135
|
-
is_reverse = False
|
|
136
|
-
try:
|
|
137
|
-
with open(spec, "r", encoding="utf-8", errors="replace") as fh:
|
|
138
|
-
head = fh.read(4096)
|
|
139
|
-
if re.search(r"reverse-engineered", head, re.IGNORECASE):
|
|
140
|
-
is_reverse = True
|
|
141
|
-
except OSError:
|
|
142
|
-
pass
|
|
143
|
-
if is_reverse:
|
|
144
|
-
reverse_count += 1
|
|
145
|
-
slices.append({
|
|
146
|
-
"name": name,
|
|
147
|
-
"spec": _rel(spec, root),
|
|
148
|
-
"design": _rel(design, root),
|
|
149
|
-
"source_map": _rel(smap, root) if os.path.isfile(smap) else "",
|
|
150
|
-
"origin": "reverse-engineered" if is_reverse else "unknown",
|
|
151
|
-
})
|
|
152
|
-
spec_context.append(_rel(spec, root))
|
|
153
|
-
design_context.append(_rel(design, root))
|
|
154
|
-
if os.path.isfile(smap):
|
|
155
|
-
source_maps.append(_rel(smap, root))
|
|
156
|
-
|
|
157
|
-
# The OKF architecture overview names the actual tech stack (technology half input).
|
|
114
|
+
# The OKF architecture overview names the actual tech stack.
|
|
158
115
|
overview = ""
|
|
159
116
|
overview_abs = ""
|
|
160
117
|
for cand in ("architecture/overview.md", "architecture/architecture.md"):
|
|
@@ -164,26 +121,18 @@ def cmd_list_adopted(args):
|
|
|
164
121
|
overview_abs = p
|
|
165
122
|
break
|
|
166
123
|
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
# technical author routes any unmet template ideal / stub to, instead of asserting it).
|
|
171
|
-
# These feed the DESIGN side so the technical half is EVIDENCE-FIRST, not template-first.
|
|
124
|
+
# The shared-platform note: cross-cutting persistence / tenancy / concurrency as
|
|
125
|
+
# built, including its Open Questions (tenancy may be inactive). Domain-free, so it
|
|
126
|
+
# is technical evidence as well as project framing.
|
|
172
127
|
platform_ref = ""
|
|
173
128
|
p = os.path.join(bundle_dir, "architecture/platform.md")
|
|
174
129
|
if os.path.isfile(p):
|
|
175
130
|
platform_ref = _rel(p, root)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
# Project-level OKF framing — the PROJECT AS A WHOLE, not just one adopted slice:
|
|
182
|
-
# the overview (mission + aggregate map + external refs + stack), the aggregate
|
|
183
|
-
# index (bounded-context map), and the shared-platform note. These are what let the
|
|
184
|
-
# DOMAIN author write a project charter instead of a single-aggregate spec. The
|
|
185
|
-
# technical half deliberately does NOT get the aggregate index or specs (stays
|
|
186
|
-
# domain-free); it DOES get platform.md + the backlog as technical evidence (below).
|
|
131
|
+
|
|
132
|
+
# The PROJECT AS A WHOLE: the overview (mission + aggregate map + external refs +
|
|
133
|
+
# stack), the aggregate index (bounded-context map), the shared-platform note and
|
|
134
|
+
# the bundle index. This is what lets the DOMAIN author write a project charter
|
|
135
|
+
# instead of a single-aggregate spec.
|
|
187
136
|
project_context = [overview] if overview else []
|
|
188
137
|
for cand in ("aggregates/index.md", "architecture/platform.md", "index.md"):
|
|
189
138
|
p = os.path.join(bundle_dir, cand)
|
|
@@ -191,51 +140,28 @@ def cmd_list_adopted(args):
|
|
|
191
140
|
project_context.append(_rel(p, root))
|
|
192
141
|
project_context = _uniq(project_context)
|
|
193
142
|
|
|
143
|
+
# The technical half gets the stack and the platform note and nothing domain-bearing:
|
|
144
|
+
# no aggregate index, so it stays domain-agnostic.
|
|
145
|
+
technical_context = _uniq([overview, platform_ref])
|
|
146
|
+
|
|
194
147
|
# The display name of the whole project the governance tier governs (from the OKF
|
|
195
148
|
# overview), so the project + constitution are named + titled after the PROJECT — not the
|
|
196
149
|
# outer tooling folder the CWD-basename fallback would pick.
|
|
197
150
|
project_name = _project_name(overview_abs)
|
|
198
151
|
|
|
199
|
-
# spec side = domain input (specs + source-maps + PROJECT framing); design side =
|
|
200
|
-
# technology input (designs + OKF stack overview + shared-platform note + as-built
|
|
201
|
-
# backlog + the per-slice source-maps — the code-coordinate bridge, so the TECHNICAL
|
|
202
|
-
# half anchors each principle to a real code file/type, not just a design section;
|
|
203
|
-
# kept domain-agnostic by the author's prompt discipline, not by starving it of the
|
|
204
|
-
# code map — rich enough to DERIVE the principle set from evidence and route unmet
|
|
205
|
-
# ideals to debt); all = the rule-set author's full picture.
|
|
206
|
-
spec_side = _uniq(spec_context + source_maps + project_context)
|
|
207
|
-
design_side = _uniq(
|
|
208
|
-
design_context
|
|
209
|
-
+ source_maps
|
|
210
|
-
+ ([overview] if overview else [])
|
|
211
|
-
+ ([platform_ref] if platform_ref else [])
|
|
212
|
-
+ ([backlog_ref] if backlog_ref else [])
|
|
213
|
-
)
|
|
214
|
-
all_context = _uniq(spec_context + design_context + source_maps + project_context)
|
|
215
|
-
|
|
216
152
|
result = {
|
|
217
|
-
"slices": slices,
|
|
218
|
-
"slice_count": len(slices),
|
|
219
|
-
"reverse_count": reverse_count,
|
|
220
|
-
"spec_context": spec_side,
|
|
221
|
-
"design_context": design_side,
|
|
222
|
-
"all_context": all_context,
|
|
223
153
|
"project_context": project_context,
|
|
154
|
+
"technical_context": technical_context,
|
|
224
155
|
"project_name": project_name,
|
|
225
156
|
"overview": overview,
|
|
226
157
|
"platform": platform_ref,
|
|
227
|
-
"backlog": backlog_ref,
|
|
228
158
|
"root": root,
|
|
229
159
|
}
|
|
230
160
|
|
|
231
|
-
names = ", ".join(s["name"] for s in slices) or "(none)"
|
|
232
161
|
print(
|
|
233
|
-
f"[govern-backward]
|
|
234
|
-
f"
|
|
235
|
-
f"
|
|
236
|
-
f"overview: {overview or '(none — no OKF bundle)'}\n"
|
|
237
|
-
f" technical evidence: platform={platform_ref or '(none)'} "
|
|
238
|
-
f"backlog={backlog_ref or '(none)'}",
|
|
162
|
+
f"[govern-backward] project: {project_name or '(unknown — no OKF overview)'} "
|
|
163
|
+
f"overview: {overview or '(none — no OKF bundle)'} "
|
|
164
|
+
f"platform: {platform_ref or '(none)'}",
|
|
239
165
|
file=sys.stderr,
|
|
240
166
|
)
|
|
241
167
|
_emit(result)
|
|
@@ -250,9 +176,34 @@ def _slugify(name):
|
|
|
250
176
|
return s
|
|
251
177
|
|
|
252
178
|
|
|
179
|
+
def _md_files(d, root):
|
|
180
|
+
"""The .md files under one tier folder, workspace-relative and sorted."""
|
|
181
|
+
out = []
|
|
182
|
+
if os.path.isdir(d):
|
|
183
|
+
for dp, _, fs in os.walk(d):
|
|
184
|
+
out.extend(_rel(os.path.join(dp, f), root) for f in fs if f.endswith(".md"))
|
|
185
|
+
return sorted(out)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _active_brf_project(root):
|
|
189
|
+
"""The active project from .appweaver/domain.json when it is a brownfield (`brf-`)
|
|
190
|
+
project that already holds rule files, else "". A greenfield project the workspace
|
|
191
|
+
happens to be bound to is never taken over."""
|
|
192
|
+
try:
|
|
193
|
+
with open(os.path.join(root, ".appweaver", "domain.json"), encoding="utf-8") as fh:
|
|
194
|
+
name = (json.load(fh).get("domain") or "").strip()
|
|
195
|
+
except (OSError, ValueError, AttributeError):
|
|
196
|
+
return ""
|
|
197
|
+
if not name.startswith("brf-"):
|
|
198
|
+
return ""
|
|
199
|
+
d = os.path.join(root, "guardrails", "projects", name)
|
|
200
|
+
return name if _md_files(d, root) else ""
|
|
201
|
+
|
|
202
|
+
|
|
253
203
|
def cmd_pack_setup(args):
|
|
254
204
|
root = _root(args.root)
|
|
255
|
-
|
|
205
|
+
active = "" if (args.name and args.name.strip()) else _active_brf_project(root)
|
|
206
|
+
raw = args.name if (args.name and args.name.strip()) else (active or args.fallback)
|
|
256
207
|
if not raw or not raw.strip():
|
|
257
208
|
# Last-resort fallback: the component/repo directory name.
|
|
258
209
|
raw = os.path.basename(root.rstrip(os.sep)) or "domain"
|
|
@@ -264,16 +215,17 @@ def cmd_pack_setup(args):
|
|
|
264
215
|
domain_slug = "brf-" + slug
|
|
265
216
|
|
|
266
217
|
pack_dir = os.path.join(root, "guardrails", "projects", domain_slug)
|
|
267
|
-
#
|
|
268
|
-
#
|
|
269
|
-
#
|
|
270
|
-
#
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
218
|
+
# A project that already holds .md files was authored by an earlier context: the
|
|
219
|
+
# calling workflow AMENDS it rather than authoring it again. The walk covers the
|
|
220
|
+
# whole subtree, because a project's rule files live in its service/ frontend/
|
|
221
|
+
# domain/ subfolders and only the two maps sit at its root. Each tier is reported
|
|
222
|
+
# on its own, so a lane declared after the first onboard is authored in full while
|
|
223
|
+
# the tiers that exist are amended.
|
|
224
|
+
tiers = {sub: _md_files(os.path.join(pack_dir, sub), root)
|
|
225
|
+
for sub in ("domain", "service", "frontend")}
|
|
226
|
+
present = bool(_md_files(pack_dir, root))
|
|
227
|
+
for sub in ("service", "frontend", "domain"):
|
|
228
|
+
os.makedirs(os.path.join(pack_dir, sub), exist_ok=True)
|
|
277
229
|
|
|
278
230
|
result = {
|
|
279
231
|
"domain_slug": domain_slug,
|
|
@@ -287,11 +239,17 @@ def cmd_pack_setup(args):
|
|
|
287
239
|
os.path.join(pack_dir, "domain", "domain-constitution.md"), root
|
|
288
240
|
),
|
|
289
241
|
"pack_present": "true" if present else "false",
|
|
242
|
+
"domain_present": "true" if tiers["domain"] else "false",
|
|
243
|
+
"service_present": "true" if tiers["service"] else "false",
|
|
244
|
+
"frontend_present": "true" if tiers["frontend"] else "false",
|
|
245
|
+
"domain_files": tiers["domain"],
|
|
246
|
+
"service_files": tiers["service"],
|
|
247
|
+
"frontend_files": tiers["frontend"],
|
|
290
248
|
"root": root,
|
|
291
249
|
}
|
|
292
250
|
print(
|
|
293
251
|
f"[govern-backward] project: guardrails/projects/{domain_slug}/ "
|
|
294
|
-
f"({'EXISTS —
|
|
252
|
+
f"({'EXISTS — ' + ', '.join(f'{k} {len(v)}' for k, v in tiers.items()) if present else 'created'})",
|
|
295
253
|
file=sys.stderr,
|
|
296
254
|
)
|
|
297
255
|
_emit(result)
|
|
@@ -302,11 +260,14 @@ def main(argv):
|
|
|
302
260
|
ap = argparse.ArgumentParser(prog="aw-rev-constitution.py", add_help=True)
|
|
303
261
|
sub = ap.add_subparsers(dest="cmd")
|
|
304
262
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
263
|
+
# `list-adopted` is the older name, kept so a caller written against it still runs.
|
|
264
|
+
# It also still accepts `--specs`, which nothing reads now.
|
|
265
|
+
for name in ("project-context", "list-adopted"):
|
|
266
|
+
pc = sub.add_parser(name, help="the OKF bundle's project framing for the governance authors")
|
|
267
|
+
pc.add_argument("--root", default="")
|
|
268
|
+
pc.add_argument("--bundle", default="knowledge")
|
|
269
|
+
pc.add_argument("--specs", default="specs", help=argparse.SUPPRESS)
|
|
270
|
+
pc.set_defaults(func=cmd_project_context)
|
|
310
271
|
|
|
311
272
|
# `pack-setup` is the name this subcommand shipped under and is kept as an alias:
|
|
312
273
|
# the folder it creates is a PROJECT, so the primary name says so, and a caller
|
|
@@ -9,7 +9,8 @@ powershell twin so the two stay in parity by construction rather than by review:
|
|
|
9
9
|
detect match the repo's own files against those profiles — the BROWNFIELD
|
|
10
10
|
candidate list, since an existing repo already answered the question
|
|
11
11
|
declare upsert a lane into .appweaver/stacks.json — THE SOLE WRITER
|
|
12
|
-
resolve read .appweaver/stacks.json — for every downstream reader
|
|
12
|
+
resolve read .appweaver/stacks.json — for every downstream reader; with
|
|
13
|
+
--feature-dir, narrowed to the lanes that feature has a surface in
|
|
13
14
|
verify is the declaration USABLE for the phase about to run — the gate
|
|
14
15
|
run-phase run a phase's command for EVERY declared lane, not just the first
|
|
15
16
|
probe is there evidence of a lane of this kind — read-only, never fatal
|
|
@@ -964,8 +965,47 @@ def lane_options(repo, lanes):
|
|
|
964
965
|
return out
|
|
965
966
|
|
|
966
967
|
|
|
968
|
+
def feature_lanes(repo, feature_dir):
|
|
969
|
+
"""The lane kinds a FEATURE has a surface in, or None when it records none.
|
|
970
|
+
|
|
971
|
+
A repo declares its lanes once, in stacks.json; a feature inside it may still own no
|
|
972
|
+
part of one lane's surface — a job-only or shared-kernel slice in a repo that also has
|
|
973
|
+
a UI. adopt records that fact as `lanes` in the feature's active_version.json (written
|
|
974
|
+
by `feature-lanes`), and this reads it back. None — no feature named, no file, or no
|
|
975
|
+
record — means the feature follows every declared lane, which is the ordinary case.
|
|
976
|
+
"""
|
|
977
|
+
if not feature_dir:
|
|
978
|
+
return None
|
|
979
|
+
fd = feature_dir if os.path.isabs(feature_dir) else os.path.join(repo, feature_dir)
|
|
980
|
+
try:
|
|
981
|
+
with open(os.path.join(fd, "active_version.json"), encoding="utf-8") as fh:
|
|
982
|
+
recorded = json.load(fh).get("lanes")
|
|
983
|
+
except (OSError, ValueError, AttributeError):
|
|
984
|
+
return None
|
|
985
|
+
if not isinstance(recorded, list) or not recorded:
|
|
986
|
+
return None
|
|
987
|
+
return [k for k in ("backend", "frontend") if k in recorded]
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def apply_feature_lanes(payload, kinds):
|
|
991
|
+
"""Narrow the per-kind `<kind>_declared` flags to the lanes a feature has a surface in.
|
|
992
|
+
|
|
993
|
+
Only the per-kind flags move. `declared`, `lanes` and `kinds` stay the REPO's answer, so
|
|
994
|
+
a gate that asks "is anything declared at all" still reads stacks.json, while every
|
|
995
|
+
step that plans, gates or builds one lane reads the flag and skips a lane this feature
|
|
996
|
+
does not own. `<kind>_feature_excluded` says which case a false flag is.
|
|
997
|
+
"""
|
|
998
|
+
payload["feature_lanes"] = list(kinds or [])
|
|
999
|
+
for kind in ("backend", "frontend"):
|
|
1000
|
+
excluded = bool(kinds is not None and payload.get(kind + "_declared") and kind not in kinds)
|
|
1001
|
+
payload[kind + "_feature_excluded"] = excluded
|
|
1002
|
+
if excluded:
|
|
1003
|
+
payload[kind + "_declared"] = False
|
|
1004
|
+
|
|
1005
|
+
|
|
967
1006
|
def cmd_resolve(args):
|
|
968
1007
|
data = read_stacks(args.repo)
|
|
1008
|
+
fl = feature_lanes(args.repo, getattr(args, "feature_dir", ""))
|
|
969
1009
|
|
|
970
1010
|
# Absence is a valid state for READING, never a failure: `resolve` reports what
|
|
971
1011
|
# is there and lets the caller decide. It is `verify` that turns an absent or
|
|
@@ -982,6 +1022,7 @@ def cmd_resolve(args):
|
|
|
982
1022
|
}
|
|
983
1023
|
payload.update(kind_block(args.repo, [], "backend"))
|
|
984
1024
|
payload.update(kind_block(args.repo, [], "frontend"))
|
|
1025
|
+
apply_feature_lanes(payload, fl)
|
|
985
1026
|
emit(payload)
|
|
986
1027
|
return
|
|
987
1028
|
|
|
@@ -1022,6 +1063,7 @@ def cmd_resolve(args):
|
|
|
1022
1063
|
}
|
|
1023
1064
|
payload.update(kind_block(args.repo, lanes, "backend"))
|
|
1024
1065
|
payload.update(kind_block(args.repo, lanes, "frontend"))
|
|
1066
|
+
apply_feature_lanes(payload, fl)
|
|
1025
1067
|
emit(payload)
|
|
1026
1068
|
|
|
1027
1069
|
|
|
@@ -1447,6 +1489,8 @@ def main():
|
|
|
1447
1489
|
p = sub.add_parser("resolve", help="read stacks.json")
|
|
1448
1490
|
p.add_argument("--kind", choices=["backend", "frontend"])
|
|
1449
1491
|
p.add_argument("--id")
|
|
1492
|
+
p.add_argument("--feature-dir", dest="feature_dir", default="",
|
|
1493
|
+
help="report a lane this feature records no surface in as not declared")
|
|
1450
1494
|
p.set_defaults(func=cmd_resolve)
|
|
1451
1495
|
|
|
1452
1496
|
p = sub.add_parser("verify", help="is the declaration usable for the phase about to run")
|
|
@@ -451,7 +451,7 @@ def _read_fm(text):
|
|
|
451
451
|
|
|
452
452
|
def _render_fm(fm):
|
|
453
453
|
order = ["type", "title", "description", "origin", "bounded_context", "features",
|
|
454
|
-
"source_files", "ui_source_files"]
|
|
454
|
+
"adopted_features", "source_files", "ui_source_files"]
|
|
455
455
|
keys = [k for k in order if k in fm] + [k for k in fm if k not in order]
|
|
456
456
|
out = ["---"]
|
|
457
457
|
for k in keys:
|
|
@@ -1544,6 +1544,66 @@ def cmd_co_owners(args):
|
|
|
1544
1544
|
return 0
|
|
1545
1545
|
|
|
1546
1546
|
|
|
1547
|
+
def _map_features(root):
|
|
1548
|
+
"""(features, commit) from the confirmed boundary map — ([], "") where there is none."""
|
|
1549
|
+
try:
|
|
1550
|
+
with open(os.path.join(root, ".appweaver", "inventory", "feature-map.json"),
|
|
1551
|
+
encoding="utf-8") as fh:
|
|
1552
|
+
m = json.load(fh)
|
|
1553
|
+
except (OSError, ValueError):
|
|
1554
|
+
return [], ""
|
|
1555
|
+
return (m.get("features") or []), (m.get("commit") or "")
|
|
1556
|
+
|
|
1557
|
+
|
|
1558
|
+
def _operation_scope(root, feature, siblings):
|
|
1559
|
+
"""The operations the boundary map gives this feature, and the ones it gives ONLY to a
|
|
1560
|
+
sibling feature over the same aggregate.
|
|
1561
|
+
|
|
1562
|
+
A feature is a capability scope the developer confirmed in the map, and two features
|
|
1563
|
+
over one aggregate share its code: every file of Encounter serves RecordEncounter and
|
|
1564
|
+
AmendEncounter alike. The operation list is what tells their scopes apart. A read both
|
|
1565
|
+
features call (GetEncounter) is in both lists, so it is this feature's too and never a
|
|
1566
|
+
sibling's alone."""
|
|
1567
|
+
mapped, commit = _map_features(root)
|
|
1568
|
+
by = {_key(f.get("feature")): [o for o in dict.fromkeys(f.get("operations") or []) if o]
|
|
1569
|
+
for f in mapped}
|
|
1570
|
+
mine = by.get(_key(feature), [])
|
|
1571
|
+
others = {}
|
|
1572
|
+
for s in siblings:
|
|
1573
|
+
only = [o for o in by.get(_key(s), []) if o not in mine]
|
|
1574
|
+
if only:
|
|
1575
|
+
others[s] = only
|
|
1576
|
+
return mine, others, commit
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
def _operations_note(mine, others):
|
|
1580
|
+
"""One prompt-ready line: this feature's operations and the ones that are not its own."""
|
|
1581
|
+
if not mine:
|
|
1582
|
+
return ("The boundary map records no operation list for this feature: scope its user "
|
|
1583
|
+
"stories by the feature itself, and treat an operation whose folder or name "
|
|
1584
|
+
"belongs to another feature as that feature's.")
|
|
1585
|
+
line = "This feature's operations (the confirmed boundary map): %s." % ", ".join(mine)
|
|
1586
|
+
if others:
|
|
1587
|
+
line += (" Operations that belong ONLY to another feature over the same aggregate — "
|
|
1588
|
+
"not this feature's: %s." % "; ".join(
|
|
1589
|
+
"%s → %s" % (s, ", ".join(ops)) for s, ops in others.items()))
|
|
1590
|
+
else:
|
|
1591
|
+
line += " No other feature over the same aggregate owns an operation of its own."
|
|
1592
|
+
return line
|
|
1593
|
+
|
|
1594
|
+
|
|
1595
|
+
def _commit_time(root, commit):
|
|
1596
|
+
"""The ISO commit time of the inventory's baseline, or "" when git cannot resolve it."""
|
|
1597
|
+
if not commit:
|
|
1598
|
+
return ""
|
|
1599
|
+
try:
|
|
1600
|
+
r = subprocess.run(["git", "-C", root, "show", "-s", "--format=%cI", commit],
|
|
1601
|
+
capture_output=True, text=True, timeout=10)
|
|
1602
|
+
except (OSError, subprocess.SubprocessError):
|
|
1603
|
+
return ""
|
|
1604
|
+
return r.stdout.strip() if r.returncode == 0 else ""
|
|
1605
|
+
|
|
1606
|
+
|
|
1547
1607
|
def cmd_feature_aggregates(args):
|
|
1548
1608
|
"""WHICH AGGREGATE(S) A FEATURE ACTS ON — the name every command that reads the
|
|
1549
1609
|
bounded context's domain-model.md needs, because the model is organised by
|
|
@@ -1572,6 +1632,12 @@ def cmd_feature_aggregates(args):
|
|
|
1572
1632
|
those aggregates — the 1:1 case, even where the spec also names an aggregate it
|
|
1573
1633
|
merely touches — and otherwise every aggregate it names.
|
|
1574
1634
|
|
|
1635
|
+
It also carries what adopt needs to scope a reverse to the feature rather than to its
|
|
1636
|
+
aggregate: `operations` (this feature's, from the boundary map), `sibling_operations`
|
|
1637
|
+
(those only another feature over the same aggregate owns), `operations_note` (both as
|
|
1638
|
+
one prompt-ready line), and the inventory baseline the map was drawn at
|
|
1639
|
+
(`baseline_commit`, `baseline_time` — "" where git cannot resolve it).
|
|
1640
|
+
|
|
1575
1641
|
STRICTLY READ-ONLY and never empty: source 3 always answers, so a caller never has
|
|
1576
1642
|
to branch on a missing result."""
|
|
1577
1643
|
root = _root(args.root)
|
|
@@ -1580,8 +1646,13 @@ def cmd_feature_aggregates(args):
|
|
|
1580
1646
|
_emit({"error": "feature-dir-required", "root": root})
|
|
1581
1647
|
return 2
|
|
1582
1648
|
r = _resolve_feature(args.feature_dir, root)
|
|
1649
|
+
mine, others, commit = _operation_scope(root, r["feature"], r["shared_with"])
|
|
1650
|
+
r.update({"operations": mine, "operations_csv": ", ".join(mine),
|
|
1651
|
+
"sibling_operations": others,
|
|
1652
|
+
"operations_note": _operations_note(mine, others),
|
|
1653
|
+
"baseline_commit": commit, "baseline_time": _commit_time(root, commit)})
|
|
1583
1654
|
print(f"[xref] feature-aggregates {r['feature']}: acts on {r['aggregates_csv']} "
|
|
1584
|
-
f"(from {r['source']}); owns {r['owned_csv']}", file=sys.stderr)
|
|
1655
|
+
f"(from {r['source']}); owns {r['owned_csv']}; {len(mine)} operation(s)", file=sys.stderr)
|
|
1585
1656
|
r["root"] = root
|
|
1586
1657
|
_emit(r)
|
|
1587
1658
|
return 0
|
|
@@ -14,7 +14,9 @@ ERRORS (bundle is non-conformant):
|
|
|
14
14
|
- log.md date headings not in ISO 8601 YYYY-MM-DD form (§7)
|
|
15
15
|
E4 log.md date block missing a required `Commit: \\`<sha>\\`` line, or
|
|
16
16
|
the line is present but not a valid hex commit SHA. This is an
|
|
17
|
-
ERROR because
|
|
17
|
+
ERROR because the commit is what anchors each journal entry to the
|
|
18
|
+
code it describes (OKF §7) — an entry without one cannot be dated
|
|
19
|
+
against the history.
|
|
18
20
|
|
|
19
21
|
WARNINGS (consumers must tolerate; reported for quality):
|
|
20
22
|
W0 PyYAML not installed — using the lenient fallback parser.
|
|
@@ -23,11 +25,13 @@ WARNINGS (consumers must tolerate; reported for quality):
|
|
|
23
25
|
W3 Directory containing concepts but no index.md.
|
|
24
26
|
W4 Concept body is empty.
|
|
25
27
|
W5 Possible secret-looking string in a file.
|
|
26
|
-
W6 A `source_files` / `ui_source_files` entry does not exist on disk (
|
|
27
|
-
|
|
28
|
+
W6 A `source_files` / `ui_source_files` entry does not exist on disk (the code
|
|
29
|
+
moved or was removed since the concept was written; analyze-slice reports it
|
|
30
|
+
as stale_source_files, and nothing removes it for you — edit the concept).
|
|
28
31
|
W7 Possible duplicate concept: another file shares the same
|
|
29
32
|
`type` + `title`.
|
|
30
|
-
W8 Concept has unresolved `open_questions` (
|
|
33
|
+
W8 Concept has unresolved `open_questions` (/appweaver adopt resolves the
|
|
34
|
+
ones its reverse answers; the rest are answered by hand).
|
|
31
35
|
|
|
32
36
|
Usage: validate_okf.py <bundle_dir> [--config PATH] [--exclude GLOB]...
|
|
33
37
|
[--repo-root PATH] [--json]
|
|
@@ -169,7 +173,8 @@ def check_open_questions(data: dict, rel: str):
|
|
|
169
173
|
n = len(pending)
|
|
170
174
|
warnings.append(
|
|
171
175
|
f"W8 {rel}: {n} unresolved open question{'s' if n != 1 else ''} "
|
|
172
|
-
f"—
|
|
176
|
+
f"— /appweaver adopt of a feature acting on it resolves those its "
|
|
177
|
+
f"reverse answers; answer the rest by hand"
|
|
173
178
|
)
|
|
174
179
|
|
|
175
180
|
|
|
@@ -5,14 +5,19 @@ description: [one line — what this aggregate is and the invariant/boundary it
|
|
|
5
5
|
origin: greenfield
|
|
6
6
|
bounded_context:
|
|
7
7
|
features:
|
|
8
|
+
adopted_features:
|
|
8
9
|
source_files:
|
|
9
10
|
ui_source_files:
|
|
10
11
|
---
|
|
11
12
|
|
|
12
13
|
<!--
|
|
13
14
|
OKF CONCEPT — one aggregate's node in the shared knowledge bundle.
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
Where it lives: implement and adopt create a new concept at knowledge/<slug>/<slug>.md
|
|
16
|
+
(the dir also holds an index.md); onboard writes a bounded context's concepts under
|
|
17
|
+
knowledge/aggregates/, beside the aggregates/index.md context map. Every reader finds a
|
|
18
|
+
concept by walking the bundle for its `type` + `title`, never by its folder, so a
|
|
19
|
+
concept is never moved to match either layout. The bundle root holds index.md with
|
|
20
|
+
okf_version + an optional log.md change journal.
|
|
16
21
|
|
|
17
22
|
This single bundle is the OKF knowledge tier for BOTH pipelines:
|
|
18
23
|
• greenfield — `implement` authors this concept (frontmatter + Overview + Depends-On)
|
|
@@ -23,6 +28,9 @@ ui_source_files:
|
|
|
23
28
|
developer chose; it may act on several aggregates, and several features may act on
|
|
24
29
|
one. `features:` lists every feature known to act on this aggregate — implement and
|
|
25
30
|
adopt add their feature, and a feature is never removed from it by a later write.
|
|
31
|
+
`adopted_features:` lists the ones among them that `adopt` has deep-reversed from the
|
|
32
|
+
code; a feature in `features:` and not here is still only the discovery stub onboard
|
|
33
|
+
wrote. Empty on a greenfield concept, where nothing was reversed.
|
|
26
34
|
`bounded_context:` names the context the aggregate belongs to.
|
|
27
35
|
|
|
28
36
|
WHO WRITES WHAT (do not cross these lanes):
|