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.
Files changed (39) hide show
  1. package/assets/.appweaver/STACK-PROFILES.md +87 -8
  2. package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +30 -0
  3. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +41 -8
  4. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +62 -2
  5. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +18 -5
  6. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +115 -27
  7. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +225 -27
  8. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +39 -0
  9. package/assets/.appweaver/orchestrator/workflows/aw-spec-recall-delta.orch.yaml +28 -7
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +100 -0
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +141 -0
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +386 -0
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-workflow-model.orch.yaml +135 -0
  14. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +190 -209
  15. package/assets/.appweaver/scripts/bash/aw-xref.sh +4 -1
  16. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +182 -224
  17. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +4 -1
  18. package/assets/.appweaver/scripts/python/aw-crstate.py +127 -2
  19. package/assets/.appweaver/scripts/python/aw-gaps.py +119 -0
  20. package/assets/.appweaver/scripts/python/aw-payload-parity.py +17 -1
  21. package/assets/.appweaver/scripts/python/aw-plan-coverage.py +84 -4
  22. package/assets/.appweaver/scripts/python/aw-plangate.py +216 -0
  23. package/assets/.appweaver/scripts/python/aw-principles.py +108 -0
  24. package/assets/.appweaver/scripts/python/aw-stack.py +6 -0
  25. package/assets/.appweaver/scripts/python/aw-xref.py +141 -4
  26. package/assets/.appweaver/stack-profiles/angular.json +26 -0
  27. package/assets/.appweaver/stack-profiles/blazor-wasm.json +38 -0
  28. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +38 -0
  29. package/assets/.appweaver/stack-profiles/java-spring.json +19 -0
  30. package/assets/.appweaver/stack-profiles/nextjs.json +26 -0
  31. package/assets/.appweaver/stack-profiles/node-express.json +26 -0
  32. package/assets/.appweaver/stack-profiles/python-fastapi.json +15 -0
  33. package/assets/.appweaver/stack-profiles/react-vite.json +26 -0
  34. package/assets/.appweaver/stack-profiles/sveltekit.json +26 -0
  35. package/assets/.appweaver/stack-profiles/vue-vite.json +26 -0
  36. package/assets/.appweaver/templates/ui-model-template.md +67 -0
  37. package/assets/.appweaver/templates/workflow-model-template.md +62 -0
  38. package/assets/guardrails/technology/dotnet-webapi/framework-api-map.md +44 -0
  39. 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
