Skip to content

Phase-0 construction blockers: part-less PO lines, Parts out of Foundations, self-gated mfg jobs - #25

Merged
dhokanson-armoryworks merged 4 commits into
mainfrom
construction/phase0-blockers
Aug 19, 2026
Merged

dhokanson-armoryworks merged 4 commits into
mainfrom
construction/phase0-blockers

Conversation

@dhokanson-armoryworks

Copy link
Copy Markdown
Collaborator

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

Companion PR: forge-db construction/phase0-po-partid-nullable (schema).

🤖 Generated with Claude Code

danielhokanson and others added 2 commits August 17, 2026 01:29
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,
});
}
}
danielhokanson and others added 2 commits August 18, 2026 15:48
…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>
@dhokanson-armoryworks
dhokanson-armoryworks merged commit 18bf5c1 into main Aug 19, 2026
2 checks passed
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; }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants