arkaik 0.1.2 → 0.2.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/index.js CHANGED
@@ -14942,6 +14942,7 @@ function crossCheckJournal(bundle) {
14942
14942
  });
14943
14943
  const everNodes = new Set(snapshotNodeStatus.keys());
14944
14944
  const everEdges = new Set(snapshotEdgeIds);
14945
+ const baselined = /* @__PURE__ */ new Set();
14945
14946
  for (const { ev } of valid) {
14946
14947
  if (ev.type === "node.created") {
14947
14948
  const nid = str2(ev.node_id);
@@ -14949,6 +14950,14 @@ function crossCheckJournal(bundle) {
14949
14950
  } else if (ev.type === "edge.added") {
14950
14951
  const eid = str2(ev.edge_id);
14951
14952
  if (eid) everEdges.add(eid);
14953
+ } else if (ev.type === "journal.baseline" && Array.isArray(ev.node_ids)) {
14954
+ for (const raw of ev.node_ids) {
14955
+ const nid = str2(raw);
14956
+ if (nid) {
14957
+ baselined.add(nid);
14958
+ everNodes.add(nid);
14959
+ }
14960
+ }
14952
14961
  }
14953
14962
  }
14954
14963
  const ordered = orderEvents(valid.map((v) => v.ev));
@@ -15014,7 +15023,7 @@ function crossCheckJournal(bundle) {
15014
15023
  }
15015
15024
  }
15016
15025
  for (const [nodeId, status] of snapshotNodeStatus) {
15017
- if (!created.has(nodeId)) {
15026
+ if (!created.has(nodeId) && !baselined.has(nodeId)) {
15018
15027
  findings.push({
15019
15028
  path: "journal",
15020
15029
  rule: "journal-missing-node-created",
@@ -15046,6 +15055,26 @@ function crossCheckJournal(bundle) {
15046
15055
  }
15047
15056
  return findings;
15048
15057
  }
15058
+ function missingProvenanceNodeIds(snapshotNodeIds, events) {
15059
+ const covered = /* @__PURE__ */ new Set();
15060
+ for (const ev of events) {
15061
+ if (ev.type === "node.created") {
15062
+ if (typeof ev.node_id === "string") covered.add(ev.node_id);
15063
+ } else if (ev.type === "journal.baseline" && Array.isArray(ev.node_ids)) {
15064
+ for (const id of ev.node_ids) {
15065
+ if (typeof id === "string") covered.add(id);
15066
+ }
15067
+ }
15068
+ }
15069
+ const missing = [];
15070
+ const seen = /* @__PURE__ */ new Set();
15071
+ for (const id of snapshotNodeIds) {
15072
+ if (covered.has(id) || seen.has(id)) continue;
15073
+ seen.add(id);
15074
+ missing.push(id);
15075
+ }
15076
+ return missing;
15077
+ }
15049
15078
 
15050
15079
  // ../schema/src/journal-events.ts
15051
15080
  var envelope = {
@@ -15112,7 +15141,12 @@ var DeliverableShippedEventSchema = external_exports.object({
15112
15141
  summary: external_exports.string().optional(),
15113
15142
  url: external_exports.string().optional(),
15114
15143
  node_ids: external_exports.array(external_exports.string()).optional(),
15115
- platform: PlatformSchema.optional()
15144
+ platform: PlatformSchema.optional(),
15145
+ lab_note: external_exports.object({
15146
+ en: external_exports.object({ title: external_exports.string(), summary: external_exports.string() }).catchall(external_exports.unknown()),
15147
+ fr: external_exports.object({ title: external_exports.string().optional(), summary: external_exports.string().optional() }).catchall(external_exports.unknown()).optional(),
15148
+ suggested: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
15149
+ }).catchall(external_exports.unknown()).optional().meta({ description: "The Lab Note a merged PR carried (slice 3); en.title/en.summary required when present." })
15116
15150
  }).catchall(external_exports.unknown());
15117
15151
  var IdeaProposedEventSchema = external_exports.object({
15118
15152
  ...envelope,
@@ -15147,6 +15181,49 @@ var RefStatusChangedEventSchema = external_exports.object({
15147
15181
  to: external_exports.string(),
15148
15182
  synced_at: external_exports.string()
15149
15183
  }).catchall(external_exports.unknown());
15184
+ var JournalBaselineEventSchema = external_exports.object({ ...envelope, type: external_exports.literal("journal.baseline"), node_ids: external_exports.array(external_exports.string()) }).catchall(external_exports.unknown());
15185
+ var QualityAuditCompletedEventSchema = external_exports.object({
15186
+ ...envelope,
15187
+ type: external_exports.literal("quality.audit.completed"),
15188
+ audit_id: external_exports.string(),
15189
+ framework_version: external_exports.string(),
15190
+ commit: external_exports.string().optional(),
15191
+ scores: external_exports.record(external_exports.string(), external_exports.record(external_exports.string(), external_exports.number())).optional(),
15192
+ counts: external_exports.object({
15193
+ critical: external_exports.number().optional(),
15194
+ high: external_exports.number().optional(),
15195
+ medium: external_exports.number().optional(),
15196
+ low: external_exports.number().optional(),
15197
+ info: external_exports.number().optional()
15198
+ }).catchall(external_exports.unknown()).optional()
15199
+ }).catchall(external_exports.unknown());
15200
+ var QualityFindingOpenedEventSchema = external_exports.object({
15201
+ ...envelope,
15202
+ type: external_exports.literal("quality.finding.opened"),
15203
+ finding_id: external_exports.string(),
15204
+ criterion_id: external_exports.string(),
15205
+ surface: external_exports.string(),
15206
+ severity: external_exports.string(),
15207
+ priority: external_exports.string(),
15208
+ title: external_exports.string(),
15209
+ node_ids: external_exports.array(external_exports.string()).optional(),
15210
+ issue_url: external_exports.string().optional()
15211
+ }).catchall(external_exports.unknown());
15212
+ var QualityFindingResolvedEventSchema = external_exports.object({
15213
+ ...envelope,
15214
+ type: external_exports.literal("quality.finding.resolved"),
15215
+ finding_id: external_exports.string(),
15216
+ resolved_by: external_exports.string().optional(),
15217
+ node_ids: external_exports.array(external_exports.string()).optional()
15218
+ }).catchall(external_exports.unknown());
15219
+ var QualitySignalTrippedEventSchema = external_exports.object({
15220
+ ...envelope,
15221
+ type: external_exports.literal("quality.signal.tripped"),
15222
+ criterion_id: external_exports.string(),
15223
+ surface: external_exports.string(),
15224
+ signal: external_exports.string(),
15225
+ detail: external_exports.string().optional()
15226
+ }).catchall(external_exports.unknown());
15150
15227
  var JOURNAL_EVENT_SCHEMAS = {
15151
15228
  "node.created": NodeCreatedEventSchema,
15152
15229
  "node.updated": NodeUpdatedEventSchema,
@@ -15161,7 +15238,12 @@ var JOURNAL_EVENT_SCHEMAS = {
15161
15238
  "request.filed": RequestFiledEventSchema,
15162
15239
  "ref.added": RefAddedEventSchema,
15163
15240
  "ref.removed": RefRemovedEventSchema,
15164
- "ref.status_changed": RefStatusChangedEventSchema
15241
+ "ref.status_changed": RefStatusChangedEventSchema,
15242
+ "journal.baseline": JournalBaselineEventSchema,
15243
+ "quality.audit.completed": QualityAuditCompletedEventSchema,
15244
+ "quality.finding.opened": QualityFindingOpenedEventSchema,
15245
+ "quality.finding.resolved": QualityFindingResolvedEventSchema,
15246
+ "quality.signal.tripped": QualitySignalTrippedEventSchema
15165
15247
  };
15166
15248
  var KnownJournalEventSchema = external_exports.union([
15167
15249
  NodeCreatedEventSchema,
@@ -15177,9 +15259,114 @@ var KnownJournalEventSchema = external_exports.union([
15177
15259
  RequestFiledEventSchema,
15178
15260
  RefAddedEventSchema,
15179
15261
  RefRemovedEventSchema,
15180
- RefStatusChangedEventSchema
15262
+ RefStatusChangedEventSchema,
15263
+ JournalBaselineEventSchema,
15264
+ QualityAuditCompletedEventSchema,
15265
+ QualityFindingOpenedEventSchema,
15266
+ QualityFindingResolvedEventSchema,
15267
+ QualitySignalTrippedEventSchema
15181
15268
  ]);
15182
15269
 
15270
+ // ../schema/src/quality-schemas.ts
15271
+ var GradeSchema = external_exports.enum(["A", "B", "C", "D", "E"]);
15272
+ var KritikScalesSchema = external_exports.object({
15273
+ grades: external_exports.partialRecord(GradeSchema, external_exports.number()).optional(),
15274
+ severity_buckets: external_exports.partialRecord(external_exports.enum(["critical", "high", "medium", "low", "info"]), external_exports.tuple([external_exports.number(), external_exports.number()])).optional(),
15275
+ caps: external_exports.object({ critical_open: GradeSchema.optional(), high_open: GradeSchema.optional() }).catchall(external_exports.unknown()).optional()
15276
+ }).catchall(external_exports.unknown()).meta({
15277
+ id: "KritikScales",
15278
+ description: "Overridable maturity/severity/grade scales carried by a criteria pack. Absent entries fall back to the framework defaults (packages/kritik-library/SPEC.md \xA7 4)."
15279
+ });
15280
+ var KritikReferenceSchema = external_exports.object({ title: external_exports.string(), anchor: external_exports.string().optional(), url: external_exports.string().optional() }).catchall(external_exports.unknown()).meta({ id: "KritikReference", description: "A stable external anchor behind a criterion (OWASP control, GDPR article, WCAG SC\u2026)." });
15281
+ var KritikIssueTemplateSchema = external_exports.object({
15282
+ title_template: external_exports.string().optional(),
15283
+ labels: external_exports.array(external_exports.string()).optional(),
15284
+ body_skeleton: external_exports.string().optional()
15285
+ }).catchall(external_exports.unknown()).meta({ id: "KritikIssueTemplate", description: "Per-criterion issue skeleton \u2014 what `arkaik kritik issue` prefills." });
15286
+ var KritikDomainSchema = external_exports.object({ code: external_exports.string(), name: external_exports.string(), description: external_exports.string().optional() }).catchall(external_exports.unknown()).meta({ id: "KritikDomain", description: "A top-level criteria category owning a roll-up row in the matrix." });
15287
+ var KritikCriterionSchema = external_exports.object({
15288
+ id: external_exports.string().meta({ description: "Stable identifier, `<DOMAIN>-NN`. Never redefined \u2014 retired via superseded_by." }),
15289
+ domain: external_exports.string(),
15290
+ subcategory: external_exports.string().optional(),
15291
+ name: external_exports.string().optional(),
15292
+ question: external_exports.string().optional(),
15293
+ definition: external_exports.string().optional(),
15294
+ rationale: external_exports.string().optional(),
15295
+ applies_to: external_exports.array(external_exports.string()).optional().meta({ description: "Surface ids where the criterion is meaningful; absence means N/A." }),
15296
+ level_anchors: external_exports.record(external_exports.string(), external_exports.string()).optional().meta({ description: "Observable descriptions per maturity level, keyed l0\u2026l4." }),
15297
+ default_impact: external_exports.number().optional(),
15298
+ weight: external_exports.number().optional().meta({ description: "Weight in the domain roll-up (1\u20133; 3 = load-bearing). Absent means 1." }),
15299
+ references: external_exports.array(KritikReferenceSchema).optional(),
15300
+ checklist: external_exports.array(external_exports.string()).optional(),
15301
+ signals: external_exports.array(external_exports.string()).optional().meta({ description: "Mechanically checkable monitoring hooks run between audits." }),
15302
+ remediation: external_exports.string().optional(),
15303
+ issue: KritikIssueTemplateSchema.optional(),
15304
+ superseded_by: external_exports.string().optional()
15305
+ }).catchall(external_exports.unknown()).meta({ id: "KritikCriterion", description: "The atomic auditable unit (packages/kritik-library/SPEC.md \xA7 5)." });
15306
+ var KritikLibrarySchema = external_exports.object({
15307
+ version: external_exports.string().meta({ description: "Semver of the pack; matrices are comparable only within a major." }),
15308
+ domains: external_exports.array(KritikDomainSchema),
15309
+ criteria: external_exports.array(KritikCriterionSchema),
15310
+ scales: KritikScalesSchema.optional()
15311
+ }).catchall(external_exports.unknown()).meta({
15312
+ id: "KritikLibrary",
15313
+ description: "A versioned criteria pack. Embedded on export; repos may instead pin it as a sidecar file."
15314
+ });
15315
+ var SurfaceDefSchema = external_exports.object({
15316
+ id: external_exports.string(),
15317
+ title: external_exports.string(),
15318
+ platform: PlatformSchema.optional().meta({
15319
+ description: "Optional bridge to PLATFORM_IDS where a surface is also a view-shipping platform. Surfaces are not platforms (RFC \xA7 2)."
15320
+ }),
15321
+ path: external_exports.string().optional()
15322
+ }).catchall(external_exports.unknown()).meta({ id: "SurfaceDef", description: "One independently assessable body of code \u2014 an audit target, chosen at install." });
15323
+ var QualityProfileSchema = external_exports.object({
15324
+ surfaces: external_exports.array(SurfaceDefSchema),
15325
+ domain_weights: external_exports.record(external_exports.string(), external_exports.number()).optional().meta({
15326
+ description: "Per-domain weight in the surface roll-up, keyed by domain code. A missing weight is 1."
15327
+ })
15328
+ }).catchall(external_exports.unknown()).meta({ id: "QualityProfile", description: "The project's parameterization of the pack (RFC \xA7 6)." });
15329
+ var QualityAssessmentSchema = external_exports.object({
15330
+ criterion_id: external_exports.string(),
15331
+ surface: external_exports.string(),
15332
+ level: external_exports.union([external_exports.literal(0), external_exports.literal(1), external_exports.literal(2), external_exports.literal(3), external_exports.literal(4)]).meta({
15333
+ description: "Maturity 0\u20134. N/A is the absence of the row, never a level."
15334
+ }),
15335
+ evidence: external_exports.string().meta({ description: "file:line / config citations. A score without evidence is an opinion, not an assessment." }),
15336
+ audit_id: external_exports.string(),
15337
+ commit: external_exports.string().optional(),
15338
+ ts: external_exports.string()
15339
+ }).catchall(external_exports.unknown()).meta({ id: "QualityAssessment", description: "One (criterion \xD7 surface) score \u2014 latest wins; history lives in the journal." });
15340
+ var QualityFindingSchema = external_exports.object({
15341
+ id: external_exports.string(),
15342
+ criterion_id: external_exports.string(),
15343
+ surface: external_exports.string(),
15344
+ title: external_exports.string(),
15345
+ detail: external_exports.string(),
15346
+ evidence: external_exports.string(),
15347
+ impact: external_exports.number().meta({ description: "Worst plausible consequence, 1\u20135." }),
15348
+ likelihood: external_exports.number().meta({ description: "Probability it materializes, 1\u20135." }),
15349
+ cost: external_exports.enum(["S", "M", "L", "XL"]),
15350
+ status: external_exports.enum(["open", "resolved", "refuted", "accepted-risk"]),
15351
+ remediation: external_exports.string().optional(),
15352
+ node_ids: external_exports.array(external_exports.string()).optional().meta({ description: "Graph nodes this finding is about \u2014 the tie deliverable.shipped already uses." }),
15353
+ issue_url: external_exports.string().optional(),
15354
+ verification: external_exports.object({ verdict: external_exports.enum(["CONFIRMED", "REFUTED", "DOWNGRADED"]), note: external_exports.string().optional() }).catchall(external_exports.unknown()).optional().meta({ description: "Result of the adversarial refutation pass. Refuted findings are disclosed, not deleted." })
15355
+ }).catchall(external_exports.unknown()).meta({
15356
+ id: "QualityFinding",
15357
+ description: "One concrete defect. Severity and priority are deliberately absent \u2014 they are derived from impact \xD7 likelihood and cost (deriveQualityMatrix), so a finding can never carry a severity its own numbers disagree with."
15358
+ });
15359
+ var QualitySectionSchema = external_exports.object({
15360
+ framework_version: external_exports.string(),
15361
+ library: KritikLibrarySchema.optional(),
15362
+ profile: QualityProfileSchema,
15363
+ assessments: external_exports.array(QualityAssessmentSchema),
15364
+ findings: external_exports.array(QualityFindingSchema)
15365
+ }).catchall(external_exports.unknown()).meta({
15366
+ id: "QualitySection",
15367
+ description: "The project's Kritik state (docs/rfcs/kritik.md \xA7 4.1) \u2014 profile, assessments, findings. Criteria are library content, not graph nodes; the matrix is a projection, never stored."
15368
+ });
15369
+
15183
15370
  // ../schema/src/bundle.ts
15184
15371
  var PlatformStatusMapSchema = external_exports.partialRecord(
15185
15372
  PlatformSchema,
@@ -15321,7 +15508,8 @@ var ProjectMetadataSchema = external_exports.object({
15321
15508
  }),
15322
15509
  products: external_exports.array(ProductDefinitionSchema).optional().meta({
15323
15510
  description: "Product definitions (docs/spec/bundle-format.md \xA7 Products) \u2014 additive; unknown fields preserved."
15324
- })
15511
+ }),
15512
+ pollen: external_exports.object({ plant: external_exports.string().regex(/^[a-z0-9][a-z0-9-]*$/).optional() }).catchall(external_exports.unknown()).optional().meta({ description: "Federation settings \u2014 docs/spec/services.md \xA7 Pollen feed. `plant` enables the feed." })
15325
15513
  }).catchall(external_exports.unknown()).meta({ id: "ProjectMetadata", description: "Optional project-level UI settings." });
15326
15514
  var ProjectSchema = external_exports.object({
15327
15515
  id: external_exports.string().meta({ description: "Unique project identifier." }),
@@ -15349,6 +15537,9 @@ var ProjectBundleSchema = external_exports.object({
15349
15537
  edges: external_exports.array(EdgeSchema).meta({ description: "All edges (relationships) between nodes." }),
15350
15538
  journal: external_exports.array(JournalEventSchema).optional().meta({
15351
15539
  description: "Optional embedded journal events \u2014 the Level 2 interchange projection (docs/spec/journal.md). Canonical storage in repos is the JSONL sidecar; a bundle without a journal is Level 0/1, not an error."
15540
+ }),
15541
+ quality: QualitySectionSchema.optional().meta({
15542
+ description: "Optional Kritik quality state (docs/rfcs/kritik.md \xA7 4.1) \u2014 the pinned pack version, the project's surface profile, its assessments and findings. Additive: a bundle without it is not an error, and the comparative matrix is derived from it (deriveQualityMatrix), never stored."
15352
15543
  })
15353
15544
  }).catchall(external_exports.unknown()).meta({
15354
15545
  id: "ProjectBundle",
@@ -15356,15 +15547,6 @@ var ProjectBundleSchema = external_exports.object({
15356
15547
  description: "The import/export format for an Arkaik product graph. Contains a project, its nodes (flows, views, data models, API endpoints), and edges (relationships between nodes)."
15357
15548
  });
15358
15549
 
15359
- // ../schema/src/maps.ts
15360
- var MAP_FLOW_PLATFORMS_MODES = ["rings", "bars"];
15361
- var MAP_VIEW_PLATFORMS_MODES = ["chips", "rows"];
15362
- var MAP_MINIMAP_COLOR_MODES = ["status", "species"];
15363
- var BUILT_IN_MAP_IDS = ["journey", "system"];
15364
- function isBuiltInMapId(id) {
15365
- return BUILT_IN_MAP_IDS.includes(id);
15366
- }
15367
-
15368
15550
  // ../schema/src/products.ts
15369
15551
  var PRODUCT_MEMBERSHIP_SPECIES = ["flow", "view", "acceptance"];
15370
15552
  function resolveProducts(project) {
@@ -15382,6 +15564,214 @@ function resolveProducts(project) {
15382
15564
  return products;
15383
15565
  }
15384
15566
 
15567
+ // ../schema/src/maps.ts
15568
+ var MAP_FLOW_PLATFORMS_MODES = ["rings", "bars"];
15569
+ var MAP_VIEW_PLATFORMS_MODES = ["chips", "rows"];
15570
+ var MAP_MINIMAP_COLOR_MODES = ["status", "species"];
15571
+ var BUILT_IN_MAP_IDS = ["journey", "system"];
15572
+ function isBuiltInMapId(id) {
15573
+ return BUILT_IN_MAP_IDS.includes(id);
15574
+ }
15575
+
15576
+ // ../schema/src/quality.ts
15577
+ var asArray = (value) => Array.isArray(value) ? value : [];
15578
+ var FINDING_SEVERITIES = ["critical", "high", "medium", "low", "info"];
15579
+ var REMEDIATION_COSTS = ["S", "M", "L", "XL"];
15580
+ var QUALITY_GRADES = ["A", "B", "C", "D", "E"];
15581
+ var DEFAULT_GRADE_BANDS = { A: 85, B: 70, C: 55, D: 40, E: 0 };
15582
+ var DEFAULT_SEVERITY_BUCKETS = {
15583
+ critical: [20, 25],
15584
+ high: [12, 19],
15585
+ medium: [6, 11],
15586
+ low: [2, 5],
15587
+ info: [1, 1]
15588
+ };
15589
+ var DEFAULT_CAPS = {
15590
+ critical_open: "D",
15591
+ high_open: "B"
15592
+ };
15593
+ var CROSS_SURFACE_ID = "cross-surface";
15594
+ function mergeKritikLibrary(pack, overlay) {
15595
+ if (!overlay || typeof overlay !== "object") return pack;
15596
+ const domains = [...asArray(pack.domains)];
15597
+ for (const domain2 of asArray(overlay.domains)) {
15598
+ if (typeof domain2?.code !== "string" || domain2.code === "") continue;
15599
+ const at2 = domains.findIndex((existing) => existing?.code === domain2.code);
15600
+ if (at2 >= 0) domains[at2] = domain2;
15601
+ else domains.push(domain2);
15602
+ }
15603
+ const criteria = [...asArray(pack.criteria)];
15604
+ for (const criterion of asArray(overlay.criteria)) {
15605
+ if (typeof criterion?.id !== "string" || criterion.id === "") continue;
15606
+ const at2 = criteria.findIndex((existing) => existing?.id === criterion.id);
15607
+ if (at2 >= 0) criteria[at2] = criterion;
15608
+ else criteria.push(criterion);
15609
+ }
15610
+ const scales = pack.scales || overlay.scales ? { ...pack.scales ?? {}, ...overlay.scales ?? {} } : void 0;
15611
+ return { ...pack, domains, criteria, ...scales ? { scales } : {} };
15612
+ }
15613
+ function bucketsOf(library) {
15614
+ const stored = library?.scales?.severity_buckets;
15615
+ const out = { ...DEFAULT_SEVERITY_BUCKETS };
15616
+ if (!stored || typeof stored !== "object") return out;
15617
+ for (const severity of FINDING_SEVERITIES) {
15618
+ const band = stored[severity];
15619
+ if (Array.isArray(band) && typeof band[0] === "number" && typeof band[1] === "number") {
15620
+ out[severity] = [band[0], band[1]];
15621
+ }
15622
+ }
15623
+ return out;
15624
+ }
15625
+ function bandsOf(library) {
15626
+ const stored = library?.scales?.grades;
15627
+ const out = { ...DEFAULT_GRADE_BANDS };
15628
+ if (!stored || typeof stored !== "object") return out;
15629
+ for (const grade of QUALITY_GRADES) {
15630
+ const min = stored[grade];
15631
+ if (typeof min === "number" && Number.isFinite(min)) out[grade] = min;
15632
+ }
15633
+ return out;
15634
+ }
15635
+ function capsOf(library) {
15636
+ const stored = library?.scales?.caps;
15637
+ const pick2 = (value, fallback) => typeof value === "string" && QUALITY_GRADES.includes(value) ? value : fallback;
15638
+ return {
15639
+ critical_open: pick2(stored?.critical_open, DEFAULT_CAPS.critical_open),
15640
+ high_open: pick2(stored?.high_open, DEFAULT_CAPS.high_open)
15641
+ };
15642
+ }
15643
+ function severityOf(finding, library) {
15644
+ const impact = typeof finding.impact === "number" ? finding.impact : 0;
15645
+ const likelihood = typeof finding.likelihood === "number" ? finding.likelihood : 0;
15646
+ const score = impact * likelihood;
15647
+ const buckets = bucketsOf(library);
15648
+ const worstFirst = [...FINDING_SEVERITIES].sort((a, b) => buckets[b][0] - buckets[a][0]);
15649
+ for (const severity of worstFirst) {
15650
+ if (score >= buckets[severity][0]) return severity;
15651
+ }
15652
+ return "info";
15653
+ }
15654
+ function priorityOf(finding, library) {
15655
+ const severity = severityOf(finding, library);
15656
+ const cheap = finding.cost === "S";
15657
+ if (severity === "critical") return "P0";
15658
+ if (severity === "high") return cheap ? "P0" : "P1";
15659
+ if (severity === "medium") return cheap ? "P1" : "P2";
15660
+ if (severity === "low") return cheap ? "P2" : "P3";
15661
+ return "P3";
15662
+ }
15663
+ function gradeOf(score, library) {
15664
+ const bands = bandsOf(library);
15665
+ const bestFirst = [...QUALITY_GRADES].sort((a, b) => bands[b] - bands[a]);
15666
+ for (const grade of bestFirst) {
15667
+ if (score >= bands[grade]) return grade;
15668
+ }
15669
+ return "E";
15670
+ }
15671
+ function capGrade(grade, cap) {
15672
+ return QUALITY_GRADES[Math.max(QUALITY_GRADES.indexOf(grade), QUALITY_GRADES.indexOf(cap))];
15673
+ }
15674
+ function isOpenFinding(finding) {
15675
+ return (finding.status ?? "open") === "open";
15676
+ }
15677
+ function resolveKritikLibrary(section, library) {
15678
+ const explicit = library ?? section?.library;
15679
+ if (explicit && Array.isArray(explicit.criteria) && Array.isArray(explicit.domains)) return explicit;
15680
+ if (!section) return void 0;
15681
+ const ids = /* @__PURE__ */ new Set();
15682
+ for (const row of [...asArray(section.assessments), ...asArray(section.findings)]) {
15683
+ if (typeof row?.criterion_id === "string" && row.criterion_id !== "") ids.add(row.criterion_id);
15684
+ }
15685
+ if (ids.size === 0) return void 0;
15686
+ const domainOf = (id) => id.includes("-") ? id.slice(0, id.lastIndexOf("-")) : id;
15687
+ const criteria = [...ids].sort().map((id) => ({ id, domain: domainOf(id), weight: 1 }));
15688
+ const domains = [...new Set(criteria.map((c) => c.domain))].sort().map((code) => ({ code, name: code }));
15689
+ return { version: explicit?.version ?? "unknown", domains, criteria, scales: explicit?.scales };
15690
+ }
15691
+ function deriveQualityMatrix(bundle, library) {
15692
+ const section = bundle.quality;
15693
+ const pack = resolveKritikLibrary(section, library);
15694
+ const assessments = asArray(section?.assessments);
15695
+ const findings = asArray(section?.findings);
15696
+ const openFindings = findings.filter((finding) => isOpenFinding(finding));
15697
+ const declared = asArray(section?.profile?.surfaces).map((surface) => surface?.id).filter((id) => typeof id === "string" && id !== "");
15698
+ const surfaces = declared.length > 0 ? [...new Set(declared)] : [...new Set(assessments.map((a) => a.surface).filter((id) => typeof id === "string" && id !== ""))].sort();
15699
+ const domains = asArray(pack?.domains).map((domain2) => domain2?.code).filter((code) => typeof code === "string" && code !== "");
15700
+ const domainOfCriterion = /* @__PURE__ */ new Map();
15701
+ const weightOfCriterion = /* @__PURE__ */ new Map();
15702
+ for (const criterion of asArray(pack?.criteria)) {
15703
+ if (typeof criterion?.id !== "string") continue;
15704
+ if (typeof criterion.domain === "string") domainOfCriterion.set(criterion.id, criterion.domain);
15705
+ weightOfCriterion.set(criterion.id, typeof criterion.weight === "number" && criterion.weight > 0 ? criterion.weight : 1);
15706
+ }
15707
+ const caps = capsOf(pack);
15708
+ const weights = section?.profile?.domain_weights;
15709
+ const matrix = {};
15710
+ for (const domain2 of domains) {
15711
+ matrix[domain2] = {};
15712
+ for (const surface of surfaces) {
15713
+ const rows = assessments.filter((a) => a.surface === surface && domainOfCriterion.get(a.criterion_id) === domain2);
15714
+ if (rows.length === 0) {
15715
+ matrix[domain2][surface] = null;
15716
+ continue;
15717
+ }
15718
+ let earned = 0;
15719
+ let available = 0;
15720
+ for (const row of rows) {
15721
+ const weight = weightOfCriterion.get(row.criterion_id) ?? 1;
15722
+ const level = typeof row.level === "number" ? Math.min(Math.max(row.level, 0), 4) : 0;
15723
+ earned += level * weight;
15724
+ available += 4 * weight;
15725
+ }
15726
+ if (available === 0) {
15727
+ matrix[domain2][surface] = null;
15728
+ continue;
15729
+ }
15730
+ const score = Math.round(earned / available * 100);
15731
+ const cellSeverities = openFindings.filter((finding) => finding.surface === surface && domainOfCriterion.get(finding.criterion_id) === domain2).map((finding) => severityOf(finding, pack));
15732
+ const banded = gradeOf(score, pack);
15733
+ let grade = banded;
15734
+ if (cellSeverities.includes("critical")) grade = capGrade(grade, caps.critical_open);
15735
+ else if (cellSeverities.includes("high")) grade = capGrade(grade, caps.high_open);
15736
+ matrix[domain2][surface] = {
15737
+ score,
15738
+ grade,
15739
+ capped: grade !== banded,
15740
+ criteria: rows.length,
15741
+ findings: {
15742
+ critical: cellSeverities.filter((s) => s === "critical").length,
15743
+ high: cellSeverities.filter((s) => s === "high").length,
15744
+ medium: cellSeverities.filter((s) => s === "medium").length,
15745
+ low: cellSeverities.filter((s) => s === "low").length
15746
+ }
15747
+ };
15748
+ }
15749
+ }
15750
+ const overall = {};
15751
+ for (const surface of surfaces) {
15752
+ let weighted = 0;
15753
+ let total = 0;
15754
+ for (const domain2 of domains) {
15755
+ const cell = matrix[domain2]?.[surface];
15756
+ if (!cell) continue;
15757
+ const weight = typeof weights?.[domain2] === "number" ? weights[domain2] : 1;
15758
+ weighted += cell.score * weight;
15759
+ total += weight;
15760
+ }
15761
+ overall[surface] = total > 0 ? Math.round(weighted / total) : null;
15762
+ }
15763
+ const finding_counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
15764
+ for (const finding of openFindings) finding_counts[severityOf(finding, pack)]++;
15765
+ return {
15766
+ framework_version: section?.framework_version,
15767
+ surfaces,
15768
+ domains,
15769
+ matrix,
15770
+ overall,
15771
+ finding_counts
15772
+ };
15773
+ }
15774
+
15385
15775
  // ../schema/src/validate.ts
15386
15776
  var VALID_STAGES = ["beta", "monitoring", "deprecated"];
15387
15777
  var VALID_VIEW_CARD_VARIANTS = ["compact", "large"];
@@ -15898,13 +16288,98 @@ function validateBundle(input) {
15898
16288
  composesSet.add(`${sourceId}->${targetId}`);
15899
16289
  }
15900
16290
  });
16291
+ const PLAYLIST_ENTRY_TYPES = ["view", "flow", "condition", "junction"];
16292
+ const checkPlaylistShape = (entries, flowId, path6, depth = 0) => {
16293
+ if (depth > 50) return;
16294
+ if (!Array.isArray(entries)) {
16295
+ error51(path6, "playlist-entry-shape", `Flow ${flowId}: ${path6} must be an array of playlist entries`);
16296
+ return;
16297
+ }
16298
+ entries.forEach((raw, i) => {
16299
+ const p = `${path6}[${i}]`;
16300
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
16301
+ error51(p, "playlist-entry-shape", `Flow ${flowId}: ${p} must be a playlist entry object`);
16302
+ return;
16303
+ }
16304
+ const entry = raw;
16305
+ const type = entry.type;
16306
+ if (typeof type !== "string" || !PLAYLIST_ENTRY_TYPES.includes(type)) {
16307
+ error51(
16308
+ `${p}.type`,
16309
+ "playlist-entry-shape",
16310
+ `Flow ${flowId}: ${p}.type must be one of ${PLAYLIST_ENTRY_TYPES.join(", ")} (got ${JSON.stringify(type)})`
16311
+ );
16312
+ return;
16313
+ }
16314
+ if (type === "view") {
16315
+ if (typeof entry.view_id !== "string") {
16316
+ error51(`${p}.view_id`, "playlist-entry-shape", `Flow ${flowId}: ${p} is a view entry with no view_id string`);
16317
+ }
16318
+ } else if (type === "flow") {
16319
+ if (typeof entry.flow_id !== "string") {
16320
+ error51(`${p}.flow_id`, "playlist-entry-shape", `Flow ${flowId}: ${p} is a flow entry with no flow_id string`);
16321
+ }
16322
+ } else if (type === "condition") {
16323
+ if (typeof entry.label !== "string") {
16324
+ error51(`${p}.label`, "playlist-entry-shape", `Flow ${flowId}: ${p} is a condition with no label string`);
16325
+ }
16326
+ for (const branch of ["if_true", "if_false"]) {
16327
+ if (!Array.isArray(entry[branch])) {
16328
+ error51(
16329
+ `${p}.${branch}`,
16330
+ "playlist-entry-shape",
16331
+ `Flow ${flowId}: ${p}.${branch} must be an array of playlist entries (both branches are required, use [] for an empty one)`
16332
+ );
16333
+ } else {
16334
+ checkPlaylistShape(entry[branch], flowId, `${p}.${branch}`, depth + 1);
16335
+ }
16336
+ }
16337
+ } else {
16338
+ if (typeof entry.label !== "string") {
16339
+ error51(`${p}.label`, "playlist-entry-shape", `Flow ${flowId}: ${p} is a junction with no label string`);
16340
+ }
16341
+ if (!Array.isArray(entry.cases)) {
16342
+ error51(`${p}.cases`, "playlist-entry-shape", `Flow ${flowId}: ${p}.cases must be an array of junction cases`);
16343
+ return;
16344
+ }
16345
+ entry.cases.forEach((rawCase, j) => {
16346
+ const cp = `${p}.cases[${j}]`;
16347
+ if (typeof rawCase !== "object" || rawCase === null || Array.isArray(rawCase)) {
16348
+ error51(cp, "playlist-entry-shape", `Flow ${flowId}: ${cp} must be a junction case object`);
16349
+ return;
16350
+ }
16351
+ const branch = rawCase;
16352
+ if (typeof branch.label !== "string") {
16353
+ error51(`${cp}.label`, "playlist-entry-shape", `Flow ${flowId}: ${cp} has no label string`);
16354
+ }
16355
+ if (!Array.isArray(branch.entries)) {
16356
+ error51(
16357
+ `${cp}.entries`,
16358
+ "playlist-entry-shape",
16359
+ `Flow ${flowId}: ${cp}.entries must be an array of playlist entries \u2014 a junction case holds its own entries, not a bare view_id/flow_id`
16360
+ );
16361
+ } else {
16362
+ checkPlaylistShape(branch.entries, flowId, `${cp}.entries`, depth + 1);
16363
+ }
16364
+ });
16365
+ }
16366
+ });
16367
+ };
16368
+ nodes.forEach((node, i) => {
16369
+ const md = node.metadata;
16370
+ if (node.species === "flow" && md?.playlist?.entries) {
16371
+ checkPlaylistShape(md.playlist.entries, node.id, `nodes[${i}].metadata.playlist.entries`);
16372
+ }
16373
+ });
15901
16374
  const collectPlaylistRefs = (entries, flowId, path6, depth = 0) => {
15902
16375
  if (depth > 50) {
15903
16376
  error51(path6, "playlist-depth", `Flow ${flowId}: playlist nesting too deep (possible cycle)`);
15904
16377
  return [];
15905
16378
  }
15906
16379
  const refs = [];
16380
+ if (!Array.isArray(entries)) return refs;
15907
16381
  for (const entry of entries) {
16382
+ if (typeof entry !== "object" || entry === null) continue;
15908
16383
  if (entry.type === "view") {
15909
16384
  if (!nodeIds.has(entry.view_id)) {
15910
16385
  error51(path6, "playlist-ref-exists", `Flow ${flowId}: playlist references non-existent view "${entry.view_id}"`);
@@ -15956,7 +16431,9 @@ function validateBundle(input) {
15956
16431
  const nodeId = node.id;
15957
16432
  const subFlows = [];
15958
16433
  const findSubFlows = (entries) => {
16434
+ if (!Array.isArray(entries)) return;
15959
16435
  for (const e of entries) {
16436
+ if (typeof e !== "object" || e === null) continue;
15960
16437
  if (e.type === "flow") subFlows.push(e.flow_id);
15961
16438
  if (e.type === "condition") {
15962
16439
  if (e.if_true) findSubFlows(e.if_true);
@@ -15991,6 +16468,151 @@ function validateBundle(input) {
15991
16468
  for (const id of flowGraph.keys()) {
15992
16469
  dfs(id);
15993
16470
  }
16471
+ const quality = bundle.quality;
16472
+ if (typeof quality === "object" && quality !== null && !Array.isArray(quality)) {
16473
+ const section = quality;
16474
+ const assessments = Array.isArray(section.assessments) ? section.assessments : [];
16475
+ const qualityFindings = Array.isArray(section.findings) ? section.findings : [];
16476
+ const profile = typeof section.profile === "object" && section.profile !== null ? section.profile : void 0;
16477
+ const declaredSurfaces = /* @__PURE__ */ new Set();
16478
+ if (Array.isArray(profile?.surfaces)) {
16479
+ profile.surfaces.forEach((entry, index) => {
16480
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return;
16481
+ const surface = entry;
16482
+ const surfaceId = typeof surface.id === "string" ? surface.id : void 0;
16483
+ if (surfaceId === void 0 || surfaceId.trim() === "") return;
16484
+ if (declaredSurfaces.has(surfaceId)) {
16485
+ warn(`quality.profile.surfaces[${index}].id`, "quality-duplicate-surface", `Duplicate surface id "${surfaceId}" \u2014 the first wins`);
16486
+ }
16487
+ declaredSurfaces.add(surfaceId);
16488
+ });
16489
+ }
16490
+ if (declaredSurfaces.size === 0 && (assessments.length > 0 || qualityFindings.length > 0)) {
16491
+ warn(
16492
+ "quality.profile.surfaces",
16493
+ "quality-no-surfaces",
16494
+ "Quality data is stored but the profile declares no surfaces \u2014 the matrix falls back to the surfaces the assessments name"
16495
+ );
16496
+ }
16497
+ const library = typeof section.library === "object" && section.library !== null ? section.library : void 0;
16498
+ const knownCriteria = /* @__PURE__ */ new Set();
16499
+ const retiredCriteria = /* @__PURE__ */ new Map();
16500
+ if (Array.isArray(library?.criteria)) {
16501
+ for (const entry of library.criteria) {
16502
+ if (typeof entry !== "object" || entry === null) continue;
16503
+ const criterion = entry;
16504
+ if (typeof criterion.id !== "string") continue;
16505
+ knownCriteria.add(criterion.id);
16506
+ if (typeof criterion.superseded_by === "string" && criterion.superseded_by !== "") {
16507
+ retiredCriteria.set(criterion.id, criterion.superseded_by);
16508
+ }
16509
+ }
16510
+ } else if (assessments.length > 0 || qualityFindings.length > 0) {
16511
+ warn(
16512
+ "quality.library",
16513
+ "quality-library-missing",
16514
+ "No criteria pack is embedded \u2014 criterion ids cannot be resolved to domains or weights; the matrix uses a synthesized library"
16515
+ );
16516
+ }
16517
+ if (typeof section.framework_version !== "string" || section.framework_version === "") {
16518
+ warn("quality.framework_version", "quality-framework-version-missing", "Quality section has no framework_version \u2014 scores are not comparable across audits without one");
16519
+ }
16520
+ const checkRowRefs = (row, path6, label) => {
16521
+ const criterionId = typeof row.criterion_id === "string" ? row.criterion_id : void 0;
16522
+ if (criterionId !== void 0 && knownCriteria.size > 0 && !knownCriteria.has(criterionId)) {
16523
+ warn(`${path6}.criterion_id`, "quality-unknown-criterion", `${label}: criterion "${criterionId}" is not in the pinned library`);
16524
+ }
16525
+ const supersededBy = criterionId !== void 0 ? retiredCriteria.get(criterionId) : void 0;
16526
+ if (supersededBy !== void 0) {
16527
+ warn(`${path6}.criterion_id`, "quality-retired-criterion", `${label}: criterion "${criterionId}" is retired \u2014 superseded by "${supersededBy}"`);
16528
+ }
16529
+ const surface = typeof row.surface === "string" ? row.surface : void 0;
16530
+ if (surface !== void 0 && surface !== CROSS_SURFACE_ID && declaredSurfaces.size > 0 && !declaredSurfaces.has(surface)) {
16531
+ warn(`${path6}.surface`, "quality-unknown-surface", `${label}: surface "${surface}" is not declared in the profile`);
16532
+ }
16533
+ };
16534
+ const seenCells = /* @__PURE__ */ new Set();
16535
+ assessments.forEach((entry, index) => {
16536
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return;
16537
+ const assessment = entry;
16538
+ const path6 = `quality.assessments[${index}]`;
16539
+ const label = `Assessment ${index}`;
16540
+ checkRowRefs(assessment, path6, label);
16541
+ const level = assessment.level;
16542
+ if (typeof level !== "number" || !Number.isInteger(level) || level < 0 || level > 4) {
16543
+ warn(`${path6}.level`, "quality-level-range", `${label}: level must be an integer 0-4 (N/A is the absence of the row, not a level)`);
16544
+ }
16545
+ if (assessment.surface === CROSS_SURFACE_ID) {
16546
+ warn(`${path6}.surface`, "quality-cross-surface-assessment", `${label}: "${CROSS_SURFACE_ID}" is a findings-only lens \u2014 it carries no matrix column, so this score would render nowhere`);
16547
+ }
16548
+ if (typeof assessment.evidence !== "string" || assessment.evidence.trim() === "") {
16549
+ warn(`${path6}.evidence`, "quality-assessment-no-evidence", `${label}: scored with no evidence \u2014 a score without a citation is an opinion, not an assessment`);
16550
+ }
16551
+ const criterionId = typeof assessment.criterion_id === "string" ? assessment.criterion_id : void 0;
16552
+ const surface = typeof assessment.surface === "string" ? assessment.surface : void 0;
16553
+ if (criterionId !== void 0 && surface !== void 0) {
16554
+ const cell = `${criterionId}\0${surface}`;
16555
+ if (seenCells.has(cell)) {
16556
+ warn(`${path6}`, "quality-duplicate-assessment", `${label}: a second score for (${criterionId} x ${surface}) \u2014 the section stores latest-per-cell, so this cell is ambiguous`);
16557
+ }
16558
+ seenCells.add(cell);
16559
+ }
16560
+ });
16561
+ const seenFindingIds = /* @__PURE__ */ new Set();
16562
+ qualityFindings.forEach((entry, index) => {
16563
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return;
16564
+ const finding = entry;
16565
+ const path6 = `quality.findings[${index}]`;
16566
+ const label = `Finding ${index}`;
16567
+ checkRowRefs(finding, path6, label);
16568
+ const findingId = typeof finding.id === "string" ? finding.id : void 0;
16569
+ if (findingId !== void 0) {
16570
+ if (seenFindingIds.has(findingId)) {
16571
+ warn(`${path6}.id`, "quality-duplicate-finding-id", `Duplicate finding id "${findingId}"`);
16572
+ }
16573
+ seenFindingIds.add(findingId);
16574
+ }
16575
+ for (const field of ["impact", "likelihood"]) {
16576
+ const value = finding[field];
16577
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 5) {
16578
+ warn(`${path6}.${field}`, "quality-risk-range", `${label}: ${field} must be an integer 1-5 \u2014 severity is derived from impact x likelihood`);
16579
+ }
16580
+ }
16581
+ for (const derived of ["severity", "priority"]) {
16582
+ if (derived in finding) {
16583
+ warn(`${path6}.${derived}`, "quality-derived-field-stored", `${label}: ${derived} is derived from impact, likelihood and cost \u2014 storing it lets it drift from the numbers behind it`);
16584
+ }
16585
+ }
16586
+ if (finding.status === "accepted-risk" && (typeof finding.detail !== "string" || finding.detail.trim() === "")) {
16587
+ warn(`${path6}.detail`, "quality-accepted-risk-no-note", `${label}: accepted-risk with no note \u2014 an accepted risk is a decision and reads like one`);
16588
+ }
16589
+ });
16590
+ const openedInJournal = /* @__PURE__ */ new Set();
16591
+ const journalEvents = Array.isArray(bundle.journal) ? bundle.journal : [];
16592
+ journalEvents.forEach((entry) => {
16593
+ if (typeof entry !== "object" || entry === null) return;
16594
+ const event = entry;
16595
+ if (event.type === "quality.finding.opened" && typeof event.finding_id === "string") openedInJournal.add(event.finding_id);
16596
+ });
16597
+ journalEvents.forEach((entry, index) => {
16598
+ if (typeof entry !== "object" || entry === null) return;
16599
+ const event = entry;
16600
+ const type = typeof event.type === "string" ? event.type : "";
16601
+ if (!type.startsWith("quality.")) return;
16602
+ if (typeof event.actor !== "string" || event.actor.trim() === "") {
16603
+ warn(`journal[${index}].actor`, "quality-event-no-actor", `Journal event ${index}: ${type} has no actor \u2014 human, agent and CI scores become indistinguishable`);
16604
+ }
16605
+ if (type === "quality.finding.resolved" && typeof event.finding_id === "string") {
16606
+ if (!openedInJournal.has(event.finding_id) && !seenFindingIds.has(event.finding_id)) {
16607
+ warn(
16608
+ `journal[${index}].finding_id`,
16609
+ "quality-resolved-never-opened",
16610
+ `Journal event ${index}: resolves finding "${event.finding_id}", which no quality.finding.opened event or stored finding ever declared`
16611
+ );
16612
+ }
16613
+ }
16614
+ });
16615
+ }
15994
16616
  for (const finding of crossCheckJournal(bundle)) findings.push(finding);
15995
16617
  return result();
15996
16618
  }
@@ -16379,6 +17001,215 @@ function promotionPatch(node, promotion) {
16379
17001
  };
16380
17002
  }
16381
17003
 
17004
+ // ../schema/src/quality-ops.ts
17005
+ var DEFAULT_TARGET_LEVEL = 3;
17006
+ function upsertAssessment(assessments, next) {
17007
+ const index = assessments.findIndex(
17008
+ (candidate) => candidate.criterion_id === next.criterion_id && candidate.surface === next.surface
17009
+ );
17010
+ if (index === -1) return { assessments: [...assessments, next] };
17011
+ const updated = [...assessments];
17012
+ const replaced = updated[index];
17013
+ updated[index] = next;
17014
+ return { assessments: updated, replaced };
17015
+ }
17016
+ function parseSurfaceSpec(spec) {
17017
+ const parts = spec.split(":");
17018
+ const id = (parts[0] ?? "").trim();
17019
+ if (id === "") throw new Error(`--surface needs an id (got "${spec}")`);
17020
+ if (id === CROSS_SURFACE_ID) {
17021
+ throw new Error(
17022
+ `"${CROSS_SURFACE_ID}" is reserved \u2014 it is the contract lens between surfaces. Findings may carry it; it holds no assessments and never becomes a matrix column, so it is not declared here.`
17023
+ );
17024
+ }
17025
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(id)) {
17026
+ throw new Error(
17027
+ `surface id "${id}" is not kebab-case \u2014 assessments reference it, so it has to be stable and typo-proof`
17028
+ );
17029
+ }
17030
+ const title2 = (parts[1] ?? "").trim() || id;
17031
+ const platform = (parts[2] ?? "").trim();
17032
+ if (platform !== "" && !PLATFORM_IDS.includes(platform)) {
17033
+ throw new Error(
17034
+ `"${platform}" is not an Arkaik platform (${PLATFORM_IDS.join(", ")}). A surface that ships no views simply has none \u2014 leave it off rather than inventing one.`
17035
+ );
17036
+ }
17037
+ return platform === "" ? { id, title: title2 } : { id, title: title2, platform };
17038
+ }
17039
+ function domainCodeOf(criterionId, library) {
17040
+ const criterion = (library?.criteria ?? []).find((candidate) => candidate.id === criterionId);
17041
+ if (typeof criterion?.domain === "string" && criterion.domain !== "") return criterion.domain;
17042
+ const dash = criterionId.indexOf("-");
17043
+ return dash > 0 ? criterionId.slice(0, dash) : criterionId;
17044
+ }
17045
+ function mintFindingId(auditId, criterionId, surface, taken, library) {
17046
+ const prefix = `F-${auditId}-${domainCodeOf(criterionId, library)}-${surface}-`;
17047
+ const used = new Set(taken);
17048
+ for (let n = 1; n < 1e3; n++) {
17049
+ const candidate = `${prefix}${String(n).padStart(2, "0")}`;
17050
+ if (!used.has(candidate)) return candidate;
17051
+ }
17052
+ throw new Error(`Kritik: exhausted finding ids for ${prefix}NN \u2014 999 findings on one cell is a data problem, not a numbering one`);
17053
+ }
17054
+ function upsertFinding(findings, next) {
17055
+ const index = findings.findIndex((candidate) => candidate.id === next.id);
17056
+ if (index === -1) return { findings: [...findings, next] };
17057
+ const updated = [...findings];
17058
+ const replaced = updated[index];
17059
+ updated[index] = next;
17060
+ return { findings: updated, replaced };
17061
+ }
17062
+ function patchFinding(findings, id, patch) {
17063
+ const index = findings.findIndex((candidate) => candidate.id === id);
17064
+ if (index === -1) return { findings: [...findings] };
17065
+ const updated = [...findings];
17066
+ const previous = updated[index];
17067
+ const finding = { ...previous, ...patch };
17068
+ updated[index] = finding;
17069
+ return { findings: updated, finding, previous };
17070
+ }
17071
+ function resolveFinding(findings, id, resolvedBy) {
17072
+ return patchFinding(findings, id, {
17073
+ status: "resolved",
17074
+ ...resolvedBy !== void 0 ? { resolved_by: resolvedBy } : {}
17075
+ });
17076
+ }
17077
+ function acceptFinding(findings, id, note) {
17078
+ const existing = findings.find((candidate) => candidate.id === id);
17079
+ const detail = existing === void 0 ? note : `${existing.detail}
17080
+
17081
+ Accepted risk: ${note}`.trim();
17082
+ return patchFinding(findings, id, { status: "accepted-risk", detail });
17083
+ }
17084
+ function fillTemplate(text, values) {
17085
+ return text.replace(/\{(\w+)\}/g, (whole, key) => values[key] ?? whole);
17086
+ }
17087
+ function renderIssue(criterion, options) {
17088
+ const { surface, finding, library } = options;
17089
+ const anchors = criterion.level_anchors ?? {};
17090
+ const level = options.level === void 0 || options.level === "" ? "" : String(options.level);
17091
+ const observed = level === "" ? "{observed_level}" : level;
17092
+ const target = options.targetLevel !== void 0 && options.targetLevel !== "" ? String(options.targetLevel) : level === "" ? "{target_level}" : String(Math.min(Number(level) + 1, 4));
17093
+ const values = {
17094
+ surface,
17095
+ observed_level: observed,
17096
+ target_level: target,
17097
+ observed_level_name: anchors[`l${observed}`] ?? "{observed_level_name}",
17098
+ target_level_name: anchors[`l${target}`] ?? "{target_level_name}",
17099
+ target_anchor_text: anchors[`l${target}`] ?? "{target_anchor_text}",
17100
+ impact: String(finding?.impact ?? criterion.default_impact ?? 3)
17101
+ };
17102
+ if (finding !== void 0) {
17103
+ values.likelihood = String(finding.likelihood);
17104
+ values.evidence_bullets_with_file_paths = finding.evidence;
17105
+ values.risk_narrative = finding.detail;
17106
+ if (typeof finding.remediation === "string" && finding.remediation !== "") {
17107
+ values.remediation_step_1 = finding.remediation;
17108
+ }
17109
+ }
17110
+ const issue2 = criterion.issue ?? {};
17111
+ const labels = [.../* @__PURE__ */ new Set([...issue2.labels ?? ["quality"], surface])];
17112
+ const body = fillTemplate(issue2.body_skeleton ?? "", values);
17113
+ return {
17114
+ title: fillTemplate(issue2.title_template ?? `[Quality] ${criterion.id} on {surface}`, values),
17115
+ labels,
17116
+ body: finding === void 0 ? body : `${body}
17117
+ ${findingFooter(finding, library)}`,
17118
+ ...typeof criterion.remediation === "string" && criterion.remediation !== "" ? { remediation: criterion.remediation } : {}
17119
+ };
17120
+ }
17121
+ function findingFooter(finding, library) {
17122
+ const lines = [
17123
+ ``,
17124
+ `---`,
17125
+ `<sub>Kritik finding \`${finding.id}\` \u2014 **${severityOf(finding, library)} / ${priorityOf(finding, library)}** (impact ${finding.impact} x likelihood ${finding.likelihood}, cost ${finding.cost})</sub>`,
17126
+ ``,
17127
+ `**Evidence**`,
17128
+ finding.evidence
17129
+ ];
17130
+ if (typeof finding.remediation === "string" && finding.remediation !== "") {
17131
+ lines.push(``, `**Remediation**`, finding.remediation);
17132
+ }
17133
+ return lines.join("\n");
17134
+ }
17135
+ function signalRunSheet(library, surfaces, filter = {}) {
17136
+ const surfaceIds = surfaces.map((surface) => surface.id).filter((id) => id !== CROSS_SURFACE_ID);
17137
+ const rows = [];
17138
+ for (const criterion of library.criteria ?? []) {
17139
+ if (criterion.superseded_by !== void 0) continue;
17140
+ if (filter.criterion !== void 0 && criterion.id !== filter.criterion) continue;
17141
+ if (filter.domain !== void 0 && criterion.domain !== filter.domain) continue;
17142
+ const signals = criterion.signals ?? [];
17143
+ if (signals.length === 0) continue;
17144
+ const appliesTo = criterion.applies_to;
17145
+ for (const surfaceId of surfaceIds) {
17146
+ if (Array.isArray(appliesTo) && !appliesTo.includes(surfaceId)) continue;
17147
+ if (filter.surface !== void 0 && surfaceId !== filter.surface) continue;
17148
+ signals.forEach((signal, index) => {
17149
+ rows.push({ criterion_id: criterion.id, surface: surfaceId, index, signal });
17150
+ });
17151
+ }
17152
+ }
17153
+ return rows;
17154
+ }
17155
+ function auditCompletedInput(matrix, meta3) {
17156
+ const scores = {};
17157
+ for (const surface of matrix.surfaces) {
17158
+ const bySurface = {};
17159
+ for (const domain2 of matrix.domains) {
17160
+ const cell = matrix.matrix[domain2]?.[surface];
17161
+ if (cell) bySurface[domain2] = cell.score;
17162
+ }
17163
+ scores[surface] = bySurface;
17164
+ }
17165
+ return {
17166
+ type: "quality.audit.completed",
17167
+ payload: {
17168
+ audit_id: meta3.audit_id,
17169
+ framework_version: meta3.framework_version,
17170
+ ...meta3.commit !== void 0 ? { commit: meta3.commit } : {},
17171
+ scores,
17172
+ counts: matrix.finding_counts
17173
+ }
17174
+ };
17175
+ }
17176
+ function findingOpenedInput(finding, library) {
17177
+ return {
17178
+ type: "quality.finding.opened",
17179
+ payload: {
17180
+ finding_id: finding.id,
17181
+ criterion_id: finding.criterion_id,
17182
+ surface: finding.surface,
17183
+ severity: severityOf(finding, library),
17184
+ priority: priorityOf(finding, library),
17185
+ title: finding.title,
17186
+ ...finding.node_ids !== void 0 ? { node_ids: finding.node_ids } : {},
17187
+ ...finding.issue_url !== void 0 ? { issue_url: finding.issue_url } : {}
17188
+ }
17189
+ };
17190
+ }
17191
+ function findingResolvedInput(finding, resolvedBy) {
17192
+ return {
17193
+ type: "quality.finding.resolved",
17194
+ payload: {
17195
+ finding_id: finding.id,
17196
+ ...resolvedBy !== void 0 ? { resolved_by: resolvedBy } : {},
17197
+ ...finding.node_ids !== void 0 ? { node_ids: finding.node_ids } : {}
17198
+ }
17199
+ };
17200
+ }
17201
+ function signalTrippedInput(trip) {
17202
+ return {
17203
+ type: "quality.signal.tripped",
17204
+ payload: {
17205
+ criterion_id: trip.criterion_id,
17206
+ surface: trip.surface,
17207
+ signal: trip.signal,
17208
+ ...trip.detail !== void 0 ? { detail: trip.detail } : {}
17209
+ }
17210
+ };
17211
+ }
17212
+
16382
17213
  // src/commands/init.ts
16383
17214
  var DEFAULT_BUNDLE_PATH = "docs/arkaik/bundle.json";
16384
17215
  var DEFAULT_JOURNAL_PATH = "docs/arkaik/journal.jsonl";
@@ -16672,7 +17503,7 @@ function runInit(args) {
16672
17503
  }
16673
17504
 
16674
17505
  // src/commands/validate.ts
16675
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
17506
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
16676
17507
 
16677
17508
  // src/lib/bundle-io.ts
16678
17509
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
@@ -16703,9 +17534,80 @@ function nodesByIdOf(bundle) {
16703
17534
  }
16704
17535
 
16705
17536
  // src/lib/bundle-validate.ts
16706
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
17537
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
17538
+ import { basename as basename2 } from "node:path";
17539
+
17540
+ // src/lib/journal-io.ts
17541
+ import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
16707
17542
  import { dirname as dirname2, join as join2 } from "node:path";
16708
17543
  var JOURNAL_SIDECAR = "journal.jsonl";
17544
+ function journalPathFor(bundlePath) {
17545
+ return join2(dirname2(bundlePath), JOURNAL_SIDECAR);
17546
+ }
17547
+ function archivePathFor(journalPath, version2) {
17548
+ return join2(dirname2(journalPath), "journal", `archive-${version2}.jsonl`);
17549
+ }
17550
+ function archivePathsFor(journalPath) {
17551
+ const dir = join2(dirname2(journalPath), "journal");
17552
+ if (!existsSync3(dir)) return [];
17553
+ return readdirSync(dir).filter((name) => name.startsWith("archive-") && name.endsWith(".jsonl")).sort().map((name) => join2(dir, name));
17554
+ }
17555
+ function readJournalEvents(journalPath) {
17556
+ if (!existsSync3(journalPath)) return [];
17557
+ return parseJournalLines(readFileSync3(journalPath, "utf8")).events;
17558
+ }
17559
+ function readFullJournalEvents(journalPath) {
17560
+ const events = readJournalEvents(journalPath);
17561
+ for (const archivePath of archivePathsFor(journalPath)) {
17562
+ events.push(...readJournalEvents(archivePath));
17563
+ }
17564
+ return events;
17565
+ }
17566
+ function loadJournalEvents(bundle, bundlePath) {
17567
+ if (Array.isArray(bundle.journal)) return bundle.journal;
17568
+ return readJournalEvents(journalPathFor(bundlePath));
17569
+ }
17570
+ function toLine(event) {
17571
+ return JSON.stringify(event) + "\n";
17572
+ }
17573
+ function appendJournalEvent(journalPath, event) {
17574
+ const line2 = toLine(event);
17575
+ if (!existsSync3(journalPath)) {
17576
+ mkdirSync2(dirname2(journalPath), { recursive: true });
17577
+ writeFileSync2(journalPath, line2);
17578
+ return;
17579
+ }
17580
+ const existing = readFileSync3(journalPath, "utf8");
17581
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
17582
+ appendFileSync(journalPath, prefix + line2);
17583
+ }
17584
+ function ensureJournalBaseline(journalPath, bundle, actor) {
17585
+ const snapshotNodeIds = (Array.isArray(bundle.nodes) ? bundle.nodes : []).map((node) => node?.id).filter((id) => typeof id === "string");
17586
+ const missing = missingProvenanceNodeIds(snapshotNodeIds, readFullJournalEvents(journalPath));
17587
+ if (missing.length === 0) return void 0;
17588
+ const event = makeEvent("journal.baseline", { node_ids: missing }, { actor });
17589
+ appendJournalEvent(journalPath, event);
17590
+ return event;
17591
+ }
17592
+ function compactSlice(journalPath, slice, version2) {
17593
+ if (slice.length === 0) return;
17594
+ const sliceIds = new Set(slice.map((ev) => ev.id));
17595
+ const all = readJournalEvents(journalPath);
17596
+ const surviving = all.filter((ev) => !sliceIds.has(ev.id));
17597
+ const archivePath = archivePathFor(journalPath, version2);
17598
+ mkdirSync2(dirname2(archivePath), { recursive: true });
17599
+ const archiveLines = slice.map(toLine).join("");
17600
+ if (existsSync3(archivePath)) {
17601
+ const existing = readFileSync3(archivePath, "utf8");
17602
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
17603
+ appendFileSync(archivePath, prefix + archiveLines);
17604
+ } else {
17605
+ writeFileSync2(archivePath, archiveLines);
17606
+ }
17607
+ writeFileSync2(journalPath, surviving.map(toLine).join(""));
17608
+ }
17609
+
17610
+ // src/lib/bundle-validate.ts
16709
17611
  function validateBundleAt(filePath) {
16710
17612
  const bundle = readBundle(filePath);
16711
17613
  const loose = bundle;
@@ -16714,21 +17616,49 @@ function validateBundleAt(filePath) {
16714
17616
  }
16715
17617
  let sidecarFindings = [];
16716
17618
  let sidecarLoaded = false;
17619
+ const archiveFindings = [];
17620
+ const archivesLoaded = [];
16717
17621
  if (loose.journal === void 0) {
16718
- const sidecarPath = join2(dirname2(filePath), JOURNAL_SIDECAR);
16719
- if (existsSync3(sidecarPath)) {
16720
- const { events, findings } = parseJournalLines(readFileSync3(sidecarPath, "utf8"));
17622
+ const sidecarPath = journalPathFor(filePath);
17623
+ const folded = [];
17624
+ if (existsSync4(sidecarPath)) {
17625
+ const { events, findings } = parseJournalLines(readFileSync4(sidecarPath, "utf8"));
16721
17626
  sidecarFindings = findings;
16722
17627
  sidecarLoaded = true;
16723
- loose.journal = events;
17628
+ folded.push(...events);
17629
+ }
17630
+ for (const archivePath of archivePathsFor(sidecarPath)) {
17631
+ const { events, findings } = parseJournalLines(readFileSync4(archivePath, "utf8"));
17632
+ const file2 = basename2(archivePath);
17633
+ for (const finding of findings) archiveFindings.push({ ...finding, file: file2 });
17634
+ archivesLoaded.push(file2);
17635
+ folded.push(...events);
16724
17636
  }
17637
+ if (sidecarLoaded || archivesLoaded.length > 0) loose.journal = folded;
16725
17638
  }
16726
17639
  const nodes = Array.isArray(loose.nodes) ? loose.nodes : [];
16727
17640
  const edges = Array.isArray(loose.edges) ? loose.edges : [];
16728
17641
  const journal = Array.isArray(loose.journal) ? loose.journal : [];
16729
17642
  const result = validateBundle(bundle);
16730
- const valid = sidecarFindings.length === 0 && result.errors.length === 0;
16731
- return { bundle, nodes, edges, journal, sidecarLoaded, sidecarFindings, result, valid };
17643
+ const valid = sidecarFindings.length === 0 && archiveFindings.length === 0 && result.errors.length === 0;
17644
+ return {
17645
+ bundle,
17646
+ nodes,
17647
+ edges,
17648
+ journal,
17649
+ sidecarLoaded,
17650
+ sidecarFindings,
17651
+ archivesLoaded,
17652
+ archiveFindings,
17653
+ result,
17654
+ valid
17655
+ };
17656
+ }
17657
+ function journalLineErrorLines(v) {
17658
+ return [
17659
+ ...v.sidecarFindings.map((f) => `ERROR [${f.rule}] line ${f.line}: ${f.message}`),
17660
+ ...v.archiveFindings.map((f) => `ERROR [${f.rule}] ${f.file} line ${f.line}: ${f.message}`)
17661
+ ];
16732
17662
  }
16733
17663
 
16734
17664
  // src/commands/validate.ts
@@ -16765,12 +17695,12 @@ function fixFormat(filePath) {
16765
17695
  } catch (e) {
16766
17696
  return fail2(`FATAL: ${e.message}`);
16767
17697
  }
16768
- const before = readFileSync4(filePath, "utf8");
17698
+ const before = readFileSync5(filePath, "utf8");
16769
17699
  const after = serializeBundle(bundle);
16770
17700
  if (after === before) {
16771
17701
  console.log(`Already canonical: ${filePath}`);
16772
17702
  } else {
16773
- writeFileSync2(filePath, after);
17703
+ writeFileSync3(filePath, after);
16774
17704
  console.log(`Reformatted: ${filePath}`);
16775
17705
  }
16776
17706
  process.exit(0);
@@ -16789,8 +17719,12 @@ function validate(filePath) {
16789
17719
  ` Nodes: ${v.nodes.length} (${countBySpecies(v.nodes, "view")} views, ${countBySpecies(v.nodes, "flow")} flows, ${countBySpecies(v.nodes, "data-model")} data-models, ${countBySpecies(v.nodes, "api-endpoint")} api-endpoints)`
16790
17720
  );
16791
17721
  console.log(` Edges: ${v.edges.length}`);
16792
- if (v.sidecarLoaded) {
16793
- console.log(` Journal: ${v.journal.length} event(s) from ${JOURNAL_SIDECAR} sidecar`);
17722
+ if (v.sidecarLoaded || v.archivesLoaded.length > 0) {
17723
+ const from = [
17724
+ ...v.sidecarLoaded ? [`${JOURNAL_SIDECAR} sidecar`] : [],
17725
+ ...v.archivesLoaded.length > 0 ? [`${v.archivesLoaded.length} archive(s)`] : []
17726
+ ].join(" + ");
17727
+ console.log(` Journal: ${v.journal.length} event(s) from ${from}`);
16794
17728
  } else if (v.journal.length > 0) {
16795
17729
  console.log(` Journal: ${v.journal.length} embedded event(s)`);
16796
17730
  }
@@ -16800,10 +17734,7 @@ function validate(filePath) {
16800
17734
  v.result.warnings.forEach((w) => console.log(` ${formatFinding(w)}`));
16801
17735
  console.log("");
16802
17736
  }
16803
- const errorLines = [
16804
- ...v.sidecarFindings.map((f) => `ERROR [${f.rule}] line ${f.line}: ${f.message}`),
16805
- ...v.result.errors.map(formatFinding)
16806
- ];
17737
+ const errorLines = [...journalLineErrorLines(v), ...v.result.errors.map(formatFinding)];
16807
17738
  if (errorLines.length === 0) {
16808
17739
  console.log(" Result: VALID\n");
16809
17740
  process.exit(0);
@@ -16843,64 +17774,14 @@ ${USAGE2}`);
16843
17774
  }
16844
17775
  }
16845
17776
 
16846
- // src/lib/journal-io.ts
16847
- import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
16848
- import { dirname as dirname3, join as join3 } from "node:path";
16849
- var JOURNAL_SIDECAR2 = "journal.jsonl";
16850
- function journalPathFor(bundlePath) {
16851
- return join3(dirname3(bundlePath), JOURNAL_SIDECAR2);
16852
- }
16853
- function archivePathFor(journalPath, version2) {
16854
- return join3(dirname3(journalPath), "journal", `archive-${version2}.jsonl`);
16855
- }
16856
- function readJournalEvents(journalPath) {
16857
- if (!existsSync4(journalPath)) return [];
16858
- return parseJournalLines(readFileSync5(journalPath, "utf8")).events;
16859
- }
16860
- function loadJournalEvents(bundle, bundlePath) {
16861
- if (Array.isArray(bundle.journal)) return bundle.journal;
16862
- return readJournalEvents(journalPathFor(bundlePath));
17777
+ // src/lib/render-event.ts
17778
+ function str(value) {
17779
+ return typeof value === "string" ? value : void 0;
16863
17780
  }
16864
- function toLine(event) {
16865
- return JSON.stringify(event) + "\n";
16866
- }
16867
- function appendJournalEvent(journalPath, event) {
16868
- const line2 = toLine(event);
16869
- if (!existsSync4(journalPath)) {
16870
- mkdirSync2(dirname3(journalPath), { recursive: true });
16871
- writeFileSync3(journalPath, line2);
16872
- return;
16873
- }
16874
- const existing = readFileSync5(journalPath, "utf8");
16875
- const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
16876
- appendFileSync(journalPath, prefix + line2);
16877
- }
16878
- function compactSlice(journalPath, slice, version2) {
16879
- if (slice.length === 0) return;
16880
- const sliceIds = new Set(slice.map((ev) => ev.id));
16881
- const all = readJournalEvents(journalPath);
16882
- const surviving = all.filter((ev) => !sliceIds.has(ev.id));
16883
- const archivePath = archivePathFor(journalPath, version2);
16884
- mkdirSync2(dirname3(archivePath), { recursive: true });
16885
- const archiveLines = slice.map(toLine).join("");
16886
- if (existsSync4(archivePath)) {
16887
- const existing = readFileSync5(archivePath, "utf8");
16888
- const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
16889
- appendFileSync(archivePath, prefix + archiveLines);
16890
- } else {
16891
- writeFileSync3(archivePath, archiveLines);
16892
- }
16893
- writeFileSync3(journalPath, surviving.map(toLine).join(""));
16894
- }
16895
-
16896
- // src/lib/render-event.ts
16897
- function str(value) {
16898
- return typeof value === "string" ? value : void 0;
16899
- }
16900
- function title(id, nodesById) {
16901
- const nodeId = str(id);
16902
- if (nodeId === void 0) return "?";
16903
- return nodesById?.get(nodeId)?.title ?? nodeId;
17781
+ function title(id, nodesById) {
17782
+ const nodeId = str(id);
17783
+ if (nodeId === void 0) return "?";
17784
+ return nodesById?.get(nodeId)?.title ?? nodeId;
16904
17785
  }
16905
17786
  function renderEventLine(event, nodesById) {
16906
17787
  switch (event.type) {
@@ -16957,6 +17838,10 @@ function renderEventLine(event, nodesById) {
16957
17838
  const to = str(event.to) ?? "?";
16958
17839
  return `${title(event.node_id, nodesById)}: reference ${from ? `${from} -> ${to}` : to}`;
16959
17840
  }
17841
+ case "journal.baseline": {
17842
+ const count = Array.isArray(event.node_ids) ? event.node_ids.length : 0;
17843
+ return `Journal baseline: ${count} pre-existing node(s) recorded`;
17844
+ }
16960
17845
  default:
16961
17846
  return event.type;
16962
17847
  }
@@ -17160,7 +18045,12 @@ ${USAGE4}`);
17160
18045
  } catch (e) {
17161
18046
  fail4(`FATAL: could not build release event \u2014 ${e.message}`);
17162
18047
  }
18048
+ const baseline = ensureJournalBaseline(journalPath, bundle, ACTOR);
17163
18049
  appendJournalEvent(journalPath, event);
18050
+ if (baseline !== void 0) {
18051
+ console.log(`
18052
+ ${renderEventLine(baseline)} -> ${journalPath}`);
18053
+ }
17164
18054
  console.log(`
17165
18055
  Tagged release ${version2}${platform ? ` [${platform}]` : ""} -> ${journalPath}`);
17166
18056
  const all = [...existing, event];
@@ -17317,7 +18207,12 @@ ${USAGE5}`);
17317
18207
  fail5(`FATAL: could not build deliverable event \u2014 ${e.message}`);
17318
18208
  }
17319
18209
  const journalPath = journalPathFor(filePath);
18210
+ const baseline = ensureJournalBaseline(journalPath, bundle, ACTOR2);
17320
18211
  appendJournalEvent(journalPath, event);
18212
+ if (baseline !== void 0) {
18213
+ console.log(`
18214
+ ${renderEventLine(baseline)} -> ${journalPath}`);
18215
+ }
17321
18216
  console.log(
17322
18217
  `
17323
18218
  Recorded deliverable ${deliverableId} \u2014 ${title2} -> ${journalPath}
@@ -17504,6 +18399,13 @@ async function runSync(options = {}) {
17504
18399
  const errors = [];
17505
18400
  const nodeTitles = /* @__PURE__ */ new Map();
17506
18401
  let dirty = false;
18402
+ let baseline;
18403
+ let baselineChecked = false;
18404
+ const adoptJournal = () => {
18405
+ if (baselineChecked) return;
18406
+ baselineChecked = true;
18407
+ baseline = ensureJournalBaseline(journalPath, bundle, actor);
18408
+ };
17507
18409
  for (const node of nodes) {
17508
18410
  const nodeId = typeof node.id === "string" ? node.id : void 0;
17509
18411
  if (nodeId !== void 0 && typeof node.title === "string") nodeTitles.set(nodeId, node.title);
@@ -17556,6 +18458,7 @@ async function runSync(options = {}) {
17556
18458
  },
17557
18459
  { actor, ts: syncedAt }
17558
18460
  );
18461
+ adoptJournal();
17559
18462
  appendJournalEvent(journalPath, event);
17560
18463
  }
17561
18464
  }
@@ -17570,6 +18473,7 @@ async function runSync(options = {}) {
17570
18473
  const patch = promotionPatch(node, promotion);
17571
18474
  Object.assign(node, patch);
17572
18475
  dirty = true;
18476
+ adoptJournal();
17573
18477
  appendJournalEvent(
17574
18478
  journalPath,
17575
18479
  makeEvent(
@@ -17588,7 +18492,7 @@ async function runSync(options = {}) {
17588
18492
  if (dirty && !dryRun) {
17589
18493
  writeFileSync4(filePath, serializeBundle(bundle));
17590
18494
  }
17591
- return { ok: true, bundlePath: filePath, journalPath, dryRun, changed, unchanged, skipped, errors, nodeTitles, promoted };
18495
+ return { ok: true, bundlePath: filePath, journalPath, dryRun, changed, unchanged, skipped, errors, nodeTitles, promoted, baseline };
17592
18496
  }
17593
18497
  function changeLine(change, nodeTitles) {
17594
18498
  const event = {
@@ -17626,6 +18530,9 @@ function report(result) {
17626
18530
  for (const s of stubSkips) byProvider.set(s.provider ?? "?", (byProvider.get(s.provider ?? "?") ?? 0) + 1);
17627
18531
  for (const [provider, count] of byProvider) console.log(` - ${provider}: ${count} ref(s)`);
17628
18532
  }
18533
+ if (result.baseline !== void 0) {
18534
+ console.log(` ${renderEventLine(result.baseline)}`);
18535
+ }
17629
18536
  if (result.errors.length > 0) {
17630
18537
  console.log(` Errors: ${result.errors.length}`);
17631
18538
  result.errors.forEach((e) => console.log(` - ${e.nodeId}/${e.refId} (${e.refType}): ${e.message}`));
@@ -17670,7 +18577,7 @@ ${USAGE6}`);
17670
18577
 
17671
18578
  // src/commands/pack.ts
17672
18579
  import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
17673
- import { dirname as dirname4, extname, resolve as resolve3 } from "node:path";
18580
+ import { dirname as dirname3, extname, resolve as resolve3 } from "node:path";
17674
18581
  var DEFAULT_BUNDLE_PATH5 = "docs/arkaik/bundle.json";
17675
18582
  var USAGE7 = `arkaik pack [--no-journal] [--inline-assets] [--out <path>] [path]
17676
18583
 
@@ -17756,7 +18663,7 @@ function runPack(options = {}) {
17756
18663
  const inlinedAssets = [];
17757
18664
  const assetWarnings = [];
17758
18665
  if (inlineAssets) {
17759
- const bundleDir = dirname4(filePath);
18666
+ const bundleDir = dirname3(filePath);
17760
18667
  const nodes = Array.isArray(bundle.nodes) ? bundle.nodes : [];
17761
18668
  for (const node of nodes) {
17762
18669
  const nodeId = typeof node.id === "string" ? node.id : "?";
@@ -17783,7 +18690,7 @@ function runPack(options = {}) {
17783
18690
  let outPath;
17784
18691
  if (options.out !== void 0) {
17785
18692
  outPath = resolve3(cwd, options.out);
17786
- mkdirSync3(dirname4(outPath), { recursive: true });
18693
+ mkdirSync3(dirname3(outPath), { recursive: true });
17787
18694
  writeFileSync5(outPath, output);
17788
18695
  }
17789
18696
  return { ok: true, bundlePath: filePath, outPath, journalIncluded, journalEventCount, inlinedAssets, assetWarnings, output };
@@ -17844,7 +18751,7 @@ ${USAGE7}`);
17844
18751
  import { spawn } from "node:child_process";
17845
18752
  import { mkdtempSync, writeFileSync as writeFileSync6 } from "node:fs";
17846
18753
  import { tmpdir } from "node:os";
17847
- import { join as join4, resolve as resolve4 } from "node:path";
18754
+ import { join as join3, resolve as resolve4 } from "node:path";
17848
18755
  var DEFAULT_BUNDLE_PATH6 = "docs/arkaik/bundle.json";
17849
18756
  var OPEN_URL = "https://arkaik.app/projects";
17850
18757
  var USAGE8 = `arkaik open [--out <path>] [--no-open] [path]
@@ -17887,10 +18794,7 @@ async function runOpen(options = {}) {
17887
18794
  return fatalResult3(filePath, e.message);
17888
18795
  }
17889
18796
  const warningLines = v.result.warnings.map(formatFinding);
17890
- const errorLines = [
17891
- ...v.sidecarFindings.map((f) => `ERROR [${f.rule}] line ${f.line}: ${f.message}`),
17892
- ...v.result.errors.map(formatFinding)
17893
- ];
18797
+ const errorLines = [...journalLineErrorLines(v), ...v.result.errors.map(formatFinding)];
17894
18798
  if (!v.valid) {
17895
18799
  return { ok: true, bundlePath: filePath, valid: false, errorLines, warningLines, opened: false };
17896
18800
  }
@@ -17900,8 +18804,8 @@ async function runOpen(options = {}) {
17900
18804
  }
17901
18805
  let outPath = packed.outPath;
17902
18806
  if (outPath === void 0) {
17903
- const dir = mkdtempSync(join4(tmpdir(), "arkaik-open-"));
17904
- outPath = join4(dir, "bundle.json");
18807
+ const dir = mkdtempSync(join3(tmpdir(), "arkaik-open-"));
18808
+ outPath = join3(dir, "bundle.json");
17905
18809
  writeFileSync6(outPath, packed.output);
17906
18810
  }
17907
18811
  let opened = false;
@@ -18025,10 +18929,7 @@ async function runPush(options = {}) {
18025
18929
  return fatalResult4(filePath, e.message);
18026
18930
  }
18027
18931
  const warningLines = v.result.warnings.map(formatFinding);
18028
- const errorLines = [
18029
- ...v.sidecarFindings.map((f) => `ERROR [${f.rule}] line ${f.line}: ${f.message}`),
18030
- ...v.result.errors.map(formatFinding)
18031
- ];
18932
+ const errorLines = [...journalLineErrorLines(v), ...v.result.errors.map(formatFinding)];
18032
18933
  if (!v.valid) {
18033
18934
  return { ok: true, bundlePath: filePath, valid: false, errorLines, warningLines, requestSent: false };
18034
18935
  }
@@ -18236,7 +19137,7 @@ ${USAGE9}`);
18236
19137
 
18237
19138
  // src/commands/link.ts
18238
19139
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
18239
- import { dirname as dirname5, join as join5, resolve as resolve6 } from "node:path";
19140
+ import { dirname as dirname4, join as join4, resolve as resolve6 } from "node:path";
18240
19141
  var LINK_FILE = "docs/arkaik/arkaik.json";
18241
19142
  var DEFAULT_BASE_URL = "https://arkaik.app";
18242
19143
  var USAGE10 = `arkaik link \u2014 point this repo at a hosted Arkaik project
@@ -18308,8 +19209,8 @@ async function runLink(argv, options = {}) {
18308
19209
  }
18309
19210
  const { bundle } = await res.json();
18310
19211
  const target = resolve6(cwd, argv.find((a) => !a.startsWith("--") && a !== projectId && a !== baseUrl) ?? ".");
18311
- const linkPath = join5(target, LINK_FILE);
18312
- mkdirSync4(dirname5(linkPath), { recursive: true });
19212
+ const linkPath = join4(target, LINK_FILE);
19213
+ mkdirSync4(dirname4(linkPath), { recursive: true });
18313
19214
  let existing = {};
18314
19215
  try {
18315
19216
  existing = JSON.parse(readFileSync7(linkPath, "utf8"));
@@ -18343,7 +19244,7 @@ function runLinkCli(argv) {
18343
19244
 
18344
19245
  // src/commands/restore.ts
18345
19246
  import { existsSync as existsSync6, linkSync, mkdirSync as mkdirSync5, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync8 } from "node:fs";
18346
- import { join as join6, resolve as resolve7 } from "node:path";
19247
+ import { join as join5, resolve as resolve7 } from "node:path";
18347
19248
  var LINK_FILE2 = "docs/arkaik/arkaik.json";
18348
19249
  var DEFAULT_BUNDLE_PATH8 = "docs/arkaik/bundle.json";
18349
19250
  var DEFAULT_API_BASE2 = "https://arkaik.app";
@@ -18374,6 +19275,13 @@ Options:
18374
19275
  means a missing/gitignored journal.jsonl or a bundle
18375
19276
  from the wrong directory, not an intended history
18376
19277
  rewrite.
19278
+ --allow-deletions Proceed even though the local bundle drops nodes or
19279
+ edges the hosted project currently has. Without this
19280
+ flag, that refuses outright and names the ids \u2014 it
19281
+ usually means the hosted project moved ahead of your
19282
+ local copy (edited in the app), not an intended
19283
+ deletion. Undoing a restore from a backup is the
19284
+ common case where it IS intended.
18377
19285
  --api <base-url> Override the remote from docs/arkaik/arkaik.json
18378
19286
  (also overridable with $ARKAIK_URL).
18379
19287
  -h, --help Show this help.
@@ -18484,13 +19392,48 @@ function writeBackupFile(filePath, content) {
18484
19392
  unlinkSync(tmpPath);
18485
19393
  }
18486
19394
  }
19395
+ function removedIds(before, after) {
19396
+ const kept = /* @__PURE__ */ new Set();
19397
+ for (const item of after) {
19398
+ const id = item?.id;
19399
+ if (typeof id === "string") kept.add(id);
19400
+ }
19401
+ const removed = [];
19402
+ for (const item of before) {
19403
+ const id = item?.id;
19404
+ if (typeof id === "string" && !kept.has(id)) removed.push(id);
19405
+ }
19406
+ return removed;
19407
+ }
19408
+ function listIds(ids, limit = 10) {
19409
+ if (ids.length <= limit) return ids.join(", ");
19410
+ return `${ids.slice(0, limit).join(", ")}, and ${ids.length - limit} more`;
19411
+ }
19412
+ function describeDeletions(removedNodes, removedEdges, bundlePath) {
19413
+ const parts = [];
19414
+ if (removedNodes.length > 0) parts.push(`${removedNodes.length} node${removedNodes.length === 1 ? "" : "s"}`);
19415
+ if (removedEdges.length > 0) parts.push(`${removedEdges.length} edge${removedEdges.length === 1 ? "" : "s"}`);
19416
+ const lines = [
19417
+ `This restore would DELETE ${parts.join(" and ")} the hosted project currently has and ${bundlePath} does not. Nothing was sent.`
19418
+ ];
19419
+ if (removedNodes.length > 0) lines.push(` nodes: ${listIds(removedNodes)}`);
19420
+ if (removedEdges.length > 0) lines.push(` edges: ${listIds(removedEdges)}`);
19421
+ lines.push(
19422
+ `Usually this means the hosted project moved ahead of your local copy (someone edited it in the app) and the local bundle is the stale side \u2014 not that you meant to delete anything. Adopt the hosted state locally first: \`GET /api/graph/projects/<id>/export\` is what answers "what am I about to overwrite".`
19423
+ );
19424
+ lines.push(
19425
+ `If the deletions really are intended \u2014 undoing an earlier restore from a backup legitimately removes what that restore added \u2014 re-run with --allow-deletions.`
19426
+ );
19427
+ return lines.join("\n");
19428
+ }
18487
19429
  async function runRestore(options = {}) {
18488
19430
  const cwd = options.cwd ?? process.cwd();
18489
19431
  const env = options.env ?? process.env;
18490
19432
  const dryRun = options.dryRun ?? false;
18491
19433
  const allowHistoryLoss = options.allowHistoryLoss ?? false;
19434
+ const allowDeletions = options.allowDeletions ?? false;
18492
19435
  const httpClient = options.httpClient ?? DEFAULT_HTTP_CLIENT;
18493
- const linkPath = join6(cwd, LINK_FILE2);
19436
+ const linkPath = join5(cwd, LINK_FILE2);
18494
19437
  if (!existsSync6(linkPath)) {
18495
19438
  return fatalResult5(dryRun, `No ${LINK_FILE2}. Run \`arkaik link\` first \u2014 restore only targets hosted projects.`);
18496
19439
  }
@@ -18578,9 +19521,14 @@ async function runRestore(options = {}) {
18578
19521
  `This restore would replace ${hostedEventCount} hosted journal events with ${journalEvents.length}. Nothing was sent. If that is intended, re-run with --allow-history-loss; otherwise check that ${journalPathFor(bundlePath)} exists and is current.`
18579
19522
  );
18580
19523
  }
18581
- const backupDir = join6(cwd, "docs", "arkaik", ".backups");
19524
+ const removedNodes = removedIds(exportedBundle.nodes, Array.isArray(local.nodes) ? local.nodes : []);
19525
+ const removedEdges = removedIds(exportedBundle.edges, Array.isArray(local.edges) ? local.edges : []);
19526
+ if ((removedNodes.length > 0 || removedEdges.length > 0) && !allowDeletions) {
19527
+ return fatalResult5(dryRun, describeDeletions(removedNodes, removedEdges, bundlePath));
19528
+ }
19529
+ const backupDir = join5(cwd, "docs", "arkaik", ".backups");
18582
19530
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
18583
- const backupPath = join6(backupDir, `${stamp}-bundle.json`);
19531
+ const backupPath = join5(backupDir, `${stamp}-bundle.json`);
18584
19532
  const backupContent = `${JSON.stringify(exported, null, 2)}
18585
19533
  `;
18586
19534
  try {
@@ -18625,6 +19573,17 @@ function printDelta(delta) {
18625
19573
  console.log(
18626
19574
  ` events ${n("eventsBefore")} -> ${n("eventsAfter")} (+${n("eventsAdded")} -${n("eventsDropped")} ~${n("eventsChanged")}${delta.eventsMalformed ? `, ${delta.eventsMalformed} malformed` : ""})`
18627
19575
  );
19576
+ const count = (k) => typeof delta[k] === "number" ? delta[k] : 0;
19577
+ const nodesRemoved = count("nodesRemoved");
19578
+ const edgesRemoved = count("edgesRemoved");
19579
+ if (nodesRemoved > 0 || edgesRemoved > 0) {
19580
+ const parts = [];
19581
+ if (nodesRemoved > 0) parts.push(`${nodesRemoved} node${nodesRemoved === 1 ? "" : "s"}`);
19582
+ if (edgesRemoved > 0) parts.push(`${edgesRemoved} edge${edgesRemoved === 1 ? "" : "s"}`);
19583
+ console.log(
19584
+ ` WARNING: this DELETES ${parts.join(" and ")} from the hosted project. Bootstrap never deletes \u2014 check the hosted project has not moved ahead of your local bundle.`
19585
+ );
19586
+ }
18628
19587
  }
18629
19588
  function reportRestore(result) {
18630
19589
  if (!result.ok) fail10(`FATAL: ${result.fatal}`);
@@ -18659,6 +19618,7 @@ function reportRestore(result) {
18659
19618
  function runRestoreCli(argv) {
18660
19619
  let dryRun = false;
18661
19620
  let allowHistoryLoss = false;
19621
+ let allowDeletions = false;
18662
19622
  let apiBase;
18663
19623
  const positionals = [];
18664
19624
  for (let i = 0; i < argv.length; i++) {
@@ -18671,6 +19631,8 @@ function runRestoreCli(argv) {
18671
19631
  dryRun = true;
18672
19632
  } else if (arg === "--allow-history-loss") {
18673
19633
  allowHistoryLoss = true;
19634
+ } else if (arg === "--allow-deletions") {
19635
+ allowDeletions = true;
18674
19636
  } else if (arg === "--api") {
18675
19637
  const value = argv[++i];
18676
19638
  if (value === void 0) fail10(`Missing value for --api
@@ -18688,7 +19650,7 @@ ${USAGE11}`);
18688
19650
  if (positionals.length > 1) fail10(`Unexpected argument(s): ${positionals.slice(1).join(" ")}
18689
19651
 
18690
19652
  ${USAGE11}`);
18691
- runRestore({ path: positionals[0], dryRun, allowHistoryLoss, apiBase }).then((result) => reportRestore(result)).catch((e) => fail10(`FATAL: ${e.message}`));
19653
+ runRestore({ path: positionals[0], dryRun, allowHistoryLoss, allowDeletions, apiBase }).then((result) => reportRestore(result)).catch((e) => fail10(`FATAL: ${e.message}`));
18692
19654
  }
18693
19655
 
18694
19656
  // src/commands/bootstrap.ts
@@ -18698,7 +19660,7 @@ import path5 from "node:path";
18698
19660
 
18699
19661
  // src/lib/bootstrap/corpus.ts
18700
19662
  import { spawnSync } from "node:child_process";
18701
- import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
19663
+ import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
18702
19664
  import path2 from "node:path";
18703
19665
 
18704
19666
  // src/lib/bootstrap/paths.ts
@@ -18823,7 +19785,7 @@ function fetchPrsViaGit(cwd) {
18823
19785
  function walk(root, cwd, out) {
18824
19786
  let entries;
18825
19787
  try {
18826
- entries = readdirSync(root, { withFileTypes: true });
19788
+ entries = readdirSync2(root, { withFileTypes: true });
18827
19789
  } catch {
18828
19790
  return;
18829
19791
  }
@@ -19320,7 +20282,7 @@ function mergeJournal(base, fresh) {
19320
20282
  }
19321
20283
 
19322
20284
  // src/lib/bootstrap/merge.ts
19323
- function asArray(value) {
20285
+ function asArray2(value) {
19324
20286
  return Array.isArray(value) ? value : [];
19325
20287
  }
19326
20288
  function mergeFragments(input) {
@@ -19328,14 +20290,14 @@ function mergeFragments(input) {
19328
20290
  const projectId = String(input.base.project?.id ?? "");
19329
20291
  const nodes = /* @__PURE__ */ new Map();
19330
20292
  const nodeOrigin = /* @__PURE__ */ new Map();
19331
- for (const node of asArray(input.base.nodes)) {
20293
+ for (const node of asArray2(input.base.nodes)) {
19332
20294
  const id = String(node.id);
19333
20295
  nodes.set(id, { ...node });
19334
20296
  nodeOrigin.set(id, "(already in the bundle)");
19335
20297
  }
19336
20298
  const edges = /* @__PURE__ */ new Map();
19337
20299
  const edgeOrigin = /* @__PURE__ */ new Map();
19338
- for (const edge of asArray(input.base.edges)) {
20300
+ for (const edge of asArray2(input.base.edges)) {
19339
20301
  const id = String(edge.id ?? edgeId(String(edge.source_id), String(edge.target_id)));
19340
20302
  edges.set(id, { ...edge });
19341
20303
  edgeOrigin.set(id, "(already in the bundle)");
@@ -20023,8 +20985,1119 @@ ${USAGE12}`);
20023
20985
  }
20024
20986
  }
20025
20987
 
20988
+ // ../schema/src/cli/kritik-audit.ts
20989
+ import { existsSync as existsSync14, readdirSync as readdirSync3, statSync } from "node:fs";
20990
+ import { join as join7 } from "node:path";
20991
+
20992
+ // ../schema/src/cli/kritik-paths.ts
20993
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync13 } from "node:fs";
20994
+ import { dirname as dirname5, join as join6, resolve as resolve8 } from "node:path";
20995
+ var QUALITY_DIR = "docs/quality";
20996
+ var PROFILE_FILE2 = "profile.json";
20997
+ var OVERLAY_FILE = "criteria.custom.json";
20998
+ var AUDITS_DIR = "audits";
20999
+ var PACK_FILE = "library.json";
21000
+ function readJson(path6) {
21001
+ return JSON.parse(readFileSync14(path6, "utf8"));
21002
+ }
21003
+ function writeJson(path6, value) {
21004
+ mkdirSync8(dirname5(path6), { recursive: true });
21005
+ writeFileSync13(path6, JSON.stringify(value, null, 2) + "\n");
21006
+ }
21007
+ var profilePath = (root) => join6(root, QUALITY_DIR, PROFILE_FILE2);
21008
+ var overlayPath = (root) => join6(root, QUALITY_DIR, OVERLAY_FILE);
21009
+ var auditDir = (root, auditId) => join6(root, QUALITY_DIR, AUDITS_DIR, auditId);
21010
+ function loadProfile(root) {
21011
+ const path6 = profilePath(root);
21012
+ return existsSync13(path6) ? readJson(path6) : null;
21013
+ }
21014
+ function loadOverlay(root) {
21015
+ const path6 = overlayPath(root);
21016
+ return existsSync13(path6) ? readJson(path6) : null;
21017
+ }
21018
+
21019
+ // ../schema/src/cli/kritik-audit.ts
21020
+ var SCORES_FILE = "scores.json";
21021
+ var FINDINGS_FILE = "findings.json";
21022
+ var MATRIX_FILE = "matrix.json";
21023
+ var scoresPath = (root, auditId) => join7(auditDir(root, auditId), SCORES_FILE);
21024
+ var findingsPath = (root, auditId) => join7(auditDir(root, auditId), FINDINGS_FILE);
21025
+ var matrixPath = (root, auditId) => join7(auditDir(root, auditId), MATRIX_FILE);
21026
+ function listAuditIds(root) {
21027
+ const dir = join7(root, QUALITY_DIR, AUDITS_DIR);
21028
+ if (!existsSync14(dir)) return [];
21029
+ return readdirSync3(dir).filter((name) => statSync(join7(dir, name)).isDirectory()).sort();
21030
+ }
21031
+ function newestAuditId(root) {
21032
+ const dir = join7(root, QUALITY_DIR, AUDITS_DIR);
21033
+ if (!existsSync14(dir)) throw new Error(`no audits directory at ${dir}`);
21034
+ const ids = listAuditIds(root);
21035
+ if (ids.length === 0) throw new Error(`no audits found under ${dir}`);
21036
+ return ids[ids.length - 1];
21037
+ }
21038
+ function loadScores(root, auditId) {
21039
+ const path6 = scoresPath(root, auditId);
21040
+ if (!existsSync14(path6)) throw new Error(`no ${SCORES_FILE} at ${path6}`);
21041
+ const file2 = readJson(path6);
21042
+ return { ...file2, assessments: Array.isArray(file2.assessments) ? file2.assessments : [] };
21043
+ }
21044
+ function loadScoresOrEmpty(root, auditId) {
21045
+ return existsSync14(scoresPath(root, auditId)) ? loadScores(root, auditId) : { audit_id: auditId, assessments: [] };
21046
+ }
21047
+ function loadFindings(root, auditId) {
21048
+ const path6 = findingsPath(root, auditId);
21049
+ if (!existsSync14(path6)) return { audit_id: auditId, findings: [] };
21050
+ const file2 = readJson(path6);
21051
+ return { ...file2, findings: Array.isArray(file2.findings) ? file2.findings : [] };
21052
+ }
21053
+ function saveScores(root, auditId, file2) {
21054
+ writeJson(scoresPath(root, auditId), file2);
21055
+ }
21056
+ function saveFindings(root, auditId, file2) {
21057
+ writeJson(findingsPath(root, auditId), file2);
21058
+ }
21059
+ function requireProfile(root) {
21060
+ const profile = loadProfile(root);
21061
+ if (!profile) {
21062
+ throw new Error(
21063
+ `no profile at ${join7(root, QUALITY_DIR, "profile.json")} \u2014 pick this project's surfaces first (\`arkaik kritik profile\`, or the plugin's init-profile.js).`
21064
+ );
21065
+ }
21066
+ return profile;
21067
+ }
21068
+ function loadQualitySection(root, auditId, library, scores = loadScores(root, auditId)) {
21069
+ const findings = loadFindings(root, auditId);
21070
+ return {
21071
+ framework_version: scores.framework_version ?? library.version,
21072
+ profile: requireProfile(root),
21073
+ assessments: scores.assessments,
21074
+ findings: findings.findings
21075
+ };
21076
+ }
21077
+ function computeAuditMatrix(root, auditId, library) {
21078
+ const scores = loadScores(root, auditId);
21079
+ const section = loadQualitySection(root, auditId, library, scores);
21080
+ const matrix = deriveQualityMatrix({ quality: section }, library);
21081
+ const file2 = {
21082
+ audit_id: auditId,
21083
+ commit: scores.commit,
21084
+ framework_version: section.framework_version,
21085
+ matrix: matrix.matrix,
21086
+ overall: matrix.overall,
21087
+ finding_counts: matrix.finding_counts
21088
+ };
21089
+ writeJson(matrixPath(root, auditId), file2);
21090
+ return { section, matrix, file: file2 };
21091
+ }
21092
+ function renderMatrixMarkdown(matrix, domainNames) {
21093
+ const cell = (value) => value ? `${value.score} (${value.grade}${value.capped ? "*" : ""})` : "\u2014";
21094
+ const lines = [];
21095
+ lines.push(`| Domain | ${matrix.surfaces.join(" | ")} |`);
21096
+ lines.push(`| --- | ${matrix.surfaces.map(() => "---").join(" | ")} |`);
21097
+ for (const domain2 of matrix.domains) {
21098
+ const label = domainNames.get(domain2) ?? domain2;
21099
+ lines.push(
21100
+ `| **${domain2}** ${label} | ${matrix.surfaces.map((s) => cell(matrix.matrix[domain2]?.[s])).join(" | ")} |`
21101
+ );
21102
+ }
21103
+ lines.push(
21104
+ `| **Overall (weighted)** | ${matrix.surfaces.map((s) => {
21105
+ const score = matrix.overall[s];
21106
+ return score === null || score === void 0 ? "\u2014" : `**${score} (${gradeOf(score)})**`;
21107
+ }).join(" | ")} |`
21108
+ );
21109
+ return lines.join("\n");
21110
+ }
21111
+ function locateFinding(root, id) {
21112
+ for (const auditId of [...listAuditIds(root)].reverse()) {
21113
+ const file2 = loadFindings(root, auditId);
21114
+ const finding = file2.findings.find((candidate) => candidate.id === id);
21115
+ if (finding) return { auditId, file: file2, finding };
21116
+ }
21117
+ return void 0;
21118
+ }
21119
+
21120
+ // ../schema/src/cli/kritik-overlay.ts
21121
+ var ANCHOR_KEYS = ["l0", "l1", "l2", "l3", "l4"];
21122
+ var CRITERION_TEMPLATE = {
21123
+ id: "X-01",
21124
+ domain: "ARC",
21125
+ subcategory: "conventions",
21126
+ name: "Short criterion name",
21127
+ question: "The criterion as one question an auditor can actually answer?",
21128
+ definition: "What good looks like on this surface, concretely.",
21129
+ rationale: "Why this matters for this product in particular.",
21130
+ applies_to: ["web"],
21131
+ level_anchors: {
21132
+ l0: "Not addressed at all.",
21133
+ l1: "Addressed accidentally or in one spot; no visible intent.",
21134
+ l2: "Deliberately addressed; visible intent; known gaps.",
21135
+ l3: "Systematic across the surface; tested or reviewed.",
21136
+ l4: "Enforced by automation or a CI gate; drift is detected, not hoped against."
21137
+ },
21138
+ default_impact: 3,
21139
+ weight: 1,
21140
+ references: [],
21141
+ checklist: ["The grep, file, or flow an auditor should actually run."],
21142
+ signals: ["A check that can run between audits and fail mechanically."],
21143
+ remediation: "The typical fix path.",
21144
+ issue: {
21145
+ title_template: "[Quality] X-01 <name> at level {observed_level} on {surface} (target {target_level})",
21146
+ labels: ["quality"],
21147
+ body_skeleton: "## Quality finding: X-01 <name>\n\n**Surface:** {surface}\n**Observed level:** {observed_level}\n**Target level:** {target_level}\n\n### Evidence\n{evidence_bullets_with_file_paths}\n\n### Remediation\n- [ ] {remediation_step_1}\n\n### Acceptance criteria\n- [ ] Target anchor holds: {target_anchor_text}"
21148
+ }
21149
+ };
21150
+ function buildCriterion(draft) {
21151
+ for (const [label, value] of [
21152
+ ["id", draft.id],
21153
+ ["domain", draft.domain],
21154
+ ["name", draft.name],
21155
+ ["question", draft.question]
21156
+ ]) {
21157
+ if (typeof value !== "string" || value.trim() === "") throw new Error(`--${label} is required`);
21158
+ }
21159
+ if (draft.appliesTo.length === 0) throw new Error(`--applies-to is required \u2014 a criterion applies to at least one surface`);
21160
+ const missing = ANCHOR_KEYS.filter((key) => !draft.anchors[key]);
21161
+ if (missing.length > 0) {
21162
+ throw new Error(
21163
+ `missing anchors ${missing.join(", ")}.
21164
+ All five are required: a criterion without observable descriptions of each level cannot be scored consistently,
21165
+ and an inconsistently scored criterion makes its whole row incomparable.`
21166
+ );
21167
+ }
21168
+ const weight = draft.weight ?? 1;
21169
+ if (!Number.isInteger(weight) || weight < 1 || weight > 3) throw new Error(`--weight must be 1, 2 or 3`);
21170
+ const impact = draft.impact ?? 3;
21171
+ if (!Number.isInteger(impact) || impact < 1 || impact > 5) throw new Error(`--impact must be 1-5`);
21172
+ const { id, name } = draft;
21173
+ return {
21174
+ id,
21175
+ domain: draft.domain,
21176
+ ...draft.subcategory ? { subcategory: draft.subcategory } : {},
21177
+ name,
21178
+ question: draft.question,
21179
+ ...draft.definition ? { definition: draft.definition } : {},
21180
+ ...draft.rationale ? { rationale: draft.rationale } : {},
21181
+ applies_to: draft.appliesTo,
21182
+ level_anchors: draft.anchors,
21183
+ default_impact: impact,
21184
+ weight,
21185
+ references: [],
21186
+ checklist: draft.checklist ?? [],
21187
+ signals: draft.signals ?? [],
21188
+ ...draft.remediation ? { remediation: draft.remediation } : {},
21189
+ issue: {
21190
+ title_template: `[Quality] ${id} ${name} at level {observed_level} on {surface} (target {target_level})`,
21191
+ labels: draft.labels && draft.labels.length > 0 ? draft.labels : ["quality"],
21192
+ body_skeleton: `## Quality finding: ${id} ${name}
21193
+
21194
+ **Surface:** {surface}
21195
+ **Observed level:** {observed_level}
21196
+ **Target level:** {target_level}
21197
+ **Severity seed:** impact {impact} x likelihood {likelihood}
21198
+
21199
+ ### Evidence
21200
+ {evidence_bullets_with_file_paths}
21201
+
21202
+ ### Risk
21203
+ {risk_narrative}
21204
+
21205
+ ### Remediation
21206
+ - [ ] {remediation_step_1}
21207
+
21208
+ ### Acceptance criteria
21209
+ - [ ] Target anchor holds: {target_anchor_text}`
21210
+ }
21211
+ };
21212
+ }
21213
+ function addCriterionToOverlay(root, pack, criterion, options = {}) {
21214
+ if (typeof criterion.id !== "string" || criterion.id === "") throw new Error(`the criterion has no id`);
21215
+ if ((pack.criteria ?? []).some((c) => c.id === criterion.id) && !options.force) {
21216
+ throw new Error(
21217
+ `"${criterion.id}" is already a pack criterion.
21218
+ Overriding it changes what every score recorded against that id means.
21219
+ Use a project-reserved id (X-01, X-02, \u2026) instead, or pass --force if the override is deliberate.`
21220
+ );
21221
+ }
21222
+ const path6 = overlayPath(root);
21223
+ const overlay = loadOverlay(root) ?? { extends: pack.version, criteria: [] };
21224
+ overlay.criteria = Array.isArray(overlay.criteria) ? overlay.criteria : [];
21225
+ const at2 = overlay.criteria.findIndex((c) => c?.id === criterion.id);
21226
+ if (at2 >= 0 && !options.force) {
21227
+ throw new Error(`"${criterion.id}" is already in the overlay \u2014 pass --force to replace it`);
21228
+ }
21229
+ if (at2 >= 0) overlay.criteria[at2] = criterion;
21230
+ else overlay.criteria.push(criterion);
21231
+ const domainCode = criterion.domain;
21232
+ const knownDomain = (pack.domains ?? []).some((d) => d.code === domainCode) || (overlay.domains ?? []).some((d) => d.code === domainCode);
21233
+ let addedDomain;
21234
+ if (!knownDomain) {
21235
+ if (!options.domainName) {
21236
+ throw new Error(
21237
+ `"${domainCode}" is not a pack domain and the overlay does not define it.
21238
+ Pass --domain-name "<display name>" to define it, or use an existing domain code.`
21239
+ );
21240
+ }
21241
+ overlay.domains = [...overlay.domains ?? [], { code: domainCode, name: options.domainName }];
21242
+ addedDomain = domainCode;
21243
+ }
21244
+ writeJson(path6, overlay);
21245
+ return { path: path6, overlay, replaced: at2 >= 0, ...addedDomain !== void 0 ? { addedDomain } : {} };
21246
+ }
21247
+
21248
+ // src/commands/kritik.ts
21249
+ import { existsSync as existsSync16, readFileSync as readFileSync15 } from "node:fs";
21250
+
21251
+ // src/lib/kritik-io.ts
21252
+ import { existsSync as existsSync15 } from "node:fs";
21253
+ import { dirname as dirname6, join as join8 } from "node:path";
21254
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
21255
+ var KRITIK_ACTOR = "arkaik-cli";
21256
+ var DEFAULT_BUNDLE_PATH9 = join8("docs", "arkaik", "bundle.json");
21257
+ var VENDORED_PACK = join8(QUALITY_DIR, PACK_FILE);
21258
+ var BUNDLED_PACK = join8(dirname6(fileURLToPath2(import.meta.url)), "assets", "kritik", "library.json");
21259
+ function resolvePack(root) {
21260
+ const vendored = join8(root, VENDORED_PACK);
21261
+ if (existsSync15(vendored)) return { library: readJson(vendored), path: vendored, vendored: true };
21262
+ if (!existsSync15(BUNDLED_PACK)) {
21263
+ throw new Error(
21264
+ `no criteria pack found. Looked in:
21265
+ ${vendored}
21266
+ ${BUNDLED_PACK}
21267
+ The second is shipped with this CLI, so its absence means a broken install \u2014 reinstall \`arkaik\`.`
21268
+ );
21269
+ }
21270
+ return { library: readJson(BUNDLED_PACK), path: BUNDLED_PACK, vendored: false };
21271
+ }
21272
+ function loadKritikLibrary(root) {
21273
+ const pack = resolvePack(root);
21274
+ return { library: mergeKritikLibrary(pack.library, loadOverlay(root)), pack };
21275
+ }
21276
+ function resolveJournal(root, bundlePath) {
21277
+ const resolved = bundlePath ?? join8(root, DEFAULT_BUNDLE_PATH9);
21278
+ return { bundlePath: resolved, journalPath: journalPathFor(resolved), present: existsSync15(resolved) };
21279
+ }
21280
+ function appendQualityEvents(root, inputs, options = {}) {
21281
+ if (inputs.length === 0) return { events: [] };
21282
+ const actor = options.actor ?? KRITIK_ACTOR;
21283
+ const journal = resolveJournal(root, options.bundlePath);
21284
+ if (!journal.present) return { events: [] };
21285
+ const bundle = readBundle(journal.bundlePath);
21286
+ const baseline = ensureJournalBaseline(journal.journalPath, bundle, actor);
21287
+ const events = inputs.map((input) => makeEvent(input.type, input.payload, { actor }));
21288
+ for (const event of events) appendJournalEvent(journal.journalPath, event);
21289
+ return { journalPath: journal.journalPath, events, ...baseline !== void 0 ? { baseline } : {} };
21290
+ }
21291
+
21292
+ // src/commands/kritik.ts
21293
+ var USAGE13 = `arkaik kritik <subcommand> [options]
21294
+
21295
+ Audit this product's quality with the Kritik framework: a maturity level per
21296
+ criterion per surface backed by evidence, findings carrying risk and cost, and
21297
+ a comparative matrix whose caps stop one open Critical being averaged into a B.
21298
+
21299
+ Subcommands:
21300
+ profile Pick this project's surfaces (writes docs/quality/profile.json).
21301
+ score <c> <s> <lvl> Record one maturity level, with its evidence.
21302
+ finding open ... Open a finding on a (criterion x surface) cell.
21303
+ finding resolve <id> Close it because the fix merged.
21304
+ finding accept <id> Accept it as a known, owned risk.
21305
+ matrix [audit-id] Roll an audit up (writes matrix.json).
21306
+ signals The signal pack, and what has tripped since the last audit.
21307
+ issue <criterion> Print the prefilled GitHub issue skeleton.
21308
+ criterion add ... Add a project-specific criterion to the overlay.
21309
+
21310
+ Common options:
21311
+ --root <dir> Repo root holding docs/quality/ (default: the current directory).
21312
+ --actor <name> Who is writing (default: ${KRITIK_ACTOR}). Recorded on every quality.* event.
21313
+ --bundle <path> The Arkaik bundle whose journal receives events
21314
+ (default: docs/arkaik/bundle.json; skipped when absent).
21315
+ --no-journal Write the audit files only; append no journal events.
21316
+ -h, --help Show this help, or a subcommand's.
21317
+
21318
+ Run "arkaik kritik <subcommand> --help" for the flags each one takes.`;
21319
+ var SCORE_USAGE = `arkaik kritik score <criterion> <surface> <level> --evidence <text|@file> [options]
21320
+
21321
+ Record one (criterion x surface) maturity level. A cell holds exactly one level,
21322
+ so re-scoring replaces in place \u2014 the correction reads as a correction.
21323
+
21324
+ Arguments:
21325
+ criterion A criterion id from the pack or this project's overlay (e.g. SEC-01).
21326
+ surface A surface id declared in docs/quality/profile.json.
21327
+ level 0-4. Absent from the file means N/A; 0 means "not addressed".
21328
+
21329
+ Options:
21330
+ --evidence <e> file:line / config citations. \`@path\` reads them from a file.
21331
+ Required: a score without a citation is an opinion.
21332
+ --audit <id> The audit run (default: the newest, or the current YYYY-MM).
21333
+ --commit <sha> The commit the evidence is pinned to.
21334
+
21335
+ Writes docs/quality/audits/<id>/scores.json`;
21336
+ var FINDING_USAGE = `arkaik kritik finding <open|resolve|accept> [options]
21337
+
21338
+ open <criterion> <surface> --title <t> --impact <1-5> --likelihood <1-5>
21339
+ --cost <S|M|L|XL> --evidence <text|@file> [--detail <d>] [--remediation <r>]
21340
+ [--nodes <id,id>] [--issue-url <u>] [--id <finding-id>] [--audit <id>]
21341
+
21342
+ Opens a finding and appends quality.finding.opened. Severity and priority are
21343
+ printed, never stored \u2014 they are derived from impact x likelihood and cost, so
21344
+ they can never drift from the numbers behind them. The surface may be
21345
+ "${CROSS_SURFACE_ID}" for a defect that belongs to the contract between surfaces.
21346
+
21347
+ resolve <finding-id> [--by <pr-or-commit-url>]
21348
+
21349
+ Marks it resolved and appends quality.finding.resolved. Searched across every
21350
+ audit: a finding opened in 2026-08 is routinely fixed during 2026-09.
21351
+
21352
+ accept <finding-id> --note <why>
21353
+
21354
+ Records it as a known, owned risk. The note is required \u2014 an accepted risk is a
21355
+ decision and reads like one. No journal event: acceptance is a state of the
21356
+ finding, not something that happened to the product.`;
21357
+ var MATRIX_USAGE = `arkaik kritik matrix [audit-id] [--json] [--record]
21358
+
21359
+ Roll one audit up into its comparative matrix and write matrix.json \u2014 the only
21360
+ thing that may write that file.
21361
+
21362
+ audit-id The audit under docs/quality/audits/ (default: the newest).
21363
+ --json Print matrix.json instead of the markdown table.
21364
+ --record Also append quality.audit.completed, carrying these scores
21365
+ and counts. Do this once per finished audit.`;
21366
+ var SIGNALS_USAGE = `arkaik kritik signals [--surface <s>] [--criterion <c>] [--domain <CODE>] [--json]
21367
+ arkaik kritik signals --trip <criterion> --surface <s> --signal <index|text> [--detail <d>]
21368
+
21369
+ Signals are the cheap checks between full audits: a grep that must return
21370
+ nothing, a CI job that must exist. They are statements to check, not commands to
21371
+ run \u2014 the pack spans greps, CI introspection and database queries, so this
21372
+ prints the run sheet rather than pretending to execute it.
21373
+
21374
+ Exits 1 when anything has tripped since the last recorded audit, which is what
21375
+ makes it usable as a CI step.
21376
+
21377
+ --trip Record one as tripped: appends quality.signal.tripped.
21378
+ --signal takes the row's index from the run sheet, or the text.
21379
+ --json The full run sheet as JSON (what an agent should read).`;
21380
+ var ISSUE_USAGE = `arkaik kritik issue <criterion> --surface <s> [--level <n>] [--finding <id>]
21381
+
21382
+ Print the criterion's GitHub issue skeleton, filled as far as what we know
21383
+ allows. Placeholders we cannot fill are left standing \u2014 a skeleton is a form to
21384
+ finish, and an empty "### Risk" reads as "no risk" where {risk_narrative} reads
21385
+ as "your turn".
21386
+
21387
+ --level <n> The observed maturity, so the anchors fill in.
21388
+ --finding <id> Fill the risk numbers, evidence and narrative from a finding.
21389
+ This is the P0/P1 path: the issue exists because a defect does.`;
21390
+ var CRITERION_USAGE = `arkaik kritik criterion add --id <ID> --domain <CODE> --name <t> --question <q> \\
21391
+ --applies-to <s1,s2> --anchor l0=<text> ... --anchor l4=<text> [options]
21392
+
21393
+ Add a criterion the pack does not have to this project's overlay
21394
+ (docs/quality/criteria.custom.json). A pack upgrade never touches it.
21395
+
21396
+ --id A project-reserved id, e.g. X-01. Never a pack id.
21397
+ --domain Owning domain code \u2014 an existing one (SEC, PRV, \u2026) or your own.
21398
+ --domain-name Display name, required only when --domain is a new code.
21399
+ --anchor lN=<t> What each level looks like HERE. All five are required: a
21400
+ criterion scored inconsistently makes its whole row incomparable.
21401
+ --weight <1-3> Weight in the domain roll-up (default 1).
21402
+ --impact <1-5> Seeds finding severity (default 3).
21403
+ --signal <s> A mechanically checkable hook; repeatable.
21404
+ --check <s> A concrete audit step; repeatable.
21405
+ --label <s> An issue label; repeatable (default: quality).
21406
+ --from <file> Read a complete criterion from JSON instead of these flags.
21407
+ --template Print a starting-point criterion and exit.
21408
+ --force Replace an existing criterion with this id.`;
21409
+ function fail12(message) {
21410
+ console.error(message);
21411
+ process.exit(1);
21412
+ }
21413
+ function takeCommon(args) {
21414
+ const rest = [];
21415
+ const common = { root: process.cwd(), actor: KRITIK_ACTOR, journal: true };
21416
+ for (let i = 0; i < args.length; i++) {
21417
+ const arg = args[i];
21418
+ if (arg === "--root") common.root = args[++i] ?? common.root;
21419
+ else if (arg === "--actor") common.actor = args[++i] ?? common.actor;
21420
+ else if (arg === "--bundle") common.bundlePath = args[++i];
21421
+ else if (arg === "--no-journal") common.journal = false;
21422
+ else rest.push(arg);
21423
+ }
21424
+ return { rest, common };
21425
+ }
21426
+ function collect(args, repeatable = [], boolean4 = []) {
21427
+ const single = {};
21428
+ const many = Object.fromEntries(repeatable.map((key) => [key, []]));
21429
+ const flags = /* @__PURE__ */ new Set();
21430
+ const positionals = [];
21431
+ for (let i = 0; i < args.length; i++) {
21432
+ const arg = args[i];
21433
+ if (!arg.startsWith("--")) {
21434
+ positionals.push(arg);
21435
+ continue;
21436
+ }
21437
+ const key = arg.slice(2);
21438
+ if (boolean4.includes(key) || key === "help") {
21439
+ flags.add(key);
21440
+ continue;
21441
+ }
21442
+ const value = args[++i];
21443
+ if (value === void 0) fail12(`kritik: --${key} needs a value`);
21444
+ if (key in many) many[key].push(value);
21445
+ else single[key] = value;
21446
+ }
21447
+ return { single, many, flags, positionals };
21448
+ }
21449
+ function textOrFile(value) {
21450
+ if (!value.startsWith("@")) return value;
21451
+ const path6 = value.slice(1);
21452
+ if (!existsSync16(path6)) fail12(`kritik: no file at ${path6}`);
21453
+ return readFileSync15(path6, "utf8").trim();
21454
+ }
21455
+ function currentAuditId() {
21456
+ const now = /* @__PURE__ */ new Date();
21457
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
21458
+ }
21459
+ function auditForWrite(root, requested) {
21460
+ if (requested !== void 0) return requested;
21461
+ const existing = listAuditIds(root);
21462
+ return existing.length > 0 ? existing[existing.length - 1] : currentAuditId();
21463
+ }
21464
+ function loadLibraryOrFail(root) {
21465
+ try {
21466
+ return loadKritikLibrary(root).library;
21467
+ } catch (error51) {
21468
+ return fail12(`kritik: ${error51.message}`);
21469
+ }
21470
+ }
21471
+ function profileOrFail(root) {
21472
+ try {
21473
+ return requireProfile(root);
21474
+ } catch (error51) {
21475
+ return fail12(`kritik: ${error51.message}`);
21476
+ }
21477
+ }
21478
+ function criterionOrFail(library, criterionId) {
21479
+ const criterion = (library.criteria ?? []).find((candidate) => candidate.id === criterionId);
21480
+ if (!criterion) {
21481
+ const domain2 = criterionId.split("-")[0];
21482
+ const siblings = (library.criteria ?? []).filter((c) => c.domain === domain2).map((c) => c.id);
21483
+ return fail12(
21484
+ `kritik: no criterion "${criterionId}" in the pack or this project's overlay.
21485
+ ` + (siblings.length > 0 ? `Criteria in ${domain2}: ${siblings.join(", ")}` : `Domains: ${(library.domains ?? []).map((d) => d.code).join(", ")}`)
21486
+ );
21487
+ }
21488
+ if (typeof criterion.superseded_by === "string") {
21489
+ return fail12(
21490
+ `kritik: criterion "${criterionId}" is retired \u2014 superseded by "${criterion.superseded_by}".
21491
+ Score that one instead; the old id is kept only so past audits still mean what they meant.`
21492
+ );
21493
+ }
21494
+ return criterion;
21495
+ }
21496
+ function surfaceOrFail(profile, surface, options = {}) {
21497
+ if (surface === CROSS_SURFACE_ID) {
21498
+ if (options.allowCrossSurface) return surface;
21499
+ return fail12(
21500
+ `kritik: "${CROSS_SURFACE_ID}" is a findings-only lens \u2014 it carries no matrix column, so a score there would render nowhere.`
21501
+ );
21502
+ }
21503
+ const declared = (profile.surfaces ?? []).map((s) => s.id);
21504
+ if (!declared.includes(surface)) {
21505
+ return fail12(
21506
+ `kritik: surface "${surface}" is not declared in ${profilePath(".")}.
21507
+ This project's surfaces: ${declared.join(", ") || "(none \u2014 run `arkaik kritik profile` first)"}`
21508
+ );
21509
+ }
21510
+ return surface;
21511
+ }
21512
+ function intOrFail(label, value, min, max) {
21513
+ if (value === void 0) fail12(`kritik: ${label} is required
21514
+ `);
21515
+ const parsed = Number(value);
21516
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
21517
+ fail12(`kritik: ${label} must be an integer ${min}-${max} (got "${value}")`);
21518
+ }
21519
+ return parsed;
21520
+ }
21521
+ function reportJournal(root, inputs, common) {
21522
+ if (!common.journal) {
21523
+ console.log(` journal: skipped (--no-journal)`);
21524
+ return;
21525
+ }
21526
+ const journal = resolveJournal(root, common.bundlePath);
21527
+ if (!journal.present) {
21528
+ console.log(` journal: none at ${journal.bundlePath} \u2014 Kritik does not need one, so nothing was appended`);
21529
+ return;
21530
+ }
21531
+ const written = appendQualityEvents(root, inputs, { actor: common.actor, bundlePath: common.bundlePath });
21532
+ if (written.baseline !== void 0) {
21533
+ console.log(` journal: adopted ${written.baseline.node_ids?.length ?? 0} pre-existing nodes first`);
21534
+ }
21535
+ console.log(` journal: ${written.events.map((e) => e.type).join(", ")} -> ${written.journalPath}`);
21536
+ }
21537
+ var PROFILE_USAGE = `arkaik kritik profile --surface <id>[:<title>[:<platform>]] [...] [--weight <CODE>=<n>] [--force]
21538
+
21539
+ Pick this project's surfaces \u2014 the one decision everything downstream is shaped
21540
+ by. Each criterion's applies_to intersects this list to produce the audit's
21541
+ cells, and the matrix has exactly these columns.
21542
+
21543
+ --surface id[:title[:platform]]. \`platform\` (web|ios|android) is the
21544
+ optional bridge to the product map, for surfaces that ship
21545
+ views. A database contract or a CLI simply has none.
21546
+ --weight CODE=n How hard this product is graded on a domain (default 1).
21547
+ --force Overwrite an existing profile. Changing the surface list
21548
+ invalidates every score recorded against the old one.
21549
+
21550
+ Writes docs/quality/profile.json`;
21551
+ function runProfile(args, common) {
21552
+ const { many, flags } = collect(args, ["surface", "weight"], ["force"]);
21553
+ if (flags.has("help")) {
21554
+ console.log(PROFILE_USAGE);
21555
+ process.exit(0);
21556
+ }
21557
+ if (many.surface.length === 0) fail12(`kritik: at least one --surface is required
21558
+
21559
+ ${PROFILE_USAGE}`);
21560
+ const surfaces = [];
21561
+ const seen = /* @__PURE__ */ new Set();
21562
+ for (const spec of many.surface) {
21563
+ let surface;
21564
+ try {
21565
+ surface = parseSurfaceSpec(spec);
21566
+ } catch (error51) {
21567
+ return fail12(`kritik: ${error51.message}`);
21568
+ }
21569
+ if (seen.has(surface.id)) fail12(`kritik: duplicate surface id "${surface.id}"`);
21570
+ seen.add(surface.id);
21571
+ surfaces.push(surface);
21572
+ }
21573
+ const weights = {};
21574
+ for (const spec of many.weight) {
21575
+ const at2 = spec.indexOf("=");
21576
+ if (at2 <= 0) fail12(`kritik: --weight wants CODE=number (got "${spec}")`);
21577
+ const code = spec.slice(0, at2).trim();
21578
+ const value = Number(spec.slice(at2 + 1));
21579
+ if (!Number.isFinite(value) || value <= 0) fail12(`kritik: weight for "${code}" must be a positive number`);
21580
+ weights[code] = value;
21581
+ }
21582
+ const path6 = profilePath(common.root);
21583
+ if (existsSync16(path6) && !flags.has("force")) {
21584
+ fail12(
21585
+ `kritik: ${path6} already exists.
21586
+ Changing the surface list invalidates every score recorded against the old one, so this refuses by default.
21587
+ Pass --force if that is genuinely what you want.`
21588
+ );
21589
+ }
21590
+ const profile = Object.keys(weights).length > 0 ? { surfaces, domain_weights: weights } : { surfaces };
21591
+ writeJson(path6, profile);
21592
+ console.log(
21593
+ `
21594
+ wrote ${path6}
21595
+ ${surfaces.length} surface${surfaces.length === 1 ? "" : "s"}: ${surfaces.map((s) => s.id).join(", ")}
21596
+ ${Object.keys(weights).length > 0 ? `weights: ${Object.entries(weights).map(([k, v]) => `${k}=${v}`).join(" ")}` : "weights: 1 across every domain"}
21597
+ `
21598
+ );
21599
+ process.exit(0);
21600
+ }
21601
+ function runScore(args, common) {
21602
+ const { single, flags, positionals } = collect(args);
21603
+ if (flags.has("help")) {
21604
+ console.log(SCORE_USAGE);
21605
+ process.exit(0);
21606
+ }
21607
+ const [criterionId, surfaceArg, levelArg] = positionals;
21608
+ if (criterionId === void 0 || surfaceArg === void 0 || levelArg === void 0) {
21609
+ fail12(`kritik: score takes <criterion> <surface> <level>
21610
+
21611
+ ${SCORE_USAGE}`);
21612
+ }
21613
+ const library = loadLibraryOrFail(common.root);
21614
+ const profile = profileOrFail(common.root);
21615
+ const criterion = criterionOrFail(library, criterionId);
21616
+ const surface = surfaceOrFail(profile, surfaceArg);
21617
+ const level = intOrFail("level", levelArg, 0, 4);
21618
+ const appliesTo = criterion.applies_to;
21619
+ if (Array.isArray(appliesTo) && !appliesTo.includes(surface)) {
21620
+ fail12(
21621
+ `kritik: ${criterionId} does not apply to "${surface}" (applies to: ${appliesTo.join(", ")}).
21622
+ Scoring it there would produce a cell nothing rolls up.`
21623
+ );
21624
+ }
21625
+ if (single.evidence === void 0 || single.evidence.trim() === "") {
21626
+ fail12(`kritik: --evidence is required \u2014 a score without a citation is an opinion, not an assessment.
21627
+
21628
+ ${SCORE_USAGE}`);
21629
+ }
21630
+ const evidence = textOrFile(single.evidence);
21631
+ const auditId = auditForWrite(common.root, single.audit);
21632
+ const file2 = loadScoresOrEmpty(common.root, auditId);
21633
+ const assessment = {
21634
+ criterion_id: criterionId,
21635
+ surface,
21636
+ level,
21637
+ evidence,
21638
+ audit_id: auditId,
21639
+ ...single.commit !== void 0 ? { commit: single.commit } : {},
21640
+ ts: (/* @__PURE__ */ new Date()).toISOString()
21641
+ };
21642
+ const { assessments, replaced } = upsertAssessment(file2.assessments, assessment);
21643
+ saveScores(common.root, auditId, {
21644
+ ...file2,
21645
+ audit_id: auditId,
21646
+ framework_version: file2.framework_version ?? library.version,
21647
+ ...single.commit !== void 0 ? { commit: single.commit } : {},
21648
+ assessments
21649
+ });
21650
+ const anchor = criterion.level_anchors?.[`l${level}`];
21651
+ console.log(
21652
+ `
21653
+ ${replaced ? `re-scored ${criterionId} x ${surface}: ${replaced.level} -> ${level}` : `scored ${criterionId} x ${surface} at ${level}`}${anchor ? ` \u2014 ${anchor}` : ""}
21654
+ ${assessments.length} assessment${assessments.length === 1 ? "" : "s"} in ${auditId} -> ${scoresPath(common.root, auditId)}`
21655
+ );
21656
+ if (level < DEFAULT_TARGET_LEVEL) {
21657
+ console.log(
21658
+ ` below the level-${DEFAULT_TARGET_LEVEL} target \u2014 open a finding, or say in the evidence why this surface should not reach it.`
21659
+ );
21660
+ }
21661
+ console.log("");
21662
+ process.exit(0);
21663
+ }
21664
+ function runFinding(args, common) {
21665
+ const [action, ...rest] = args;
21666
+ if (action === void 0 || action === "--help" || action === "-h") {
21667
+ console.log(FINDING_USAGE);
21668
+ process.exit(action === void 0 ? 1 : 0);
21669
+ }
21670
+ if (action === "open") return runFindingOpen(rest, common);
21671
+ if (action === "resolve") return runFindingResolve(rest, common);
21672
+ if (action === "accept") return runFindingAccept(rest, common);
21673
+ fail12(`kritik: unknown finding action "${action}"
21674
+
21675
+ ${FINDING_USAGE}`);
21676
+ }
21677
+ function runFindingOpen(args, common) {
21678
+ const { single, flags, positionals } = collect(args);
21679
+ if (flags.has("help")) {
21680
+ console.log(FINDING_USAGE);
21681
+ process.exit(0);
21682
+ }
21683
+ const [criterionId, surfaceArg] = positionals;
21684
+ if (criterionId === void 0 || surfaceArg === void 0) {
21685
+ fail12(`kritik: finding open takes <criterion> <surface>
21686
+
21687
+ ${FINDING_USAGE}`);
21688
+ }
21689
+ const library = loadLibraryOrFail(common.root);
21690
+ const profile = profileOrFail(common.root);
21691
+ criterionOrFail(library, criterionId);
21692
+ const surface = surfaceOrFail(profile, surfaceArg, { allowCrossSurface: true });
21693
+ if (single.title === void 0 || single.title.trim() === "") {
21694
+ fail12(`kritik: --title is required \u2014 one line naming the actual defect, not the category.
21695
+
21696
+ ${FINDING_USAGE}`);
21697
+ }
21698
+ if (single.evidence === void 0 || single.evidence.trim() === "") {
21699
+ fail12(`kritik: --evidence is required \u2014 file:line citations someone can check.
21700
+
21701
+ ${FINDING_USAGE}`);
21702
+ }
21703
+ const impact = intOrFail("--impact", single.impact, 1, 5);
21704
+ const likelihood = intOrFail("--likelihood", single.likelihood, 1, 5);
21705
+ const cost = single.cost;
21706
+ if (cost === void 0 || !REMEDIATION_COSTS.includes(cost)) {
21707
+ fail12(
21708
+ `kritik: --cost must be one of ${REMEDIATION_COSTS.join(", ")} \u2014 it decides priority, so a High that is cheap to fix outranks a High that is not.`
21709
+ );
21710
+ }
21711
+ const auditId = auditForWrite(common.root, single.audit);
21712
+ const file2 = loadFindings(common.root, auditId);
21713
+ const taken = new Set(file2.findings.map((f) => f.id));
21714
+ const id = single.id ?? mintFindingId(auditId, criterionId, surface, taken, library);
21715
+ if (single.id !== void 0 && taken.has(single.id)) {
21716
+ fail12(`kritik: finding "${single.id}" already exists in ${auditId} \u2014 resolve or accept it rather than reopening the id.`);
21717
+ }
21718
+ const nodes = single.nodes?.split(",").map((n) => n.trim()).filter((n) => n !== "");
21719
+ const finding = {
21720
+ id,
21721
+ criterion_id: criterionId,
21722
+ surface,
21723
+ title: single.title,
21724
+ detail: single.detail ?? single.title,
21725
+ evidence: textOrFile(single.evidence),
21726
+ impact,
21727
+ likelihood,
21728
+ cost,
21729
+ status: "open",
21730
+ ...single.remediation !== void 0 ? { remediation: single.remediation } : {},
21731
+ ...nodes !== void 0 && nodes.length > 0 ? { node_ids: nodes } : {},
21732
+ ...single["issue-url"] !== void 0 ? { issue_url: single["issue-url"] } : {}
21733
+ };
21734
+ const { findings } = upsertFinding(file2.findings, finding);
21735
+ saveFindings(common.root, auditId, {
21736
+ ...file2,
21737
+ audit_id: auditId,
21738
+ framework_version: file2.framework_version ?? library.version,
21739
+ findings
21740
+ });
21741
+ const severity = severityOf(finding, library);
21742
+ const priority = priorityOf(finding, library);
21743
+ console.log(
21744
+ `
21745
+ opened ${id} \u2014 ${finding.title}
21746
+ ${severity} / ${priority} (impact ${impact} x likelihood ${likelihood} = ${impact * likelihood}, cost ${cost}) on ${surface}
21747
+ ${findings.length} finding${findings.length === 1 ? "" : "s"} in ${auditId}`
21748
+ );
21749
+ reportJournal(common.root, [findingOpenedInput(finding, library)], common);
21750
+ if (priority === "P0") {
21751
+ console.log(` P0: file the issue now \u2014 \`arkaik kritik issue ${criterionId} --surface ${surface} --finding ${id}\``);
21752
+ }
21753
+ console.log("");
21754
+ process.exit(0);
21755
+ }
21756
+ function runFindingResolve(args, common) {
21757
+ const { single, flags, positionals } = collect(args);
21758
+ if (flags.has("help")) {
21759
+ console.log(FINDING_USAGE);
21760
+ process.exit(0);
21761
+ }
21762
+ const id = positionals[0];
21763
+ if (id === void 0) fail12(`kritik: finding resolve takes <finding-id>
21764
+
21765
+ ${FINDING_USAGE}`);
21766
+ const located = locateFinding(common.root, id);
21767
+ if (!located) fail12(`kritik: no finding "${id}" in any audit under ${common.root}/docs/quality/audits/`);
21768
+ if (located.finding.status === "resolved") {
21769
+ console.log(`
21770
+ ${id} is already resolved \u2014 nothing written.
21771
+ `);
21772
+ process.exit(0);
21773
+ }
21774
+ const { findings, finding } = resolveFinding(located.file.findings, id, single.by);
21775
+ saveFindings(common.root, located.auditId, { ...located.file, findings });
21776
+ console.log(`
21777
+ resolved ${id} \u2014 ${located.finding.title}` + (single.by ? `
21778
+ by ${single.by}` : ""));
21779
+ if (single.by === void 0) {
21780
+ console.log(` no --by: without the PR or commit that closed it, "resolved" means "we stopped looking".`);
21781
+ }
21782
+ reportJournal(common.root, [findingResolvedInput(finding ?? located.finding, single.by)], common);
21783
+ console.log("");
21784
+ process.exit(0);
21785
+ }
21786
+ function runFindingAccept(args, common) {
21787
+ const { single, flags, positionals } = collect(args);
21788
+ if (flags.has("help")) {
21789
+ console.log(FINDING_USAGE);
21790
+ process.exit(0);
21791
+ }
21792
+ const id = positionals[0];
21793
+ if (id === void 0) fail12(`kritik: finding accept takes <finding-id>
21794
+
21795
+ ${FINDING_USAGE}`);
21796
+ if (single.note === void 0 || single.note.trim() === "") {
21797
+ fail12(`kritik: --note is required \u2014 an accepted risk is a decision and reads like one.`);
21798
+ }
21799
+ const located = locateFinding(common.root, id);
21800
+ if (!located) fail12(`kritik: no finding "${id}" in any audit under ${common.root}/docs/quality/audits/`);
21801
+ const { findings } = acceptFinding(located.file.findings, id, single.note);
21802
+ saveFindings(common.root, located.auditId, { ...located.file, findings });
21803
+ console.log(
21804
+ `
21805
+ accepted ${id} as a known risk \u2014 ${located.finding.title}
21806
+ ${single.note}
21807
+ no journal event: acceptance is a state of the finding, not something that happened to the product.
21808
+ `
21809
+ );
21810
+ process.exit(0);
21811
+ }
21812
+ function runMatrix(args, common) {
21813
+ const { flags, positionals } = collect(args, [], ["json", "record"]);
21814
+ if (flags.has("help")) {
21815
+ console.log(MATRIX_USAGE);
21816
+ process.exit(0);
21817
+ }
21818
+ const library = loadLibraryOrFail(common.root);
21819
+ let auditId;
21820
+ try {
21821
+ auditId = positionals[0] ?? newestAuditId(common.root);
21822
+ } catch (error51) {
21823
+ return fail12(`kritik: ${error51.message}`);
21824
+ }
21825
+ let computed;
21826
+ try {
21827
+ computed = computeAuditMatrix(common.root, auditId, library);
21828
+ } catch (error51) {
21829
+ return fail12(`kritik: ${error51.message}`);
21830
+ }
21831
+ const { section, matrix, file: file2 } = computed;
21832
+ if (flags.has("json")) {
21833
+ console.log(JSON.stringify(file2, null, 2));
21834
+ } else {
21835
+ const domainNames = new Map((library.domains ?? []).map((d) => [d.code, d.name]));
21836
+ console.log(`
21837
+ ${renderMatrixMarkdown(matrix, domainNames)}
21838
+ `);
21839
+ const open = section.findings.filter(isOpenFinding);
21840
+ const lanes = { P0: 0, P1: 0, P2: 0, P3: 0 };
21841
+ for (const finding of open) lanes[priorityOf(finding, library)]++;
21842
+ const counts = matrix.finding_counts;
21843
+ console.log(
21844
+ `${section.assessments.length} assessments \xB7 ${open.length} open findings (${counts.critical} critical, ${counts.high} high, ${counts.medium} medium, ${counts.low} low)
21845
+ lanes: P0 ${lanes.P0} \xB7 P1 ${lanes.P1} \xB7 P2 ${lanes.P2} \xB7 P3 ${lanes.P3}`
21846
+ );
21847
+ const p0 = open.filter((finding) => priorityOf(finding, library) === "P0");
21848
+ if (p0.length > 0) {
21849
+ console.log(`
21850
+ P0 \u2014 fix first:`);
21851
+ for (const finding of p0) {
21852
+ console.log(` [${severityOf(finding, library)}] ${finding.surface} \xB7 ${finding.id} \u2014 ${finding.title}`);
21853
+ }
21854
+ }
21855
+ console.log(`
21856
+ wrote ${matrixPath(common.root, auditId)}`);
21857
+ }
21858
+ if (flags.has("record")) {
21859
+ reportJournal(
21860
+ common.root,
21861
+ [auditCompletedInput(matrix, { audit_id: auditId, framework_version: file2.framework_version, ...file2.commit !== void 0 ? { commit: file2.commit } : {} })],
21862
+ common
21863
+ );
21864
+ }
21865
+ console.log("");
21866
+ process.exit(0);
21867
+ }
21868
+ function tripsSinceLastAudit(root, common) {
21869
+ const journal = resolveJournal(root, common.bundlePath);
21870
+ if (!journal.present) return [];
21871
+ const events = orderEvents(readFullJournalEvents(journal.journalPath));
21872
+ const lastAudit = events.map((event) => event.type).lastIndexOf("quality.audit.completed");
21873
+ return events.slice(lastAudit + 1).filter((event) => event.type === "quality.signal.tripped");
21874
+ }
21875
+ function runSignals(args, common) {
21876
+ const { single, flags } = collect(args, [], ["json"]);
21877
+ if (flags.has("help")) {
21878
+ console.log(SIGNALS_USAGE);
21879
+ process.exit(0);
21880
+ }
21881
+ const library = loadLibraryOrFail(common.root);
21882
+ const profile = profileOrFail(common.root);
21883
+ if (single.trip !== void 0) {
21884
+ const criterion = criterionOrFail(library, single.trip);
21885
+ if (single.surface === void 0) fail12(`kritik: --trip needs --surface`);
21886
+ const surface = surfaceOrFail(profile, single.surface);
21887
+ if (single.signal === void 0) fail12(`kritik: --trip needs --signal (a run-sheet index, or the statement itself)`);
21888
+ const signals = criterion.signals ?? [];
21889
+ const index = Number(single.signal);
21890
+ const signal = Number.isInteger(index) && index >= 0 && index < signals.length ? signals[index] : single.signal;
21891
+ console.log(`
21892
+ tripped ${criterion.id} x ${surface}
21893
+ ${signal}`);
21894
+ reportJournal(
21895
+ common.root,
21896
+ [signalTrippedInput({ criterion_id: criterion.id, surface, signal, ...single.detail !== void 0 ? { detail: single.detail } : {} })],
21897
+ common
21898
+ );
21899
+ console.log(` a tripped signal is not a finding \u2014 it is the prompt to go look.
21900
+ `);
21901
+ process.exit(0);
21902
+ }
21903
+ const filter = {
21904
+ ...single.surface !== void 0 ? { surface: surfaceOrFail(profile, single.surface) } : {},
21905
+ ...single.criterion !== void 0 ? { criterion: criterionOrFail(library, single.criterion).id } : {},
21906
+ ...single.domain !== void 0 ? { domain: single.domain } : {}
21907
+ };
21908
+ const rows = signalRunSheet(library, profile.surfaces ?? [], filter);
21909
+ const trips = tripsSinceLastAudit(common.root, common);
21910
+ if (flags.has("json")) {
21911
+ console.log(JSON.stringify({ signals: rows, tripped_since_last_audit: trips }, null, 2));
21912
+ process.exit(trips.length > 0 ? 1 : 0);
21913
+ }
21914
+ const filtered = Object.keys(filter).length > 0;
21915
+ if (filtered) {
21916
+ console.log("");
21917
+ let current = "";
21918
+ for (const row of rows) {
21919
+ const key = `${row.criterion_id} x ${row.surface}`;
21920
+ if (key !== current) {
21921
+ current = key;
21922
+ console.log(` ${key}`);
21923
+ }
21924
+ console.log(` [${row.index}] ${row.signal}`);
21925
+ }
21926
+ console.log(`
21927
+ ${rows.length} check${rows.length === 1 ? "" : "s"}.`);
21928
+ } else {
21929
+ const criteria = new Set(rows.map((row) => row.criterion_id)).size;
21930
+ console.log(
21931
+ `
21932
+ ${rows.length} checks across ${criteria} criteria and ${(profile.surfaces ?? []).length} surfaces.
21933
+ Narrow it (--surface, --criterion, --domain) to read them, or --json to take the lot.`
21934
+ );
21935
+ }
21936
+ if (trips.length > 0) {
21937
+ console.log(`
21938
+ tripped since the last recorded audit:`);
21939
+ for (const trip of trips) {
21940
+ const event = trip;
21941
+ console.log(` ${event.criterion_id} x ${event.surface} \u2014 ${event.signal}`);
21942
+ }
21943
+ console.log("");
21944
+ process.exit(1);
21945
+ }
21946
+ console.log("");
21947
+ process.exit(0);
21948
+ }
21949
+ function runIssue(args, common) {
21950
+ const { single, flags, positionals } = collect(args);
21951
+ if (flags.has("help")) {
21952
+ console.log(ISSUE_USAGE);
21953
+ process.exit(0);
21954
+ }
21955
+ const criterionId = positionals[0];
21956
+ if (criterionId === void 0) fail12(`kritik: issue takes <criterion>
21957
+
21958
+ ${ISSUE_USAGE}`);
21959
+ if (single.surface === void 0) fail12(`kritik: --surface is required
21960
+
21961
+ ${ISSUE_USAGE}`);
21962
+ const library = loadLibraryOrFail(common.root);
21963
+ const criterion = criterionOrFail(library, criterionId);
21964
+ const declared = loadProfile(common.root);
21965
+ if (declared) surfaceOrFail(declared, single.surface, { allowCrossSurface: true });
21966
+ let finding;
21967
+ if (single.finding !== void 0) {
21968
+ const located = locateFinding(common.root, single.finding);
21969
+ if (!located) fail12(`kritik: no finding "${single.finding}" in any audit`);
21970
+ finding = located.finding;
21971
+ }
21972
+ const rendered = renderIssue(criterion, {
21973
+ surface: single.surface,
21974
+ ...single.level !== void 0 ? { level: single.level } : {},
21975
+ ...finding !== void 0 ? { finding, library } : {}
21976
+ });
21977
+ process.stdout.write(`Title: ${rendered.title}
21978
+ `);
21979
+ process.stdout.write(`Labels: ${rendered.labels.join(", ")}
21980
+
21981
+ `);
21982
+ process.stdout.write(`${rendered.body}
21983
+ `);
21984
+ if (rendered.remediation) process.stdout.write(`
21985
+ <!-- Typical remediation: ${rendered.remediation} -->
21986
+ `);
21987
+ process.exit(0);
21988
+ }
21989
+ function runCriterion(args, common) {
21990
+ const [action, ...rest] = args;
21991
+ if (action === void 0 || action === "--help" || action === "-h") {
21992
+ console.log(CRITERION_USAGE);
21993
+ process.exit(action === void 0 ? 1 : 0);
21994
+ }
21995
+ if (action !== "add") fail12(`kritik: unknown criterion action "${action}" (only \`add\`)
21996
+
21997
+ ${CRITERION_USAGE}`);
21998
+ const { single, many, flags } = collect(rest, ["anchor", "signal", "check", "label"], ["template", "force"]);
21999
+ if (flags.has("help")) {
22000
+ console.log(CRITERION_USAGE);
22001
+ process.exit(0);
22002
+ }
22003
+ const { library: pack } = (() => {
22004
+ try {
22005
+ const loaded = loadKritikLibrary(common.root);
22006
+ return { library: loaded.pack.library };
22007
+ } catch (error51) {
22008
+ return fail12(`kritik: ${error51.message}`);
22009
+ }
22010
+ })();
22011
+ if (flags.has("template")) {
22012
+ console.log(JSON.stringify(CRITERION_TEMPLATE, null, 2));
22013
+ process.exit(0);
22014
+ }
22015
+ let criterion;
22016
+ try {
22017
+ if (single.from !== void 0) {
22018
+ if (!existsSync16(single.from)) fail12(`kritik: no file at ${single.from}`);
22019
+ criterion = JSON.parse(readFileSync15(single.from, "utf8"));
22020
+ } else {
22021
+ const anchors = {};
22022
+ for (const spec of many.anchor) {
22023
+ const at2 = spec.indexOf("=");
22024
+ if (at2 <= 0) fail12(`kritik: --anchor wants lN=<text> (got "${spec}")`);
22025
+ anchors[spec.slice(0, at2).trim()] = spec.slice(at2 + 1);
22026
+ }
22027
+ const draft = {
22028
+ id: single.id ?? "",
22029
+ domain: single.domain ?? "",
22030
+ ...single.subcategory !== void 0 ? { subcategory: single.subcategory } : {},
22031
+ name: single.name ?? "",
22032
+ question: single.question ?? "",
22033
+ ...single.definition !== void 0 ? { definition: single.definition } : {},
22034
+ ...single.rationale !== void 0 ? { rationale: single.rationale } : {},
22035
+ appliesTo: (single["applies-to"] ?? "").split(",").map((s) => s.trim()).filter(Boolean),
22036
+ anchors,
22037
+ ...single.weight !== void 0 ? { weight: Number(single.weight) } : {},
22038
+ ...single.impact !== void 0 ? { impact: Number(single.impact) } : {},
22039
+ signals: many.signal,
22040
+ checklist: many.check,
22041
+ ...single.remediation !== void 0 ? { remediation: single.remediation } : {},
22042
+ labels: many.label
22043
+ };
22044
+ criterion = buildCriterion(draft);
22045
+ }
22046
+ } catch (error51) {
22047
+ return fail12(`kritik: ${error51.message}
22048
+
22049
+ ${CRITERION_USAGE}`);
22050
+ }
22051
+ let written;
22052
+ try {
22053
+ written = addCriterionToOverlay(common.root, pack, criterion, {
22054
+ force: flags.has("force"),
22055
+ ...single["domain-name"] !== void 0 ? { domainName: single["domain-name"] } : {}
22056
+ });
22057
+ } catch (error51) {
22058
+ return fail12(`kritik: ${error51.message}`);
22059
+ }
22060
+ console.log(
22061
+ `
22062
+ ${written.replaced ? "replaced" : "added"} ${criterion.id} (${criterion.domain}) -> ${written.path}
22063
+ applies to ${(criterion.applies_to ?? []).join(", ") || "every surface"}` + (written.addedDomain !== void 0 ? `
22064
+ declared a new domain: ${written.addedDomain}` : "") + `
22065
+ it scores and rolls up exactly like a pack criterion \u2014 \`arkaik kritik issue ${criterion.id} --surface <s>\`
22066
+ `
22067
+ );
22068
+ process.exit(0);
22069
+ }
22070
+ function runKritik(args) {
22071
+ const { rest, common } = takeCommon(args);
22072
+ const [sub, ...subArgs] = rest;
22073
+ if (sub === void 0 || sub === "--help" || sub === "-h" || sub === "help") {
22074
+ console.log(USAGE13);
22075
+ process.exit(sub === void 0 ? 1 : 0);
22076
+ }
22077
+ switch (sub) {
22078
+ case "profile":
22079
+ return runProfile(subArgs, common);
22080
+ case "score":
22081
+ return runScore(subArgs, common);
22082
+ case "finding":
22083
+ return runFinding(subArgs, common);
22084
+ case "matrix":
22085
+ return runMatrix(subArgs, common);
22086
+ case "signals":
22087
+ return runSignals(subArgs, common);
22088
+ case "issue":
22089
+ return runIssue(subArgs, common);
22090
+ case "criterion":
22091
+ return runCriterion(subArgs, common);
22092
+ default:
22093
+ fail12(`kritik: unknown subcommand "${sub}"
22094
+
22095
+ ${USAGE13}`);
22096
+ }
22097
+ }
22098
+
20026
22099
  // src/index.ts
20027
- var USAGE13 = `arkaik \u2014 CLI for Arkaik project bundles
22100
+ var USAGE14 = `arkaik \u2014 CLI for Arkaik project bundles
20028
22101
 
20029
22102
  Usage:
20030
22103
  arkaik <command> [options]
@@ -20044,17 +22117,18 @@ Commands:
20044
22117
  --list shows the projects your token can reach.
20045
22118
  restore [options] [path] Replace the linked hosted project's bundle + journal (backs up first).
20046
22119
  bootstrap <sub> [options] One-time onboarding: mine, plan, slice, merge a map from a repo.
22120
+ kritik <sub> [options] Quality audits: score criteria, open findings, roll up the matrix.
20047
22121
 
20048
22122
  Options:
20049
22123
  -h, --help Show this help.
20050
22124
  -v, --version Print the version.
20051
22125
 
20052
22126
  Run "arkaik <command> --help" for command-specific help.`;
20053
- var VERSION = "0.1.1";
22127
+ var VERSION = "0.2.0";
20054
22128
  function main(argv) {
20055
22129
  const [command, ...rest] = argv;
20056
22130
  if (command === void 0 || command === "--help" || command === "-h" || command === "help") {
20057
- console.log(USAGE13);
22131
+ console.log(USAGE14);
20058
22132
  process.exit(0);
20059
22133
  }
20060
22134
  if (command === "--version" || command === "-v" || command === "version") {
@@ -20098,10 +22172,13 @@ function main(argv) {
20098
22172
  case "bootstrap":
20099
22173
  runBootstrap(rest);
20100
22174
  return;
22175
+ case "kritik":
22176
+ runKritik(rest);
22177
+ return;
20101
22178
  default:
20102
22179
  console.error(`Unknown command: ${command}
20103
22180
  `);
20104
- console.error(USAGE13);
22181
+ console.error(USAGE14);
20105
22182
  process.exit(1);
20106
22183
  }
20107
22184
  }