Phase-0 construction blockers: part-less PO lines, Parts out of Foundations, self-gated mfg jobs - #25
Merged
Conversation
Foundations, self-gated manufacturing jobs
Three fixes that unblock a manufacturing-free install (construction vertical,
Option B seams-only plan) and also repair the shipping Pro Services preset:
1. PurchaseOrderLine.PartId is nullable. The capability graph deliberately
removed the PO->Parts dependency ("Quote/SO/PO schemas already make PartId
optional") — true for quote/SO/invoice lines, false for PO lines until now.
A part-less line (service, or material described in words) requires a
Description, is never binned at receipt, and posts to operating expense
rather than inventory (GRNI). VendorBillLine.PartId was already nullable —
this closes the asymmetry. Touched consumers filter part-less lines where
parts are the subject (MRP supply, reorder/burn-rate analysis, ABC costing,
sankey vendor->part flow, material-ready check) and null-guard projections.
Companion schema change: forge-db construction/phase0-po-partid-nullable
(ALTER ... DROP NOT NULL — additive, no backfill).
2. CAP-MD-PARTS moved out of ModuleCatalog.Foundations. Foundations-membership
made the first-run picker unable to express "no parts" while PRESET-08
removes the capability — the two mechanisms contradicted each other. The
honest dependency edge CAP-INV-CORE -> CAP-MD-PARTS is added so an
inventory-module install still closes over the item master.
3. Eight manufacturing jobs now self-gate on their owning capability, following
the VarianceWatchdogJob pattern (first-line snapshot check; the schedule
still ticks, the job no-ops, toggles take effect without a restart):
MrpRunJob (CAP-PLAN-MRP), ReorderAnalysisJob (CAP-PLAN-SAFETYSTOCK),
AutoPurchaseOrderJob (CAP-P2P-AUTOPO), CheckInventoryLevelsJob
(CAP-INV-CORE), ChannelInventorySyncJob (CAP-EXT-ECOMMERCE),
OverdueMaintenanceJob (CAP-MAINT-PM), ShipmentDeliverySweepJob
(CAP-O2C-SHIP), CheckJobCostOverrunJob (CAP-MFG-WO-RELEASE).
Demo seed data was already gated (SEED_DEMO_DATA, default false).
Gates: dotnet build -warnaserror green; dotnet test = 2,176 passed with a
failure set byte-identical to main (62 pre-existing docker/postgres-dependent
tests that cannot run on this machine — verified by running the suite on a main
worktree back to back).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turning CAP-ACCT-FULLGL off via the capability toggle is now refused (409 capability-gl-live-ledger) when any active book has opening balances loaded. Stopping GL posting mid-period desyncs the sub-ledgers from the ledger; the off-ramp is the governed deactivation cutover (see fullgl-deactivation design). Mirrors the existing opening-balances enable-gate; reuses AreOpeningBalancesLoadedAsync.
Comment on lines
+200
to
+218
| foreach (var bookId in activeBookIds) | ||
| { | ||
| if (await glGate.AreOpeningBalancesLoadedAsync(bookId, cancellationToken)) | ||
| { | ||
| throw new CapabilityMutationException( | ||
| StatusCodes.Status409Conflict, | ||
| "capability-gl-live-ledger", | ||
| $"CAP-ACCT-FULLGL cannot be turned off for book {bookId} by toggling the " + | ||
| "capability: the ledger is live (opening balances are loaded), so stopping " + | ||
| "posting mid-period would desync the sub-ledgers from the general ledger. Turn " + | ||
| "the ledger off through the governed deactivation cutover (close and tie out the " + | ||
| "period, then deactivate with a successor system).", | ||
| new Dictionary<string, object?> | ||
| { | ||
| ["capability"] = request.Code, | ||
| ["bookId"] = bookId, | ||
| }); | ||
| } | ||
| } |
…uto code
Adds a Source {System|Manual} axis to the barcode registry. The one auto-assigned
System code per entity (internal code or GS1 GTIN) is unchanged and RefreshPartBarcode
now only touches it; users can register additional Manual barcodes (manufacturer UPC,
vendor SKU, legacy label) that coexist and all resolve on scan (global value uniqueness).
New POST /barcodes and DELETE /barcodes/{id} (manual-only). Tests cover coexistence,
scan resolution, uniqueness, manual-only removal, and refresh not clobbering aliases.
…itive (CAP-CROSS-SEQUENCES) A general-purpose Petri-net-with-guards-and-clocks module for routing gates, inspection sign-offs, lot expiry and permit/inspection chains. Open core per the construction-vertical boundary rule (private holds operations, never workflow). - forge.core: 8 enums, 9 entities (SequenceDefinition/Step/Edge/Gate, SequenceInstance/StepInstance/GateInstance, SequenceEvent append-only log, SequenceResourceClock that travels with a resource), pure engine in Forge.Core.Sequences (SequenceNet, SequenceNetValidator, SequenceEvaluator, IGateSource, contexts/results), ISequenceEvaluationService, 14 models. - forge.data: DbSets + 9 configurations; embedded forge-schema.sql regenerated from forge-db (9 tables, 14 indexes). - forge.api: Features/Sequences (definitions lifecycle: create/update/publish/ new-version/retire; instances: start/list/get/events/reevaluate/cancel/rework; steps start/complete/skip; gates clear/override; resource clocks), built-in gate sources ManualClearance/TimeWindow/ResourceClock/Approval + Custom (fail-closed when unregistered), SequenceEvaluationService, SequencesController at api/v1/sequences gated by CAP-CROSS-SEQUENCES (new catalog row, off by default, 165 total; CLAUDE.md count bumped), SequenceClockJob (Hangfire minutely — fires resource/dwell clocks once, re-evaluates time gates), domain events SequenceStepReady/InstanceCompleted/ClockExpired, reaction OnApprovalCompleted_ReevaluateSequences. - forge.tests/Sequences: 27 tests (validator, evaluator idempotency/joins/ override/blocked, definition lifecycle, full instance flows, time-window + resource-clock + dwell via the clock job, approval reaction, custom source). - CLAUDE.md: Gated Sequence Engine section (rules for extending it). Release -warnaserror clean; 2,210 tests pass (62 Postgres/Testcontainers tests need Docker, unavailable on this box). Design + implementation record in forge/docs/delivery/in-progress/gated-sequence-engine/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines
+35
to
+46
| foreach (var gate in instance.Gates) | ||
| { | ||
| if (!verdicts.TryGetValue((gate.StepKey, gate.GateKey), out var v)) continue; | ||
| var effective = gate.OverriddenAt.HasValue ? SequenceGateVerdict.Go : v.Verdict; | ||
| var reason = gate.OverriddenAt.HasValue ? $"Overridden: {gate.OverrideReason}" : v.Reason; | ||
| gate.LastEvaluatedAt = now; | ||
| if (gate.Verdict == effective && gate.Reason == reason) continue; | ||
| gate.Verdict = effective; | ||
| gate.Reason = reason; | ||
| result.Events.Add(Event(instance, SequenceEventType.GateEvaluated, now, actorUserId, gate.StepKey, gate.GateKey, | ||
| $"{{\"verdict\":\"{effective}\",\"reason\":{Json(reason)}}}")); | ||
| } |
Comment on lines
+54
to
+81
| foreach (var stepDef in net.Steps) | ||
| { | ||
| if (!steps.TryGetValue(stepDef.Key, out var step)) continue; | ||
| if (step.Status is SequenceStepStatus.InProgress or SequenceStepStatus.Complete or SequenceStepStatus.Skipped) continue; | ||
|
|
||
| var predsOk = PredecessorsSatisfied(net, stepDef, steps); | ||
| var gatesOk = net.GatesOf(stepDef.Key).All(g => | ||
| gates.TryGetValue((stepDef.Key, g.Key), out var gi) && gi.Verdict == SequenceGateVerdict.Go); | ||
|
|
||
| if (predsOk && gatesOk && step.Status == SequenceStepStatus.Pending) | ||
| { | ||
| step.Status = SequenceStepStatus.Ready; | ||
| step.ReadyAt = now; | ||
| result.NewlyReady.Add(step.StepKey); | ||
| result.Events.Add(Event(instance, SequenceEventType.StepReady, now, actorUserId, step.StepKey)); | ||
| changed = true; | ||
| } | ||
| else if (!(predsOk && gatesOk) && step.Status == SequenceStepStatus.Ready) | ||
| { | ||
| step.Status = SequenceStepStatus.Pending; | ||
| step.ReadyAt = null; | ||
| result.Events.Add(Event(instance, SequenceEventType.StepBlocked, now, actorUserId, step.StepKey, | ||
| $"{{\"reason\":{Json(BlockedReason(net, stepDef, gates))}}}")); | ||
| changed = true; | ||
| } | ||
|
|
||
| if (predsOk && !gatesOk) result.Blocked.Add(step.StepKey); | ||
| } |
Comment on lines
+28
to
+31
| foreach (var g in definition.Gates) | ||
| { | ||
| if (_gates.TryGetValue(g.StepKey, out var list)) list.Add(g); | ||
| } |
Comment on lines
+52
to
+55
| foreach (var k in keys) | ||
| { | ||
| if (colour[k] == 0 && HasCycle(net, k, colour)) { errors.Add("The dependency graph has a cycle that is not marked as rework."); break; } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three fixes that unblock a manufacturing-free install (construction vertical,
Option B seams-only plan) and also repair the shipping Pro Services preset:
PurchaseOrderLine.PartId is nullable. The capability graph deliberately
removed the PO->Parts dependency ("Quote/SO/PO schemas already make PartId
optional") — true for quote/SO/invoice lines, false for PO lines until now.
A part-less line (service, or material described in words) requires a
Description, is never binned at receipt, and posts to operating expense
rather than inventory (GRNI). VendorBillLine.PartId was already nullable —
this closes the asymmetry. Touched consumers filter part-less lines where
parts are the subject (MRP supply, reorder/burn-rate analysis, ABC costing,
sankey vendor->part flow, material-ready check) and null-guard projections.
Companion schema change: forge-db construction/phase0-po-partid-nullable
(ALTER ... DROP NOT NULL — additive, no backfill).
CAP-MD-PARTS moved out of ModuleCatalog.Foundations. Foundations-membership
made the first-run picker unable to express "no parts" while PRESET-08
removes the capability — the two mechanisms contradicted each other. The
honest dependency edge CAP-INV-CORE -> CAP-MD-PARTS is added so an
inventory-module install still closes over the item master.
Eight manufacturing jobs now self-gate on their owning capability, following
the VarianceWatchdogJob pattern (first-line snapshot check; the schedule
still ticks, the job no-ops, toggles take effect without a restart):
MrpRunJob (CAP-PLAN-MRP), ReorderAnalysisJob (CAP-PLAN-SAFETYSTOCK),
AutoPurchaseOrderJob (CAP-P2P-AUTOPO), CheckInventoryLevelsJob
(CAP-INV-CORE), ChannelInventorySyncJob (CAP-EXT-ECOMMERCE),
OverdueMaintenanceJob (CAP-MAINT-PM), ShipmentDeliverySweepJob
(CAP-O2C-SHIP), CheckJobCostOverrunJob (CAP-MFG-WO-RELEASE).
Demo seed data was already gated (SEED_DEMO_DATA, default false).
Gates: dotnet build -warnaserror green; dotnet test = 2,176 passed with a
failure set byte-identical to main (62 pre-existing docker/postgres-dependent
tests that cannot run on this machine — verified by running the suite on a main
worktree back to back).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Companion PR: forge-db
construction/phase0-po-partid-nullable(schema).🤖 Generated with Claude Code