@patronage/software-factory 0.30.0 → 1.0.0-alpha.1

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/dist/schemas.d.ts CHANGED
@@ -484,6 +484,31 @@ interface PrReadyProof {
484
484
  waivedDemands?: WaivedDemand[];
485
485
  }
486
486
  //#endregion
487
+ //#region src/impact-stamp.d.ts
488
+ /**
489
+ * The recorded stamp. `targets` satisfies the completeness invariant: every
490
+ * target the profile declares is present, each with its exact basis — never
491
+ * silently absent. `basis: "conservative"` means an unmodelled or unprovable
492
+ * input widened every target to full impact.
493
+ */
494
+ declare const impactStampSchema: z.ZodObject<{
495
+ basis: z.ZodEnum<{
496
+ "target-scoped": "target-scoped";
497
+ conservative: "conservative";
498
+ }>;
499
+ reasons: z.ZodArray<z.ZodString>;
500
+ stampVersion: z.ZodLiteral<2>;
501
+ targets: z.ZodArray<z.ZodObject<{
502
+ basis: z.ZodString;
503
+ impact: z.ZodEnum<{
504
+ affected: "affected";
505
+ "not-affected": "not-affected";
506
+ }>;
507
+ name: z.ZodString;
508
+ }, z.core.$strip>>;
509
+ }, z.core.$strip>;
510
+ type ImpactStamp = z.infer<typeof impactStampSchema>;
511
+ //#endregion
487
512
  //#region src/pr-readiness/external-evidence.d.ts
