Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .specify/presets/intake-authoring-governance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,18 +328,19 @@ die hashgebundene Zielmenge des Serienmanifests als aktiven Bestand. Beide Modi
berechnen Bestandszahlen; handgepflegte Zaehler sind keine Evidence.

Eine aktive Lieferserie hat genau ein `Eligible`-Ziel. Eine `Completed`-Serie
bewahrt ausschließlich abgeschlossene Mitglieder und hat kein `Eligible`-Ziel;
für Mitglieder über aktive und archivierte Ablagen hinweg gilt
`SeriesManifest`.
bewahrt ausschließlich abgeschlossene Mitglieder in der konfigurierten
Archiv-Collection und hat kein `Eligible`-Ziel. `SeriesManifest` erlaubt einer
laufenden Serie, aktive Ziele und bereits archivierte Vorgänger gemeinsam zu
referenzieren.

*Version 0.3.0 describes requirements collections with schema 2.0.
Documentation language is explicit and independent from implementation
language and locale. Portable roles resolve localized names. Migration is
authority-bound, hash-bound, and atomic. `Eligible` selects order but grants no
implementation or remote authority. `DirectoryStrict` validates a dedicated
active directory; `SeriesManifest` supports established flat or mixed layouts
without trusting manual counts. A completed Series preserves terminal members
without inventing an eligible candidate.*
without trusting manual counts. A completed Series preserves its members in
the configured archive collection without inventing an eligible candidate.*