- Set-HeaderStamp -Path $target -Name 'Wave' -Value (Get-WaveOf -FeatureDir $fd -CrId $crId -Repo $repo)
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
- { $_ -in '--also','-Also' } { $also = $Rest[++$i] }
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; 1 is the floor for a change
2718
- # opened before waves existed. Twin of bash _wave_of.
2719
- function Get-WaveOf {
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+)') { return "$([int]$Matches[1])" }
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 not None and current < required:
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 not deps:
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
- # EVERY open change in the context, both halves. changes/<id>/ under each feature is the
3951
- # authority for "open"; the pending model deltas are unioned in so a model-only artifact
3952
- # of a half-built change is still seen.
3953
- $opens = [ordered]@{}
3954
- if (Test-Path -LiteralPath $ctx) {
3955
- foreach ($feat in (Get-ChildItem -LiteralPath $ctx -Directory -ErrorAction SilentlyContinue | Sort-Object Name)) {
3956
- $ch = Join-Path $feat.FullName 'changes'
3957
- if (-not (Test-Path -LiteralPath $ch)) { continue }
3958
- foreach ($d in (Get-ChildItem -LiteralPath $ch -Directory -ErrorAction SilentlyContinue | Sort-Object Name)) {
3959
- if ($d.Name -eq 'archive') { continue }
3960
- if (-not $opens.Contains($d.Name)) {
3961
- $opens[$d.Name] = [ordered]@{ cr_id = $d.Name
3962
- wave = (Get-WaveFile (Join-Path $d.FullName 'delta-spec.md'))
3963
- feature = $feat.Name }
3964
- }
3965
- }
3966
- }
3967
- foreach ($f in (Get-ChildItem -LiteralPath $ctx -File -ErrorAction SilentlyContinue | Sort-Object Name)) {
3968
- $m = [regex]::Match($f.Name, '^domain-model\.(.+)\.delta\.md$')
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
- if ($pyMerged -and (Test-Path $crstate)) {
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 = (& $pyMerged $crstate 'merged-open' '--cr' $crId '--feature-dir' $fd '--context-dir' $ctx | Select-Object -First 1)
3997
- if ($line) { $mergedOpen = @(($line | ConvertFrom-Json).blocking) }
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
- $mergedOpen = @([pscustomobject]@{ cr_id = '(merged-open check failed)'; blocking = $true })
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; wave = $wave
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 = (@($blockers | ForEach-Object { '{0} (wave {1})' -f $_.cr_id, $_.wave }) -join ', ')
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 = (@($mergedOpen | ForEach-Object { $_.cr_id }) -join ', ')
4019
- # All three refusals as ONE positive flag, because the engine takes no compound
4020
- # condition and a gate reading three booleans would need three passes over one question.
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
- $pyMerged = Get-Py
4091
+ $py = Get-Py
4058
4092
  $crstate = Join-Path $PSScriptRoot '..' 'python' 'aw-crstate.py'
4059
- if ($pyMerged -and (Test-Path $crstate)) {
4093
+ $order = $null; $why = 'python or aw-crstate.py not found'
4094
+ if ($py -and (Test-Path $crstate)) {
4060
4095
  try {
4061
- $line = (& $pyMerged $crstate 'merged' '--cr' $crId '--doc' $spec '--doc' (Join-Path $ctx 'domain-model.md') | Select-Object -First 1)
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
- } catch { }
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
- $wave = if (Test-Path $crDir) { Get-WaveOf $crDir } else { 1 }
4070
- $dependants = @()
4071
- if (Test-Path $changes) {
4072
- foreach ($d in (Get-ChildItem -LiteralPath $changes -Directory | Sort-Object Name)) {
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 = $true
4080
- cr_id = $crId
4081
- exists = (Test-Path $crDir)
4082
- wave = $wave
4083
- merged = $merged
4084
- merge_evidence = $evidence
4085
- dependants = @($dependants)
4086
- dependant_ids = (@($dependants | ForEach-Object { '{0} (wave {1})' -f $_.cr_id, $_.wave }) -join ', ')
4087
- dependant_count = @($dependants).Count
4088
- has_dependants = (@($dependants).Count -gt 0)
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 = ''; $specVersion = ''; $mode = 'master'; $verdict = 'conformant'; $planGaps = '0'; $routedRaw = '[]'
4225
+ $fdArg = ''; $pass = @()
4180
4226
  for ($i = 0; $i -lt $Rest.Count; $i++) {
4181
- switch ($Rest[$i]) {
4182
- { $_ -in '--feature-dir','-FeatureDir' } { $fdArg = $Rest[++$i] }
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
- # Every document the gate READ. A lane that is not declared has no plan on disk and is
4204
- # simply absent from the map — the check treats a name it does not carry as not gated,
4205
- # never as changed, so declaring a second lane later does not read as tampering.
4206
- #
4207
- # THE LEGACY NAMES ARE HERE FOR THE SAME REASON version-stamp CARRIES THEM. feature-paths
4208
- # resolves design.md / design.ui.md under the backend_plan / frontend_plan keys, so a tree
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
- $p = Join-Path $fd '.aw-plan-gate.json'
4254
- $rec = $null
4255
- if (Test-Path -LiteralPath $p -PathType Leaf) {
4256
- try { $rec = Get-Content -LiteralPath $p -Raw | ConvertFrom-Json } catch { $rec = $null }
4257
- }
4258
- $out = [ordered]@{
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> [--root DIR] [--bundle knowledge]
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)