488
513
  declare const evidenceEnvelopeBaseSchema: z.ZodObject<{
489
514
  check: z.ZodString;
@@ -520,6 +545,15 @@ type PrVerifySchemaVersion = (typeof SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS$1)[numb
520
545
  interface PrVerifyCommandResult {
521
546
  command: string;
522
547
  description: string;
548
+ /**
549
+ * Epic #550 wave 3 (#430): the profile's declared impact target for this
550
+ * command, recorded from v4 on. It is the proof's own command→target
551
+ * mapping: a `notRequiredCommands` entry must name the target recorded HERE
552
+ * for the same command, so a withholding cannot point at whichever released
553
+ * target happens to be convenient. Absent for a command the profile does
554
+ * not scope, and for v1–v3 proofs, which predate the field.
555
+ */
556
+ impactTarget?: string;
523
557
  name: string;
524
558
  scope: "always" | "docs-only" | "trivial" | "full";
525
559
  }
@@ -534,6 +568,21 @@ interface PrVerifyExecutedCommand {
534
568
  name: string;
535
569
  scope: "always" | "docs-only" | "trivial" | "full";
536
570
  }
571
+ /**
572
+ * Epic #550 wave 3 (#430): a verification command the impact stamp proved this
573
+ * candidate cannot reach, so the battery did not run it.
574
+ *
575
+ * The completeness invariant lives in the pairing: `verificationCommands`
576
+ * records every command the resolved mode selected, `executedCommands` records
577
+ * what ran, and this list records the rest WITH the stamp's own basis. A
578
+ * withheld stack is therefore never silently absent from the proof — it is
579
+ * present, named, and explained.
580
+ */
581
+ interface PrVerifyNotRequiredCommand {
582
+ basis: string;
583
+ impactTarget: string;
584
+ name: string;
585
+ }
537
586
  interface PrVerifyProof {
538
587
  schemaVersion: PrVerifySchemaVersion;
539
588
  authoringSession?: string;
@@ -554,8 +603,10 @@ interface PrVerifyProof {
554
603
  endedAt: string;
555
604
  executedCommands?: PrVerifyExecutedCommand[];
556
605
  headSha: string;
606
+ impactStamp?: ImpactStamp;
557
607
  mode: ResolvedPrVerifyMode;
558
608
  mergeBaseSha?: string;
609
+ notRequiredCommands?: PrVerifyNotRequiredCommand[];
559
610
  outcome?: "aborted" | "passed";
560
611
  patchId?: string;
561
612
  profilePath: string;
@@ -1309,12 +1360,33 @@ declare const prVerifyProofSchema: z.ZodObject<{
1309
1360
  }>;
1310
1361
  }, z.core.$strip>>>;
1311
1362
  headSha: z.ZodString;
1363
+ impactStamp: z.ZodOptional<z.ZodObject<{
1364
+ basis: z.ZodEnum<{
1365
+ "target-scoped": "target-scoped";
1366
+ conservative: "conservative";
1367
+ }>;
1368
+ reasons: z.ZodArray<z.ZodString>;
1369
+ stampVersion: z.ZodLiteral<2>;
1370
+ targets: z.ZodArray<z.ZodObject<{
1371
+ basis: z.ZodString;
1372
+ impact: z.ZodEnum<{
1373
+ affected: "affected";
1374
+ "not-affected": "not-affected";
1375
+ }>;
1376
+ name: z.ZodString;
1377
+ }, z.core.$strip>>;
1378
+ }, z.core.$strip>>;
1312
1379
  mergeBaseSha: z.ZodOptional<z.ZodString>;
1313
1380
  mode: z.ZodEnum<{
1314
1381
  trivial: "trivial";
1315
1382
  "docs-only": "docs-only";
1316
1383
  full: "full";
1317
1384
  }>;
1385
+ notRequiredCommands: z.ZodOptional<z.ZodArray<z.ZodObject<{
1386
+ basis: z.ZodString;
1387
+ impactTarget: z.ZodString;
1388
+ name: z.ZodString;
1389
+ }, z.core.$strip>>>;
1318
1390
  outcome: z.ZodOptional<z.ZodEnum<{
1319
1391
  aborted: "aborted";
1320
1392
  passed: "passed";
@@ -1328,6 +1400,7 @@ declare const prVerifyProofSchema: z.ZodObject<{
1328
1400
  verificationCommands: z.ZodArray<z.ZodObject<{
1329
1401
  command: z.ZodString;
1330
1402
  description: z.ZodString;
1403
+ impactTarget: z.ZodOptional<z.ZodString>;
1331
1404
  name: z.ZodString;
1332
1405
  scope: z.ZodEnum<{
1333
1406
  trivial: "trivial";
package/dist/schemas.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { z } from "zod";
2
+ import "yaml";
3
+ import "picomatch";
2
4
  //#region src/review-rungs.ts
3
5
  const EVIDENCE_REVIEW_RUNGS$1 = [
4
6
  "independent-model",
@@ -226,6 +228,294 @@ const waivedDemandSchema = z.object({
226
228
  session: z.string().trim().min(1).optional(),
227
229
  unmetReasons: z.array(z.string().min(1)).min(1)
228
230
  });
231
+ const impactStampTargetSchema = z.object({
232
+ basis: z.string().min(1),
233
+ impact: z.enum(["affected", "not-affected"]),
234
+ name: z.string().min(1)
235
+ });
236
+ /**
237
+ * The recorded stamp. `targets` satisfies the completeness invariant: every
238
+ * target the profile declares is present, each with its exact basis — never
239
+ * silently absent. `basis: "conservative"` means an unmodelled or unprovable
240
+ * input widened every target to full impact.
241
+ */
242
+ const impactStampSchema = z.object({
243
+ basis: z.enum(["target-scoped", "conservative"]),
244
+ reasons: z.array(z.string()),
245
+ stampVersion: z.literal(2),
246
+ targets: z.array(impactStampTargetSchema).superRefine((targets, context) => {
247
+ const names = targets.map((target) => target.name);
248
+ if (new Set(names).size !== names.length) context.addIssue({
249
+ code: "custom",
250
+ message: "impact stamp targets must have distinct names."
251
+ });
252
+ })
253
+ });
254
+ /**
255
+ * The surfaces that consume the stamp. One classification, consumed
256
+ * everywhere (#430): every surface routes through
257
+ * {@link impactStampScopeDecision}, so no surface can be more trusting than
258
+ * another, and no surface re-derives path rules of its own. The value only
259
+ * selects the wording of the conservative floor a refusal falls back to.
260
+ */
261
+ const IMPACT_SCOPE_SURFACES = {
262
+ demand: "full demand floor",
263
+ "preview-lifecycle": "full preview-lifecycle floor",
264
+ "verification-battery": "full verification-battery floor"
265
+ };
266
+ /**
267
+ * Whether a trusted stamp RELEASES one target-scoped unit of work — an
268
+ * external proof demand (#542 wave 2), a verification-battery command, or a
269
+ * preview (Alchemy) lifecycle stack (#430 wave 3).
270
+ *
271
+ * The only release path is a `target-scoped` stamp of exactly this build's
272
+ * stamp version whose target entry for exactly this name is provably
273
+ * `not-affected`. Every other input — no trusted stamp, an unknown stamp
274
+ * version, a conservative stamp, a name the stamp does not classify, or an
275
+ * `affected` verdict — keeps today's full-work floor. Work only ever gets
276
+ * released, never added or satisfied: this function can withdraw a demand, it
277
+ * can never certify one.
278
+ *
279
+ * Identity binding is the CALLER's precondition: pass only a stamp computed
280
+ * for, or read from a pr:verify proof bound to, the current candidate (see
281
+ * `trustedImpactStamp` in `pr-readiness/verification-proof.ts`); pass
282
+ * `undefined` otherwise.
283
+ */
284
+ const impactStampScopeDecision = ({ stamp, surface, targetName }) => {
285
+ const floor = IMPACT_SCOPE_SURFACES[surface];
286
+ if (stamp === void 0) return {
287
+ reason: `no trusted identity-bound impact stamp covers this candidate; ${floor}`,
288
+ scoped: false
289
+ };
290
+ if (stamp.stampVersion !== 2) return {
291
+ reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version 2; ${floor}`,
292
+ scoped: false
293
+ };
294
+ if (stamp.basis !== "target-scoped") return {
295
+ reason: `impact stamp is conservative (full impact assumed); ${floor}`,
296
+ scoped: false
297
+ };
298
+ const matches = stamp.targets.filter((candidate) => candidate.name === targetName);
299
+ if (matches.length > 1) return {
300
+ reason: `impact stamp is self-contradictory: ${matches.length} rows classify a target named "${targetName}"; ${floor}`,
301
+ scoped: false
302
+ };
303
+ const [target] = matches;
304
+ if (target === void 0) return {
305
+ reason: `impact stamp does not classify a target named "${targetName}"; ${floor}`,
306
+ scoped: false
307
+ };
308
+ if (target.impact !== "not-affected") return {
309
+ reason: `impact stamp records target "${targetName}" as affected (${target.basis})`,
310
+ scoped: false
311
+ };
312
+ return {
313
+ reason: `impact stamp released: target "${targetName}" is provably not affected by this candidate's delta (${target.basis})`,
314
+ scoped: true
315
+ };
316
+ };
317
+ const importerDependencySchema = z.looseObject({
318
+ specifier: z.string().optional(),
319
+ version: z.string()
320
+ });
321
+ const importerSectionSchema = z.looseObject({
322
+ dependencies: z.record(z.string(), importerDependencySchema).optional(),
323
+ devDependencies: z.record(z.string(), importerDependencySchema).optional(),
324
+ optionalDependencies: z.record(z.string(), importerDependencySchema).optional()
325
+ });
326
+ const snapshotSectionSchema = z.looseObject({
327
+ dependencies: z.record(z.string(), z.string()).optional(),
328
+ devDependencies: z.record(z.string(), z.string()).optional(),
329
+ optionalDependencies: z.record(z.string(), z.string()).optional()
330
+ });
331
+ z.looseObject({
332
+ importers: z.record(z.string(), importerSectionSchema),
333
+ lockfileVersion: z.unknown().optional(),
334
+ packages: z.record(z.string(), z.unknown()).optional(),
335
+ snapshots: z.record(z.string(), snapshotSectionSchema).optional()
336
+ });
337
+ //#endregion
338
+ //#region src/verification-battery.ts
339
+ /**
340
+ * The completeness invariant as an enforceable control (ADR 0024: controls
341
+ * fail loudly), shared by both pr:verify proof schemas so they cannot drift.
342
+ *
343
+ * Four rules:
344
+ * 1. A withheld command names one the resolved mode actually selected.
345
+ * 2. A command is never recorded as both executed and not-required.
346
+ * 3. Withheld commands are distinct — a duplicated disposition would let one
347
+ * name carry two different bases.
348
+ * 4. On a PASSED proof, every selected command has a disposition: silence is
349
+ * not a disposition, and "absent from both lists" is exactly the silent
350
+ * skip this epic forbids.
351
+ *
352
+ * Rule 4 is scoped to `outcome: "passed"` because an ABORTED proof legitimately
353
+ * records partial execution — the run stopped at the failing command. It is
354
+ * also scoped to schemaVersion >= 4: `executedCommands` postdates v1–v3, so a
355
+ * legacy proof that never recorded it is not making a false completeness claim.
356
+ *
357
+ * The check is one-directional on purpose. Every SELECTED command must be
358
+ * accounted for, but `executedCommands` may legitimately carry MORE than the
359
+ * profile selected — full mode appends the `workspace:install-resolves` probe,
360
+ * which is real work no profile declares.
361
+ */
362
+ const assertBatteryCompleteness = (proof, context) => {
363
+ const selected = proof.verificationCommands.map((command) => command.name);
364
+ const selectedSet = new Set(selected);
365
+ const executed = new Set((proof.executedCommands ?? []).map((command) => command.name));
366
+ const notRequired = proof.notRequiredCommands ?? [];
367
+ const seen = /* @__PURE__ */ new Set();
368
+ for (const [index, entry] of notRequired.entries()) {
369
+ if (!selectedSet.has(entry.name)) context.addIssue({
370
+ code: "custom",
371
+ message: `notRequiredCommands entry "${entry.name}" is not one of this proof's verificationCommands.`,
372
+ path: [
373
+ "notRequiredCommands",
374
+ index,
375
+ "name"
376
+ ]
377
+ });
378
+ if (executed.has(entry.name)) context.addIssue({
379
+ code: "custom",
380
+ message: `Command "${entry.name}" is recorded both as executed and as not-required.`,
381
+ path: [
382
+ "notRequiredCommands",
383
+ index,
384
+ "name"
385
+ ]
386
+ });
387
+ if (seen.has(entry.name)) context.addIssue({
388
+ code: "custom",
389
+ message: `notRequiredCommands records "${entry.name}" more than once.`,
390
+ path: [
391
+ "notRequiredCommands",
392
+ index,
393
+ "name"
394
+ ]
395
+ });
396
+ seen.add(entry.name);
397
+ }
398
+ if (proof.outcome !== "passed" || proof.schemaVersion < 4) return;
399
+ const disposed = new Set([...executed, ...seen]);
400
+ const undisposed = selected.filter((name) => !disposed.has(name));
401
+ if (undisposed.length > 0) context.addIssue({
402
+ code: "custom",
403
+ message: `A passed pr:verify proof must give every selected verification command a disposition; [${undisposed.join(", ")}] appear in verificationCommands but in neither executedCommands nor notRequiredCommands.`,
404
+ path: ["executedCommands"]
405
+ });
406
+ };
407
+ /**
408
+ * The stamp-authorization control: a passed v4 proof may only claim a command
409
+ * was NOT REQUIRED if its own recorded stamp says so.
410
+ *
411
+ * `assertBatteryCompleteness` proves the two lists partition the selected
412
+ * commands — that no command is silently absent. It does not prove the
413
+ * withholding was EARNED. Without this rule a proof could name every expensive
414
+ * command in `notRequiredCommands`, carry no stamp at all (or a conservative
415
+ * one), and still read as full verification: the omission would be recorded,
416
+ * accounted for, and completely unauthorized.
417
+ *
418
+ * Two things have to hold, and the first is what keeps the second honest:
419
+ *
420
+ * 1. COHERENCE. The entry's `impactTarget` must equal the `impactTarget` the
421
+ * proof records for that same command in `verificationCommands`. The entry
422
+ * does not get to nominate its own target; the proof's command→target
423
+ * mapping (written by `pr:verify` from the loaded profile) does. Without
424
+ * this, authorization validates a self-reported field and a crafted proof
425
+ * can withhold an affected command while pointing at an unrelated released
426
+ * target.
427
+ * 2. AUTHORIZATION. That recorded target must be one the proof's own stamp
428
+ * provably released.
429
+ *
430
+ * The proof is one artifact and a determined forger controls all of it; this
431
+ * is internal-coherence belt-and-braces in the same trust domain as the
432
+ * version guards, and write-time truth stays `pr:verify`'s job.
433
+ *
434
+ * The authorizing evidence is the proof's OWN `impactStamp` — the same stamp
435
+ * `pr:verify` computed for this candidate's identity triple and recorded here,
436
+ * so authorization is bound to the same identities the proof binds. The
437
+ * predicate is the shared {@link impactStampScopeDecision}, not a second
438
+ * reading of the stamp: an entry is authorized exactly when the stamp would
439
+ * have released that target's command in the first place (target-scoped
440
+ * basis, this build's stamp version, exactly one row for the target,
441
+ * `not-affected`). A conservative stamp, an unknown version, a
442
+ * self-contradictory stamp, an unclassified name, or an `affected` verdict all
443
+ * fail the same way scoping itself would.
444
+ *
445
+ * Scoped to `outcome: "passed"` and schemaVersion >= 4 for the same reasons as
446
+ * the completeness rule: an aborted run records partial execution, and a
447
+ * legacy proof predates both fields, so neither is making a false completeness
448
+ * claim.
449
+ */
450
+ const assertNotRequiredStampAuthorization = (proof, context) => {
451
+ const notRequired = proof.notRequiredCommands ?? [];
452
+ if (proof.outcome !== "passed" || proof.schemaVersion < 4 || notRequired.length === 0) return;
453
+ const { impactStamp } = proof;
454
+ if (impactStamp === void 0) {
455
+ context.addIssue({
456
+ code: "custom",
457
+ message: `A passed pr:verify proof that withholds verification commands must record the impact stamp that authorized the withholding; [${notRequired.map((entry) => entry.name).join(", ")}] are recorded not-required by a proof carrying no impactStamp.`,
458
+ path: ["impactStamp"]
459
+ });
460
+ return;
461
+ }
462
+ const recordedTargets = new Map(proof.verificationCommands.map((command) => [command.name, command.impactTarget]));
463
+ for (const [index, entry] of notRequired.entries()) {
464
+ const recorded = recordedTargets.get(entry.name);
465
+ if (recorded !== entry.impactTarget) {
466
+ context.addIssue({
467
+ code: "custom",
468
+ message: recorded === void 0 ? `notRequiredCommands entry "${entry.name}" claims impactTarget "${entry.impactTarget}", but this proof records no impactTarget for that command; only a command the profile scopes can be withheld.` : `notRequiredCommands entry "${entry.name}" claims impactTarget "${entry.impactTarget}", but this proof records impactTarget "${recorded}" for that command.`,
469
+ path: [
470
+ "notRequiredCommands",
471
+ index,
472
+ "impactTarget"
473
+ ]
474
+ });
475
+ continue;
476
+ }
477
+ const decision = impactStampScopeDecision({
478
+ stamp: impactStamp,
479
+ surface: "verification-battery",
480
+ targetName: entry.impactTarget
481
+ });
482
+ if (!decision.scoped) context.addIssue({
483
+ code: "custom",
484
+ message: `notRequiredCommands entry "${entry.name}" is not authorized by this proof's impactStamp: ${decision.reason}.`,
485
+ path: [
486
+ "notRequiredCommands",
487
+ index,
488
+ "impactTarget"
489
+ ]
490
+ });
491
+ }
492
+ };
493
+ //#endregion
494
+ //#region src/pr-verify-proof-rules.ts
495
+ const assertPrVerifyProofRules = (proof, context) => {
496
+ if (proof.schemaVersion >= 4 && proof.authoringSession === void 0) context.addIssue({
497
+ code: "custom",
498
+ message: "schemaVersion 4 pr:verify proof requires authoringSession (the recorded authoring identity, or the `unknown` sentinel).",
499
+ path: ["authoringSession"]
500
+ });
501
+ if (proof.schemaVersion < 4 && proof.authoringSession !== void 0) context.addIssue({
502
+ code: "custom",
503
+ message: "authoringSession is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
504
+ path: ["authoringSession"]
505
+ });
506
+ if (proof.schemaVersion < 4 && proof.impactStamp !== void 0) context.addIssue({
507
+ code: "custom",
508
+ message: "impactStamp is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
509
+ path: ["impactStamp"]
510
+ });
511
+ if (proof.schemaVersion < 4 && proof.notRequiredCommands !== void 0) context.addIssue({
512
+ code: "custom",
513
+ message: "notRequiredCommands is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
514
+ path: ["notRequiredCommands"]
515
+ });
516
+ assertBatteryCompleteness(proof, context);
517
+ assertNotRequiredStampAuthorization(proof, context);
518
+ };
229
519
  const LEGACY_CLOSEOUT_SCHEMA_VERSION = 3;
230
520
  /** Current writer plus retained reader versions accepted at ingest. */
231
521
  const SUPPORTED_CLOSEOUT_SCHEMA_VERSIONS = [LEGACY_CLOSEOUT_SCHEMA_VERSION, 4];
@@ -702,6 +992,11 @@ const executedCommandSchema = z.object({
702
992
  "full"
703
993
  ])
704
994
  });
995
+ const notRequiredCommandSchema = z.object({
996
+ basis: z.string().min(1),
997
+ impactTarget: z.string().min(1),
998
+ name: z.string().min(1)
999
+ });
705
1000
  const prVerifySchemaVersionSchema = z.number().refine((value) => SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS.includes(value), { message: `schemaVersion must be one of: ${SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS.join(", ")}` });
706
1001
  const prVerifyProofSchema = z.object({
707
1002
  authoringSession: z.string().trim().min(1).optional(),
@@ -722,12 +1017,14 @@ const prVerifyProofSchema = z.object({
722
1017
  endedAt: z.iso.datetime(),
723
1018
  executedCommands: z.array(executedCommandSchema).optional(),
724
1019
  headSha: z.string().regex(/^[0-9a-f]{40}$/u),
1020
+ impactStamp: impactStampSchema.optional(),
725
1021
  mergeBaseSha: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
726
1022
  mode: z.enum([
727
1023
  "docs-only",
728
1024
  "trivial",
729
1025
  "full"
730
1026
  ]),
1027
+ notRequiredCommands: z.array(notRequiredCommandSchema).min(1).optional(),
731
1028
  outcome: z.enum(["aborted", "passed"]).optional(),
732
1029
  patchId: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
733
1030
  profilePath: z.string().min(1),
@@ -738,6 +1035,7 @@ const prVerifyProofSchema = z.object({
738
1035
  verificationCommands: z.array(z.object({
739
1036
  command: z.string().min(1),
740
1037
  description: z.string().min(1),
1038
+ impactTarget: z.string().min(1).optional(),
741
1039
  name: z.string().min(1),
742
1040
  scope: z.enum([
743
1041
  "always",
@@ -746,18 +1044,7 @@ const prVerifyProofSchema = z.object({
746
1044
  "full"
747
1045
  ])
748
1046
  }))
749
- }).superRefine((proof, context) => {
750
- if (proof.schemaVersion >= 4 && proof.authoringSession === void 0) context.addIssue({
751
- code: "custom",
752
- message: "schemaVersion 4 pr:verify proof requires authoringSession (the recorded authoring identity, or the `unknown` sentinel).",
753
- path: ["authoringSession"]
754
- });
755
- if (proof.schemaVersion < 4 && proof.authoringSession !== void 0) context.addIssue({
756
- code: "custom",
757
- message: "authoringSession is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
758
- path: ["authoringSession"]
759
- });
760
- });
1047
+ }).superRefine(assertPrVerifyProofRules);
761
1048
  const PR_REVIEW_SCHEMA_VERSION = 2;
762
1049
  const PR_REVIEW_FINDING_PROVENANCE_VERSION = 1;
763
1050
  const nonBlankString = z.string().refine((value) => value.trim().length > 0, { message: "must not be blank" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patronage/software-factory",
3
- "version": "0.30.0",
3
+ "version": "1.0.0-alpha.1",
4
4
  "description": "Shared Patronage software factory CLI and project-profile validation tools",
5
5
  "license": "MIT",
6
6
  "repository": {