## Status / Statuspruefung

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ repository.
Configured collections report Aligned, MigrationRequired, NeedsClarification,
or Blocked. Eligibility is an ordering result and grants no delivery authority.
A completed non-empty series is Aligned only when every retained target is
Completed and no target is Eligible. SeriesManifest permits retained members
across active and archive collections.
Completed, stored in the configured archive collection, and no target is
Eligible. SeriesManifest permits an active series to reference active targets
and already archived predecessors together.
.SH EXIT STATUS
Zero means Aligned or a valid journal. One means migration or clarification is
required. Two means blocked or invalid input.
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def validate_series_manifest(
path: Path,
repo: Path,
active_dir: Path,
archive_dir: Path,
pattern: str,
inventory_mode: str,
) -> dict:
Expand All @@ -126,6 +127,8 @@ def validate_series_manifest(
for item in active_dir.iterdir()
if item.is_file() and intake_name_matches(item.name, pattern)
}
active_prefix = f"{active_dir.relative_to(repo).as_posix().rstrip('/')}/"
archive_prefix = f"{archive_dir.relative_to(repo).as_posix().rstrip('/')}/"
target_paths: list[str] = []
target_statuses: dict[str, str] = {}
eligible: list[str] = []
Expand All @@ -147,6 +150,12 @@ def validate_series_manifest(
fail("RIG015", f"hash drift for {target_path}")
status = required_text(target, "status", "RIG017")
target_statuses[target_path] = status
if not target_path.startswith((active_prefix, archive_prefix)):
fail("RIG017", f"series target is outside active and archive collections: {target_path}")
if status == "Completed" and not target_path.startswith(archive_prefix):
fail("RIG017", f"Completed target must be stored in archive collection: {target_path}")
if status != "Completed" and target_path.startswith(archive_prefix):
fail("RIG017", f"non-completed target must not be stored in archive collection: {target_path}")
if status == "Eligible":
eligible.append(target_path)

Expand Down Expand Up @@ -182,7 +191,7 @@ def validate_series_manifest(
fail("RIG016", f"dependency {source} -> {target} contradicts order")

return {
"activeIntakeCount": len(target_paths),
"activeIntakeCount": sum(target.startswith(active_prefix) for target in target_paths),
"seriesTargetCount": len(target_paths),
"eligibleCandidate": eligible[0] if eligible else "N/A",
"dependencyCount": len(dependencies),
Expand Down Expand Up @@ -296,6 +305,7 @@ def validate_config(data: dict, repo: Path) -> dict:
repo / collections["seriesManifest"],
repo,
active_dir,
repo / collections["archive"],
pattern,
inventory_mode,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ Version: `0.3.1`
profiles passed with four portable roles, six collection paths, one
canonical index, complete active inventory, current normalized hashes, one
`Eligible` candidate for an active delivery series, and zero candidates for
a terminal `Completed` series retained through `SeriesManifest`. Schema 1.0,
ambiguous language, path traversal, duplicate collections, empty manifests,
hash drift, incomplete completed series, and multiple eligible candidates
failed with stable error classes.
a terminal `Completed` series whose members are retained in the configured
archive collection through `SeriesManifest`. Schema 1.0, ambiguous language,
path traversal, duplicate collections, empty manifests, hash drift,
Completed targets in the active collection, incomplete completed series, and
multiple eligible candidates failed with stable error classes.
- Eleven-preset development stack: installation, `list`, `info`, `resolve`,
disable, enable, remove, and reinstall passed with priorities `10` through
`80` and Authoring at `64`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$PresetRoot = Split-Path -Parent (Split-Path -Parent $PSCommandPath)
$Validator = Join-Path $PresetRoot 'scripts/validate-intake-governance-config.py'
$BashValidator = Join-Path $PresetRoot 'scripts/validate-intake-governance-config.sh'
$PowerShellValidator = Join-Path $PresetRoot 'scripts/validate-intake-governance-config.ps1'
$Root = Join-Path ([System.IO.Path]::GetTempPath()) ("intake-governance-" + [guid]::NewGuid())
New-Item -ItemType Directory -Path $Root | Out-Null

Expand All @@ -26,13 +27,19 @@ function Get-NormalizedSha256 {

function Invoke-Fixture {
param([string]$Path, [int]$ExpectedExit, [string]$ExpectedText)
$Output = & python3 $Validator --config $Path --repo $Root --json 2>&1
$Exit = $LASTEXITCODE
if ($Exit -ne $ExpectedExit) {
throw "Expected exit $ExpectedExit, got ${Exit}: $Output"
}
if (($Output -join "`n") -notmatch [regex]::Escape($ExpectedText)) {
throw "Expected '$ExpectedText': $Output"
$Runs = @(
@{ Name = 'Bash'; Output = @(& bash $BashValidator --config $Path --repo $Root --json 2>&1); Exit = $LASTEXITCODE },
@{ Name = 'PowerShell'; Output = @(& pwsh -NoProfile -File $PowerShellValidator -Config $Path -Repo $Root -Json 2>&1); Exit = $LASTEXITCODE }
)
foreach ($Run in $Runs) {
$Output = $Run.Output
$Exit = $Run.Exit
if ($Exit -ne $ExpectedExit) {
throw "$($Run.Name): expected exit $ExpectedExit, got ${Exit}: $Output"
}
if (($Output -join "`n") -notmatch [regex]::Escape($ExpectedText)) {
throw "$($Run.Name): expected '$ExpectedText': $Output"
}
}
}

Expand Down Expand Up @@ -139,16 +146,46 @@ try {
Set-Content -LiteralPath $ManifestPath -Encoding utf8NoBOM
Invoke-Fixture (Write-JsonFixture 'completed-series.json' $ManifestInventory) 0 '"eligibleCandidate": "N/A"'

$CompletedWithEligible = $CompletedManifest.Clone()
$CompletedWithEligible.orderedTargets = @($CompletedManifest.orderedTargets[0].Clone())
$CompletedWithEligible.orderedTargets[0].status = 'Eligible'
$CompletedInActive = $Manifest.Clone()
$CompletedInActive.status = 'Completed'
$CompletedInActive.orderedTargets = @($Manifest.orderedTargets[0].Clone())
$CompletedInActive.orderedTargets[0].status = 'Completed'
$CompletedInActive | ConvertTo-Json -Depth 12 |
Set-Content -LiteralPath $ManifestPath -Encoding utf8NoBOM
Invoke-Fixture (Write-JsonFixture 'completed-in-active.json' $ManifestInventory) 2 `
'Completed target must be stored in archive collection'

$MixedManifest = $Manifest.Clone()
$MixedManifest.orderedTargets = @(
$CompletedManifest.orderedTargets[0].Clone(),
$Manifest.orderedTargets[0].Clone()
)
$MixedManifest.roots = @($MixedManifest.orderedTargets[0].path, $MixedManifest.orderedTargets[1].path)
$MixedManifest | ConvertTo-Json -Depth 12 |
Set-Content -LiteralPath $ManifestPath -Encoding utf8NoBOM
Invoke-Fixture (Write-JsonFixture 'mixed-active-series.json' $ManifestInventory) 0 `
'"eligibleCandidate": "requirements/intakes/active/Lastenheft_Beispiel.md"'

$EligibleInArchive = $MixedManifest.Clone()
$EligibleInArchive.orderedTargets = @($CompletedManifest.orderedTargets[0].Clone())
$EligibleInArchive.orderedTargets[0].status = 'Eligible'
$EligibleInArchive.roots = @($EligibleInArchive.orderedTargets[0].path)
$EligibleInArchive | ConvertTo-Json -Depth 12 |
Set-Content -LiteralPath $ManifestPath -Encoding utf8NoBOM
Invoke-Fixture (Write-JsonFixture 'eligible-in-archive.json' $ManifestInventory) 2 `
'non-completed target must not be stored in archive collection'

$CompletedWithEligible = $Manifest.Clone()
$CompletedWithEligible.status = 'Completed'
$CompletedWithEligible.orderedTargets = @($Manifest.orderedTargets[0].Clone())
$CompletedWithEligible | ConvertTo-Json -Depth 12 |
Set-Content -LiteralPath $ManifestPath -Encoding utf8NoBOM
Invoke-Fixture (Write-JsonFixture 'completed-with-eligible.json' $ManifestInventory) 2 `
'Completed series must not contain an Eligible target'

$CompletedWithPending = $CompletedManifest.Clone()
$CompletedWithPending.orderedTargets = @($CompletedManifest.orderedTargets[0].Clone())
$CompletedWithPending = $Manifest.Clone()
$CompletedWithPending.status = 'Completed'
$CompletedWithPending.orderedTargets = @($Manifest.orderedTargets[0].Clone())
$CompletedWithPending.orderedTargets[0].status = 'Pending'
$CompletedWithPending | ConvertTo-Json -Depth 12 |
Set-Content -LiteralPath $ManifestPath -Encoding utf8NoBOM
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ repository.
Configured collections report Aligned, MigrationRequired, NeedsClarification,
or Blocked. Eligibility is an ordering result and grants no delivery authority.
A completed non-empty series is Aligned only when every retained target is
Completed and no target is Eligible. SeriesManifest permits retained members
across active and archive collections.
Completed, stored in the configured archive collection, and no target is
Eligible. SeriesManifest permits an active series to reference active targets
and already archived predecessors together.
.SH EXIT STATUS
Zero means Aligned or a valid journal. One means migration or clarification is
required. Two means blocked or invalid input.
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def validate_series_manifest(
path: Path,
repo: Path,
active_dir: Path,
archive_dir: Path,
pattern: str,
inventory_mode: str,
) -> dict:
Expand All @@ -126,6 +127,8 @@ def validate_series_manifest(
for item in active_dir.iterdir()
if item.is_file() and intake_name_matches(item.name, pattern)
}
active_prefix = f"{active_dir.relative_to(repo).as_posix().rstrip('/')}/"
archive_prefix = f"{archive_dir.relative_to(repo).as_posix().rstrip('/')}/"
target_paths: list[str] = []
target_statuses: dict[str, str] = {}
eligible: list[str] = []
Expand All @@ -147,6 +150,12 @@ def validate_series_manifest(
fail("RIG015", f"hash drift for {target_path}")
status = required_text(target, "status", "RIG017")
target_statuses[target_path] = status
if not target_path.startswith((active_prefix, archive_prefix)):
fail("RIG017", f"series target is outside active and archive collections: {target_path}")
if status == "Completed" and not target_path.startswith(archive_prefix):
fail("RIG017", f"Completed target must be stored in archive collection: {target_path}")
if status != "Completed" and target_path.startswith(archive_prefix):
fail("RIG017", f"non-completed target must not be stored in archive collection: {target_path}")
if status == "Eligible":
eligible.append(target_path)

Expand Down Expand Up @@ -182,7 +191,7 @@ def validate_series_manifest(
fail("RIG016", f"dependency {source} -> {target} contradicts order")

return {
"activeIntakeCount": len(target_paths),
"activeIntakeCount": sum(target.startswith(active_prefix) for target in target_paths),
"seriesTargetCount": len(target_paths),
"eligibleCandidate": eligible[0] if eligible else "N/A",
"dependencyCount": len(dependencies),
Expand Down Expand Up @@ -296,6 +305,7 @@ def validate_config(data: dict, repo: Path) -> dict:
repo / collections["seriesManifest"],
repo,
active_dir,
repo / collections["archive"],
pattern,
inventory_mode,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Never infer a missing predecessor or silently accept request drift.
When schema 2.0 is present, review explicit documentation language, naming
profile, portable roles, resolved paths, hashes, receipts, references, and the
Series lifecycle. An active delivery series has exactly one evidenced
`Eligible` candidate; a `Completed` series retains only terminal members and
has none. Implementation language and locale are not documentation-language
evidence. A Ready review grants neither implementation nor remote authority.
`Eligible` candidate; a `Completed` series retains only `Completed` members in
the configured archive collection and has none. Implementation language and
locale are not documentation-language evidence. A Ready review grants neither
implementation nor remote authority.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
index, bounded aliases, and computed inventory agree.
- [ ] Every Series member occurs once with a current normalized SHA-256; an
active delivery series has exactly one `Eligible` candidate, while a
`Completed` series retains only terminal members and has none.
`Completed` series retains only archived `Completed` members and has none.
- [ ] IDs, order, dependency graph, handoffs, and future-scope boundaries are consistent.
- [ ] Schema 1.1 binds the repository-relative request path and normalized SHA-256.
- [ ] Every target occurs exactly once in `orderedTargetPaths`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ before semantic review. The explicit BCP-47 documentation language, naming
profile, four portable roles, collection paths, bounded aliases, canonical
index, current hashes, and Series inventory must agree. An active delivery
series has exactly one evidenced `Eligible` candidate. A `Completed` series
retains only terminal members and has none. Eligibility selects the next intake
but grants no implementation or remote-delivery authority.
retains only `Completed` members in the configured archive collection and has
none. Eligibility selects the next intake but grants no implementation or
remote-delivery authority.

## Consumer Contract

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$PresetRoot = Split-Path -Parent (Split-Path -Parent $PSCommandPath)
$Validator = Join-Path $PresetRoot 'scripts/validate-intake-governance-config.py'
$BashValidator = Join-Path $PresetRoot 'scripts/validate-intake-governance-config.sh'
$PowerShellValidator = Join-Path $PresetRoot 'scripts/validate-intake-governance-config.ps1'
$Root = Join-Path ([System.IO.Path]::GetTempPath()) ("intake-governance-" + [guid]::NewGuid())
New-Item -ItemType Directory -Path $Root | Out-Null

Expand All @@ -26,13 +27,19 @@ function Get-NormalizedSha256 {

function Invoke-Fixture {
param([string]$Path, [int]$ExpectedExit, [string]$ExpectedText)
$Output = & python3 $Validator --config $Path --repo $Root --json 2>&1
$Exit = $LASTEXITCODE
if ($Exit -ne $ExpectedExit) {
throw "Expected exit $ExpectedExit, got ${Exit}: $Output"
}
if (($Output -join "`n") -notmatch [regex]::Escape($ExpectedText)) {
throw "Expected '$ExpectedText': $Output"
$Runs = @(
@{ Name = 'Bash'; Output = @(& bash $BashValidator --config $Path --repo $Root --json 2>&1); Exit = $LASTEXITCODE },
@{ Name = 'PowerShell'; Output = @(& pwsh -NoProfile -File $PowerShellValidator -Config $Path -Repo $Root -Json 2>&1); Exit = $LASTEXITCODE }
)
foreach ($Run in $Runs) {
$Output = $Run.Output
$Exit = $Run.Exit
if ($Exit -ne $ExpectedExit) {
throw "$($Run.Name): expected exit $ExpectedExit, got ${Exit}: $Output"
}
if (($Output -join "`n") -notmatch [regex]::Escape($ExpectedText)) {
throw "$($Run.Name): expected '$ExpectedText': $Output"
}
}
}

Expand Down Expand Up @@ -139,16 +146,46 @@ try {
Set-Content -LiteralPath $ManifestPath -Encoding utf8NoBOM
Invoke-Fixture (Write-JsonFixture 'completed-series.json' $ManifestInventory) 0 '"eligibleCandidate": "N/A"'

$CompletedWithEligible = $CompletedManifest.Clone()
$CompletedWithEligible.orderedTargets = @($CompletedManifest.orderedTargets[0].Clone())
$CompletedWithEligible.orderedTargets[0].status = 'Eligible'
$CompletedInActive = $Manifest.Clone()
$CompletedInActive.status = 'Completed'
$CompletedInActive.orderedTargets = @($Manifest.orderedTargets[0].Clone())
$CompletedInActive.orderedTargets[0].status = 'Completed'
$CompletedInActive | ConvertTo-Json -Depth 12 |
Set-Content -LiteralPath $ManifestPath -Encoding utf8NoBOM
Invoke-Fixture (Write-JsonFixture 'completed-in-active.json' $ManifestInventory) 2 `
'Completed target must be stored in archive collection'

$MixedManifest = $Manifest.Clone()
$MixedManifest.orderedTargets = @(
$CompletedManifest.orderedTargets[0].Clone(),
$Manifest.orderedTargets[0].Clone()
)
$MixedManifest.roots = @($MixedManifest.orderedTargets[0].path, $MixedManifest.orderedTargets[1].path)
$MixedManifest | ConvertTo-Json -Depth 12 |
Set-Content -LiteralPath $ManifestPath -Encoding utf8NoBOM
Invoke-Fixture (Write-JsonFixture 'mixed-active-series.json' $ManifestInventory) 0 `
'"eligibleCandidate": "requirements/intakes/active/Lastenheft_Beispiel.md"'

$EligibleInArchive = $MixedManifest.Clone()
$EligibleInArchive.orderedTargets = @($CompletedManifest.orderedTargets[0].Clone())
$EligibleInArchive.orderedTargets[0].status = 'Eligible'
$EligibleInArchive.roots = @($EligibleInArchive.orderedTargets[0].path)
$EligibleInArchive | ConvertTo-Json -Depth 12 |
Set-Content -LiteralPath $ManifestPath -Encoding utf8NoBOM
Invoke-Fixture (Write-JsonFixture 'eligible-in-archive.json' $ManifestInventory) 2 `
'non-completed target must not be stored in archive collection'

$CompletedWithEligible = $Manifest.Clone()
$CompletedWithEligible.status = 'Completed'
$CompletedWithEligible.orderedTargets = @($Manifest.orderedTargets[0].Clone())
$CompletedWithEligible | ConvertTo-Json -Depth 12 |
Set-Content -LiteralPath $ManifestPath -Encoding utf8NoBOM
Invoke-Fixture (Write-JsonFixture 'completed-with-eligible.json' $ManifestInventory) 2 `
'Completed series must not contain an Eligible target'

$CompletedWithPending = $CompletedManifest.Clone()
$CompletedWithPending.orderedTargets = @($CompletedManifest.orderedTargets[0].Clone())
$CompletedWithPending = $Manifest.Clone()
$CompletedWithPending.status = 'Completed'
$CompletedWithPending.orderedTargets = @($Manifest.orderedTargets[0].Clone())
$CompletedWithPending.orderedTargets[0].status = 'Pending'
$CompletedWithPending | ConvertTo-Json -Depth 12 |
Set-Content -LiteralPath $ManifestPath -Encoding utf8NoBOM
Expand Down
Loading
Loading