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

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