aw-spec-cli 0.1.21 → 0.1.22
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/STACK-PROFILES.md +87 -8
- package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +30 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +41 -8
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +62 -2
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +18 -5
- package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +115 -27
- package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +225 -27
- package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +39 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +28 -7
- package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +100 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +141 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +386 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-workflow-model.orch.yaml +135 -0
- package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +190 -209
- package/assets/.appweaver/scripts/bash/aw-xref.sh +4 -1
- package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +182 -224
- package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +4 -1
- package/assets/.appweaver/scripts/python/aw-crstate.py +127 -2
- package/assets/.appweaver/scripts/python/aw-gaps.py +119 -0
- package/assets/.appweaver/scripts/python/aw-payload-parity.py +17 -1
- package/assets/.appweaver/scripts/python/aw-plan-coverage.py +84 -4
- package/assets/.appweaver/scripts/python/aw-plangate.py +216 -0
- package/assets/.appweaver/scripts/python/aw-principles.py +108 -0
- package/assets/.appweaver/scripts/python/aw-stack.py +6 -0
- package/assets/.appweaver/scripts/python/aw-xref.py +141 -4
- package/assets/.appweaver/stack-profiles/angular.json +26 -0
- package/assets/.appweaver/stack-profiles/blazor-wasm.json +38 -0
- package/assets/.appweaver/stack-profiles/dotnet-webapi.json +38 -0
- package/assets/.appweaver/stack-profiles/java-spring.json +19 -0
- package/assets/.appweaver/stack-profiles/nextjs.json +26 -0
- package/assets/.appweaver/stack-profiles/node-express.json +26 -0
- package/assets/.appweaver/stack-profiles/python-fastapi.json +15 -0
- package/assets/.appweaver/stack-profiles/react-vite.json +26 -0
- package/assets/.appweaver/stack-profiles/sveltekit.json +26 -0
- package/assets/.appweaver/stack-profiles/vue-vite.json +26 -0
- package/assets/.appweaver/templates/ui-model-template.md +67 -0
- package/assets/.appweaver/templates/workflow-model-template.md +62 -0
- package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +44 -0
- package/package.json +1 -1
|
@@ -1946,6 +1946,21 @@ function Get-LabeledVersion {
|
|
|
1946
1946
|
}
|
|
1947
1947
|
if ($vals.Count -gt 0) { return $vals[-1] } else { return $null }
|
|
1948
1948
|
}
|
|
1949
|
+
|
|
1950
|
+
# The ratified NON-NEGOTIABLE principles. A thin wrapper: the scan lives in
|
|
1951
|
+
# python/aw-principles.py, which BOTH shells invoke, so the list a gate cites cannot
|
|
1952
|
+
# differ between them. Defaults to the two halves specify loads; --file overrides.
|
|
1953
|
+
function Invoke-PrinciplesList {
|
|
1954
|
+
param([string[]]$Rest)
|
|
1955
|
+
$repo = Get-RepoRoot
|
|
1956
|
+
$py = Get-Py
|
|
1957
|
+
if (-not $py) { Write-Error 'principles-list: python3 is required'; exit 1 }
|
|
1958
|
+
$core = Join-Path $PSScriptRoot '..' 'python' 'aw-principles.py'
|
|
1959
|
+
if (-not (Test-Path $core)) { Write-Error "principles-list: core not found at $core"; exit 1 }
|
|
1960
|
+
& $py $core list --root $repo @Rest
|
|
1961
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1949
1964
|
function Invoke-ConstitutionVerify {
|
|
1950
1965
|
param([string[]]$Rest)
|
|
1951
1966
|
$file = ''; $template = ''
|
|
@@ -2332,7 +2347,12 @@ function Invoke-DeltaPlanScaffold {
|
|
|
2332
2347
|
if ($tpl -and (Test-Path $tpl)) { Copy-Item $tpl $target } else { New-Item -ItemType File -Force -Path $target | Out-Null }
|
|
2333
2348
|
$created = $true
|
|
2334
2349
|
}
|
|
2335
|
-
|
|
2350
|
+
# The lane plan carries the change's wave so a reader of the plan alone can see where it
|
|
2351
|
+
# sits. It is COPIED from the delta-spec, never decided here — so when the spec has no
|
|
2352
|
+
# readable wave the template's `[N]` is left standing, which is what the merge and recall
|
|
2353
|
+
# fences read as "not ordered yet" and refuse on, with the command that stamps it.
|
|
2354
|
+
$scaffoldWave = Get-WaveOfReadable -FeatureDir $fd -CrId $crId -Repo $repo
|
|
2355
|
+
if ($scaffoldWave) { Set-HeaderStamp -Path $target -Name 'Wave' -Value $scaffoldWave }
|
|
2336
2356
|
$overrideActive = Test-Path (Join-Path $repo (".appweaver/templates/overrides/" + $tplName + ".md"))
|
|
2337
2357
|
$templateSource = Resolve-NamedTemplate -Name $tplName -RepoRoot $repo
|
|
2338
2358
|
$tsOut = if ($templateSource) { $templateSource } else { '' }
|
|
@@ -2418,7 +2438,8 @@ function Invoke-TestRun {
|
|
|
2418
2438
|
switch ($Rest[$i]) {
|
|
2419
2439
|
{ $_ -in '--feature-dir','-FeatureDir' } { $featureDir = $Rest[++$i] }
|
|
2420
2440
|
{ $_ -in '--scope','-Scope' } { $scope = $Rest[++$i] }
|
|
2421
|
-
|
|
2441
|
+
# Repeatable, lists UNION — see the bash twin for why the caller passes two.
|
|
2442
|
+
{ $_ -in '--also','-Also' } { $v = $Rest[++$i]; $also = if ($also) { "$also,$v" } else { $v } }
|
|
2422
2443
|
}
|
|
2423
2444
|
}
|
|
2424
2445
|
$repo = Get-RepoRoot
|
|
@@ -2714,16 +2735,33 @@ function Get-CrIdToken {
|
|
|
2714
2735
|
}
|
|
2715
2736
|
|
|
2716
2737
|
# One change's wave, read from the documents that carry it: the delta-spec first, then the
|
|
2717
|
-
# model delta. Both are stamped together, so either answers
|
|
2718
|
-
#
|
|
2719
|
-
|
|
2738
|
+
# model delta. Both are stamped together, so either answers. EMPTY when neither carries a
|
|
2739
|
+
# readable one — `**Wave**: [N]` is the unfilled template line, so "no wave" is a state that
|
|
2740
|
+
# happens, and a caller who WRITES the answer somewhere must not write a number nobody
|
|
2741
|
+
# derived. A phantom wave is worse than none: the merge and recall fences can see that a
|
|
2742
|
+
# wave is missing and say so, but a 1 invented downstream reads to them as fact.
|
|
2743
|
+
# Twin of bash _wave_of_readable.
|
|
2744
|
+
function Get-WaveOfReadable {
|
|
2720
2745
|
param([string]$FeatureDir, [string]$CrId, [string]$Repo)
|
|
2721
2746
|
$ctx = Get-ContextDirForFeature -FeatureDir $FeatureDir -Repo $Repo
|
|
2722
2747
|
foreach ($f in @((Join-Path $FeatureDir "changes/$CrId/delta-spec.md"), (Join-Path $ctx "domain-model.$CrId.delta.md"))) {
|
|
2723
2748
|
if (-not (Test-Path $f)) { continue }
|
|
2724
2749
|
$t = Get-Content $f -Raw -ErrorAction SilentlyContinue
|
|
2725
|
-
if ($t -match '(?m)^\*\*Wave\*\*:\s*(\d+)') {
|
|
2750
|
+
if ($t -match '(?m)^\*\*Wave\*\*:\s*(\d+)') {
|
|
2751
|
+
$w = [int]$Matches[1]
|
|
2752
|
+
if ($w -ge 1) { return "$w" }
|
|
2753
|
+
}
|
|
2726
2754
|
}
|
|
2755
|
+
return ''
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
# The same reading with the floor applied, for the readers that must show a number — a
|
|
2759
|
+
# listing has a column to fill and 1 is the floor for a change opened before waves existed.
|
|
2760
|
+
# Twin of bash _wave_of.
|
|
2761
|
+
function Get-WaveOf {
|
|
2762
|
+
param([string]$FeatureDir, [string]$CrId, [string]$Repo)
|
|
2763
|
+
$w = Get-WaveOfReadable -FeatureDir $FeatureDir -CrId $CrId -Repo $Repo
|
|
2764
|
+
if ($w) { return $w }
|
|
2727
2765
|
return '1'
|
|
2728
2766
|
}
|
|
2729
2767
|
|
|
@@ -3253,8 +3291,12 @@ required = max([d["wave"] for d in known], default=0) + 1
|
|
|
3253
3291
|
current = wave_of(cr)
|
|
3254
3292
|
ok = current is not None and current >= required
|
|
3255
3293
|
|
|
3294
|
+
# A change with NO readable wave is stamped too, not only one sitting too low. The merge
|
|
3295
|
+
# and recall fences refuse on an unordered change and send the operator here, so the one
|
|
3296
|
+
# command they are given has to be able to write the first wave as well as correct a wrong
|
|
3297
|
+
# one — a remedy that declines the case it was named for is no remedy.
|
|
3256
3298
|
stamped = []
|
|
3257
|
-
if stamp and current is
|
|
3299
|
+
if stamp and (current is None or current < required):
|
|
3258
3300
|
line = "**Wave**: %d" % required
|
|
3259
3301
|
pat = re.compile(r"^\*\*Wave\*\*:.*$", re.M)
|
|
3260
3302
|
for p in present:
|
|
@@ -3265,13 +3307,16 @@ if stamp and current is not None and current < required:
|
|
|
3265
3307
|
if stamped:
|
|
3266
3308
|
current, ok = required, True
|
|
3267
3309
|
|
|
3268
|
-
if
|
|
3269
|
-
msg = "%s declares no predecessor, so wave %d is the floor; it carries %s" % (
|
|
3270
|
-
cr, required, current if current is not None else "no wave")
|
|
3271
|
-
elif stamped:
|
|
3310
|
+
if stamped and deps:
|
|
3272
3311
|
msg = "%s depends on %s, so it was raised to wave %d in %s" % (
|
|
3273
3312
|
cr, ", ".join("%s (wave %s)" % (d["cr_id"], d["wave"]) for d in known),
|
|
3274
3313
|
required, ", ".join(stamped))
|
|
3314
|
+
elif stamped:
|
|
3315
|
+
msg = "%s declares no predecessor, so it was stamped at wave %d — the floor — in %s" % (
|
|
3316
|
+
cr, required, ", ".join(stamped))
|
|
3317
|
+
elif not deps:
|
|
3318
|
+
msg = "%s declares no predecessor, so wave %d is the floor; it carries %s" % (
|
|
3319
|
+
cr, required, current if current is not None else "no wave")
|
|
3275
3320
|
elif ok:
|
|
3276
3321
|
msg = "%s is at wave %s, at or above the %d its `## Depends on` requires" % (
|
|
3277
3322
|
cr, current, required)
|
|
@@ -3917,13 +3962,6 @@ function Invoke-CrMergeCheck {
|
|
|
3917
3962
|
if (-not $fd) { Write-Error 'cr-merge-check: no feature set and no --feature-dir'; exit 1 }
|
|
3918
3963
|
$ctx = Get-ContextDirForFeature -FeatureDir $fd -Repo $repo
|
|
3919
3964
|
|
|
3920
|
-
function Get-WaveFile([string]$path) {
|
|
3921
|
-
if (Test-Path -LiteralPath $path) {
|
|
3922
|
-
$m = [regex]::Match((Get-Content -LiteralPath $path -Raw), '(?m)^\*\*Wave\*\*:[ \t]*(\d+)')
|
|
3923
|
-
if ($m.Success) { return [int]$m.Groups[1].Value }
|
|
3924
|
-
}
|
|
3925
|
-
return 1
|
|
3926
|
-
}
|
|
3927
3965
|
function Format-Ver([string]$v) {
|
|
3928
3966
|
$v = "$v".Trim()
|
|
3929
3967
|
if (-not $v) { return '' }
|
|
@@ -3931,7 +3969,6 @@ function Invoke-CrMergeCheck {
|
|
|
3931
3969
|
}
|
|
3932
3970
|
|
|
3933
3971
|
$mySpec = Join-Path $fd "changes/$crId/delta-spec.md"
|
|
3934
|
-
$wave = Get-WaveFile $mySpec
|
|
3935
3972
|
$baseVersion = Format-Ver (Get-MdHeader -Path $mySpec -Name 'Base version')
|
|
3936
3973
|
|
|
3937
3974
|
# The master's CURRENT version, from the ledger — the same authority version-read
|
|
@@ -3947,63 +3984,59 @@ function Invoke-CrMergeCheck {
|
|
|
3947
3984
|
# is absence of evidence and is not treated as a move.
|
|
3948
3985
|
$rebaseNeeded = [bool]($baseVersion -and $current -and ($baseVersion -ne $current))
|
|
3949
3986
|
|
|
3950
|
-
#
|
|
3951
|
-
#
|
|
3952
|
-
#
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
if (-not $m.Success) { continue }
|
|
3970
|
-
$other = $m.Groups[1].Value
|
|
3971
|
-
if (-not $opens.Contains($other)) {
|
|
3972
|
-
$opens[$other] = [ordered]@{ cr_id = $other; wave = (Get-WaveFile $f.FullName); feature = '' }
|
|
3973
|
-
}
|
|
3974
|
-
}
|
|
3975
|
-
}
|
|
3976
|
-
$blockers = @(foreach ($k in ($opens.Keys | Sort-Object)) {
|
|
3977
|
-
if ($k -ne $crId -and $opens[$k].wave -lt $wave) { $opens[$k] }
|
|
3978
|
-
})
|
|
3979
|
-
|
|
3980
|
-
# THE MASTERS CAN MOVE WITHOUT THE VERSION MOVING. finalize-delta merges and close-delta
|
|
3981
|
-
# bumps, so between the two spec.md carries a change the number does not yet reflect —
|
|
3982
|
-
# and a second delta sharing the same base_version would compare equal and merge onto
|
|
3983
|
-
# masters that had already shifted under it. An id appearing in spec.md is the on-disk
|
|
3984
|
-
# proof that the merge ran (the Change History row finalize-delta appends), the same
|
|
3985
|
-
# evidence cr-recall-check reads for the same question.
|
|
3986
|
-
# aw-crstate owns the test, for both shells and all three readers of it: a change is
|
|
3987
|
-
# MERGED when its id is the CR cell of a Change History row — in this feature's spec or
|
|
3988
|
-
# in the context model every feature shares, so a SIBLING feature's merged-unclosed
|
|
3989
|
-
# change is no longer invisible. A sibling blocks only where it touched an aggregate
|
|
3990
|
-
# this delta also names. Fails CLOSED: an unreadable helper reports blocking.
|
|
3991
|
-
$mergedOpen = @()
|
|
3992
|
-
$pyMerged = Get-Py
|
|
3987
|
+
# BOTH CONTEXT-WIDE QUESTIONS COME FROM aw-crstate, the same module the bash payload
|
|
3988
|
+
# loads, so the two shells cannot answer them differently — which they did while each
|
|
3989
|
+
# carried its own copy of the scan.
|
|
3990
|
+
#
|
|
3991
|
+
# PREDECESSORS. Every open change in the context, both halves: changes/<id>/ under each
|
|
3992
|
+
# feature is the authority for "open", and the pending model deltas are unioned in so a
|
|
3993
|
+
# model-only artifact of a half-built change is still seen. A change whose `**Wave**:`
|
|
3994
|
+
# is unreadable is reported as unordered rather than assumed to be wave 1.
|
|
3995
|
+
#
|
|
3996
|
+
# MERGED-OPEN. The masters can move without the version moving: finalize-delta merges
|
|
3997
|
+
# and close-delta bumps, so between the two spec.md carries a change the number does not
|
|
3998
|
+
# yet reflect, and a second delta sharing the same base_version would compare equal and
|
|
3999
|
+
# merge onto masters that had already shifted under it. A change is MERGED when its id
|
|
4000
|
+
# is the CR cell of a Change History row — in this feature's spec or in the context
|
|
4001
|
+
# model every feature shares, so a SIBLING feature's merged-unclosed change is no longer
|
|
4002
|
+
# invisible. A sibling blocks only where it touched an aggregate this delta also names.
|
|
4003
|
+
#
|
|
4004
|
+
# Fails CLOSED: an unreadable helper reports blocking and nothing ordered.
|
|
4005
|
+
$py = Get-Py
|
|
3993
4006
|
$crstate = Join-Path $PSScriptRoot '..' 'python' 'aw-crstate.py'
|
|
3994
|
-
|
|
4007
|
+
# `answered` is what says the reader ran, not the shape of what it returned: NO blocking
|
|
4008
|
+
# merged-open change is an empty array, which is exactly what a failed read looks like.
|
|
4009
|
+
$order = $null; $mergedOpen = @(); $mergedText = ''; $answered = $false
|
|
4010
|
+
$why = 'python or aw-crstate.py not found'
|
|
4011
|
+
if ($py -and (Test-Path $crstate)) {
|
|
3995
4012
|
try {
|
|
3996
|
-
$line = (& $
|
|
3997
|
-
if ($line) { $
|
|
4013
|
+
$line = (& $py $crstate 'wave-order' '--cr' $crId '--feature-dir' $fd '--context-dir' $ctx '--direction' 'lower' '--scope' 'context' | Select-Object -First 1)
|
|
4014
|
+
if ($line) { $order = ($line | ConvertFrom-Json) }
|
|
4015
|
+
$line = (& $py $crstate 'merged-open' '--cr' $crId '--feature-dir' $fd '--context-dir' $ctx | Select-Object -First 1)
|
|
4016
|
+
if ($line) {
|
|
4017
|
+
$parsed = ($line | ConvertFrom-Json)
|
|
4018
|
+
$mergedOpen = @($parsed.blocking); $mergedText = "$($parsed.blocking_text)"; $answered = $true
|
|
4019
|
+
}
|
|
3998
4020
|
} catch {
|
|
3999
|
-
$
|
|
4021
|
+
$order = $null; $answered = $false; $why = "$_"
|
|
4000
4022
|
}
|
|
4001
|
-
} else {
|
|
4002
|
-
$mergedOpen = @([pscustomobject]@{ cr_id = '(merged-open check unavailable)'; blocking = $true })
|
|
4003
4023
|
}
|
|
4024
|
+
if (-not $order) {
|
|
4025
|
+
$order = [pscustomobject]@{ wave = $null; wave_known = $false; related = @(); related_count = 0
|
|
4026
|
+
related_ids = ''; unknown = @(); unknown_count = 0; unknown_ids = ''
|
|
4027
|
+
unknown_blocks = $true; wave_ordering_ok = $false
|
|
4028
|
+
wave_problem = "the wave reader could not be run ($why), so no open change in this context is ordered against $crId at all" }
|
|
4029
|
+
}
|
|
4030
|
+
if (-not $answered) {
|
|
4031
|
+
$mergedOpen = @([pscustomobject]@{ cr_id = "(merged-open check failed: $why)"; blocking = $true })
|
|
4032
|
+
$mergedText = $mergedOpen[0].cr_id
|
|
4033
|
+
}
|
|
4034
|
+
$blockers = @($order.related)
|
|
4004
4035
|
|
|
4005
4036
|
Write-Json ([ordered]@{
|
|
4006
|
-
ok = $true; cr_id = $crId
|
|
4037
|
+
ok = $true; cr_id = $crId
|
|
4038
|
+
wave = $(if ($null -ne $order.wave) { $order.wave } else { '' })
|
|
4039
|
+
wave_known = [bool]$order.wave_known
|
|
4007
4040
|
exists = (Test-Path -LiteralPath (Join-Path $fd "changes/$crId"))
|
|
4008
4041
|
# REBASE half — the masters moved because a change CLOSED and bumped the number.
|
|
4009
4042
|
base_version = $baseVersion; current_version = $current
|
|
@@ -4011,14 +4044,19 @@ function Invoke-CrMergeCheck {
|
|
|
4011
4044
|
base_stamped = [bool]$baseVersion
|
|
4012
4045
|
blockers = @($blockers); blocker_count = @($blockers).Count
|
|
4013
4046
|
has_blockers = (@($blockers).Count -gt 0)
|
|
4014
|
-
blocker_ids =
|
|
4047
|
+
blocker_ids = $order.related_ids
|
|
4048
|
+
# The changes this one cannot be ordered against at all, in either direction.
|
|
4049
|
+
unordered = @($order.unknown); unordered_count = $order.unknown_count
|
|
4050
|
+
unordered_ids = $order.unknown_ids
|
|
4051
|
+
wave_ordering_ok = [bool]$order.wave_ordering_ok
|
|
4052
|
+
wave_problem = $order.wave_problem
|
|
4015
4053
|
# The other way the masters move: a change MERGED and has not closed yet.
|
|
4016
4054
|
merged_open = @($mergedOpen); merged_open_count = @($mergedOpen).Count
|
|
4017
4055
|
has_merged_open = (@($mergedOpen).Count -gt 0)
|
|
4018
|
-
merged_open_ids =
|
|
4019
|
-
# All
|
|
4020
|
-
# condition and a gate reading
|
|
4021
|
-
merge_ok = ((-not $rebaseNeeded) -and (@($blockers).Count -eq 0) -and (@($mergedOpen).Count -eq 0))
|
|
4056
|
+
merged_open_ids = $mergedText
|
|
4057
|
+
# All four refusals as ONE positive flag, because the engine takes no compound
|
|
4058
|
+
# condition and a gate reading four booleans would need four passes over one question.
|
|
4059
|
+
merge_ok = ((-not $rebaseNeeded) -and [bool]$order.wave_ordering_ok -and (@($blockers).Count -eq 0) -and (@($mergedOpen).Count -eq 0))
|
|
4022
4060
|
})
|
|
4023
4061
|
}
|
|
4024
4062
|
|
|
@@ -4039,53 +4077,57 @@ function Invoke-CrRecallCheck {
|
|
|
4039
4077
|
$changes = Join-Path $fd 'changes'
|
|
4040
4078
|
$crDir = Join-Path $changes $crId
|
|
4041
4079
|
|
|
4042
|
-
function Get-WaveOf([string]$dir) {
|
|
4043
|
-
$p = Join-Path $dir 'delta-spec.md'
|
|
4044
|
-
if (Test-Path $p) {
|
|
4045
|
-
$m = [regex]::Match((Get-Content $p -Raw), '(?m)^\*\*Wave\*\*:[ \t]*(\d+)')
|
|
4046
|
-
if ($m.Success) { return [int]$m.Groups[1].Value }
|
|
4047
|
-
}
|
|
4048
|
-
return 1
|
|
4049
|
-
}
|
|
4050
|
-
|
|
4051
4080
|
# MERGED = a Change History row whose CR cell IS this change, in this feature's spec or
|
|
4052
4081
|
# in the shared context model — aw-crstate owns that test for all three readers of it.
|
|
4053
4082
|
# Fails CLOSED: an unreadable helper reports merged, never "never merged", which is the
|
|
4054
4083
|
# answer that would let close-delta archive a change nobody merged.
|
|
4084
|
+
#
|
|
4085
|
+
# DEPENDANTS come from the same module the merge fence reads in the opposite direction.
|
|
4086
|
+
# An unreadable `**Wave**:` is not wave 1 here either: it used to mean the change was
|
|
4087
|
+
# not counted as higher, so recall withdrew the ground from under a dependant it could
|
|
4088
|
+
# not see. Waves have no ceiling, so any unordered change could sit above this one.
|
|
4055
4089
|
$spec = Join-Path $fd 'spec.md'
|
|
4056
4090
|
$merged = $true; $evidence = '(merged check unavailable — treated as merged)'
|
|
4057
|
-
$
|
|
4091
|
+
$py = Get-Py
|
|
4058
4092
|
$crstate = Join-Path $PSScriptRoot '..' 'python' 'aw-crstate.py'
|
|
4059
|
-
|
|
4093
|
+
$order = $null; $why = 'python or aw-crstate.py not found'
|
|
4094
|
+
if ($py -and (Test-Path $crstate)) {
|
|
4060
4095
|
try {
|
|
4061
|
-
$line = (& $
|
|
4096
|
+
$line = (& $py $crstate 'merged' '--cr' $crId '--doc' $spec '--doc' (Join-Path $ctx 'domain-model.md') | Select-Object -First 1)
|
|
4062
4097
|
if ($line) {
|
|
4063
4098
|
$h = $line | ConvertFrom-Json
|
|
4064
4099
|
$merged = [bool]$h.merged; $evidence = [string]$h.evidence
|
|
4065
4100
|
}
|
|
4066
|
-
|
|
4101
|
+
$line = (& $py $crstate 'wave-order' '--cr' $crId '--feature-dir' $fd '--context-dir' $ctx '--direction' 'higher' '--scope' 'feature' | Select-Object -First 1)
|
|
4102
|
+
if ($line) { $order = ($line | ConvertFrom-Json) }
|
|
4103
|
+
} catch { $order = $null; $why = "$_" }
|
|
4067
4104
|
}
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
if ($d.Name -eq $crId -or $d.Name -eq 'archive') { continue }
|
|
4074
|
-
$w = Get-WaveOf $d.FullName
|
|
4075
|
-
if ($w -gt $wave) { $dependants += [ordered]@{ cr_id = $d.Name; wave = $w } }
|
|
4076
|
-
}
|
|
4105
|
+
if (-not $order) {
|
|
4106
|
+
$order = [pscustomobject]@{ wave = $null; wave_known = $false; related = @(); related_count = 0
|
|
4107
|
+
related_ids = ''; unknown = @(); unknown_count = 0; unknown_ids = ''
|
|
4108
|
+
unknown_blocks = $true; wave_ordering_ok = $false
|
|
4109
|
+
wave_problem = "the wave reader could not be run ($why), so no open change in this feature is ordered against $crId at all" }
|
|
4077
4110
|
}
|
|
4111
|
+
$dependants = @($order.related)
|
|
4112
|
+
|
|
4078
4113
|
Write-Json ([ordered]@{
|
|
4079
|
-
ok
|
|
4080
|
-
cr_id
|
|
4081
|
-
exists
|
|
4082
|
-
wave
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4114
|
+
ok = $true
|
|
4115
|
+
cr_id = $crId
|
|
4116
|
+
exists = (Test-Path $crDir)
|
|
4117
|
+
wave = $(if ($null -ne $order.wave) { $order.wave } else { '' })
|
|
4118
|
+
wave_known = [bool]$order.wave_known
|
|
4119
|
+
merged = $merged
|
|
4120
|
+
merge_evidence = $evidence
|
|
4121
|
+
dependants = @($dependants)
|
|
4122
|
+
dependant_ids = $order.related_ids
|
|
4123
|
+
dependant_count = @($dependants).Count
|
|
4124
|
+
has_dependants = (@($dependants).Count -gt 0)
|
|
4125
|
+
# The changes this one cannot be ordered against at all, in either direction.
|
|
4126
|
+
unordered = @($order.unknown)
|
|
4127
|
+
unordered_count = $order.unknown_count
|
|
4128
|
+
unordered_ids = $order.unknown_ids
|
|
4129
|
+
wave_ordering_ok = [bool]$order.wave_ordering_ok
|
|
4130
|
+
wave_problem = $order.wave_problem
|
|
4089
4131
|
})
|
|
4090
4132
|
}
|
|
4091
4133
|
|
|
@@ -4174,71 +4216,26 @@ function Get-FileSha {
|
|
|
4174
4216
|
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLower()
|
|
4175
4217
|
}
|
|
4176
4218
|
|
|
4219
|
+
# THE RULE ITSELF LIVES IN python/aw-plangate.py, which both shells invoke. It was a set
|
|
4220
|
+
# of sha comparisons written twice — a heredoc in bash, this function natively — and every
|
|
4221
|
+
# change to it had to be re-derived here by hand and kept in step by eye. It changed twice
|
|
4222
|
+
# that way. These two are now thin wrappers: resolve the feature, hand over.
|
|
4177
4223
|
function Invoke-PlanGateStamp {
|
|
4178
4224
|
param([string[]]$Rest)
|
|
4179
|
-
$fdArg = ''; $
|
|
4225
|
+
$fdArg = ''; $pass = @()
|
|
4180
4226
|
for ($i = 0; $i -lt $Rest.Count; $i++) {
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
{ $_ -in '--spec-version','-SpecVersion' } { $specVersion = $Rest[++$i] }
|
|
4184
|
-
{ $_ -in '--mode','-Mode' } { $mode = $Rest[++$i] }
|
|
4185
|
-
{ $_ -in '--verdict','-Verdict' } { $verdict = $Rest[++$i] }
|
|
4186
|
-
{ $_ -in '--plan-gaps','-PlanGaps' } { $planGaps = $Rest[++$i] }
|
|
4187
|
-
{ $_ -in '--routed','-Routed' } { $routedRaw = $Rest[++$i] }
|
|
4188
|
-
}
|
|
4227
|
+
if ($Rest[$i] -in '--feature-dir','-FeatureDir') { $fdArg = $Rest[++$i] }
|
|
4228
|
+
else { $pass += $Rest[$i] }
|
|
4189
4229
|
}
|
|
4190
|
-
# An unrecognised verdict is recorded as `gaps`, the one that refuses.
|
|
4191
|
-
$verdict = "$verdict".Trim()
|
|
4192
|
-
if ($verdict -notin @('conformant','routed-gaps','gaps')) { $verdict = 'gaps' }
|
|
4193
|
-
$planGapCount = 0
|
|
4194
|
-
if (-not [int]::TryParse("$planGaps", [ref]$planGapCount)) { $planGapCount = 0 }
|
|
4195
|
-
$routed = @()
|
|
4196
|
-
try {
|
|
4197
|
-
$parsedRouted = ConvertFrom-Json -InputObject $(if ("$routedRaw".Trim()) { $routedRaw } else { '[]' }) -NoEnumerate -ErrorAction Stop
|
|
4198
|
-
$routed = @($parsedRouted | ForEach-Object { "$_" })
|
|
4199
|
-
} catch { $routed = @() }
|
|
4200
4230
|
$repo = Get-RepoRoot; $fd = Resolve-FeatureDir -Arg $fdArg -RepoRoot $repo
|
|
4201
4231
|
if (-not $fd) { Write-Error 'plan-gate-stamp: no feature set and no --feature-dir'; exit 1 }
|
|
4202
4232
|
$ctx = Get-ContextDirForFeature -FeatureDir $fd -Repo $repo
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
# that has not been through `plan` since the rename — every brownfield feature `adopt`
|
|
4210
|
-
# produces — is GATED on those documents. Hashing only the new names would leave such a
|
|
4211
|
-
# feature with an empty plan map: a receipt that looks tracked and fresh while the document
|
|
4212
|
-
# implement actually generates from is unwatched.
|
|
4213
|
-
$plans = [ordered]@{}
|
|
4214
|
-
foreach ($name in @('backend-plan.md','frontend-plan.md','design.md','design.ui.md')) {
|
|
4215
|
-
$h = Get-FileSha -Path (Join-Path $fd $name)
|
|
4216
|
-
if ($h) { $plans[$name] = $h }
|
|
4217
|
-
}
|
|
4218
|
-
$rec = [ordered]@{
|
|
4219
|
-
gated = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
|
|
4220
|
-
gate_mode = $mode
|
|
4221
|
-
spec_version = $specVersion.Trim()
|
|
4222
|
-
verdict = $verdict
|
|
4223
|
-
plan_gaps = $planGapCount
|
|
4224
|
-
routed_gaps = @($routed)
|
|
4225
|
-
spec_sha = (Get-FileSha -Path (Join-Path $fd 'spec.md'))
|
|
4226
|
-
model_sha = (Get-FileSha -Path (Join-Path $ctx 'domain-model.md'))
|
|
4227
|
-
plans = $plans
|
|
4228
|
-
note = ('Written by every master-mode analyze-plan run. It records the bytes that were ' +
|
|
4229
|
-
'checked against each other and the verdict on them, not a version — the plans'' ' +
|
|
4230
|
-
'**Version**: headers are stamped in lockstep by close-delta and cannot answer ' +
|
|
4231
|
-
'this. implement reads it through plan-gate-check: moved bytes or verdict `gaps` ' +
|
|
4232
|
-
'refuse; `routed-gaps` lists what /appweaver specify still has to lift.')
|
|
4233
|
-
}
|
|
4234
|
-
$p = Join-Path $fd '.aw-plan-gate.json'
|
|
4235
|
-
(ConvertTo-Json -InputObject $rec -Depth 6) + "`n" | Set-Content -LiteralPath $p -NoNewline -Encoding utf8
|
|
4236
|
-
Write-Json ([ordered]@{
|
|
4237
|
-
file = (ConvertTo-RepoRelative -Path $p -RepoRoot $repo); gated = $rec.gated
|
|
4238
|
-
spec_version = $rec.spec_version; verdict = $verdict; plan_gaps = $planGapCount
|
|
4239
|
-
routed_gaps = $routed.Count; hashed = @($plans.Keys | Sort-Object)
|
|
4240
|
-
count = $plans.Count; ok = $true
|
|
4241
|
-
})
|
|
4233
|
+
$py = Get-Py
|
|
4234
|
+
if (-not $py) { Write-Error 'plan-gate-stamp: python3 is required'; exit 1 }
|
|
4235
|
+
$core = Join-Path $PSScriptRoot '..' 'python' 'aw-plangate.py'
|
|
4236
|
+
if (-not (Test-Path $core)) { Write-Error "plan-gate-stamp: core not found at $core"; exit 1 }
|
|
4237
|
+
& $py $core stamp --feature-dir $fd --context-dir $ctx --root $repo @pass
|
|
4238
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
4242
4239
|
}
|
|
4243
4240
|
|
|
4244
4241
|
function Invoke-PlanGateCheck {
|
|
@@ -4250,70 +4247,29 @@ function Invoke-PlanGateCheck {
|
|
|
4250
4247
|
$repo = Get-RepoRoot; $fd = Resolve-FeatureDir -Arg $fdArg -RepoRoot $repo
|
|
4251
4248
|
if (-not $fd) { Write-Error 'plan-gate-check: no feature set and no --feature-dir'; exit 1 }
|
|
4252
4249
|
$ctx = Get-ContextDirForFeature -FeatureDir $fd -Repo $repo
|
|
4253
|
-
$
|
|
4254
|
-
$
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
$
|
|
4259
|
-
file = (ConvertTo-RepoRelative -Path $p -RepoRoot $repo); tracked = [bool]$rec
|
|
4260
|
-
gate_fresh = $false; verdict = ''; may_generate = $true; plan_gaps = 0
|
|
4261
|
-
routed_gaps = @(); routed_csv = ''; spec_moved = $false; model_moved = $false
|
|
4262
|
-
plans_changed = @(); plans_changed_csv = ''; gated = ''; spec_version = ''
|
|
4263
|
-
reason = ''; ok = $true
|
|
4264
|
-
}
|
|
4265
|
-
if (-not $rec) {
|
|
4266
|
-
# Never gated. Not a failure and not a stale plan — an unknown, and the only honest
|
|
4267
|
-
# thing to report about a tree that has not run analyze-plan since this op existed.
|
|
4268
|
-
$out.reason = 'no .aw-plan-gate.json — this feature has not been through analyze-plan in master mode since the gate began leaving a receipt'
|
|
4269
|
-
} else {
|
|
4270
|
-
# ConvertFrom-Json turns an ISO timestamp into a DateTime; render it back in the
|
|
4271
|
-
# form the receipt was written in, as the bash twin reports it.
|
|
4272
|
-
$out.gated = $(if ($rec.gated -is [datetime]) { $rec.gated.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') } else { [string]$rec.gated })
|
|
4273
|
-
$out.spec_version = [string]$rec.spec_version
|
|
4274
|
-
# A receipt from before the verdict existed was only ever written on a clean pass.
|
|
4275
|
-
$out.verdict = $(if ("$($rec.verdict)") { [string]$rec.verdict } else { 'conformant' })
|
|
4276
|
-
$gapCount = 0
|
|
4277
|
-
if ($null -ne $rec.plan_gaps) { [void][int]::TryParse("$($rec.plan_gaps)", [ref]$gapCount) }
|
|
4278
|
-
$out.plan_gaps = $gapCount
|
|
4279
|
-
$out.routed_gaps = @(@($rec.routed_gaps) | Where-Object { $null -ne $_ } | ForEach-Object { "$_" })
|
|
4280
|
-
$out.routed_csv = ($out.routed_gaps -join '; ')
|
|
4281
|
-
$specSha = [string]$rec.spec_sha
|
|
4282
|
-
$out.spec_moved = ($specSha -and $specSha -ne (Get-FileSha -Path (Join-Path $fd 'spec.md')))
|
|
4283
|
-
# A model recorded as absent that is still absent has not moved. A context that
|
|
4284
|
-
# GAINED a domain model since the gate has moved: check 4 never ran against it.
|
|
4285
|
-
$out.model_moved = ([string]$rec.model_sha -ne (Get-FileSha -Path (Join-Path $ctx 'domain-model.md')))
|
|
4286
|
-
$changed = @()
|
|
4287
|
-
if ($rec.plans) {
|
|
4288
|
-
foreach ($name in @($rec.plans.PSObject.Properties.Name | Sort-Object)) {
|
|
4289
|
-
if ((Get-FileSha -Path (Join-Path $fd $name)) -ne [string]$rec.plans.$name) { $changed += $name }
|
|
4290
|
-
}
|
|
4291
|
-
}
|
|
4292
|
-
$out.plans_changed = @($changed)
|
|
4293
|
-
$out.plans_changed_csv = ($changed -join ', ')
|
|
4294
|
-
$out.gate_fresh = (-not ($out.spec_moved -or $out.model_moved -or $changed.Count -gt 0))
|
|
4295
|
-
# Moved bytes refuse whatever the verdict was; unmoved bytes refuse only on `gaps`.
|
|
4296
|
-
$out.may_generate = ($out.gate_fresh -and $out.verdict -ne 'gaps')
|
|
4297
|
-
if (-not $out.gate_fresh) {
|
|
4298
|
-
$bits = @()
|
|
4299
|
-
if ($out.spec_moved) { $bits += 'spec.md has changed' }
|
|
4300
|
-
if ($out.model_moved) { $bits += 'domain-model.md has changed' }
|
|
4301
|
-
if ($changed.Count -gt 0) { $bits += ('{0} changed (a re-derive by `plan main`, or a hand edit)' -f ($changed -join ', ')) }
|
|
4302
|
-
$out.reason = 'since analyze-plan checked them on {0}: {1}' -f $out.gated, ($bits -join '; ')
|
|
4303
|
-
} elseif ($out.verdict -eq 'gaps') {
|
|
4304
|
-
$n = $(if ($out.plan_gaps) { $out.plan_gaps } else { 'some' })
|
|
4305
|
-
$out.reason = 'analyze-plan on {0} left {1} gap(s) a plan rewrite closes, unrepaired' -f $out.gated, $n
|
|
4306
|
-
} elseif ($out.verdict -eq 'routed-gaps') {
|
|
4307
|
-
$out.reason = 'the spec, the model and every gated plan are byte-identical to what analyze-plan checked on {0}; it left {1} gap(s) only /appweaver specify can close: {2}' -f $out.gated, $out.routed_gaps.Count, $out.routed_csv
|
|
4308
|
-
} else {
|
|
4309
|
-
$out.reason = 'the spec, the model and every gated plan are byte-identical to what analyze-plan passed on {0}' -f $out.gated
|
|
4310
|
-
}
|
|
4311
|
-
}
|
|
4312
|
-
Write-Json $out
|
|
4250
|
+
$py = Get-Py
|
|
4251
|
+
if (-not $py) { Write-Error 'plan-gate-check: python3 is required'; exit 1 }
|
|
4252
|
+
$core = Join-Path $PSScriptRoot '..' 'python' 'aw-plangate.py'
|
|
4253
|
+
if (-not (Test-Path $core)) { Write-Error "plan-gate-check: core not found at $core"; exit 1 }
|
|
4254
|
+
& $py $core check --feature-dir $fd --context-dir $ctx --root $repo
|
|
4255
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
4313
4256
|
}
|
|
4314
4257
|
|
|
4315
4258
|
# Which of a lane plan's elements the generated code realises. One implementation
|
|
4316
4259
|
# (python/aw-plan-coverage.py), called identically by both shells — see the bash twin.
|
|
4260
|
+
# Split a gate's findings by the command that closes each one. Twin of the bash
|
|
4261
|
+
# cmd_gap_route, calling the SAME aw-gaps.py — see the bash comment for why a plan
|
|
4262
|
+
# defect must never be routed to `implement`.
|
|
4263
|
+
function Invoke-GapRoute {
|
|
4264
|
+
param([string[]]$Rest)
|
|
4265
|
+
$py = Get-Py
|
|
4266
|
+
if (-not $py) { Write-Error 'gap-route: python3 is required'; exit 1 }
|
|
4267
|
+
$core = Join-Path $PSScriptRoot '..' 'python' 'aw-gaps.py'
|
|
4268
|
+
if (-not (Test-Path $core)) { Write-Error "gap-route: core not found at $core"; exit 1 }
|
|
4269
|
+
& $py $core route @Rest
|
|
4270
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4317
4273
|
function Invoke-PlanCoverage {
|
|
4318
4274
|
param([string[]]$Rest)
|
|
4319
4275
|
$py = Get-Py
|
|
@@ -4468,6 +4424,7 @@ switch ($sub) {
|
|
|
4468
4424
|
'plan-gate-stamp' { Invoke-PlanGateStamp -Rest $rest }
|
|
4469
4425
|
'plan-gate-check' { Invoke-PlanGateCheck -Rest $rest }
|
|
4470
4426
|
'plan-coverage' { Invoke-PlanCoverage -Rest $rest }
|
|
4427
|
+
'gap-route' { Invoke-GapRoute -Rest $rest }
|
|
4471
4428
|
'checklist-read' { Invoke-ChecklistRead -Rest $rest }
|
|
4472
4429
|
'checklist-apply' { Invoke-ChecklistApply -Rest $rest }
|
|
4473
4430
|
'detect-project' { Invoke-DetectProject -Rest $rest }
|
|
@@ -4483,6 +4440,7 @@ switch ($sub) {
|
|
|
4483
4440
|
'setup-constitution' { Invoke-SetupConstitution -Rest $rest }
|
|
4484
4441
|
'compose-constitution' { Invoke-ComposeConstitution -Rest $rest }
|
|
4485
4442
|
'constitution-verify' { Invoke-ConstitutionVerify -Rest $rest }
|
|
4443
|
+
'principles-list' { Invoke-PrinciplesList -Rest $rest }
|
|
4486
4444
|
'agent-context-update' { Invoke-AgentContextUpdate -Rest $rest }
|
|
4487
4445
|
default { Write-Error "Unknown subcommand: $sub"; exit 1 }
|
|
4488
4446
|
}
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
# aw-xref.ps1 list --aggregate Y [--root DIR] [--bundle knowledge]
|
|
9
9
|
# aw-xref.ps1 concept-upsert --aggregate X [--source-files a,b,c] [--description D] [--origin ...] [--root DIR]
|
|
10
10
|
# aw-xref.ps1 concept-overview --aggregate X (--text PROSE | --text-file F) [--root DIR]
|
|
11
|
-
# aw-xref.ps1 concept-paths --aggregate X | --feature-dir <dir> [--
|
|
11
|
+
# aw-xref.ps1 concept-paths --aggregate X | --feature-dir <dir> [--under <subtree>]...
|
|
12
|
+
# [--root DIR] [--bundle knowledge]
|
|
13
|
+
# (--under keeps only paths inside that subtree, so a caller
|
|
14
|
+
# generating ONE lane is not handed another lane's files)
|
|
12
15
|
# aw-xref.ps1 feature-aggregates --feature-dir <dir> [--root DIR]
|
|
13
16
|
# (the aggregate(s) a feature acts on: its spec's Acts-on line,
|
|
14
17
|
# then the boundary map, then the feature name itself)
|