arkaik 0.1.2 → 0.3.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/LICENSE +21 -0
- package/dist/assets/kritik/library.json +5854 -0
- package/dist/assets/skill/references/schema.md +10 -1
- package/dist/assets/skill/scripts/validate-bundle.js +4 -4
- package/dist/index.js +2755 -209
- package/dist/io.js +686 -52
- package/package.json +3 -2
- package/src/io.ts +18 -1
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,292 @@ 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
|
+
|
|
17213
|
+
// ../schema/src/quality-regressions.ts
|
|
17214
|
+
var REGRESSION_SIGNALS = {
|
|
17215
|
+
"level-drop": "maturity on this cell does not regress",
|
|
17216
|
+
"new-severe-finding": "no open Critical or High finding on this cell",
|
|
17217
|
+
"reopened-finding": "a resolved finding stays resolved"
|
|
17218
|
+
};
|
|
17219
|
+
var cellKey = (criterionId, surface) => `${criterionId}::${surface}`;
|
|
17220
|
+
var rowsOf = (value) => Array.isArray(value) ? value : [];
|
|
17221
|
+
function assessmentsByCell(state) {
|
|
17222
|
+
const cells = /* @__PURE__ */ new Map();
|
|
17223
|
+
for (const assessment of rowsOf(state?.assessments)) {
|
|
17224
|
+
if (typeof assessment?.criterion_id !== "string" || typeof assessment?.surface !== "string") continue;
|
|
17225
|
+
cells.set(cellKey(assessment.criterion_id, assessment.surface), assessment);
|
|
17226
|
+
}
|
|
17227
|
+
return cells;
|
|
17228
|
+
}
|
|
17229
|
+
function severeByCell(state, library) {
|
|
17230
|
+
const cells = /* @__PURE__ */ new Map();
|
|
17231
|
+
for (const finding of rowsOf(state?.findings)) {
|
|
17232
|
+
if (typeof finding?.criterion_id !== "string" || typeof finding?.surface !== "string") continue;
|
|
17233
|
+
if (!isOpenFinding(finding)) continue;
|
|
17234
|
+
const severity = severityOf(finding, library);
|
|
17235
|
+
if (severity !== "critical" && severity !== "high") continue;
|
|
17236
|
+
const key = cellKey(finding.criterion_id, finding.surface);
|
|
17237
|
+
const existing = cells.get(key);
|
|
17238
|
+
if (existing === void 0) cells.set(key, [finding]);
|
|
17239
|
+
else existing.push(finding);
|
|
17240
|
+
}
|
|
17241
|
+
return cells;
|
|
17242
|
+
}
|
|
17243
|
+
function detectRegressions(previous, next, library) {
|
|
17244
|
+
const before = assessmentsByCell(previous);
|
|
17245
|
+
const after = assessmentsByCell(next);
|
|
17246
|
+
const comparable = (key) => before.has(key) && after.has(key);
|
|
17247
|
+
const regressions = [];
|
|
17248
|
+
for (const [key, current] of after) {
|
|
17249
|
+
const earlier = before.get(key);
|
|
17250
|
+
if (earlier === void 0) continue;
|
|
17251
|
+
if (!(Number(current.level) < Number(earlier.level))) continue;
|
|
17252
|
+
regressions.push({
|
|
17253
|
+
kind: "level-drop",
|
|
17254
|
+
criterion_id: current.criterion_id,
|
|
17255
|
+
surface: current.surface,
|
|
17256
|
+
signal: REGRESSION_SIGNALS["level-drop"],
|
|
17257
|
+
detail: `level ${earlier.level} \u2192 ${current.level} (${earlier.audit_id} \u2192 ${current.audit_id})`
|
|
17258
|
+
});
|
|
17259
|
+
}
|
|
17260
|
+
const severeBefore = severeByCell(previous, library);
|
|
17261
|
+
for (const [key, findings] of severeByCell(next, library)) {
|
|
17262
|
+
if (!comparable(key)) continue;
|
|
17263
|
+
if ((severeBefore.get(key) ?? []).length > 0) continue;
|
|
17264
|
+
for (const finding of findings) {
|
|
17265
|
+
regressions.push({
|
|
17266
|
+
kind: "new-severe-finding",
|
|
17267
|
+
criterion_id: finding.criterion_id,
|
|
17268
|
+
surface: finding.surface,
|
|
17269
|
+
signal: REGRESSION_SIGNALS["new-severe-finding"],
|
|
17270
|
+
detail: `${finding.id} \u2014 ${severityOf(finding, library)} (impact ${finding.impact} \xD7 likelihood ${finding.likelihood})`
|
|
17271
|
+
});
|
|
17272
|
+
}
|
|
17273
|
+
}
|
|
17274
|
+
const resolvedBefore = new Set(
|
|
17275
|
+
rowsOf(previous?.findings).filter((finding) => finding?.status === "resolved").map((finding) => finding.id)
|
|
17276
|
+
);
|
|
17277
|
+
for (const finding of rowsOf(next?.findings)) {
|
|
17278
|
+
if (!isOpenFinding(finding) || !resolvedBefore.has(finding.id)) continue;
|
|
17279
|
+
regressions.push({
|
|
17280
|
+
kind: "reopened-finding",
|
|
17281
|
+
criterion_id: finding.criterion_id,
|
|
17282
|
+
surface: finding.surface,
|
|
17283
|
+
signal: REGRESSION_SIGNALS["reopened-finding"],
|
|
17284
|
+
detail: `${finding.id} \u2014 "${finding.title}"`
|
|
17285
|
+
});
|
|
17286
|
+
}
|
|
17287
|
+
return regressions;
|
|
17288
|
+
}
|
|
17289
|
+
|
|
16382
17290
|
// src/commands/init.ts
|
|
16383
17291
|
var DEFAULT_BUNDLE_PATH = "docs/arkaik/bundle.json";
|
|
16384
17292
|
var DEFAULT_JOURNAL_PATH = "docs/arkaik/journal.jsonl";
|
|
@@ -16672,7 +17580,7 @@ function runInit(args) {
|
|
|
16672
17580
|
}
|
|
16673
17581
|
|
|
16674
17582
|
// src/commands/validate.ts
|
|
16675
|
-
import { readFileSync as
|
|
17583
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
16676
17584
|
|
|
16677
17585
|
// src/lib/bundle-io.ts
|
|
16678
17586
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
@@ -16703,9 +17611,80 @@ function nodesByIdOf(bundle) {
|
|
|
16703
17611
|
}
|
|
16704
17612
|
|
|
16705
17613
|
// src/lib/bundle-validate.ts
|
|
16706
|
-
import { existsSync as
|
|
17614
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
17615
|
+
import { basename as basename2 } from "node:path";
|
|
17616
|
+
|
|
17617
|
+
// src/lib/journal-io.ts
|
|
17618
|
+
import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
16707
17619
|
import { dirname as dirname2, join as join2 } from "node:path";
|
|
16708
17620
|
var JOURNAL_SIDECAR = "journal.jsonl";
|
|
17621
|
+
function journalPathFor(bundlePath) {
|
|
17622
|
+
return join2(dirname2(bundlePath), JOURNAL_SIDECAR);
|
|
17623
|
+
}
|
|
17624
|
+
function archivePathFor(journalPath, version2) {
|
|
17625
|
+
return join2(dirname2(journalPath), "journal", `archive-${version2}.jsonl`);
|
|
17626
|
+
}
|
|
17627
|
+
function archivePathsFor(journalPath) {
|
|
17628
|
+
const dir = join2(dirname2(journalPath), "journal");
|
|
17629
|
+
if (!existsSync3(dir)) return [];
|
|
17630
|
+
return readdirSync(dir).filter((name) => name.startsWith("archive-") && name.endsWith(".jsonl")).sort().map((name) => join2(dir, name));
|
|
17631
|
+
}
|
|
17632
|
+
function readJournalEvents(journalPath) {
|
|
17633
|
+
if (!existsSync3(journalPath)) return [];
|
|
17634
|
+
return parseJournalLines(readFileSync3(journalPath, "utf8")).events;
|
|
17635
|
+
}
|
|
17636
|
+
function readFullJournalEvents(journalPath) {
|
|
17637
|
+
const events = readJournalEvents(journalPath);
|
|
17638
|
+
for (const archivePath of archivePathsFor(journalPath)) {
|
|
17639
|
+
events.push(...readJournalEvents(archivePath));
|
|
17640
|
+
}
|
|
17641
|
+
return events;
|
|
17642
|
+
}
|
|
17643
|
+
function loadJournalEvents(bundle, bundlePath) {
|
|
17644
|
+
if (Array.isArray(bundle.journal)) return bundle.journal;
|
|
17645
|
+
return readJournalEvents(journalPathFor(bundlePath));
|
|
17646
|
+
}
|
|
17647
|
+
function toLine(event) {
|
|
17648
|
+
return JSON.stringify(event) + "\n";
|
|
17649
|
+
}
|
|
17650
|
+
function appendJournalEvent(journalPath, event) {
|
|
17651
|
+
const line2 = toLine(event);
|
|
17652
|
+
if (!existsSync3(journalPath)) {
|
|
17653
|
+
mkdirSync2(dirname2(journalPath), { recursive: true });
|
|
17654
|
+
writeFileSync2(journalPath, line2);
|
|
17655
|
+
return;
|
|
17656
|
+
}
|
|
17657
|
+
const existing = readFileSync3(journalPath, "utf8");
|
|
17658
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
17659
|
+
appendFileSync(journalPath, prefix + line2);
|
|
17660
|
+
}
|
|
17661
|
+
function ensureJournalBaseline(journalPath, bundle, actor) {
|
|
17662
|
+
const snapshotNodeIds = (Array.isArray(bundle.nodes) ? bundle.nodes : []).map((node) => node?.id).filter((id) => typeof id === "string");
|
|
17663
|
+
const missing = missingProvenanceNodeIds(snapshotNodeIds, readFullJournalEvents(journalPath));
|
|
17664
|
+
if (missing.length === 0) return void 0;
|
|
17665
|
+
const event = makeEvent("journal.baseline", { node_ids: missing }, { actor });
|
|
17666
|
+
appendJournalEvent(journalPath, event);
|
|
17667
|
+
return event;
|
|
17668
|
+
}
|
|
17669
|
+
function compactSlice(journalPath, slice, version2) {
|
|
17670
|
+
if (slice.length === 0) return;
|
|
17671
|
+
const sliceIds = new Set(slice.map((ev) => ev.id));
|
|
17672
|
+
const all = readJournalEvents(journalPath);
|
|
17673
|
+
const surviving = all.filter((ev) => !sliceIds.has(ev.id));
|
|
17674
|
+
const archivePath = archivePathFor(journalPath, version2);
|
|
17675
|
+
mkdirSync2(dirname2(archivePath), { recursive: true });
|
|
17676
|
+
const archiveLines = slice.map(toLine).join("");
|
|
17677
|
+
if (existsSync3(archivePath)) {
|
|
17678
|
+
const existing = readFileSync3(archivePath, "utf8");
|
|
17679
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
17680
|
+
appendFileSync(archivePath, prefix + archiveLines);
|
|
17681
|
+
} else {
|
|
17682
|
+
writeFileSync2(archivePath, archiveLines);
|
|
17683
|
+
}
|
|
17684
|
+
writeFileSync2(journalPath, surviving.map(toLine).join(""));
|
|
17685
|
+
}
|
|
17686
|
+
|
|
17687
|
+
// src/lib/bundle-validate.ts
|
|
16709
17688
|
function validateBundleAt(filePath) {
|
|
16710
17689
|
const bundle = readBundle(filePath);
|
|
16711
17690
|
const loose = bundle;
|
|
@@ -16714,21 +17693,49 @@ function validateBundleAt(filePath) {
|
|
|
16714
17693
|
}
|
|
16715
17694
|
let sidecarFindings = [];
|
|
16716
17695
|
let sidecarLoaded = false;
|
|
17696
|
+
const archiveFindings = [];
|
|
17697
|
+
const archivesLoaded = [];
|
|
16717
17698
|
if (loose.journal === void 0) {
|
|
16718
|
-
const sidecarPath =
|
|
16719
|
-
|
|
16720
|
-
|
|
17699
|
+
const sidecarPath = journalPathFor(filePath);
|
|
17700
|
+
const folded = [];
|
|
17701
|
+
if (existsSync4(sidecarPath)) {
|
|
17702
|
+
const { events, findings } = parseJournalLines(readFileSync4(sidecarPath, "utf8"));
|
|
16721
17703
|
sidecarFindings = findings;
|
|
16722
17704
|
sidecarLoaded = true;
|
|
16723
|
-
|
|
17705
|
+
folded.push(...events);
|
|
16724
17706
|
}
|
|
17707
|
+
for (const archivePath of archivePathsFor(sidecarPath)) {
|
|
17708
|
+
const { events, findings } = parseJournalLines(readFileSync4(archivePath, "utf8"));
|
|
17709
|
+
const file2 = basename2(archivePath);
|
|
17710
|
+
for (const finding of findings) archiveFindings.push({ ...finding, file: file2 });
|
|
17711
|
+
archivesLoaded.push(file2);
|
|
17712
|
+
folded.push(...events);
|
|
17713
|
+
}
|
|
17714
|
+
if (sidecarLoaded || archivesLoaded.length > 0) loose.journal = folded;
|
|
16725
17715
|
}
|
|
16726
17716
|
const nodes = Array.isArray(loose.nodes) ? loose.nodes : [];
|
|
16727
17717
|
const edges = Array.isArray(loose.edges) ? loose.edges : [];
|
|
16728
17718
|
const journal = Array.isArray(loose.journal) ? loose.journal : [];
|
|
16729
17719
|
const result = validateBundle(bundle);
|
|
16730
|
-
const valid = sidecarFindings.length === 0 && result.errors.length === 0;
|
|
16731
|
-
return {
|
|
17720
|
+
const valid = sidecarFindings.length === 0 && archiveFindings.length === 0 && result.errors.length === 0;
|
|
17721
|
+
return {
|
|
17722
|
+
bundle,
|
|
17723
|
+
nodes,
|
|
17724
|
+
edges,
|
|
17725
|
+
journal,
|
|
17726
|
+
sidecarLoaded,
|
|
17727
|
+
sidecarFindings,
|
|
17728
|
+
archivesLoaded,
|
|
17729
|
+
archiveFindings,
|
|
17730
|
+
result,
|
|
17731
|
+
valid
|
|
17732
|
+
};
|
|
17733
|
+
}
|
|
17734
|
+
function journalLineErrorLines(v) {
|
|
17735
|
+
return [
|
|
17736
|
+
...v.sidecarFindings.map((f) => `ERROR [${f.rule}] line ${f.line}: ${f.message}`),
|
|
17737
|
+
...v.archiveFindings.map((f) => `ERROR [${f.rule}] ${f.file} line ${f.line}: ${f.message}`)
|
|
17738
|
+
];
|
|
16732
17739
|
}
|
|
16733
17740
|
|
|
16734
17741
|
// src/commands/validate.ts
|
|
@@ -16765,12 +17772,12 @@ function fixFormat(filePath) {
|
|
|
16765
17772
|
} catch (e) {
|
|
16766
17773
|
return fail2(`FATAL: ${e.message}`);
|
|
16767
17774
|
}
|
|
16768
|
-
const before =
|
|
17775
|
+
const before = readFileSync5(filePath, "utf8");
|
|
16769
17776
|
const after = serializeBundle(bundle);
|
|
16770
17777
|
if (after === before) {
|
|
16771
17778
|
console.log(`Already canonical: ${filePath}`);
|
|
16772
17779
|
} else {
|
|
16773
|
-
|
|
17780
|
+
writeFileSync3(filePath, after);
|
|
16774
17781
|
console.log(`Reformatted: ${filePath}`);
|
|
16775
17782
|
}
|
|
16776
17783
|
process.exit(0);
|
|
@@ -16789,8 +17796,12 @@ function validate(filePath) {
|
|
|
16789
17796
|
` 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
17797
|
);
|
|
16791
17798
|
console.log(` Edges: ${v.edges.length}`);
|
|
16792
|
-
if (v.sidecarLoaded) {
|
|
16793
|
-
|
|
17799
|
+
if (v.sidecarLoaded || v.archivesLoaded.length > 0) {
|
|
17800
|
+
const from = [
|
|
17801
|
+
...v.sidecarLoaded ? [`${JOURNAL_SIDECAR} sidecar`] : [],
|
|
17802
|
+
...v.archivesLoaded.length > 0 ? [`${v.archivesLoaded.length} archive(s)`] : []
|
|
17803
|
+
].join(" + ");
|
|
17804
|
+
console.log(` Journal: ${v.journal.length} event(s) from ${from}`);
|
|
16794
17805
|
} else if (v.journal.length > 0) {
|
|
16795
17806
|
console.log(` Journal: ${v.journal.length} embedded event(s)`);
|
|
16796
17807
|
}
|
|
@@ -16800,10 +17811,7 @@ function validate(filePath) {
|
|
|
16800
17811
|
v.result.warnings.forEach((w) => console.log(` ${formatFinding(w)}`));
|
|
16801
17812
|
console.log("");
|
|
16802
17813
|
}
|
|
16803
|
-
const errorLines = [
|
|
16804
|
-
...v.sidecarFindings.map((f) => `ERROR [${f.rule}] line ${f.line}: ${f.message}`),
|
|
16805
|
-
...v.result.errors.map(formatFinding)
|
|
16806
|
-
];
|
|
17814
|
+
const errorLines = [...journalLineErrorLines(v), ...v.result.errors.map(formatFinding)];
|
|
16807
17815
|
if (errorLines.length === 0) {
|
|
16808
17816
|
console.log(" Result: VALID\n");
|
|
16809
17817
|
process.exit(0);
|
|
@@ -16843,59 +17851,9 @@ ${USAGE2}`);
|
|
|
16843
17851
|
}
|
|
16844
17852
|
}
|
|
16845
17853
|
|
|
16846
|
-
// src/lib/
|
|
16847
|
-
|
|
16848
|
-
|
|
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));
|
|
16863
|
-
}
|
|
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;
|
|
17854
|
+
// src/lib/render-event.ts
|
|
17855
|
+
function str(value) {
|
|
17856
|
+
return typeof value === "string" ? value : void 0;
|
|
16899
17857
|
}
|
|
16900
17858
|
function title(id, nodesById) {
|
|
16901
17859
|
const nodeId = str(id);
|
|
@@ -16957,6 +17915,10 @@ function renderEventLine(event, nodesById) {
|
|
|
16957
17915
|
const to = str(event.to) ?? "?";
|
|
16958
17916
|
return `${title(event.node_id, nodesById)}: reference ${from ? `${from} -> ${to}` : to}`;
|
|
16959
17917
|
}
|
|
17918
|
+
case "journal.baseline": {
|
|
17919
|
+
const count = Array.isArray(event.node_ids) ? event.node_ids.length : 0;
|
|
17920
|
+
return `Journal baseline: ${count} pre-existing node(s) recorded`;
|
|
17921
|
+
}
|
|
16960
17922
|
default:
|
|
16961
17923
|
return event.type;
|
|
16962
17924
|
}
|
|
@@ -17160,7 +18122,12 @@ ${USAGE4}`);
|
|
|
17160
18122
|
} catch (e) {
|
|
17161
18123
|
fail4(`FATAL: could not build release event \u2014 ${e.message}`);
|
|
17162
18124
|
}
|
|
18125
|
+
const baseline = ensureJournalBaseline(journalPath, bundle, ACTOR);
|
|
17163
18126
|
appendJournalEvent(journalPath, event);
|
|
18127
|
+
if (baseline !== void 0) {
|
|
18128
|
+
console.log(`
|
|
18129
|
+
${renderEventLine(baseline)} -> ${journalPath}`);
|
|
18130
|
+
}
|
|
17164
18131
|
console.log(`
|
|
17165
18132
|
Tagged release ${version2}${platform ? ` [${platform}]` : ""} -> ${journalPath}`);
|
|
17166
18133
|
const all = [...existing, event];
|
|
@@ -17317,7 +18284,12 @@ ${USAGE5}`);
|
|
|
17317
18284
|
fail5(`FATAL: could not build deliverable event \u2014 ${e.message}`);
|
|
17318
18285
|
}
|
|
17319
18286
|
const journalPath = journalPathFor(filePath);
|
|
18287
|
+
const baseline = ensureJournalBaseline(journalPath, bundle, ACTOR2);
|
|
17320
18288
|
appendJournalEvent(journalPath, event);
|
|
18289
|
+
if (baseline !== void 0) {
|
|
18290
|
+
console.log(`
|
|
18291
|
+
${renderEventLine(baseline)} -> ${journalPath}`);
|
|
18292
|
+
}
|
|
17321
18293
|
console.log(
|
|
17322
18294
|
`
|
|
17323
18295
|
Recorded deliverable ${deliverableId} \u2014 ${title2} -> ${journalPath}
|
|
@@ -17504,6 +18476,13 @@ async function runSync(options = {}) {
|
|
|
17504
18476
|
const errors = [];
|
|
17505
18477
|
const nodeTitles = /* @__PURE__ */ new Map();
|
|
17506
18478
|
let dirty = false;
|
|
18479
|
+
let baseline;
|
|
18480
|
+
let baselineChecked = false;
|
|
18481
|
+
const adoptJournal = () => {
|
|
18482
|
+
if (baselineChecked) return;
|
|
18483
|
+
baselineChecked = true;
|
|
18484
|
+
baseline = ensureJournalBaseline(journalPath, bundle, actor);
|
|
18485
|
+
};
|
|
17507
18486
|
for (const node of nodes) {
|
|
17508
18487
|
const nodeId = typeof node.id === "string" ? node.id : void 0;
|
|
17509
18488
|
if (nodeId !== void 0 && typeof node.title === "string") nodeTitles.set(nodeId, node.title);
|
|
@@ -17556,6 +18535,7 @@ async function runSync(options = {}) {
|
|
|
17556
18535
|
},
|
|
17557
18536
|
{ actor, ts: syncedAt }
|
|
17558
18537
|
);
|
|
18538
|
+
adoptJournal();
|
|
17559
18539
|
appendJournalEvent(journalPath, event);
|
|
17560
18540
|
}
|
|
17561
18541
|
}
|
|
@@ -17570,6 +18550,7 @@ async function runSync(options = {}) {
|
|
|
17570
18550
|
const patch = promotionPatch(node, promotion);
|
|
17571
18551
|
Object.assign(node, patch);
|
|
17572
18552
|
dirty = true;
|
|
18553
|
+
adoptJournal();
|
|
17573
18554
|
appendJournalEvent(
|
|
17574
18555
|
journalPath,
|
|
17575
18556
|
makeEvent(
|
|
@@ -17588,7 +18569,7 @@ async function runSync(options = {}) {
|
|
|
17588
18569
|
if (dirty && !dryRun) {
|
|
17589
18570
|
writeFileSync4(filePath, serializeBundle(bundle));
|
|
17590
18571
|
}
|
|
17591
|
-
return { ok: true, bundlePath: filePath, journalPath, dryRun, changed, unchanged, skipped, errors, nodeTitles, promoted };
|
|
18572
|
+
return { ok: true, bundlePath: filePath, journalPath, dryRun, changed, unchanged, skipped, errors, nodeTitles, promoted, baseline };
|
|
17592
18573
|
}
|
|
17593
18574
|
function changeLine(change, nodeTitles) {
|
|
17594
18575
|
const event = {
|
|
@@ -17626,6 +18607,9 @@ function report(result) {
|
|
|
17626
18607
|
for (const s of stubSkips) byProvider.set(s.provider ?? "?", (byProvider.get(s.provider ?? "?") ?? 0) + 1);
|
|
17627
18608
|
for (const [provider, count] of byProvider) console.log(` - ${provider}: ${count} ref(s)`);
|
|
17628
18609
|
}
|
|
18610
|
+
if (result.baseline !== void 0) {
|
|
18611
|
+
console.log(` ${renderEventLine(result.baseline)}`);
|
|
18612
|
+
}
|
|
17629
18613
|
if (result.errors.length > 0) {
|
|
17630
18614
|
console.log(` Errors: ${result.errors.length}`);
|
|
17631
18615
|
result.errors.forEach((e) => console.log(` - ${e.nodeId}/${e.refId} (${e.refType}): ${e.message}`));
|
|
@@ -17669,18 +18653,288 @@ ${USAGE6}`);
|
|
|
17669
18653
|
}
|
|
17670
18654
|
|
|
17671
18655
|
// src/commands/pack.ts
|
|
18656
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
|
|
18657
|
+
import { dirname as dirname5, extname, resolve as resolve5 } from "node:path";
|
|
18658
|
+
|
|
18659
|
+
// src/lib/kritik-io.ts
|
|
18660
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
18661
|
+
import { basename as basename3, dirname as dirname4, join as join5, resolve as resolve4 } from "node:path";
|
|
18662
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
18663
|
+
|
|
18664
|
+
// ../schema/src/cli/kritik-audit.ts
|
|
18665
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, statSync } from "node:fs";
|
|
18666
|
+
import { join as join4 } from "node:path";
|
|
18667
|
+
|
|
18668
|
+
// ../schema/src/cli/kritik-paths.ts
|
|
17672
18669
|
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
17673
|
-
import { dirname as
|
|
17674
|
-
var
|
|
17675
|
-
var
|
|
18670
|
+
import { dirname as dirname3, join as join3, resolve as resolve3 } from "node:path";
|
|
18671
|
+
var QUALITY_DIR = "docs/quality";
|
|
18672
|
+
var PROFILE_FILE = "profile.json";
|
|
18673
|
+
var OVERLAY_FILE = "criteria.custom.json";
|
|
18674
|
+
var AUDITS_DIR = "audits";
|
|
18675
|
+
var PACK_FILE = "library.json";
|
|
18676
|
+
function readJson(path6) {
|
|
18677
|
+
const text = readFileSync6(path6, "utf8");
|
|
18678
|
+
try {
|
|
18679
|
+
return JSON.parse(text);
|
|
18680
|
+
} catch (e) {
|
|
18681
|
+
throw new Error(`${path6}: not valid JSON \u2014 ${e.message}`);
|
|
18682
|
+
}
|
|
18683
|
+
}
|
|
18684
|
+
function writeJson(path6, value) {
|
|
18685
|
+
mkdirSync3(dirname3(path6), { recursive: true });
|
|
18686
|
+
writeFileSync5(path6, JSON.stringify(value, null, 2) + "\n");
|
|
18687
|
+
}
|
|
18688
|
+
var profilePath = (root) => join3(root, QUALITY_DIR, PROFILE_FILE);
|
|
18689
|
+
var overlayPath = (root) => join3(root, QUALITY_DIR, OVERLAY_FILE);
|
|
18690
|
+
var auditsDir = (root) => join3(root, QUALITY_DIR, AUDITS_DIR);
|
|
18691
|
+
var auditDir = (root, auditId) => join3(auditsDir(root), auditId);
|
|
18692
|
+
function loadProfile(root) {
|
|
18693
|
+
const path6 = profilePath(root);
|
|
18694
|
+
return existsSync5(path6) ? readJson(path6) : null;
|
|
18695
|
+
}
|
|
18696
|
+
function loadOverlay(root) {
|
|
18697
|
+
const path6 = overlayPath(root);
|
|
18698
|
+
return existsSync5(path6) ? readJson(path6) : null;
|
|
18699
|
+
}
|
|
18700
|
+
|
|
18701
|
+
// ../schema/src/cli/kritik-audit.ts
|
|
18702
|
+
var SCORES_FILE = "scores.json";
|
|
18703
|
+
var FINDINGS_FILE = "findings.json";
|
|
18704
|
+
var MATRIX_FILE = "matrix.json";
|
|
18705
|
+
var scoresPath = (root, auditId) => join4(auditDir(root, auditId), SCORES_FILE);
|
|
18706
|
+
var findingsPath = (root, auditId) => join4(auditDir(root, auditId), FINDINGS_FILE);
|
|
18707
|
+
var matrixPath = (root, auditId) => join4(auditDir(root, auditId), MATRIX_FILE);
|
|
18708
|
+
function listAuditIds(root) {
|
|
18709
|
+
const dir = auditsDir(root);
|
|
18710
|
+
if (!existsSync6(dir)) return [];
|
|
18711
|
+
return readdirSync2(dir).filter((name) => statSync(join4(dir, name)).isDirectory()).sort();
|
|
18712
|
+
}
|
|
18713
|
+
function newestAuditId(root) {
|
|
18714
|
+
const dir = auditsDir(root);
|
|
18715
|
+
if (!existsSync6(dir)) throw new Error(`no audits directory at ${dir}`);
|
|
18716
|
+
const ids = listAuditIds(root);
|
|
18717
|
+
if (ids.length === 0) throw new Error(`no audits found under ${dir}`);
|
|
18718
|
+
return ids[ids.length - 1];
|
|
18719
|
+
}
|
|
18720
|
+
function loadScores(root, auditId) {
|
|
18721
|
+
const path6 = scoresPath(root, auditId);
|
|
18722
|
+
if (!existsSync6(path6)) throw new Error(`no ${SCORES_FILE} at ${path6}`);
|
|
18723
|
+
const file2 = readJson(path6);
|
|
18724
|
+
return { ...file2, assessments: Array.isArray(file2.assessments) ? file2.assessments : [] };
|
|
18725
|
+
}
|
|
18726
|
+
function loadScoresOrEmpty(root, auditId) {
|
|
18727
|
+
return existsSync6(scoresPath(root, auditId)) ? loadScores(root, auditId) : { audit_id: auditId, assessments: [] };
|
|
18728
|
+
}
|
|
18729
|
+
function loadFindings(root, auditId) {
|
|
18730
|
+
const path6 = findingsPath(root, auditId);
|
|
18731
|
+
if (!existsSync6(path6)) return { audit_id: auditId, findings: [] };
|
|
18732
|
+
const file2 = readJson(path6);
|
|
18733
|
+
return { ...file2, findings: Array.isArray(file2.findings) ? file2.findings : [] };
|
|
18734
|
+
}
|
|
18735
|
+
function saveScores(root, auditId, file2) {
|
|
18736
|
+
writeJson(scoresPath(root, auditId), file2);
|
|
18737
|
+
}
|
|
18738
|
+
function saveFindings(root, auditId, file2) {
|
|
18739
|
+
writeJson(findingsPath(root, auditId), file2);
|
|
18740
|
+
}
|
|
18741
|
+
function requireProfile(root) {
|
|
18742
|
+
const profile = loadProfile(root);
|
|
18743
|
+
if (!profile) {
|
|
18744
|
+
throw new Error(
|
|
18745
|
+
`no profile at ${join4(root, QUALITY_DIR, "profile.json")} \u2014 pick this project's surfaces first (\`arkaik kritik profile\`, or the plugin's init-profile.js).`
|
|
18746
|
+
);
|
|
18747
|
+
}
|
|
18748
|
+
return profile;
|
|
18749
|
+
}
|
|
18750
|
+
function loadQualitySection(root, auditId, library, scores = loadScores(root, auditId)) {
|
|
18751
|
+
const findings = loadFindings(root, auditId);
|
|
18752
|
+
return {
|
|
18753
|
+
framework_version: scores.framework_version ?? library.version,
|
|
18754
|
+
profile: requireProfile(root),
|
|
18755
|
+
assessments: scores.assessments,
|
|
18756
|
+
findings: findings.findings
|
|
18757
|
+
};
|
|
18758
|
+
}
|
|
18759
|
+
function stripDerived(finding) {
|
|
18760
|
+
const { severity: _severity, priority: _priority, ...rest } = finding;
|
|
18761
|
+
void _severity;
|
|
18762
|
+
void _priority;
|
|
18763
|
+
return rest;
|
|
18764
|
+
}
|
|
18765
|
+
function requireAudit(root, auditId) {
|
|
18766
|
+
if (!listAuditIds(root).includes(auditId)) {
|
|
18767
|
+
throw new Error(`no audit "${auditId}" under ${auditsDir(root)}`);
|
|
18768
|
+
}
|
|
18769
|
+
}
|
|
18770
|
+
function loadCurrentQualitySection(root, library) {
|
|
18771
|
+
const auditIds = listAuditIds(root);
|
|
18772
|
+
if (auditIds.length === 0) return void 0;
|
|
18773
|
+
const profile = loadProfile(root);
|
|
18774
|
+
if (!profile) return void 0;
|
|
18775
|
+
const cells = /* @__PURE__ */ new Map();
|
|
18776
|
+
const findings = [];
|
|
18777
|
+
let frameworkVersion;
|
|
18778
|
+
for (const id of auditIds) {
|
|
18779
|
+
const scores = loadScoresOrEmpty(root, id);
|
|
18780
|
+
if (typeof scores.framework_version === "string") frameworkVersion = scores.framework_version;
|
|
18781
|
+
for (const assessment of scores.assessments) {
|
|
18782
|
+
cells.set(`${assessment.criterion_id}\0${assessment.surface}`, assessment);
|
|
18783
|
+
}
|
|
18784
|
+
for (const finding of loadFindings(root, id).findings) findings.push(stripDerived(finding));
|
|
18785
|
+
}
|
|
18786
|
+
return {
|
|
18787
|
+
framework_version: frameworkVersion ?? library.version,
|
|
18788
|
+
library,
|
|
18789
|
+
profile,
|
|
18790
|
+
assessments: [...cells.values()],
|
|
18791
|
+
findings
|
|
18792
|
+
};
|
|
18793
|
+
}
|
|
18794
|
+
function loadAuditQualitySection(root, auditId, library) {
|
|
18795
|
+
requireAudit(root, auditId);
|
|
18796
|
+
const section = loadQualitySection(root, auditId, library, loadScoresOrEmpty(root, auditId));
|
|
18797
|
+
return { ...section, library, findings: section.findings.map(stripDerived) };
|
|
18798
|
+
}
|
|
18799
|
+
function computeAuditMatrix(root, auditId, library) {
|
|
18800
|
+
const scores = loadScores(root, auditId);
|
|
18801
|
+
const section = loadQualitySection(root, auditId, library, scores);
|
|
18802
|
+
const matrix = deriveQualityMatrix({ quality: section }, library);
|
|
18803
|
+
const file2 = {
|
|
18804
|
+
audit_id: auditId,
|
|
18805
|
+
commit: scores.commit,
|
|
18806
|
+
framework_version: section.framework_version,
|
|
18807
|
+
matrix: matrix.matrix,
|
|
18808
|
+
overall: matrix.overall,
|
|
18809
|
+
finding_counts: matrix.finding_counts
|
|
18810
|
+
};
|
|
18811
|
+
writeJson(matrixPath(root, auditId), file2);
|
|
18812
|
+
return { section, matrix, file: file2 };
|
|
18813
|
+
}
|
|
18814
|
+
function renderMatrixMarkdown(matrix, domainNames) {
|
|
18815
|
+
const cell = (value) => value ? `${value.score} (${value.grade}${value.capped ? "*" : ""})` : "\u2014";
|
|
18816
|
+
const lines = [];
|
|
18817
|
+
lines.push(`| Domain | ${matrix.surfaces.join(" | ")} |`);
|
|
18818
|
+
lines.push(`| --- | ${matrix.surfaces.map(() => "---").join(" | ")} |`);
|
|
18819
|
+
for (const domain2 of matrix.domains) {
|
|
18820
|
+
const label = domainNames.get(domain2) ?? domain2;
|
|
18821
|
+
lines.push(
|
|
18822
|
+
`| **${domain2}** ${label} | ${matrix.surfaces.map((s) => cell(matrix.matrix[domain2]?.[s])).join(" | ")} |`
|
|
18823
|
+
);
|
|
18824
|
+
}
|
|
18825
|
+
lines.push(
|
|
18826
|
+
`| **Overall (weighted)** | ${matrix.surfaces.map((s) => {
|
|
18827
|
+
const score = matrix.overall[s];
|
|
18828
|
+
return score === null || score === void 0 ? "\u2014" : `**${score} (${gradeOf(score)})**`;
|
|
18829
|
+
}).join(" | ")} |`
|
|
18830
|
+
);
|
|
18831
|
+
return lines.join("\n");
|
|
18832
|
+
}
|
|
18833
|
+
function locateFinding(root, id) {
|
|
18834
|
+
for (const auditId of [...listAuditIds(root)].reverse()) {
|
|
18835
|
+
const file2 = loadFindings(root, auditId);
|
|
18836
|
+
const finding = file2.findings.find((candidate) => candidate.id === id);
|
|
18837
|
+
if (finding) return { auditId, file: file2, finding };
|
|
18838
|
+
}
|
|
18839
|
+
return void 0;
|
|
18840
|
+
}
|
|
18841
|
+
|
|
18842
|
+
// src/lib/kritik-io.ts
|
|
18843
|
+
var KRITIK_ACTOR = "arkaik-cli";
|
|
18844
|
+
var DEFAULT_BUNDLE_PATH5 = join5("docs", "arkaik", "bundle.json");
|
|
18845
|
+
var VENDORED_PACK = join5(QUALITY_DIR, PACK_FILE);
|
|
18846
|
+
var BUNDLED_PACK = join5(dirname4(fileURLToPath2(import.meta.url)), "assets", "kritik", "library.json");
|
|
18847
|
+
function resolvePack(root) {
|
|
18848
|
+
const vendored = join5(root, VENDORED_PACK);
|
|
18849
|
+
if (existsSync7(vendored)) return { library: readJson(vendored), path: vendored, vendored: true };
|
|
18850
|
+
if (!existsSync7(BUNDLED_PACK)) {
|
|
18851
|
+
throw new Error(
|
|
18852
|
+
`no criteria pack found. Looked in:
|
|
18853
|
+
${vendored}
|
|
18854
|
+
${BUNDLED_PACK}
|
|
18855
|
+
The second is shipped with this CLI, so its absence means a broken install \u2014 reinstall \`arkaik\`.`
|
|
18856
|
+
);
|
|
18857
|
+
}
|
|
18858
|
+
return { library: readJson(BUNDLED_PACK), path: BUNDLED_PACK, vendored: false };
|
|
18859
|
+
}
|
|
18860
|
+
function loadKritikLibrary(root) {
|
|
18861
|
+
const pack = resolvePack(root);
|
|
18862
|
+
return { library: mergeKritikLibrary(pack.library, loadOverlay(root)), pack };
|
|
18863
|
+
}
|
|
18864
|
+
function resolveJournal(root, bundlePath) {
|
|
18865
|
+
const resolved = bundlePath ?? join5(root, DEFAULT_BUNDLE_PATH5);
|
|
18866
|
+
return { bundlePath: resolved, journalPath: journalPathFor(resolved), present: existsSync7(resolved) };
|
|
18867
|
+
}
|
|
18868
|
+
function appendQualityEvents(root, inputs, options = {}) {
|
|
18869
|
+
if (inputs.length === 0) return { events: [] };
|
|
18870
|
+
const actor = options.actor ?? KRITIK_ACTOR;
|
|
18871
|
+
const journal = resolveJournal(root, options.bundlePath);
|
|
18872
|
+
if (!journal.present) return { events: [] };
|
|
18873
|
+
const bundle = readBundle(journal.bundlePath);
|
|
18874
|
+
const baseline = ensureJournalBaseline(journal.journalPath, bundle, actor);
|
|
18875
|
+
const events = inputs.map((input) => makeEvent(input.type, input.payload, { actor }));
|
|
18876
|
+
for (const event of events) appendJournalEvent(journal.journalPath, event);
|
|
18877
|
+
return { journalPath: journal.journalPath, events, ...baseline !== void 0 ? { baseline } : {} };
|
|
18878
|
+
}
|
|
18879
|
+
function isQualitySection(value) {
|
|
18880
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18881
|
+
}
|
|
18882
|
+
function foldQualitySection(bundle, root, auditId) {
|
|
18883
|
+
const notFolded = (reason) => {
|
|
18884
|
+
const carriedSection = isQualitySection(bundle.quality);
|
|
18885
|
+
return {
|
|
18886
|
+
folded: false,
|
|
18887
|
+
carriedSection,
|
|
18888
|
+
notice: carriedSection ? `Quality: ${reason}
|
|
18889
|
+
Kept the quality section this bundle already carried \u2014 nothing replaced it.` : `Quality: ${reason}`
|
|
18890
|
+
};
|
|
18891
|
+
};
|
|
18892
|
+
if (auditId !== void 0) requireAudit(root, auditId);
|
|
18893
|
+
const auditIds = listAuditIds(root);
|
|
18894
|
+
if (auditIds.length === 0) {
|
|
18895
|
+
return notFolded(`none to fold \u2014 no audits under ${auditsDir(root)} (run \`arkaik kritik score\` to open one)`);
|
|
18896
|
+
}
|
|
18897
|
+
if (!loadProfile(root)) {
|
|
18898
|
+
return notFolded(`skipped, no profile \u2014 nothing at ${profilePath(root)} (run \`arkaik kritik profile\`)`);
|
|
18899
|
+
}
|
|
18900
|
+
let library;
|
|
18901
|
+
try {
|
|
18902
|
+
({ library } = loadKritikLibrary(root));
|
|
18903
|
+
} catch (e) {
|
|
18904
|
+
return notFolded(`skipped, no pack \u2014 ${e.message}`);
|
|
18905
|
+
}
|
|
18906
|
+
const section = auditId === void 0 ? loadCurrentQualitySection(root, library) : loadAuditQualitySection(root, auditId, library);
|
|
18907
|
+
if (section === void 0) {
|
|
18908
|
+
return notFolded(`nothing to fold \u2014 no audits under ${auditsDir(root)}, or no profile at ${profilePath(root)}`);
|
|
18909
|
+
}
|
|
18910
|
+
bundle.quality = section;
|
|
18911
|
+
const count = auditId === void 0 ? auditIds.length : 1;
|
|
18912
|
+
return {
|
|
18913
|
+
folded: true,
|
|
18914
|
+
carriedSection: false,
|
|
18915
|
+
notice: `Quality: folded ${section.assessments.length} assessment(s), ${section.findings.length} finding(s) from ${count} audit(s)`
|
|
18916
|
+
};
|
|
18917
|
+
}
|
|
18918
|
+
function resolveQualityRoot(cwd, filePath, root) {
|
|
18919
|
+
if (root !== void 0) return resolve4(cwd, root);
|
|
18920
|
+
const dir = dirname4(filePath);
|
|
18921
|
+
if (basename3(dir) === "arkaik" && basename3(dirname4(dir)) === "docs") return resolve4(dir, "..", "..");
|
|
18922
|
+
return cwd;
|
|
18923
|
+
}
|
|
18924
|
+
|
|
18925
|
+
// src/commands/pack.ts
|
|
18926
|
+
var DEFAULT_BUNDLE_PATH6 = "docs/arkaik/bundle.json";
|
|
18927
|
+
var USAGE7 = `arkaik pack [--no-journal] [--no-quality] [--inline-assets] [--audit <id>]
|
|
18928
|
+
[--root <dir>] [--out <path>] [path]
|
|
17676
18929
|
|
|
17677
18930
|
Produce a single self-contained interchange bundle: fold in the sidecar
|
|
17678
|
-
journal (or keep an existing embedded one)
|
|
17679
|
-
|
|
17680
|
-
|
|
18931
|
+
journal (or keep an existing embedded one), fold docs/quality/ into the
|
|
18932
|
+
quality section, and, with --inline-assets, inline local screenshot files as
|
|
18933
|
+
data: URIs. Written canonically via serializeBundle. Unknown top-level keys
|
|
18934
|
+
and unknown fields always round-trip.
|
|
17681
18935
|
|
|
17682
18936
|
Arguments:
|
|
17683
|
-
path Path to the bundle JSON file (default: ${
|
|
18937
|
+
path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH6}).
|
|
17684
18938
|
|
|
17685
18939
|
Options:
|
|
17686
18940
|
--no-journal Omit the embedded journal[] (Publik-safe posture \u2014 history
|
|
@@ -17689,12 +18943,33 @@ Options:
|
|
|
17689
18943
|
interchange) \u2014 embedded wins over the sidecar when the
|
|
17690
18944
|
bundle already carries one, otherwise the sidecar is used
|
|
17691
18945
|
(same precedence "arkaik validate" folds by).
|
|
18946
|
+
--no-quality DELETE the quality section rather than folding one in \u2014
|
|
18947
|
+
not merely "skip the fold", because the source bundle may
|
|
18948
|
+
already carry a section of its own and that one goes too.
|
|
18949
|
+
For any bundle that must not travel with open findings:
|
|
18950
|
+
a finding names an unfixed vulnerability and the file to
|
|
18951
|
+
find it in. ("arkaik push" packs this way by default;
|
|
18952
|
+
its --include-quality opts back in.)
|
|
18953
|
+
Default: docs/quality/ IS folded in.
|
|
17692
18954
|
--inline-assets Convert relative-path metadata.platformScreenshots values
|
|
17693
18955
|
into data: URIs by reading the file from disk (resolved
|
|
17694
18956
|
against the bundle's directory). Absolute https:// URLs
|
|
17695
18957
|
and existing data: URIs are left as-is. v1 scope: local
|
|
17696
18958
|
files only \u2014 uploading a remote/hosted copy is not
|
|
17697
18959
|
implemented.
|
|
18960
|
+
--audit <id> Pin ONE audit's snapshot instead of the default merge.
|
|
18961
|
+
They answer different questions: the merge (every audit,
|
|
18962
|
+
latest score per criterion x surface) answers "where does
|
|
18963
|
+
the product stand", while a pinned audit answers "how did
|
|
18964
|
+
that audit go" \u2014 the question its own matrix.json
|
|
18965
|
+
answers. An id that is not on disk is an error, not an
|
|
18966
|
+
empty section.
|
|
18967
|
+
--root <dir> Where docs/quality/ lives. Default: NOT the current
|
|
18968
|
+
directory \u2014 it is derived from the bundle's own path, so
|
|
18969
|
+
packing <repo>/docs/arkaik/bundle.json folds <repo>'s
|
|
18970
|
+
audits whatever directory you run from. Only a bundle
|
|
18971
|
+
kept outside that conventional layout falls back to the
|
|
18972
|
+
cwd, and that is the case this flag is for.
|
|
17698
18973
|
--out <path> Write the packed bundle here instead of stdout.
|
|
17699
18974
|
-h, --help Show this help.`;
|
|
17700
18975
|
function fail7(message) {
|
|
@@ -17732,7 +19007,7 @@ function fatalResult2(bundlePath, message) {
|
|
|
17732
19007
|
}
|
|
17733
19008
|
function runPack(options = {}) {
|
|
17734
19009
|
const cwd = options.cwd ?? process.cwd();
|
|
17735
|
-
const filePath =
|
|
19010
|
+
const filePath = resolve5(cwd, options.path ?? DEFAULT_BUNDLE_PATH6);
|
|
17736
19011
|
const noJournal = options.noJournal ?? false;
|
|
17737
19012
|
const inlineAssets = options.inlineAssets ?? false;
|
|
17738
19013
|
let bundle;
|
|
@@ -17753,10 +19028,24 @@ function runPack(options = {}) {
|
|
|
17753
19028
|
journalEventCount = events.length;
|
|
17754
19029
|
}
|
|
17755
19030
|
}
|
|
19031
|
+
let qualityFolded;
|
|
19032
|
+
let qualityNotice;
|
|
19033
|
+
if (options.noQuality ?? false) {
|
|
19034
|
+
delete bundle.quality;
|
|
19035
|
+
} else {
|
|
19036
|
+
const root = resolveQualityRoot(cwd, filePath, options.root);
|
|
19037
|
+
try {
|
|
19038
|
+
const fold = foldQualitySection(bundle, root, options.audit);
|
|
19039
|
+
qualityFolded = fold.folded;
|
|
19040
|
+
qualityNotice = fold.notice;
|
|
19041
|
+
} catch (e) {
|
|
19042
|
+
return fatalResult2(filePath, e.message);
|
|
19043
|
+
}
|
|
19044
|
+
}
|
|
17756
19045
|
const inlinedAssets = [];
|
|
17757
19046
|
const assetWarnings = [];
|
|
17758
19047
|
if (inlineAssets) {
|
|
17759
|
-
const bundleDir =
|
|
19048
|
+
const bundleDir = dirname5(filePath);
|
|
17760
19049
|
const nodes = Array.isArray(bundle.nodes) ? bundle.nodes : [];
|
|
17761
19050
|
for (const node of nodes) {
|
|
17762
19051
|
const nodeId = typeof node.id === "string" ? node.id : "?";
|
|
@@ -17767,12 +19056,12 @@ function runPack(options = {}) {
|
|
|
17767
19056
|
const map2 = screenshots;
|
|
17768
19057
|
for (const [platform, value] of Object.entries(map2)) {
|
|
17769
19058
|
if (typeof value !== "string" || !isRelativeAssetPath(value)) continue;
|
|
17770
|
-
const assetPath =
|
|
17771
|
-
if (!
|
|
19059
|
+
const assetPath = resolve5(bundleDir, value);
|
|
19060
|
+
if (!existsSync8(assetPath)) {
|
|
17772
19061
|
assetWarnings.push(`${nodeId}/${platform}: asset not found at ${assetPath} \u2014 left as-is`);
|
|
17773
19062
|
continue;
|
|
17774
19063
|
}
|
|
17775
|
-
const bytes =
|
|
19064
|
+
const bytes = readFileSync7(assetPath);
|
|
17776
19065
|
const mime = mimeForExtension(extname(assetPath));
|
|
17777
19066
|
map2[platform] = `data:${mime};base64,${bytes.toString("base64")}`;
|
|
17778
19067
|
inlinedAssets.push({ nodeId, platform, path: value });
|
|
@@ -17782,15 +19071,18 @@ function runPack(options = {}) {
|
|
|
17782
19071
|
const output = serializeBundle(bundle);
|
|
17783
19072
|
let outPath;
|
|
17784
19073
|
if (options.out !== void 0) {
|
|
17785
|
-
outPath =
|
|
17786
|
-
|
|
17787
|
-
|
|
19074
|
+
outPath = resolve5(cwd, options.out);
|
|
19075
|
+
mkdirSync4(dirname5(outPath), { recursive: true });
|
|
19076
|
+
writeFileSync6(outPath, output);
|
|
17788
19077
|
}
|
|
17789
|
-
return { ok: true, bundlePath: filePath, outPath, journalIncluded, journalEventCount, inlinedAssets, assetWarnings, output };
|
|
19078
|
+
return { ok: true, bundlePath: filePath, outPath, journalIncluded, journalEventCount, inlinedAssets, assetWarnings, qualityFolded, qualityNotice, output };
|
|
17790
19079
|
}
|
|
17791
19080
|
function runPackCli(args) {
|
|
17792
19081
|
let noJournal = false;
|
|
19082
|
+
let noQuality = false;
|
|
17793
19083
|
let inlineAssets = false;
|
|
19084
|
+
let audit;
|
|
19085
|
+
let root;
|
|
17794
19086
|
let out;
|
|
17795
19087
|
const positionals = [];
|
|
17796
19088
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -17800,8 +19092,22 @@ function runPackCli(args) {
|
|
|
17800
19092
|
process.exit(0);
|
|
17801
19093
|
} else if (arg === "--no-journal") {
|
|
17802
19094
|
noJournal = true;
|
|
19095
|
+
} else if (arg === "--no-quality") {
|
|
19096
|
+
noQuality = true;
|
|
17803
19097
|
} else if (arg === "--inline-assets") {
|
|
17804
19098
|
inlineAssets = true;
|
|
19099
|
+
} else if (arg === "--audit") {
|
|
19100
|
+
const value = args[++i];
|
|
19101
|
+
if (value === void 0) fail7(`Missing value for --audit
|
|
19102
|
+
|
|
19103
|
+
${USAGE7}`);
|
|
19104
|
+
audit = value;
|
|
19105
|
+
} else if (arg === "--root") {
|
|
19106
|
+
const value = args[++i];
|
|
19107
|
+
if (value === void 0) fail7(`Missing value for --root
|
|
19108
|
+
|
|
19109
|
+
${USAGE7}`);
|
|
19110
|
+
root = value;
|
|
17805
19111
|
} else if (arg === "--out") {
|
|
17806
19112
|
const value = args[++i];
|
|
17807
19113
|
if (value === void 0) fail7(`Missing value for --out
|
|
@@ -17816,8 +19122,13 @@ ${USAGE7}`);
|
|
|
17816
19122
|
positionals.push(arg);
|
|
17817
19123
|
}
|
|
17818
19124
|
}
|
|
17819
|
-
|
|
17820
|
-
|
|
19125
|
+
if (noQuality && audit !== void 0) {
|
|
19126
|
+
fail7(`--audit and --no-quality contradict each other: one names an audit to fold, the other removes the section
|
|
19127
|
+
|
|
19128
|
+
${USAGE7}`);
|
|
19129
|
+
}
|
|
19130
|
+
const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH6;
|
|
19131
|
+
const result = runPack({ path: filePath, out, noJournal, inlineAssets, noQuality, audit, root });
|
|
17821
19132
|
if (!result.ok) fail7(`FATAL: ${result.fatal}`);
|
|
17822
19133
|
if (result.journalIncluded) {
|
|
17823
19134
|
console.error(`Journal: embedded ${result.journalEventCount} event(s)`);
|
|
@@ -17826,6 +19137,9 @@ ${USAGE7}`);
|
|
|
17826
19137
|
} else {
|
|
17827
19138
|
console.error("Journal: none to embed (no embedded journal, no sidecar)");
|
|
17828
19139
|
}
|
|
19140
|
+
if (result.qualityNotice !== void 0) {
|
|
19141
|
+
console.error(result.qualityNotice);
|
|
19142
|
+
}
|
|
17829
19143
|
for (const asset of result.inlinedAssets) {
|
|
17830
19144
|
console.error(`Inlined asset: ${asset.nodeId}/${asset.platform} (${asset.path})`);
|
|
17831
19145
|
}
|
|
@@ -17842,10 +19156,10 @@ ${USAGE7}`);
|
|
|
17842
19156
|
|
|
17843
19157
|
// src/commands/open.ts
|
|
17844
19158
|
import { spawn } from "node:child_process";
|
|
17845
|
-
import { mkdtempSync, writeFileSync as
|
|
19159
|
+
import { mkdtempSync, writeFileSync as writeFileSync7 } from "node:fs";
|
|
17846
19160
|
import { tmpdir } from "node:os";
|
|
17847
|
-
import { join as
|
|
17848
|
-
var
|
|
19161
|
+
import { join as join6, resolve as resolve6 } from "node:path";
|
|
19162
|
+
var DEFAULT_BUNDLE_PATH7 = "docs/arkaik/bundle.json";
|
|
17849
19163
|
var OPEN_URL = "https://arkaik.app/projects";
|
|
17850
19164
|
var USAGE8 = `arkaik open [--out <path>] [--no-open] [path]
|
|
17851
19165
|
|
|
@@ -17856,7 +19170,7 @@ ${OPEN_URL} (the project list's "Import JSON" picker). On an invalid bundle,
|
|
|
17856
19170
|
findings are printed and nothing is packed, written, or opened.
|
|
17857
19171
|
|
|
17858
19172
|
Arguments:
|
|
17859
|
-
path Path to the bundle JSON file (default: ${
|
|
19173
|
+
path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH7}).
|
|
17860
19174
|
|
|
17861
19175
|
Options:
|
|
17862
19176
|
--out <path> Write the packed bundle here instead of a temp file.
|
|
@@ -17878,7 +19192,7 @@ function fatalResult3(bundlePath, message) {
|
|
|
17878
19192
|
}
|
|
17879
19193
|
async function runOpen(options = {}) {
|
|
17880
19194
|
const cwd = options.cwd ?? process.cwd();
|
|
17881
|
-
const filePath =
|
|
19195
|
+
const filePath = resolve6(cwd, options.path ?? DEFAULT_BUNDLE_PATH7);
|
|
17882
19196
|
const noOpen = options.noOpen ?? false;
|
|
17883
19197
|
let v;
|
|
17884
19198
|
try {
|
|
@@ -17887,10 +19201,7 @@ async function runOpen(options = {}) {
|
|
|
17887
19201
|
return fatalResult3(filePath, e.message);
|
|
17888
19202
|
}
|
|
17889
19203
|
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
|
-
];
|
|
19204
|
+
const errorLines = [...journalLineErrorLines(v), ...v.result.errors.map(formatFinding)];
|
|
17894
19205
|
if (!v.valid) {
|
|
17895
19206
|
return { ok: true, bundlePath: filePath, valid: false, errorLines, warningLines, opened: false };
|
|
17896
19207
|
}
|
|
@@ -17900,9 +19211,9 @@ async function runOpen(options = {}) {
|
|
|
17900
19211
|
}
|
|
17901
19212
|
let outPath = packed.outPath;
|
|
17902
19213
|
if (outPath === void 0) {
|
|
17903
|
-
const dir = mkdtempSync(
|
|
17904
|
-
outPath =
|
|
17905
|
-
|
|
19214
|
+
const dir = mkdtempSync(join6(tmpdir(), "arkaik-open-"));
|
|
19215
|
+
outPath = join6(dir, "bundle.json");
|
|
19216
|
+
writeFileSync7(outPath, packed.output);
|
|
17906
19217
|
}
|
|
17907
19218
|
let opened = false;
|
|
17908
19219
|
if (!noOpen) {
|
|
@@ -17910,7 +19221,17 @@ async function runOpen(options = {}) {
|
|
|
17910
19221
|
await opener(OPEN_URL);
|
|
17911
19222
|
opened = true;
|
|
17912
19223
|
}
|
|
17913
|
-
return {
|
|
19224
|
+
return {
|
|
19225
|
+
ok: true,
|
|
19226
|
+
bundlePath: filePath,
|
|
19227
|
+
valid: true,
|
|
19228
|
+
errorLines,
|
|
19229
|
+
warningLines,
|
|
19230
|
+
outPath,
|
|
19231
|
+
url: OPEN_URL,
|
|
19232
|
+
opened,
|
|
19233
|
+
qualityNotice: packed.qualityNotice
|
|
19234
|
+
};
|
|
17914
19235
|
}
|
|
17915
19236
|
function runOpenCli(args) {
|
|
17916
19237
|
let out;
|
|
@@ -17937,7 +19258,7 @@ ${USAGE8}`);
|
|
|
17937
19258
|
positionals.push(arg);
|
|
17938
19259
|
}
|
|
17939
19260
|
}
|
|
17940
|
-
const filePath = positionals[0] ??
|
|
19261
|
+
const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH7;
|
|
17941
19262
|
runOpen({ path: filePath, out, noOpen }).then((result) => {
|
|
17942
19263
|
if (!result.ok) fail8(`FATAL: ${result.fatal}`);
|
|
17943
19264
|
if (result.warningLines.length > 0) {
|
|
@@ -17950,6 +19271,9 @@ ${USAGE8}`);
|
|
|
17950
19271
|
console.error("\nInvalid bundle \u2014 not packed, not opened.");
|
|
17951
19272
|
process.exit(1);
|
|
17952
19273
|
}
|
|
19274
|
+
if (result.qualityNotice !== void 0) {
|
|
19275
|
+
console.error(result.qualityNotice);
|
|
19276
|
+
}
|
|
17953
19277
|
console.log(`Packed -> ${result.outPath}`);
|
|
17954
19278
|
if (result.opened) {
|
|
17955
19279
|
console.log(`Opened ${result.url}`);
|
|
@@ -17961,10 +19285,10 @@ ${USAGE8}`);
|
|
|
17961
19285
|
}
|
|
17962
19286
|
|
|
17963
19287
|
// src/commands/push.ts
|
|
17964
|
-
import { resolve as
|
|
17965
|
-
var
|
|
19288
|
+
import { resolve as resolve7 } from "node:path";
|
|
19289
|
+
var DEFAULT_BUNDLE_PATH8 = "docs/arkaik/bundle.json";
|
|
17966
19290
|
var DEFAULT_API_BASE = "https://arkaik.app";
|
|
17967
|
-
var USAGE9 = `arkaik push [--include-journal] [--api <base-url>] [path]
|
|
19291
|
+
var USAGE9 = `arkaik push [--include-journal] [--include-quality] [--api <base-url>] [path]
|
|
17968
19292
|
arkaik push --delete <id> --key <owner_key> [--api <base-url>]
|
|
17969
19293
|
|
|
17970
19294
|
Publish a project bundle to Publik (anonymous, account-less snapshot
|
|
@@ -17978,18 +19302,29 @@ Publik-safe posture (docs/spec/journal.md). --include-journal embeds it
|
|
|
17978
19302
|
(like a bare "arkaik pack") and forwards ?include_journal=true so the server
|
|
17979
19303
|
knows to keep it.
|
|
17980
19304
|
|
|
19305
|
+
A Kritik "quality" section is stripped by default too, and separately:
|
|
19306
|
+
publishing your history and publishing your open findings are two decisions,
|
|
19307
|
+
not one, and both default to no. --include-quality opts in.
|
|
19308
|
+
|
|
17981
19309
|
Snapshots are immutable: there is no update verb. Pushing again always mints
|
|
17982
19310
|
a new id. The owner key printed on success is shown exactly once and cannot
|
|
17983
19311
|
be recovered \u2014 save it if you may need to delete the snapshot later.
|
|
17984
19312
|
|
|
17985
19313
|
Arguments:
|
|
17986
|
-
path Path to the bundle JSON file (default: ${
|
|
19314
|
+
path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH8}).
|
|
17987
19315
|
Ignored with --delete.
|
|
17988
19316
|
|
|
17989
19317
|
Options:
|
|
17990
19318
|
--include-journal Embed the journal in the pushed bundle and forward
|
|
17991
19319
|
?include_journal=true. Default: stripped, omitted
|
|
17992
19320
|
entirely from the request body.
|
|
19321
|
+
--include-quality Embed the Kritik quality section and forward
|
|
19322
|
+
?include_quality=true. Opt-in rather than opt-out
|
|
19323
|
+
because an open finding is an unfixed vulnerability
|
|
19324
|
+
plus the path to find it in (docs/rfcs/kritik.md
|
|
19325
|
+
\xA7 8.3) \u2014 publishing that is a decision worth typing.
|
|
19326
|
+
Default: deleted before packing, so it is never in the
|
|
19327
|
+
request body at all.
|
|
17993
19328
|
--api <base-url> Publik API base URL (default: ${DEFAULT_API_BASE}).
|
|
17994
19329
|
Point at a self-hosted deployment.
|
|
17995
19330
|
--delete <id> Delete a snapshot by id instead of pushing. Requires
|
|
@@ -18014,8 +19349,9 @@ function fatalResult4(bundlePath, message) {
|
|
|
18014
19349
|
}
|
|
18015
19350
|
async function runPush(options = {}) {
|
|
18016
19351
|
const cwd = options.cwd ?? process.cwd();
|
|
18017
|
-
const filePath =
|
|
19352
|
+
const filePath = resolve7(cwd, options.path ?? DEFAULT_BUNDLE_PATH8);
|
|
18018
19353
|
const includeJournal = options.includeJournal ?? false;
|
|
19354
|
+
const includeQuality = options.includeQuality ?? false;
|
|
18019
19355
|
const apiBase = options.apiBase ?? DEFAULT_API_BASE;
|
|
18020
19356
|
const httpClient = options.httpClient ?? DEFAULT_HTTP_CLIENT;
|
|
18021
19357
|
let v;
|
|
@@ -18025,18 +19361,19 @@ async function runPush(options = {}) {
|
|
|
18025
19361
|
return fatalResult4(filePath, e.message);
|
|
18026
19362
|
}
|
|
18027
19363
|
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
|
-
];
|
|
19364
|
+
const errorLines = [...journalLineErrorLines(v), ...v.result.errors.map(formatFinding)];
|
|
18032
19365
|
if (!v.valid) {
|
|
18033
19366
|
return { ok: true, bundlePath: filePath, valid: false, errorLines, warningLines, requestSent: false };
|
|
18034
19367
|
}
|
|
18035
|
-
const packed = runPack({ path: filePath, noJournal: !includeJournal, cwd });
|
|
19368
|
+
const packed = runPack({ path: filePath, noJournal: !includeJournal, noQuality: !includeQuality, cwd });
|
|
18036
19369
|
if (!packed.ok) {
|
|
18037
19370
|
return fatalResult4(filePath, packed.fatal ?? "pack failed");
|
|
18038
19371
|
}
|
|
18039
|
-
const
|
|
19372
|
+
const qualityNotice = packed.qualityNotice ?? "Quality: stripped, not sent (pass --include-quality to publish it)";
|
|
19373
|
+
const params = [];
|
|
19374
|
+
if (includeJournal) params.push("include_journal=true");
|
|
19375
|
+
if (includeQuality) params.push("include_quality=true");
|
|
19376
|
+
const url2 = `${apiBase}/api/publik${params.length > 0 ? `?${params.join("&")}` : ""}`;
|
|
18040
19377
|
let res;
|
|
18041
19378
|
try {
|
|
18042
19379
|
res = await httpClient(url2, {
|
|
@@ -18066,6 +19403,7 @@ async function runPush(options = {}) {
|
|
|
18066
19403
|
warningLines,
|
|
18067
19404
|
requestSent: true,
|
|
18068
19405
|
status,
|
|
19406
|
+
qualityNotice,
|
|
18069
19407
|
id: body.id,
|
|
18070
19408
|
url: body.url,
|
|
18071
19409
|
ownerKey: body.owner_key
|
|
@@ -18084,6 +19422,7 @@ async function runPush(options = {}) {
|
|
|
18084
19422
|
warningLines,
|
|
18085
19423
|
requestSent: true,
|
|
18086
19424
|
status,
|
|
19425
|
+
qualityNotice,
|
|
18087
19426
|
serverFindings: errBody.findings,
|
|
18088
19427
|
retryAfter: status === 429 ? res.headers.get("retry-after") : void 0,
|
|
18089
19428
|
errorMessage: errBody.message ?? `Request failed with status ${status}`
|
|
@@ -18094,6 +19433,9 @@ function reportPush(result) {
|
|
|
18094
19433
|
console.error(`Warnings: ${result.warningLines.length}`);
|
|
18095
19434
|
result.warningLines.forEach((w) => console.error(` ${w}`));
|
|
18096
19435
|
}
|
|
19436
|
+
if (result.qualityNotice !== void 0) {
|
|
19437
|
+
console.error(result.qualityNotice);
|
|
19438
|
+
}
|
|
18097
19439
|
if (!result.valid) {
|
|
18098
19440
|
console.error(`Errors: ${result.errorLines.length}`);
|
|
18099
19441
|
result.errorLines.forEach((e) => console.error(` ${e}`));
|
|
@@ -18175,6 +19517,7 @@ function reportDelete(id, result) {
|
|
|
18175
19517
|
}
|
|
18176
19518
|
function runPushCli(args) {
|
|
18177
19519
|
let includeJournal = false;
|
|
19520
|
+
let includeQuality = false;
|
|
18178
19521
|
let apiBase;
|
|
18179
19522
|
let deleteId;
|
|
18180
19523
|
let key;
|
|
@@ -18186,6 +19529,8 @@ function runPushCli(args) {
|
|
|
18186
19529
|
process.exit(0);
|
|
18187
19530
|
} else if (arg === "--include-journal") {
|
|
18188
19531
|
includeJournal = true;
|
|
19532
|
+
} else if (arg === "--include-quality") {
|
|
19533
|
+
includeQuality = true;
|
|
18189
19534
|
} else if (arg === "--api") {
|
|
18190
19535
|
const value = args[++i];
|
|
18191
19536
|
if (value === void 0) fail9(`Missing value for --api
|
|
@@ -18227,16 +19572,16 @@ ${USAGE9}`);
|
|
|
18227
19572
|
if (key !== void 0) fail9(`--key is only valid with --delete
|
|
18228
19573
|
|
|
18229
19574
|
${USAGE9}`);
|
|
18230
|
-
const filePath = positionals[0] ??
|
|
18231
|
-
runPush({ path: filePath, includeJournal, apiBase }).then((result) => {
|
|
19575
|
+
const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH8;
|
|
19576
|
+
runPush({ path: filePath, includeJournal, includeQuality, apiBase }).then((result) => {
|
|
18232
19577
|
if (!result.ok) fail9(`FATAL: ${result.fatal}`);
|
|
18233
19578
|
reportPush(result);
|
|
18234
19579
|
}).catch((e) => fail9(`FATAL: ${e.message}`));
|
|
18235
19580
|
}
|
|
18236
19581
|
|
|
18237
19582
|
// src/commands/link.ts
|
|
18238
|
-
import { mkdirSync as
|
|
18239
|
-
import { dirname as
|
|
19583
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "node:fs";
|
|
19584
|
+
import { dirname as dirname6, join as join7, resolve as resolve8 } from "node:path";
|
|
18240
19585
|
var LINK_FILE = "docs/arkaik/arkaik.json";
|
|
18241
19586
|
var DEFAULT_BASE_URL = "https://arkaik.app";
|
|
18242
19587
|
var USAGE10 = `arkaik link \u2014 point this repo at a hosted Arkaik project
|
|
@@ -18307,15 +19652,15 @@ async function runLink(argv, options = {}) {
|
|
|
18307
19652
|
return { ok: false };
|
|
18308
19653
|
}
|
|
18309
19654
|
const { bundle } = await res.json();
|
|
18310
|
-
const target =
|
|
18311
|
-
const linkPath =
|
|
18312
|
-
|
|
19655
|
+
const target = resolve8(cwd, argv.find((a) => !a.startsWith("--") && a !== projectId && a !== baseUrl) ?? ".");
|
|
19656
|
+
const linkPath = join7(target, LINK_FILE);
|
|
19657
|
+
mkdirSync5(dirname6(linkPath), { recursive: true });
|
|
18313
19658
|
let existing = {};
|
|
18314
19659
|
try {
|
|
18315
|
-
existing = JSON.parse(
|
|
19660
|
+
existing = JSON.parse(readFileSync8(linkPath, "utf8"));
|
|
18316
19661
|
} catch {
|
|
18317
19662
|
}
|
|
18318
|
-
|
|
19663
|
+
writeFileSync8(
|
|
18319
19664
|
linkPath,
|
|
18320
19665
|
`${JSON.stringify({ ...existing, project_id: projectId, remote: baseUrl }, null, 2)}
|
|
18321
19666
|
`
|
|
@@ -18342,14 +19687,15 @@ function runLinkCli(argv) {
|
|
|
18342
19687
|
}
|
|
18343
19688
|
|
|
18344
19689
|
// src/commands/restore.ts
|
|
18345
|
-
import { existsSync as
|
|
18346
|
-
import { join as
|
|
19690
|
+
import { existsSync as existsSync9, linkSync, mkdirSync as mkdirSync6, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync9 } from "node:fs";
|
|
19691
|
+
import { join as join8, resolve as resolve9 } from "node:path";
|
|
18347
19692
|
var LINK_FILE2 = "docs/arkaik/arkaik.json";
|
|
18348
|
-
var
|
|
19693
|
+
var DEFAULT_BUNDLE_PATH9 = "docs/arkaik/bundle.json";
|
|
18349
19694
|
var DEFAULT_API_BASE2 = "https://arkaik.app";
|
|
18350
19695
|
var USAGE11 = `arkaik restore [options] [path]
|
|
18351
19696
|
|
|
18352
|
-
Replace the linked hosted project's bundle
|
|
19697
|
+
Replace the linked hosted project's bundle, journal AND quality section with
|
|
19698
|
+
a local bundle \u2014
|
|
18353
19699
|
the landing step for a bootstrapped map. Before sending anything, this
|
|
18354
19700
|
exports the CURRENT hosted state (snapshot + journal) to
|
|
18355
19701
|
docs/arkaik/.backups/<timestamp>-bundle.json (next to the link file \u2014 not
|
|
@@ -18359,9 +19705,10 @@ only way back if the restore turns out to be wrong.
|
|
|
18359
19705
|
|
|
18360
19706
|
Arguments:
|
|
18361
19707
|
path Path to the local bundle JSON file
|
|
18362
|
-
(default: ${
|
|
19708
|
+
(default: ${DEFAULT_BUNDLE_PATH9}). Its journal.jsonl
|
|
18363
19709
|
sidecar (or an embedded journal, which wins) is folded
|
|
18364
|
-
in automatically
|
|
19710
|
+
in automatically, as is the docs/quality/ tree of the
|
|
19711
|
+
repo that bundle belongs to.
|
|
18365
19712
|
|
|
18366
19713
|
Options:
|
|
18367
19714
|
--dry-run Ask the server what this restore WOULD do and print
|
|
@@ -18374,6 +19721,32 @@ Options:
|
|
|
18374
19721
|
means a missing/gitignored journal.jsonl or a bundle
|
|
18375
19722
|
from the wrong directory, not an intended history
|
|
18376
19723
|
rewrite.
|
|
19724
|
+
--allow-deletions Proceed even though the local bundle drops nodes or
|
|
19725
|
+
edges the hosted project currently has. Without this
|
|
19726
|
+
flag, that refuses outright and names the ids \u2014 it
|
|
19727
|
+
usually means the hosted project moved ahead of your
|
|
19728
|
+
local copy (edited in the app), not an intended
|
|
19729
|
+
deletion. Undoing a restore from a backup is the
|
|
19730
|
+
common case where it IS intended.
|
|
19731
|
+
--no-quality Do not send a quality section: skip the docs/quality/
|
|
19732
|
+
fold AND drop any section the local bundle already
|
|
19733
|
+
carries. Does NOT by itself permit erasing the hosted
|
|
19734
|
+
one \u2014 that needs --allow-quality-loss too, because
|
|
19735
|
+
"don't send mine" and "destroy theirs" are different
|
|
19736
|
+
decisions and only one of them is irreversible.
|
|
19737
|
+
--allow-quality-loss Proceed even though the restore would erase a quality
|
|
19738
|
+
section the hosted project currently has. Without this
|
|
19739
|
+
flag, that refuses outright \u2014 it usually means
|
|
19740
|
+
docs/quality/ was looked for in the wrong place, which
|
|
19741
|
+
--root fixes, rather than an intended wipe.
|
|
19742
|
+
--audit <id> Fold ONE audit's snapshot rather than merging every
|
|
19743
|
+
audit into current state. An id that is not on disk is
|
|
19744
|
+
an error, not an empty section.
|
|
19745
|
+
--root <dir> Where docs/quality/ lives. Default: derived from the
|
|
19746
|
+
bundle's own path, so restoring <repo>/docs/arkaik/
|
|
19747
|
+
bundle.json folds <repo>'s audits whatever directory
|
|
19748
|
+
you run from; only a bundle kept outside that layout
|
|
19749
|
+
falls back to the cwd.
|
|
18377
19750
|
--api <base-url> Override the remote from docs/arkaik/arkaik.json
|
|
18378
19751
|
(also overridable with $ARKAIK_URL).
|
|
18379
19752
|
-h, --help Show this help.
|
|
@@ -18386,8 +19759,8 @@ function fail10(message) {
|
|
|
18386
19759
|
console.error(message);
|
|
18387
19760
|
process.exit(1);
|
|
18388
19761
|
}
|
|
18389
|
-
function fatalResult5(dryRun, message) {
|
|
18390
|
-
return { ok: false, fatal: message, dryRun, requestSent: false };
|
|
19762
|
+
function fatalResult5(dryRun, message, quality = {}) {
|
|
19763
|
+
return { ok: false, fatal: message, dryRun, requestSent: false, ...quality };
|
|
18391
19764
|
}
|
|
18392
19765
|
async function safeJson(res) {
|
|
18393
19766
|
try {
|
|
@@ -18413,7 +19786,9 @@ async function interpretPutResponse(res, ctx) {
|
|
|
18413
19786
|
bundlePath: ctx.bundlePath,
|
|
18414
19787
|
backupPath: ctx.backupPath,
|
|
18415
19788
|
requestSent: true,
|
|
18416
|
-
status: res.status
|
|
19789
|
+
status: res.status,
|
|
19790
|
+
qualityFolded: ctx.qualityFolded,
|
|
19791
|
+
qualityNotice: ctx.qualityNotice
|
|
18417
19792
|
};
|
|
18418
19793
|
const backupNote = backupNoteFor(ctx.backupPath);
|
|
18419
19794
|
if (res.status === 200) {
|
|
@@ -18477,26 +19852,91 @@ async function interpretPutResponse(res, ctx) {
|
|
|
18477
19852
|
}
|
|
18478
19853
|
function writeBackupFile(filePath, content) {
|
|
18479
19854
|
const tmpPath = `${filePath}.tmp-${process.pid}`;
|
|
18480
|
-
|
|
19855
|
+
writeFileSync9(tmpPath, content);
|
|
18481
19856
|
try {
|
|
18482
19857
|
linkSync(tmpPath, filePath);
|
|
18483
19858
|
} finally {
|
|
18484
19859
|
unlinkSync(tmpPath);
|
|
18485
19860
|
}
|
|
18486
19861
|
}
|
|
19862
|
+
function removedIds(before, after) {
|
|
19863
|
+
const kept = /* @__PURE__ */ new Set();
|
|
19864
|
+
for (const item of after) {
|
|
19865
|
+
const id = item?.id;
|
|
19866
|
+
if (typeof id === "string") kept.add(id);
|
|
19867
|
+
}
|
|
19868
|
+
const removed = [];
|
|
19869
|
+
for (const item of before) {
|
|
19870
|
+
const id = item?.id;
|
|
19871
|
+
if (typeof id === "string" && !kept.has(id)) removed.push(id);
|
|
19872
|
+
}
|
|
19873
|
+
return removed;
|
|
19874
|
+
}
|
|
19875
|
+
function listIds(ids, limit = 10) {
|
|
19876
|
+
if (ids.length <= limit) return ids.join(", ");
|
|
19877
|
+
return `${ids.slice(0, limit).join(", ")}, and ${ids.length - limit} more`;
|
|
19878
|
+
}
|
|
19879
|
+
function describeDeletions(removedNodes, removedEdges, bundlePath) {
|
|
19880
|
+
const parts = [];
|
|
19881
|
+
if (removedNodes.length > 0) parts.push(`${removedNodes.length} node${removedNodes.length === 1 ? "" : "s"}`);
|
|
19882
|
+
if (removedEdges.length > 0) parts.push(`${removedEdges.length} edge${removedEdges.length === 1 ? "" : "s"}`);
|
|
19883
|
+
const lines = [
|
|
19884
|
+
`This restore would DELETE ${parts.join(" and ")} the hosted project currently has and ${bundlePath} does not. Nothing was sent.`
|
|
19885
|
+
];
|
|
19886
|
+
if (removedNodes.length > 0) lines.push(` nodes: ${listIds(removedNodes)}`);
|
|
19887
|
+
if (removedEdges.length > 0) lines.push(` edges: ${listIds(removedEdges)}`);
|
|
19888
|
+
lines.push(
|
|
19889
|
+
`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".`
|
|
19890
|
+
);
|
|
19891
|
+
lines.push(
|
|
19892
|
+
`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.`
|
|
19893
|
+
);
|
|
19894
|
+
return lines.join("\n");
|
|
19895
|
+
}
|
|
19896
|
+
function describeQualityLoss(hostedQuality, qualityRoot, noQuality, foldNotice) {
|
|
19897
|
+
const findings = Array.isArray(hostedQuality.findings) ? hostedQuality.findings.length : 0;
|
|
19898
|
+
const assessments = Array.isArray(hostedQuality.assessments) ? hostedQuality.assessments.length : 0;
|
|
19899
|
+
const parts = [];
|
|
19900
|
+
if (findings > 0) parts.push(`${findings} open finding${findings === 1 ? "" : "s"}`);
|
|
19901
|
+
if (assessments > 0) parts.push(`${assessments} assessment${assessments === 1 ? "" : "s"}`);
|
|
19902
|
+
const held = parts.length > 0 ? parts.join(" and ") : "no findings or assessments";
|
|
19903
|
+
const lines = [
|
|
19904
|
+
`This restore would ERASE the hosted project's quality section (${held}). Nothing was sent.`
|
|
19905
|
+
];
|
|
19906
|
+
if (noQuality) {
|
|
19907
|
+
lines.push(
|
|
19908
|
+
`You passed --no-quality, which says "do not send my quality data" \u2014 not "destroy what is already there". Those are different decisions, and this verb has no server-side undo, so the second one has to be typed: re-run with --allow-quality-loss as well if you really mean to wipe it.`
|
|
19909
|
+
);
|
|
19910
|
+
} else {
|
|
19911
|
+
if (foldNotice !== void 0) {
|
|
19912
|
+
lines.push("The outbound bundle has no section because the fold found nothing to build one from:");
|
|
19913
|
+
lines.push(` ${foldNotice}`);
|
|
19914
|
+
}
|
|
19915
|
+
if (!existsSync9(join8(qualityRoot, QUALITY_DIR))) {
|
|
19916
|
+
lines.push(
|
|
19917
|
+
`There is no ${QUALITY_DIR}/ under ${qualityRoot} at all. If this project's sidecars live somewhere else, point --root at that repo and the section is rebuilt rather than removed.`
|
|
19918
|
+
);
|
|
19919
|
+
}
|
|
19920
|
+
lines.push(`If the hosted section really is meant to go, re-run with --allow-quality-loss.`);
|
|
19921
|
+
}
|
|
19922
|
+
return lines.join("\n");
|
|
19923
|
+
}
|
|
18487
19924
|
async function runRestore(options = {}) {
|
|
18488
19925
|
const cwd = options.cwd ?? process.cwd();
|
|
18489
19926
|
const env = options.env ?? process.env;
|
|
18490
19927
|
const dryRun = options.dryRun ?? false;
|
|
18491
19928
|
const allowHistoryLoss = options.allowHistoryLoss ?? false;
|
|
19929
|
+
const allowDeletions = options.allowDeletions ?? false;
|
|
19930
|
+
const noQuality = options.noQuality ?? false;
|
|
19931
|
+
const allowQualityLoss = options.allowQualityLoss ?? false;
|
|
18492
19932
|
const httpClient = options.httpClient ?? DEFAULT_HTTP_CLIENT;
|
|
18493
|
-
const linkPath =
|
|
18494
|
-
if (!
|
|
19933
|
+
const linkPath = join8(cwd, LINK_FILE2);
|
|
19934
|
+
if (!existsSync9(linkPath)) {
|
|
18495
19935
|
return fatalResult5(dryRun, `No ${LINK_FILE2}. Run \`arkaik link\` first \u2014 restore only targets hosted projects.`);
|
|
18496
19936
|
}
|
|
18497
19937
|
let link;
|
|
18498
19938
|
try {
|
|
18499
|
-
link = JSON.parse(
|
|
19939
|
+
link = JSON.parse(readFileSync9(linkPath, "utf8"));
|
|
18500
19940
|
} catch (e) {
|
|
18501
19941
|
return fatalResult5(dryRun, `Could not parse ${LINK_FILE2}: ${e.message}`);
|
|
18502
19942
|
}
|
|
@@ -18506,11 +19946,11 @@ async function runRestore(options = {}) {
|
|
|
18506
19946
|
const encodedProjectId = encodeURIComponent(projectId);
|
|
18507
19947
|
const token = env.ARKAIK_TOKEN;
|
|
18508
19948
|
if (!token) return fatalResult5(dryRun, `ARKAIK_TOKEN is not set. Create a token at ${baseUrl}/settings/tokens and export it.`);
|
|
18509
|
-
const bundlePath =
|
|
18510
|
-
if (!
|
|
19949
|
+
const bundlePath = resolve9(cwd, options.path ?? DEFAULT_BUNDLE_PATH9);
|
|
19950
|
+
if (!existsSync9(bundlePath)) return fatalResult5(dryRun, `No bundle at ${bundlePath}. Run \`arkaik merge\` (or \`arkaik pack\`) first.`);
|
|
18511
19951
|
let localRaw;
|
|
18512
19952
|
try {
|
|
18513
|
-
localRaw = JSON.parse(
|
|
19953
|
+
localRaw = JSON.parse(readFileSync9(bundlePath, "utf8"));
|
|
18514
19954
|
} catch (e) {
|
|
18515
19955
|
return fatalResult5(dryRun, `Could not parse ${bundlePath}: ${e.message}`);
|
|
18516
19956
|
}
|
|
@@ -18520,6 +19960,22 @@ async function runRestore(options = {}) {
|
|
|
18520
19960
|
const local = localRaw;
|
|
18521
19961
|
const journalEvents = loadJournalEvents(local, bundlePath);
|
|
18522
19962
|
const outboundBundle = { ...local, journal: journalEvents };
|
|
19963
|
+
const qualityRoot = resolveQualityRoot(cwd, bundlePath, options.root);
|
|
19964
|
+
let qualityFolded = false;
|
|
19965
|
+
let qualityNotice;
|
|
19966
|
+
if (noQuality) {
|
|
19967
|
+
delete outboundBundle.quality;
|
|
19968
|
+
qualityNotice = "Quality: deleted before sending (--no-quality)";
|
|
19969
|
+
} else {
|
|
19970
|
+
try {
|
|
19971
|
+
const fold = foldQualitySection(outboundBundle, qualityRoot, options.audit);
|
|
19972
|
+
qualityFolded = fold.folded;
|
|
19973
|
+
qualityNotice = fold.notice;
|
|
19974
|
+
} catch (e) {
|
|
19975
|
+
return fatalResult5(dryRun, e.message);
|
|
19976
|
+
}
|
|
19977
|
+
}
|
|
19978
|
+
const qualityFields = { qualityFolded, qualityNotice };
|
|
18523
19979
|
const headers = { Authorization: `Bearer ${token}` };
|
|
18524
19980
|
let version2;
|
|
18525
19981
|
try {
|
|
@@ -18548,7 +20004,7 @@ async function runRestore(options = {}) {
|
|
|
18548
20004
|
} catch (e) {
|
|
18549
20005
|
return { ok: true, dryRun, bundlePath, requestSent: false, errorMessage: `Network error: ${e.message}` };
|
|
18550
20006
|
}
|
|
18551
|
-
return interpretPutResponse(res2, { dryRun, bundlePath, version: version2 });
|
|
20007
|
+
return interpretPutResponse(res2, { dryRun, bundlePath, version: version2, ...qualityFields });
|
|
18552
20008
|
}
|
|
18553
20009
|
let exported;
|
|
18554
20010
|
try {
|
|
@@ -18575,23 +20031,34 @@ async function runRestore(options = {}) {
|
|
|
18575
20031
|
if (journalEvents.length < hostedEventCount && !allowHistoryLoss) {
|
|
18576
20032
|
return fatalResult5(
|
|
18577
20033
|
dryRun,
|
|
18578
|
-
`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
|
|
20034
|
+
`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.`,
|
|
20035
|
+
qualityFields
|
|
18579
20036
|
);
|
|
18580
20037
|
}
|
|
18581
|
-
const
|
|
20038
|
+
const hostedQuality = exportedBundle.quality;
|
|
20039
|
+
if (isQualitySection(hostedQuality) && !isQualitySection(outboundBundle.quality) && !allowQualityLoss) {
|
|
20040
|
+
return fatalResult5(dryRun, describeQualityLoss(hostedQuality, qualityRoot, noQuality, qualityNotice), qualityFields);
|
|
20041
|
+
}
|
|
20042
|
+
const removedNodes = removedIds(exportedBundle.nodes, Array.isArray(local.nodes) ? local.nodes : []);
|
|
20043
|
+
const removedEdges = removedIds(exportedBundle.edges, Array.isArray(local.edges) ? local.edges : []);
|
|
20044
|
+
if ((removedNodes.length > 0 || removedEdges.length > 0) && !allowDeletions) {
|
|
20045
|
+
return fatalResult5(dryRun, describeDeletions(removedNodes, removedEdges, bundlePath), qualityFields);
|
|
20046
|
+
}
|
|
20047
|
+
const backupDir = join8(cwd, "docs", "arkaik", ".backups");
|
|
18582
20048
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
18583
|
-
const backupPath =
|
|
20049
|
+
const backupPath = join8(backupDir, `${stamp}-bundle.json`);
|
|
18584
20050
|
const backupContent = `${JSON.stringify(exported, null, 2)}
|
|
18585
20051
|
`;
|
|
18586
20052
|
try {
|
|
18587
|
-
|
|
20053
|
+
mkdirSync6(backupDir, { recursive: true });
|
|
18588
20054
|
writeBackupFile(backupPath, backupContent);
|
|
18589
|
-
JSON.parse(
|
|
20055
|
+
JSON.parse(readFileSync9(backupPath, "utf8"));
|
|
18590
20056
|
} catch (e) {
|
|
18591
20057
|
return fatalResult5(
|
|
18592
20058
|
dryRun,
|
|
18593
20059
|
`Could not write the pre-restore backup to ${backupPath}: ${e.message}
|
|
18594
|
-
Refusing to restore \u2014 this verb replaces the hosted project's snapshot AND journal, and the backup is the only way back
|
|
20060
|
+
Refusing to restore \u2014 this verb replaces the hosted project's snapshot AND journal, and the backup is the only way back.`,
|
|
20061
|
+
qualityFields
|
|
18595
20062
|
);
|
|
18596
20063
|
}
|
|
18597
20064
|
let res;
|
|
@@ -18611,7 +20078,7 @@ Refusing to restore \u2014 this verb replaces the hosted project's snapshot AND
|
|
|
18611
20078
|
errorMessage: `Network error: ${e.message}. Nothing was sent.${backupNoteFor(backupPath)}`
|
|
18612
20079
|
};
|
|
18613
20080
|
}
|
|
18614
|
-
return interpretPutResponse(res, { dryRun, bundlePath, backupPath, version: version2 });
|
|
20081
|
+
return interpretPutResponse(res, { dryRun, bundlePath, backupPath, version: version2, ...qualityFields });
|
|
18615
20082
|
}
|
|
18616
20083
|
function printDelta(delta) {
|
|
18617
20084
|
if (!delta) return;
|
|
@@ -18625,12 +20092,26 @@ function printDelta(delta) {
|
|
|
18625
20092
|
console.log(
|
|
18626
20093
|
` events ${n("eventsBefore")} -> ${n("eventsAfter")} (+${n("eventsAdded")} -${n("eventsDropped")} ~${n("eventsChanged")}${delta.eventsMalformed ? `, ${delta.eventsMalformed} malformed` : ""})`
|
|
18627
20094
|
);
|
|
20095
|
+
const count = (k) => typeof delta[k] === "number" ? delta[k] : 0;
|
|
20096
|
+
const nodesRemoved = count("nodesRemoved");
|
|
20097
|
+
const edgesRemoved = count("edgesRemoved");
|
|
20098
|
+
if (nodesRemoved > 0 || edgesRemoved > 0) {
|
|
20099
|
+
const parts = [];
|
|
20100
|
+
if (nodesRemoved > 0) parts.push(`${nodesRemoved} node${nodesRemoved === 1 ? "" : "s"}`);
|
|
20101
|
+
if (edgesRemoved > 0) parts.push(`${edgesRemoved} edge${edgesRemoved === 1 ? "" : "s"}`);
|
|
20102
|
+
console.log(
|
|
20103
|
+
` 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.`
|
|
20104
|
+
);
|
|
20105
|
+
}
|
|
18628
20106
|
}
|
|
18629
20107
|
function reportRestore(result) {
|
|
18630
20108
|
if (!result.ok) fail10(`FATAL: ${result.fatal}`);
|
|
18631
20109
|
if (result.backupPath) {
|
|
18632
20110
|
console.log(`Backed up the current hosted project (snapshot + journal) to ${result.backupPath}`);
|
|
18633
20111
|
}
|
|
20112
|
+
if (result.qualityNotice !== void 0) {
|
|
20113
|
+
console.log(result.qualityNotice);
|
|
20114
|
+
}
|
|
18634
20115
|
if (!result.requestSent) {
|
|
18635
20116
|
console.error(result.errorMessage ?? "Restore failed before a request could be sent.");
|
|
18636
20117
|
process.exit(1);
|
|
@@ -18639,7 +20120,9 @@ function reportRestore(result) {
|
|
|
18639
20120
|
if (result.dryRun) {
|
|
18640
20121
|
console.log("[dry-run] server preview \u2014 nothing was written:");
|
|
18641
20122
|
printDelta(result.delta);
|
|
18642
|
-
console.log(
|
|
20123
|
+
console.log(
|
|
20124
|
+
"Re-run without --dry-run to apply \u2014 that run takes the backup, and only that run checks the history, deletion and quality-loss guards (all three read the export, which a dry run never fetches)."
|
|
20125
|
+
);
|
|
18643
20126
|
} else {
|
|
18644
20127
|
console.log(`Restored. New version ${result.version}.`);
|
|
18645
20128
|
printDelta(result.delta);
|
|
@@ -18659,6 +20142,11 @@ function reportRestore(result) {
|
|
|
18659
20142
|
function runRestoreCli(argv) {
|
|
18660
20143
|
let dryRun = false;
|
|
18661
20144
|
let allowHistoryLoss = false;
|
|
20145
|
+
let allowDeletions = false;
|
|
20146
|
+
let noQuality = false;
|
|
20147
|
+
let allowQualityLoss = false;
|
|
20148
|
+
let audit;
|
|
20149
|
+
let root;
|
|
18662
20150
|
let apiBase;
|
|
18663
20151
|
const positionals = [];
|
|
18664
20152
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -18671,6 +20159,24 @@ function runRestoreCli(argv) {
|
|
|
18671
20159
|
dryRun = true;
|
|
18672
20160
|
} else if (arg === "--allow-history-loss") {
|
|
18673
20161
|
allowHistoryLoss = true;
|
|
20162
|
+
} else if (arg === "--allow-deletions") {
|
|
20163
|
+
allowDeletions = true;
|
|
20164
|
+
} else if (arg === "--no-quality") {
|
|
20165
|
+
noQuality = true;
|
|
20166
|
+
} else if (arg === "--allow-quality-loss") {
|
|
20167
|
+
allowQualityLoss = true;
|
|
20168
|
+
} else if (arg === "--audit") {
|
|
20169
|
+
const value = argv[++i];
|
|
20170
|
+
if (value === void 0) fail10(`Missing value for --audit
|
|
20171
|
+
|
|
20172
|
+
${USAGE11}`);
|
|
20173
|
+
audit = value;
|
|
20174
|
+
} else if (arg === "--root") {
|
|
20175
|
+
const value = argv[++i];
|
|
20176
|
+
if (value === void 0) fail10(`Missing value for --root
|
|
20177
|
+
|
|
20178
|
+
${USAGE11}`);
|
|
20179
|
+
root = value;
|
|
18674
20180
|
} else if (arg === "--api") {
|
|
18675
20181
|
const value = argv[++i];
|
|
18676
20182
|
if (value === void 0) fail10(`Missing value for --api
|
|
@@ -18688,28 +20194,33 @@ ${USAGE11}`);
|
|
|
18688
20194
|
if (positionals.length > 1) fail10(`Unexpected argument(s): ${positionals.slice(1).join(" ")}
|
|
18689
20195
|
|
|
18690
20196
|
${USAGE11}`);
|
|
18691
|
-
|
|
20197
|
+
if (noQuality && audit !== void 0) {
|
|
20198
|
+
fail10(`--audit and --no-quality contradict each other: one names an audit to fold, the other removes the section
|
|
20199
|
+
|
|
20200
|
+
${USAGE11}`);
|
|
20201
|
+
}
|
|
20202
|
+
runRestore({ path: positionals[0], dryRun, allowHistoryLoss, allowDeletions, noQuality, allowQualityLoss, audit, root, apiBase }).then((result) => reportRestore(result)).catch((e) => fail10(`FATAL: ${e.message}`));
|
|
18692
20203
|
}
|
|
18693
20204
|
|
|
18694
20205
|
// src/commands/bootstrap.ts
|
|
18695
20206
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
18696
|
-
import { existsSync as
|
|
20207
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync8, renameSync, writeFileSync as writeFileSync13 } from "node:fs";
|
|
18697
20208
|
import path5 from "node:path";
|
|
18698
20209
|
|
|
18699
20210
|
// src/lib/bootstrap/corpus.ts
|
|
18700
20211
|
import { spawnSync } from "node:child_process";
|
|
18701
|
-
import { existsSync as
|
|
20212
|
+
import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "node:fs";
|
|
18702
20213
|
import path2 from "node:path";
|
|
18703
20214
|
|
|
18704
20215
|
// src/lib/bootstrap/paths.ts
|
|
18705
|
-
import { existsSync as
|
|
20216
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
|
|
18706
20217
|
import path from "node:path";
|
|
18707
20218
|
var BOOTSTRAP_ROOT = ".arkaik";
|
|
18708
20219
|
var CORPUS_DIR = ".arkaik/corpus";
|
|
18709
20220
|
var PLAN_DIR = ".arkaik/bootstrap";
|
|
18710
20221
|
var FRAGMENTS_DIR = ".arkaik/bootstrap/fragments";
|
|
18711
20222
|
var MANIFEST_FILE = ".arkaik/bootstrap/manifest.json";
|
|
18712
|
-
var
|
|
20223
|
+
var PROFILE_FILE2 = ".arkaik/bootstrap/profile.json";
|
|
18713
20224
|
var PRS_FILE = ".arkaik/corpus/prs.jsonl";
|
|
18714
20225
|
var DOCS_FILE = ".arkaik/corpus/docs.json";
|
|
18715
20226
|
var SURFACES_FILE = ".arkaik/corpus/surfaces.json";
|
|
@@ -18717,16 +20228,16 @@ function at(cwd, relative) {
|
|
|
18717
20228
|
return path.join(cwd, relative);
|
|
18718
20229
|
}
|
|
18719
20230
|
function ensureDir(dirPath) {
|
|
18720
|
-
|
|
20231
|
+
mkdirSync7(dirPath, { recursive: true });
|
|
18721
20232
|
}
|
|
18722
20233
|
function ensureGitignored(cwd) {
|
|
18723
20234
|
const file2 = path.join(cwd, ".gitignore");
|
|
18724
20235
|
const line2 = `${BOOTSTRAP_ROOT}/`;
|
|
18725
|
-
const current =
|
|
20236
|
+
const current = existsSync10(file2) ? readFileSync10(file2, "utf8") : "";
|
|
18726
20237
|
const ignored = current.split("\n").map((l) => l.trim()).some((l) => l === line2 || l === BOOTSTRAP_ROOT);
|
|
18727
20238
|
if (ignored) return false;
|
|
18728
20239
|
const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
|
|
18729
|
-
|
|
20240
|
+
writeFileSync10(file2, `${current}${prefix}${line2}
|
|
18730
20241
|
`);
|
|
18731
20242
|
return true;
|
|
18732
20243
|
}
|
|
@@ -18823,7 +20334,7 @@ function fetchPrsViaGit(cwd) {
|
|
|
18823
20334
|
function walk(root, cwd, out) {
|
|
18824
20335
|
let entries;
|
|
18825
20336
|
try {
|
|
18826
|
-
entries =
|
|
20337
|
+
entries = readdirSync3(root, { withFileTypes: true });
|
|
18827
20338
|
} catch {
|
|
18828
20339
|
return;
|
|
18829
20340
|
}
|
|
@@ -18841,7 +20352,7 @@ function listFiles(cwd) {
|
|
|
18841
20352
|
}
|
|
18842
20353
|
function buildDocsManifest(cwd, files) {
|
|
18843
20354
|
return files.filter((f) => f.startsWith("docs/") && f.endsWith(".md")).map((f) => {
|
|
18844
|
-
const text =
|
|
20355
|
+
const text = readFileSync11(path2.join(cwd, f), "utf8");
|
|
18845
20356
|
const heading = /^#\s+(.+)$/m.exec(text);
|
|
18846
20357
|
return { path: f, title: heading ? heading[1].trim() : path2.basename(f, ".md") };
|
|
18847
20358
|
});
|
|
@@ -18856,7 +20367,7 @@ function buildSurfaceInventory(files) {
|
|
|
18856
20367
|
}
|
|
18857
20368
|
function buildCorpus(options) {
|
|
18858
20369
|
const { cwd } = options;
|
|
18859
|
-
const raw = options.fromJson ? JSON.parse(
|
|
20370
|
+
const raw = options.fromJson ? JSON.parse(readFileSync11(path2.resolve(cwd, options.fromJson), "utf8")) : options.fromGit ? fetchPrsViaGit(cwd) : fetchPrsViaGh(cwd, options.limit);
|
|
18860
20371
|
let prs = normalizePrs(raw);
|
|
18861
20372
|
let sinceDroppedUndated = 0;
|
|
18862
20373
|
if (options.since) {
|
|
@@ -18877,21 +20388,21 @@ function buildCorpus(options) {
|
|
|
18877
20388
|
const docs = buildDocsManifest(cwd, files);
|
|
18878
20389
|
const surfaces = buildSurfaceInventory(files);
|
|
18879
20390
|
ensureDir(at(cwd, CORPUS_DIR));
|
|
18880
|
-
|
|
18881
|
-
|
|
20391
|
+
writeFileSync11(at(cwd, PRS_FILE), prs.map((pr) => JSON.stringify(pr)).join("\n") + (prs.length ? "\n" : ""));
|
|
20392
|
+
writeFileSync11(at(cwd, DOCS_FILE), `${JSON.stringify(docs, null, 2)}
|
|
18882
20393
|
`);
|
|
18883
|
-
|
|
20394
|
+
writeFileSync11(at(cwd, SURFACES_FILE), `${JSON.stringify(surfaces, null, 2)}
|
|
18884
20395
|
`);
|
|
18885
20396
|
return { prs: prs.length, docs: docs.length, surfaces: surfaces.length, sinceDroppedUndated };
|
|
18886
20397
|
}
|
|
18887
20398
|
function readCorpusPrs(cwd) {
|
|
18888
20399
|
const file2 = at(cwd, PRS_FILE);
|
|
18889
|
-
if (!
|
|
18890
|
-
return
|
|
20400
|
+
if (!existsSync11(file2)) return [];
|
|
20401
|
+
return readFileSync11(file2, "utf8").split("\n").filter(Boolean).map((line2) => JSON.parse(line2));
|
|
18891
20402
|
}
|
|
18892
20403
|
|
|
18893
20404
|
// src/lib/bootstrap/fragments.ts
|
|
18894
|
-
import { existsSync as
|
|
20405
|
+
import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
|
|
18895
20406
|
import path3 from "node:path";
|
|
18896
20407
|
function isArrayOfObjects(value) {
|
|
18897
20408
|
return value === void 0 || Array.isArray(value) && value.every((v) => typeof v === "object" && v !== null && !Array.isArray(v));
|
|
@@ -18917,13 +20428,13 @@ function loadFragments(cwd, manifest) {
|
|
|
18917
20428
|
problems.push({ unit: String(unit2.id), message: err instanceof Error ? err.message : String(err) });
|
|
18918
20429
|
continue;
|
|
18919
20430
|
}
|
|
18920
|
-
if (!
|
|
20431
|
+
if (!existsSync12(file2)) {
|
|
18921
20432
|
missing.push(unit2.id);
|
|
18922
20433
|
continue;
|
|
18923
20434
|
}
|
|
18924
20435
|
let parsed;
|
|
18925
20436
|
try {
|
|
18926
|
-
parsed = JSON.parse(
|
|
20437
|
+
parsed = JSON.parse(readFileSync12(file2, "utf8"));
|
|
18927
20438
|
} catch (err) {
|
|
18928
20439
|
problems.push({ unit: unit2.id, message: `not valid JSON: ${err instanceof Error ? err.message : "parse error"}` });
|
|
18929
20440
|
continue;
|
|
@@ -18960,7 +20471,7 @@ function renderIndex(bundle) {
|
|
|
18960
20471
|
}
|
|
18961
20472
|
|
|
18962
20473
|
// src/lib/bootstrap/manifest.ts
|
|
18963
|
-
import { existsSync as
|
|
20474
|
+
import { existsSync as existsSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync12 } from "node:fs";
|
|
18964
20475
|
import path4 from "node:path";
|
|
18965
20476
|
|
|
18966
20477
|
// src/lib/bootstrap/era-window.ts
|
|
@@ -19068,19 +20579,19 @@ function assertValidProfile(profile) {
|
|
|
19068
20579
|
|
|
19069
20580
|
// src/lib/bootstrap/manifest.ts
|
|
19070
20581
|
function readProfile(cwd) {
|
|
19071
|
-
const file2 = at(cwd,
|
|
19072
|
-
if (!
|
|
20582
|
+
const file2 = at(cwd, PROFILE_FILE2);
|
|
20583
|
+
if (!existsSync13(file2)) return null;
|
|
19073
20584
|
try {
|
|
19074
|
-
return JSON.parse(
|
|
20585
|
+
return JSON.parse(readFileSync13(file2, "utf8"));
|
|
19075
20586
|
} catch (err) {
|
|
19076
|
-
throw new Error(`cannot read ${
|
|
20587
|
+
throw new Error(`cannot read ${PROFILE_FILE2}: ${err instanceof Error ? err.message : String(err)}`);
|
|
19077
20588
|
}
|
|
19078
20589
|
}
|
|
19079
20590
|
function readManifest(cwd) {
|
|
19080
20591
|
const file2 = at(cwd, MANIFEST_FILE);
|
|
19081
|
-
if (!
|
|
20592
|
+
if (!existsSync13(file2)) return null;
|
|
19082
20593
|
try {
|
|
19083
|
-
return JSON.parse(
|
|
20594
|
+
return JSON.parse(readFileSync13(file2, "utf8"));
|
|
19084
20595
|
} catch (err) {
|
|
19085
20596
|
throw new Error(`cannot read ${MANIFEST_FILE}: ${err instanceof Error ? err.message : String(err)}`);
|
|
19086
20597
|
}
|
|
@@ -19088,15 +20599,15 @@ function readManifest(cwd) {
|
|
|
19088
20599
|
function writeManifest(cwd, manifest) {
|
|
19089
20600
|
ensureDir(at(cwd, PLAN_DIR));
|
|
19090
20601
|
ensureDir(at(cwd, FRAGMENTS_DIR));
|
|
19091
|
-
|
|
20602
|
+
writeFileSync12(at(cwd, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
|
|
19092
20603
|
`);
|
|
19093
20604
|
}
|
|
19094
20605
|
function detectMode(cwd, bundlePath) {
|
|
19095
20606
|
const file2 = path4.resolve(cwd, bundlePath);
|
|
19096
|
-
if (!
|
|
20607
|
+
if (!existsSync13(file2)) return "greenfield";
|
|
19097
20608
|
let parsed;
|
|
19098
20609
|
try {
|
|
19099
|
-
parsed = JSON.parse(
|
|
20610
|
+
parsed = JSON.parse(readFileSync13(file2, "utf8"));
|
|
19100
20611
|
} catch (err) {
|
|
19101
20612
|
throw new Error(`cannot read bundle at ${bundlePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
19102
20613
|
}
|
|
@@ -19320,7 +20831,7 @@ function mergeJournal(base, fresh) {
|
|
|
19320
20831
|
}
|
|
19321
20832
|
|
|
19322
20833
|
// src/lib/bootstrap/merge.ts
|
|
19323
|
-
function
|
|
20834
|
+
function asArray2(value) {
|
|
19324
20835
|
return Array.isArray(value) ? value : [];
|
|
19325
20836
|
}
|
|
19326
20837
|
function mergeFragments(input) {
|
|
@@ -19328,14 +20839,14 @@ function mergeFragments(input) {
|
|
|
19328
20839
|
const projectId = String(input.base.project?.id ?? "");
|
|
19329
20840
|
const nodes = /* @__PURE__ */ new Map();
|
|
19330
20841
|
const nodeOrigin = /* @__PURE__ */ new Map();
|
|
19331
|
-
for (const node of
|
|
20842
|
+
for (const node of asArray2(input.base.nodes)) {
|
|
19332
20843
|
const id = String(node.id);
|
|
19333
20844
|
nodes.set(id, { ...node });
|
|
19334
20845
|
nodeOrigin.set(id, "(already in the bundle)");
|
|
19335
20846
|
}
|
|
19336
20847
|
const edges = /* @__PURE__ */ new Map();
|
|
19337
20848
|
const edgeOrigin = /* @__PURE__ */ new Map();
|
|
19338
|
-
for (const edge of
|
|
20849
|
+
for (const edge of asArray2(input.base.edges)) {
|
|
19339
20850
|
const id = String(edge.id ?? edgeId(String(edge.source_id), String(edge.target_id)));
|
|
19340
20851
|
edges.set(id, { ...edge });
|
|
19341
20852
|
edgeOrigin.set(id, "(already in the bundle)");
|
|
@@ -19549,7 +21060,7 @@ function mergeFragments(input) {
|
|
|
19549
21060
|
}
|
|
19550
21061
|
|
|
19551
21062
|
// src/lib/bootstrap/slice.ts
|
|
19552
|
-
import { existsSync as
|
|
21063
|
+
import { existsSync as existsSync14, readFileSync as readFileSync14 } from "node:fs";
|
|
19553
21064
|
|
|
19554
21065
|
// src/lib/bootstrap/body-budget.ts
|
|
19555
21066
|
var LAB_NOTE_HEADING_RE = /^##\s+Lab Note.*$/m;
|
|
@@ -19591,8 +21102,8 @@ function boundBody(body) {
|
|
|
19591
21102
|
|
|
19592
21103
|
// src/lib/bootstrap/slice.ts
|
|
19593
21104
|
function readJsonArray(file2) {
|
|
19594
|
-
if (!
|
|
19595
|
-
const parsed = JSON.parse(
|
|
21105
|
+
if (!existsSync14(file2)) return [];
|
|
21106
|
+
const parsed = JSON.parse(readFileSync14(file2, "utf8"));
|
|
19596
21107
|
return Array.isArray(parsed) ? parsed : [];
|
|
19597
21108
|
}
|
|
19598
21109
|
function toPosix(value) {
|
|
@@ -19613,7 +21124,7 @@ function eraWindows(cwd, slugs) {
|
|
|
19613
21124
|
const era = bySlug.get(slug);
|
|
19614
21125
|
if (!era) {
|
|
19615
21126
|
throw new Error(
|
|
19616
|
-
`era "${slug}" is not declared in ${
|
|
21127
|
+
`era "${slug}" is not declared in ${PROFILE_FILE2}'s "eras" list, but a work unit's slice references it. profile.json may have been edited (or the era removed) since \`arkaik bootstrap plan\` last ran. Restore the era in profile.json and re-run \`arkaik bootstrap plan\`, or reconcile the manifest by hand.`
|
|
19617
21128
|
);
|
|
19618
21129
|
}
|
|
19619
21130
|
assertEraWindow(era);
|
|
@@ -19702,7 +21213,7 @@ ${usage}`);
|
|
|
19702
21213
|
}
|
|
19703
21214
|
function writeFileAtomic(filePath, content) {
|
|
19704
21215
|
const tmpPath = `${filePath}.tmp-${process.pid}`;
|
|
19705
|
-
|
|
21216
|
+
writeFileSync13(tmpPath, content);
|
|
19706
21217
|
renameSync(tmpPath, filePath);
|
|
19707
21218
|
}
|
|
19708
21219
|
function runCorpus(argv) {
|
|
@@ -19737,7 +21248,7 @@ ${CORPUS_USAGE}`);
|
|
|
19737
21248
|
${CORPUS_USAGE}`);
|
|
19738
21249
|
}
|
|
19739
21250
|
}
|
|
19740
|
-
if (!
|
|
21251
|
+
if (!existsSync15(path5.join(cwd, ".git"))) {
|
|
19741
21252
|
fail11("`arkaik bootstrap corpus` must run from the repository root (no .git here).");
|
|
19742
21253
|
}
|
|
19743
21254
|
try {
|
|
@@ -19799,7 +21310,7 @@ ${PLAN_USAGE}`);
|
|
|
19799
21310
|
|
|
19800
21311
|
${PLAN_USAGE}`);
|
|
19801
21312
|
}
|
|
19802
|
-
if (!
|
|
21313
|
+
if (!existsSync15(path5.join(cwd, ".git"))) {
|
|
19803
21314
|
fail11("`arkaik bootstrap plan` must run from the repository root (no .git here).");
|
|
19804
21315
|
}
|
|
19805
21316
|
try {
|
|
@@ -19938,7 +21449,7 @@ ${MERGE_USAGE}`);
|
|
|
19938
21449
|
}
|
|
19939
21450
|
try {
|
|
19940
21451
|
const bundlePath = path5.resolve(cwd, manifest.bundle);
|
|
19941
|
-
const base =
|
|
21452
|
+
const base = existsSync15(bundlePath) ? readBundle(bundlePath) : {
|
|
19942
21453
|
schema_version: 3,
|
|
19943
21454
|
project: {
|
|
19944
21455
|
id: path5.basename(cwd),
|
|
@@ -19974,7 +21485,7 @@ ${MERGE_USAGE}`);
|
|
|
19974
21485
|
const journalPath = journalPathFor(bundlePath);
|
|
19975
21486
|
const journalText = result.journal.map((e) => JSON.stringify(e)).join("\n") + (result.journal.length ? "\n" : "");
|
|
19976
21487
|
if (!dryRun) {
|
|
19977
|
-
|
|
21488
|
+
mkdirSync8(path5.dirname(bundlePath), { recursive: true });
|
|
19978
21489
|
writeFileAtomic(bundlePath, serialized);
|
|
19979
21490
|
writeFileAtomic(journalPath, journalText);
|
|
19980
21491
|
}
|
|
@@ -20023,8 +21534,1039 @@ ${USAGE12}`);
|
|
|
20023
21534
|
}
|
|
20024
21535
|
}
|
|
20025
21536
|
|
|
21537
|
+
// ../schema/src/cli/kritik-overlay.ts
|
|
21538
|
+
var ANCHOR_KEYS = ["l0", "l1", "l2", "l3", "l4"];
|
|
21539
|
+
var CRITERION_TEMPLATE = {
|
|
21540
|
+
id: "X-01",
|
|
21541
|
+
domain: "ARC",
|
|
21542
|
+
subcategory: "conventions",
|
|
21543
|
+
name: "Short criterion name",
|
|
21544
|
+
question: "The criterion as one question an auditor can actually answer?",
|
|
21545
|
+
definition: "What good looks like on this surface, concretely.",
|
|
21546
|
+
rationale: "Why this matters for this product in particular.",
|
|
21547
|
+
applies_to: ["web"],
|
|
21548
|
+
level_anchors: {
|
|
21549
|
+
l0: "Not addressed at all.",
|
|
21550
|
+
l1: "Addressed accidentally or in one spot; no visible intent.",
|
|
21551
|
+
l2: "Deliberately addressed; visible intent; known gaps.",
|
|
21552
|
+
l3: "Systematic across the surface; tested or reviewed.",
|
|
21553
|
+
l4: "Enforced by automation or a CI gate; drift is detected, not hoped against."
|
|
21554
|
+
},
|
|
21555
|
+
default_impact: 3,
|
|
21556
|
+
weight: 1,
|
|
21557
|
+
references: [],
|
|
21558
|
+
checklist: ["The grep, file, or flow an auditor should actually run."],
|
|
21559
|
+
signals: ["A check that can run between audits and fail mechanically."],
|
|
21560
|
+
remediation: "The typical fix path.",
|
|
21561
|
+
issue: {
|
|
21562
|
+
title_template: "[Quality] X-01 <name> at level {observed_level} on {surface} (target {target_level})",
|
|
21563
|
+
labels: ["quality"],
|
|
21564
|
+
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}"
|
|
21565
|
+
}
|
|
21566
|
+
};
|
|
21567
|
+
function buildCriterion(draft) {
|
|
21568
|
+
for (const [label, value] of [
|
|
21569
|
+
["id", draft.id],
|
|
21570
|
+
["domain", draft.domain],
|
|
21571
|
+
["name", draft.name],
|
|
21572
|
+
["question", draft.question]
|
|
21573
|
+
]) {
|
|
21574
|
+
if (typeof value !== "string" || value.trim() === "") throw new Error(`--${label} is required`);
|
|
21575
|
+
}
|
|
21576
|
+
if (draft.appliesTo.length === 0) throw new Error(`--applies-to is required \u2014 a criterion applies to at least one surface`);
|
|
21577
|
+
const missing = ANCHOR_KEYS.filter((key) => !draft.anchors[key]);
|
|
21578
|
+
if (missing.length > 0) {
|
|
21579
|
+
throw new Error(
|
|
21580
|
+
`missing anchors ${missing.join(", ")}.
|
|
21581
|
+
All five are required: a criterion without observable descriptions of each level cannot be scored consistently,
|
|
21582
|
+
and an inconsistently scored criterion makes its whole row incomparable.`
|
|
21583
|
+
);
|
|
21584
|
+
}
|
|
21585
|
+
const weight = draft.weight ?? 1;
|
|
21586
|
+
if (!Number.isInteger(weight) || weight < 1 || weight > 3) throw new Error(`--weight must be 1, 2 or 3`);
|
|
21587
|
+
const impact = draft.impact ?? 3;
|
|
21588
|
+
if (!Number.isInteger(impact) || impact < 1 || impact > 5) throw new Error(`--impact must be 1-5`);
|
|
21589
|
+
const { id, name } = draft;
|
|
21590
|
+
return {
|
|
21591
|
+
id,
|
|
21592
|
+
domain: draft.domain,
|
|
21593
|
+
...draft.subcategory ? { subcategory: draft.subcategory } : {},
|
|
21594
|
+
name,
|
|
21595
|
+
question: draft.question,
|
|
21596
|
+
...draft.definition ? { definition: draft.definition } : {},
|
|
21597
|
+
...draft.rationale ? { rationale: draft.rationale } : {},
|
|
21598
|
+
applies_to: draft.appliesTo,
|
|
21599
|
+
level_anchors: draft.anchors,
|
|
21600
|
+
default_impact: impact,
|
|
21601
|
+
weight,
|
|
21602
|
+
references: [],
|
|
21603
|
+
checklist: draft.checklist ?? [],
|
|
21604
|
+
signals: draft.signals ?? [],
|
|
21605
|
+
...draft.remediation ? { remediation: draft.remediation } : {},
|
|
21606
|
+
issue: {
|
|
21607
|
+
title_template: `[Quality] ${id} ${name} at level {observed_level} on {surface} (target {target_level})`,
|
|
21608
|
+
labels: draft.labels && draft.labels.length > 0 ? draft.labels : ["quality"],
|
|
21609
|
+
body_skeleton: `## Quality finding: ${id} ${name}
|
|
21610
|
+
|
|
21611
|
+
**Surface:** {surface}
|
|
21612
|
+
**Observed level:** {observed_level}
|
|
21613
|
+
**Target level:** {target_level}
|
|
21614
|
+
**Severity seed:** impact {impact} x likelihood {likelihood}
|
|
21615
|
+
|
|
21616
|
+
### Evidence
|
|
21617
|
+
{evidence_bullets_with_file_paths}
|
|
21618
|
+
|
|
21619
|
+
### Risk
|
|
21620
|
+
{risk_narrative}
|
|
21621
|
+
|
|
21622
|
+
### Remediation
|
|
21623
|
+
- [ ] {remediation_step_1}
|
|
21624
|
+
|
|
21625
|
+
### Acceptance criteria
|
|
21626
|
+
- [ ] Target anchor holds: {target_anchor_text}`
|
|
21627
|
+
}
|
|
21628
|
+
};
|
|
21629
|
+
}
|
|
21630
|
+
function addCriterionToOverlay(root, pack, criterion, options = {}) {
|
|
21631
|
+
if (typeof criterion.id !== "string" || criterion.id === "") throw new Error(`the criterion has no id`);
|
|
21632
|
+
if ((pack.criteria ?? []).some((c) => c.id === criterion.id) && !options.force) {
|
|
21633
|
+
throw new Error(
|
|
21634
|
+
`"${criterion.id}" is already a pack criterion.
|
|
21635
|
+
Overriding it changes what every score recorded against that id means.
|
|
21636
|
+
Use a project-reserved id (X-01, X-02, \u2026) instead, or pass --force if the override is deliberate.`
|
|
21637
|
+
);
|
|
21638
|
+
}
|
|
21639
|
+
const path6 = overlayPath(root);
|
|
21640
|
+
const overlay = loadOverlay(root) ?? { extends: pack.version, criteria: [] };
|
|
21641
|
+
overlay.criteria = Array.isArray(overlay.criteria) ? overlay.criteria : [];
|
|
21642
|
+
const at2 = overlay.criteria.findIndex((c) => c?.id === criterion.id);
|
|
21643
|
+
if (at2 >= 0 && !options.force) {
|
|
21644
|
+
throw new Error(`"${criterion.id}" is already in the overlay \u2014 pass --force to replace it`);
|
|
21645
|
+
}
|
|
21646
|
+
if (at2 >= 0) overlay.criteria[at2] = criterion;
|
|
21647
|
+
else overlay.criteria.push(criterion);
|
|
21648
|
+
const domainCode = criterion.domain;
|
|
21649
|
+
const knownDomain = (pack.domains ?? []).some((d) => d.code === domainCode) || (overlay.domains ?? []).some((d) => d.code === domainCode);
|
|
21650
|
+
let addedDomain;
|
|
21651
|
+
if (!knownDomain) {
|
|
21652
|
+
if (!options.domainName) {
|
|
21653
|
+
throw new Error(
|
|
21654
|
+
`"${domainCode}" is not a pack domain and the overlay does not define it.
|
|
21655
|
+
Pass --domain-name "<display name>" to define it, or use an existing domain code.`
|
|
21656
|
+
);
|
|
21657
|
+
}
|
|
21658
|
+
overlay.domains = [...overlay.domains ?? [], { code: domainCode, name: options.domainName }];
|
|
21659
|
+
addedDomain = domainCode;
|
|
21660
|
+
}
|
|
21661
|
+
writeJson(path6, overlay);
|
|
21662
|
+
return { path: path6, overlay, replaced: at2 >= 0, ...addedDomain !== void 0 ? { addedDomain } : {} };
|
|
21663
|
+
}
|
|
21664
|
+
|
|
21665
|
+
// src/commands/kritik.ts
|
|
21666
|
+
import { existsSync as existsSync16, readFileSync as readFileSync15 } from "node:fs";
|
|
21667
|
+
var USAGE13 = `arkaik kritik <subcommand> [options]
|
|
21668
|
+
|
|
21669
|
+
Audit this product's quality with the Kritik framework: a maturity level per
|
|
21670
|
+
criterion per surface backed by evidence, findings carrying risk and cost, and
|
|
21671
|
+
a comparative matrix whose caps stop one open Critical being averaged into a B.
|
|
21672
|
+
|
|
21673
|
+
Subcommands:
|
|
21674
|
+
profile Pick this project's surfaces (writes docs/quality/profile.json).
|
|
21675
|
+
score <c> <s> <lvl> Record one maturity level, with its evidence.
|
|
21676
|
+
finding open ... Open a finding on a (criterion x surface) cell.
|
|
21677
|
+
finding resolve <id> Close it because the fix merged.
|
|
21678
|
+
finding accept <id> Accept it as a known, owned risk.
|
|
21679
|
+
matrix [audit-id] Roll an audit up (writes matrix.json).
|
|
21680
|
+
signals The signal pack, and what has tripped since the last audit.
|
|
21681
|
+
regressions What got worse between two audits.
|
|
21682
|
+
issue <criterion> Print the prefilled GitHub issue skeleton.
|
|
21683
|
+
criterion add ... Add a project-specific criterion to the overlay.
|
|
21684
|
+
|
|
21685
|
+
Common options:
|
|
21686
|
+
--root <dir> Repo root holding docs/quality/ (default: the current directory).
|
|
21687
|
+
--actor <name> Who is writing (default: ${KRITIK_ACTOR}). Recorded on every quality.* event.
|
|
21688
|
+
--bundle <path> The Arkaik bundle whose journal receives events
|
|
21689
|
+
(default: docs/arkaik/bundle.json; skipped when absent).
|
|
21690
|
+
--no-journal Write the audit files only; append no journal events.
|
|
21691
|
+
-h, --help Show this help, or a subcommand's.
|
|
21692
|
+
|
|
21693
|
+
Run "arkaik kritik <subcommand> --help" for the flags each one takes.`;
|
|
21694
|
+
var SCORE_USAGE = `arkaik kritik score <criterion> <surface> <level> --evidence <text|@file> [options]
|
|
21695
|
+
|
|
21696
|
+
Record one (criterion x surface) maturity level. A cell holds exactly one level,
|
|
21697
|
+
so re-scoring replaces in place \u2014 the correction reads as a correction.
|
|
21698
|
+
|
|
21699
|
+
Arguments:
|
|
21700
|
+
criterion A criterion id from the pack or this project's overlay (e.g. SEC-01).
|
|
21701
|
+
surface A surface id declared in docs/quality/profile.json.
|
|
21702
|
+
level 0-4. Absent from the file means N/A; 0 means "not addressed".
|
|
21703
|
+
|
|
21704
|
+
Options:
|
|
21705
|
+
--evidence <e> file:line / config citations. \`@path\` reads them from a file.
|
|
21706
|
+
Required: a score without a citation is an opinion.
|
|
21707
|
+
--audit <id> The audit run (default: the newest, or the current YYYY-MM).
|
|
21708
|
+
--commit <sha> The commit the evidence is pinned to.
|
|
21709
|
+
|
|
21710
|
+
Writes docs/quality/audits/<id>/scores.json`;
|
|
21711
|
+
var FINDING_USAGE = `arkaik kritik finding <open|resolve|accept> [options]
|
|
21712
|
+
|
|
21713
|
+
open <criterion> <surface> --title <t> --impact <1-5> --likelihood <1-5>
|
|
21714
|
+
--cost <S|M|L|XL> --evidence <text|@file> [--detail <d>] [--remediation <r>]
|
|
21715
|
+
[--nodes <id,id>] [--issue-url <u>] [--id <finding-id>] [--audit <id>]
|
|
21716
|
+
|
|
21717
|
+
Opens a finding and appends quality.finding.opened. Severity and priority are
|
|
21718
|
+
printed, never stored \u2014 they are derived from impact x likelihood and cost, so
|
|
21719
|
+
they can never drift from the numbers behind them. The surface may be
|
|
21720
|
+
"${CROSS_SURFACE_ID}" for a defect that belongs to the contract between surfaces.
|
|
21721
|
+
|
|
21722
|
+
resolve <finding-id> [--by <pr-or-commit-url>]
|
|
21723
|
+
|
|
21724
|
+
Marks it resolved and appends quality.finding.resolved. Searched across every
|
|
21725
|
+
audit: a finding opened in 2026-08 is routinely fixed during 2026-09.
|
|
21726
|
+
|
|
21727
|
+
accept <finding-id> --note <why>
|
|
21728
|
+
|
|
21729
|
+
Records it as a known, owned risk. The note is required \u2014 an accepted risk is a
|
|
21730
|
+
decision and reads like one. No journal event: acceptance is a state of the
|
|
21731
|
+
finding, not something that happened to the product.`;
|
|
21732
|
+
var MATRIX_USAGE = `arkaik kritik matrix [audit-id] [--json] [--record]
|
|
21733
|
+
|
|
21734
|
+
Roll one audit up into its comparative matrix and write matrix.json \u2014 the only
|
|
21735
|
+
thing that may write that file.
|
|
21736
|
+
|
|
21737
|
+
audit-id The audit under docs/quality/audits/ (default: the newest).
|
|
21738
|
+
--json Print matrix.json instead of the markdown table.
|
|
21739
|
+
--record Also append quality.audit.completed, carrying these scores
|
|
21740
|
+
and counts. Do this once per finished audit.`;
|
|
21741
|
+
var SIGNALS_USAGE = `arkaik kritik signals [--surface <s>] [--criterion <c>] [--domain <CODE>] [--json]
|
|
21742
|
+
arkaik kritik signals --trip <criterion> --surface <s> --signal <index|text> [--detail <d>]
|
|
21743
|
+
|
|
21744
|
+
Signals are the cheap checks between full audits: a grep that must return
|
|
21745
|
+
nothing, a CI job that must exist. They are statements to check, not commands to
|
|
21746
|
+
run \u2014 the pack spans greps, CI introspection and database queries, so this
|
|
21747
|
+
prints the run sheet rather than pretending to execute it.
|
|
21748
|
+
|
|
21749
|
+
Exits 1 when anything has tripped since the last recorded audit, which is what
|
|
21750
|
+
makes it usable as a CI step.
|
|
21751
|
+
|
|
21752
|
+
--trip Record one as tripped: appends quality.signal.tripped.
|
|
21753
|
+
--signal takes the row's index from the run sheet, or the text.
|
|
21754
|
+
--json The full run sheet as JSON (what an agent should read).`;
|
|
21755
|
+
var REGRESSIONS_USAGE = `arkaik kritik regressions [--from <audit>] [--to <audit>] [--record] [--json]
|
|
21756
|
+
|
|
21757
|
+
What got worse between two audits: a cell whose maturity dropped, a cell that
|
|
21758
|
+
gained an open Critical or High finding, a finding that was resolved and is open
|
|
21759
|
+
again. Cells scored in only one of the two audits are not compared \u2014 a
|
|
21760
|
+
half-finished audit is not a regression.
|
|
21761
|
+
|
|
21762
|
+
Exits 1 when anything regressed, which is what makes it usable as a CI step or a
|
|
21763
|
+
scheduled routine.
|
|
21764
|
+
|
|
21765
|
+
--from <audit> The older reading (default: the audit before --to).
|
|
21766
|
+
--to <audit> The newer reading (default: the newest on disk).
|
|
21767
|
+
--record Append one quality.signal.tripped per regression.
|
|
21768
|
+
--json The full list as JSON.`;
|
|
21769
|
+
var ISSUE_USAGE = `arkaik kritik issue <criterion> --surface <s> [--level <n>] [--finding <id>]
|
|
21770
|
+
|
|
21771
|
+
Print the criterion's GitHub issue skeleton, filled as far as what we know
|
|
21772
|
+
allows. Placeholders we cannot fill are left standing \u2014 a skeleton is a form to
|
|
21773
|
+
finish, and an empty "### Risk" reads as "no risk" where {risk_narrative} reads
|
|
21774
|
+
as "your turn".
|
|
21775
|
+
|
|
21776
|
+
--level <n> The observed maturity, so the anchors fill in.
|
|
21777
|
+
--finding <id> Fill the risk numbers, evidence and narrative from a finding.
|
|
21778
|
+
This is the P0/P1 path: the issue exists because a defect does.`;
|
|
21779
|
+
var CRITERION_USAGE = `arkaik kritik criterion add --id <ID> --domain <CODE> --name <t> --question <q> \\
|
|
21780
|
+
--applies-to <s1,s2> --anchor l0=<text> ... --anchor l4=<text> [options]
|
|
21781
|
+
|
|
21782
|
+
Add a criterion the pack does not have to this project's overlay
|
|
21783
|
+
(docs/quality/criteria.custom.json). A pack upgrade never touches it.
|
|
21784
|
+
|
|
21785
|
+
--id A project-reserved id, e.g. X-01. Never a pack id.
|
|
21786
|
+
--domain Owning domain code \u2014 an existing one (SEC, PRV, \u2026) or your own.
|
|
21787
|
+
--domain-name Display name, required only when --domain is a new code.
|
|
21788
|
+
--anchor lN=<t> What each level looks like HERE. All five are required: a
|
|
21789
|
+
criterion scored inconsistently makes its whole row incomparable.
|
|
21790
|
+
--weight <1-3> Weight in the domain roll-up (default 1).
|
|
21791
|
+
--impact <1-5> Seeds finding severity (default 3).
|
|
21792
|
+
--signal <s> A mechanically checkable hook; repeatable.
|
|
21793
|
+
--check <s> A concrete audit step; repeatable.
|
|
21794
|
+
--label <s> An issue label; repeatable (default: quality).
|
|
21795
|
+
--from <file> Read a complete criterion from JSON instead of these flags.
|
|
21796
|
+
--template Print a starting-point criterion and exit.
|
|
21797
|
+
--force Replace an existing criterion with this id.`;
|
|
21798
|
+
function fail12(message) {
|
|
21799
|
+
console.error(message);
|
|
21800
|
+
process.exit(1);
|
|
21801
|
+
}
|
|
21802
|
+
function takeCommon(args) {
|
|
21803
|
+
const rest = [];
|
|
21804
|
+
const common = { root: process.cwd(), actor: KRITIK_ACTOR, journal: true };
|
|
21805
|
+
for (let i = 0; i < args.length; i++) {
|
|
21806
|
+
const arg = args[i];
|
|
21807
|
+
if (arg === "--root") common.root = args[++i] ?? common.root;
|
|
21808
|
+
else if (arg === "--actor") common.actor = args[++i] ?? common.actor;
|
|
21809
|
+
else if (arg === "--bundle") common.bundlePath = args[++i];
|
|
21810
|
+
else if (arg === "--no-journal") common.journal = false;
|
|
21811
|
+
else rest.push(arg);
|
|
21812
|
+
}
|
|
21813
|
+
return { rest, common };
|
|
21814
|
+
}
|
|
21815
|
+
function collect(args, repeatable = [], boolean4 = []) {
|
|
21816
|
+
const single = {};
|
|
21817
|
+
const many = Object.fromEntries(repeatable.map((key) => [key, []]));
|
|
21818
|
+
const flags = /* @__PURE__ */ new Set();
|
|
21819
|
+
const positionals = [];
|
|
21820
|
+
for (let i = 0; i < args.length; i++) {
|
|
21821
|
+
const arg = args[i];
|
|
21822
|
+
if (!arg.startsWith("--")) {
|
|
21823
|
+
positionals.push(arg);
|
|
21824
|
+
continue;
|
|
21825
|
+
}
|
|
21826
|
+
const key = arg.slice(2);
|
|
21827
|
+
if (boolean4.includes(key) || key === "help") {
|
|
21828
|
+
flags.add(key);
|
|
21829
|
+
continue;
|
|
21830
|
+
}
|
|
21831
|
+
const value = args[++i];
|
|
21832
|
+
if (value === void 0) fail12(`kritik: --${key} needs a value`);
|
|
21833
|
+
if (key in many) many[key].push(value);
|
|
21834
|
+
else single[key] = value;
|
|
21835
|
+
}
|
|
21836
|
+
return { single, many, flags, positionals };
|
|
21837
|
+
}
|
|
21838
|
+
function textOrFile(value) {
|
|
21839
|
+
if (!value.startsWith("@")) return value;
|
|
21840
|
+
const path6 = value.slice(1);
|
|
21841
|
+
if (!existsSync16(path6)) fail12(`kritik: no file at ${path6}`);
|
|
21842
|
+
return readFileSync15(path6, "utf8").trim();
|
|
21843
|
+
}
|
|
21844
|
+
function currentAuditId() {
|
|
21845
|
+
const now = /* @__PURE__ */ new Date();
|
|
21846
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
|
21847
|
+
}
|
|
21848
|
+
function auditForWrite(root, requested) {
|
|
21849
|
+
if (requested !== void 0) return requested;
|
|
21850
|
+
const existing = listAuditIds(root);
|
|
21851
|
+
return existing.length > 0 ? existing[existing.length - 1] : currentAuditId();
|
|
21852
|
+
}
|
|
21853
|
+
function loadLibraryOrFail(root) {
|
|
21854
|
+
try {
|
|
21855
|
+
return loadKritikLibrary(root).library;
|
|
21856
|
+
} catch (error51) {
|
|
21857
|
+
return fail12(`kritik: ${error51.message}`);
|
|
21858
|
+
}
|
|
21859
|
+
}
|
|
21860
|
+
function profileOrFail(root) {
|
|
21861
|
+
try {
|
|
21862
|
+
return requireProfile(root);
|
|
21863
|
+
} catch (error51) {
|
|
21864
|
+
return fail12(`kritik: ${error51.message}`);
|
|
21865
|
+
}
|
|
21866
|
+
}
|
|
21867
|
+
function criterionOrFail(library, criterionId) {
|
|
21868
|
+
const criterion = (library.criteria ?? []).find((candidate) => candidate.id === criterionId);
|
|
21869
|
+
if (!criterion) {
|
|
21870
|
+
const domain2 = criterionId.split("-")[0];
|
|
21871
|
+
const siblings = (library.criteria ?? []).filter((c) => c.domain === domain2).map((c) => c.id);
|
|
21872
|
+
return fail12(
|
|
21873
|
+
`kritik: no criterion "${criterionId}" in the pack or this project's overlay.
|
|
21874
|
+
` + (siblings.length > 0 ? `Criteria in ${domain2}: ${siblings.join(", ")}` : `Domains: ${(library.domains ?? []).map((d) => d.code).join(", ")}`)
|
|
21875
|
+
);
|
|
21876
|
+
}
|
|
21877
|
+
if (typeof criterion.superseded_by === "string") {
|
|
21878
|
+
return fail12(
|
|
21879
|
+
`kritik: criterion "${criterionId}" is retired \u2014 superseded by "${criterion.superseded_by}".
|
|
21880
|
+
Score that one instead; the old id is kept only so past audits still mean what they meant.`
|
|
21881
|
+
);
|
|
21882
|
+
}
|
|
21883
|
+
return criterion;
|
|
21884
|
+
}
|
|
21885
|
+
function surfaceOrFail(profile, surface, options = {}) {
|
|
21886
|
+
if (surface === CROSS_SURFACE_ID) {
|
|
21887
|
+
if (options.allowCrossSurface) return surface;
|
|
21888
|
+
return fail12(
|
|
21889
|
+
`kritik: "${CROSS_SURFACE_ID}" is a findings-only lens \u2014 it carries no matrix column, so a score there would render nowhere.`
|
|
21890
|
+
);
|
|
21891
|
+
}
|
|
21892
|
+
const declared = (profile.surfaces ?? []).map((s) => s.id);
|
|
21893
|
+
if (!declared.includes(surface)) {
|
|
21894
|
+
return fail12(
|
|
21895
|
+
`kritik: surface "${surface}" is not declared in ${profilePath(".")}.
|
|
21896
|
+
This project's surfaces: ${declared.join(", ") || "(none \u2014 run `arkaik kritik profile` first)"}`
|
|
21897
|
+
);
|
|
21898
|
+
}
|
|
21899
|
+
return surface;
|
|
21900
|
+
}
|
|
21901
|
+
function intOrFail(label, value, min, max) {
|
|
21902
|
+
if (value === void 0) fail12(`kritik: ${label} is required
|
|
21903
|
+
`);
|
|
21904
|
+
const parsed = Number(value);
|
|
21905
|
+
if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
|
|
21906
|
+
fail12(`kritik: ${label} must be an integer ${min}-${max} (got "${value}")`);
|
|
21907
|
+
}
|
|
21908
|
+
return parsed;
|
|
21909
|
+
}
|
|
21910
|
+
function reportJournal(root, inputs, common) {
|
|
21911
|
+
if (!common.journal) {
|
|
21912
|
+
console.log(` journal: skipped (--no-journal)`);
|
|
21913
|
+
return;
|
|
21914
|
+
}
|
|
21915
|
+
const journal = resolveJournal(root, common.bundlePath);
|
|
21916
|
+
if (!journal.present) {
|
|
21917
|
+
console.log(` journal: none at ${journal.bundlePath} \u2014 Kritik does not need one, so nothing was appended`);
|
|
21918
|
+
return;
|
|
21919
|
+
}
|
|
21920
|
+
const written = appendQualityEvents(root, inputs, { actor: common.actor, bundlePath: common.bundlePath });
|
|
21921
|
+
if (written.baseline !== void 0) {
|
|
21922
|
+
console.log(` journal: adopted ${written.baseline.node_ids?.length ?? 0} pre-existing nodes first`);
|
|
21923
|
+
}
|
|
21924
|
+
console.log(` journal: ${written.events.map((e) => e.type).join(", ")} -> ${written.journalPath}`);
|
|
21925
|
+
}
|
|
21926
|
+
var PROFILE_USAGE = `arkaik kritik profile --surface <id>[:<title>[:<platform>]] [...] [--weight <CODE>=<n>] [--force]
|
|
21927
|
+
|
|
21928
|
+
Pick this project's surfaces \u2014 the one decision everything downstream is shaped
|
|
21929
|
+
by. Each criterion's applies_to intersects this list to produce the audit's
|
|
21930
|
+
cells, and the matrix has exactly these columns.
|
|
21931
|
+
|
|
21932
|
+
--surface id[:title[:platform]]. \`platform\` (web|ios|android) is the
|
|
21933
|
+
optional bridge to the product map, for surfaces that ship
|
|
21934
|
+
views. A database contract or a CLI simply has none.
|
|
21935
|
+
--weight CODE=n How hard this product is graded on a domain (default 1).
|
|
21936
|
+
--force Overwrite an existing profile. Changing the surface list
|
|
21937
|
+
invalidates every score recorded against the old one.
|
|
21938
|
+
|
|
21939
|
+
Writes docs/quality/profile.json`;
|
|
21940
|
+
function runProfile(args, common) {
|
|
21941
|
+
const { many, flags } = collect(args, ["surface", "weight"], ["force"]);
|
|
21942
|
+
if (flags.has("help")) {
|
|
21943
|
+
console.log(PROFILE_USAGE);
|
|
21944
|
+
process.exit(0);
|
|
21945
|
+
}
|
|
21946
|
+
if (many.surface.length === 0) fail12(`kritik: at least one --surface is required
|
|
21947
|
+
|
|
21948
|
+
${PROFILE_USAGE}`);
|
|
21949
|
+
const surfaces = [];
|
|
21950
|
+
const seen = /* @__PURE__ */ new Set();
|
|
21951
|
+
for (const spec of many.surface) {
|
|
21952
|
+
let surface;
|
|
21953
|
+
try {
|
|
21954
|
+
surface = parseSurfaceSpec(spec);
|
|
21955
|
+
} catch (error51) {
|
|
21956
|
+
return fail12(`kritik: ${error51.message}`);
|
|
21957
|
+
}
|
|
21958
|
+
if (seen.has(surface.id)) fail12(`kritik: duplicate surface id "${surface.id}"`);
|
|
21959
|
+
seen.add(surface.id);
|
|
21960
|
+
surfaces.push(surface);
|
|
21961
|
+
}
|
|
21962
|
+
const weights = {};
|
|
21963
|
+
for (const spec of many.weight) {
|
|
21964
|
+
const at2 = spec.indexOf("=");
|
|
21965
|
+
if (at2 <= 0) fail12(`kritik: --weight wants CODE=number (got "${spec}")`);
|
|
21966
|
+
const code = spec.slice(0, at2).trim();
|
|
21967
|
+
const value = Number(spec.slice(at2 + 1));
|
|
21968
|
+
if (!Number.isFinite(value) || value <= 0) fail12(`kritik: weight for "${code}" must be a positive number`);
|
|
21969
|
+
weights[code] = value;
|
|
21970
|
+
}
|
|
21971
|
+
const path6 = profilePath(common.root);
|
|
21972
|
+
if (existsSync16(path6) && !flags.has("force")) {
|
|
21973
|
+
fail12(
|
|
21974
|
+
`kritik: ${path6} already exists.
|
|
21975
|
+
Changing the surface list invalidates every score recorded against the old one, so this refuses by default.
|
|
21976
|
+
Pass --force if that is genuinely what you want.`
|
|
21977
|
+
);
|
|
21978
|
+
}
|
|
21979
|
+
const profile = Object.keys(weights).length > 0 ? { surfaces, domain_weights: weights } : { surfaces };
|
|
21980
|
+
writeJson(path6, profile);
|
|
21981
|
+
console.log(
|
|
21982
|
+
`
|
|
21983
|
+
wrote ${path6}
|
|
21984
|
+
${surfaces.length} surface${surfaces.length === 1 ? "" : "s"}: ${surfaces.map((s) => s.id).join(", ")}
|
|
21985
|
+
${Object.keys(weights).length > 0 ? `weights: ${Object.entries(weights).map(([k, v]) => `${k}=${v}`).join(" ")}` : "weights: 1 across every domain"}
|
|
21986
|
+
`
|
|
21987
|
+
);
|
|
21988
|
+
process.exit(0);
|
|
21989
|
+
}
|
|
21990
|
+
function runScore(args, common) {
|
|
21991
|
+
const { single, flags, positionals } = collect(args);
|
|
21992
|
+
if (flags.has("help")) {
|
|
21993
|
+
console.log(SCORE_USAGE);
|
|
21994
|
+
process.exit(0);
|
|
21995
|
+
}
|
|
21996
|
+
const [criterionId, surfaceArg, levelArg] = positionals;
|
|
21997
|
+
if (criterionId === void 0 || surfaceArg === void 0 || levelArg === void 0) {
|
|
21998
|
+
fail12(`kritik: score takes <criterion> <surface> <level>
|
|
21999
|
+
|
|
22000
|
+
${SCORE_USAGE}`);
|
|
22001
|
+
}
|
|
22002
|
+
const library = loadLibraryOrFail(common.root);
|
|
22003
|
+
const profile = profileOrFail(common.root);
|
|
22004
|
+
const criterion = criterionOrFail(library, criterionId);
|
|
22005
|
+
const surface = surfaceOrFail(profile, surfaceArg);
|
|
22006
|
+
const level = intOrFail("level", levelArg, 0, 4);
|
|
22007
|
+
const appliesTo = criterion.applies_to;
|
|
22008
|
+
if (Array.isArray(appliesTo) && !appliesTo.includes(surface)) {
|
|
22009
|
+
fail12(
|
|
22010
|
+
`kritik: ${criterionId} does not apply to "${surface}" (applies to: ${appliesTo.join(", ")}).
|
|
22011
|
+
Scoring it there would produce a cell nothing rolls up.`
|
|
22012
|
+
);
|
|
22013
|
+
}
|
|
22014
|
+
if (single.evidence === void 0 || single.evidence.trim() === "") {
|
|
22015
|
+
fail12(`kritik: --evidence is required \u2014 a score without a citation is an opinion, not an assessment.
|
|
22016
|
+
|
|
22017
|
+
${SCORE_USAGE}`);
|
|
22018
|
+
}
|
|
22019
|
+
const evidence = textOrFile(single.evidence);
|
|
22020
|
+
const auditId = auditForWrite(common.root, single.audit);
|
|
22021
|
+
const file2 = loadScoresOrEmpty(common.root, auditId);
|
|
22022
|
+
const assessment = {
|
|
22023
|
+
criterion_id: criterionId,
|
|
22024
|
+
surface,
|
|
22025
|
+
level,
|
|
22026
|
+
evidence,
|
|
22027
|
+
audit_id: auditId,
|
|
22028
|
+
...single.commit !== void 0 ? { commit: single.commit } : {},
|
|
22029
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
22030
|
+
};
|
|
22031
|
+
const { assessments, replaced } = upsertAssessment(file2.assessments, assessment);
|
|
22032
|
+
saveScores(common.root, auditId, {
|
|
22033
|
+
...file2,
|
|
22034
|
+
audit_id: auditId,
|
|
22035
|
+
framework_version: file2.framework_version ?? library.version,
|
|
22036
|
+
...single.commit !== void 0 ? { commit: single.commit } : {},
|
|
22037
|
+
assessments
|
|
22038
|
+
});
|
|
22039
|
+
const anchor = criterion.level_anchors?.[`l${level}`];
|
|
22040
|
+
console.log(
|
|
22041
|
+
`
|
|
22042
|
+
${replaced ? `re-scored ${criterionId} x ${surface}: ${replaced.level} -> ${level}` : `scored ${criterionId} x ${surface} at ${level}`}${anchor ? ` \u2014 ${anchor}` : ""}
|
|
22043
|
+
${assessments.length} assessment${assessments.length === 1 ? "" : "s"} in ${auditId} -> ${scoresPath(common.root, auditId)}`
|
|
22044
|
+
);
|
|
22045
|
+
if (level < DEFAULT_TARGET_LEVEL) {
|
|
22046
|
+
console.log(
|
|
22047
|
+
` below the level-${DEFAULT_TARGET_LEVEL} target \u2014 open a finding, or say in the evidence why this surface should not reach it.`
|
|
22048
|
+
);
|
|
22049
|
+
}
|
|
22050
|
+
console.log("");
|
|
22051
|
+
process.exit(0);
|
|
22052
|
+
}
|
|
22053
|
+
function runFinding(args, common) {
|
|
22054
|
+
const [action, ...rest] = args;
|
|
22055
|
+
if (action === void 0 || action === "--help" || action === "-h") {
|
|
22056
|
+
console.log(FINDING_USAGE);
|
|
22057
|
+
process.exit(action === void 0 ? 1 : 0);
|
|
22058
|
+
}
|
|
22059
|
+
if (action === "open") return runFindingOpen(rest, common);
|
|
22060
|
+
if (action === "resolve") return runFindingResolve(rest, common);
|
|
22061
|
+
if (action === "accept") return runFindingAccept(rest, common);
|
|
22062
|
+
fail12(`kritik: unknown finding action "${action}"
|
|
22063
|
+
|
|
22064
|
+
${FINDING_USAGE}`);
|
|
22065
|
+
}
|
|
22066
|
+
function runFindingOpen(args, common) {
|
|
22067
|
+
const { single, flags, positionals } = collect(args);
|
|
22068
|
+
if (flags.has("help")) {
|
|
22069
|
+
console.log(FINDING_USAGE);
|
|
22070
|
+
process.exit(0);
|
|
22071
|
+
}
|
|
22072
|
+
const [criterionId, surfaceArg] = positionals;
|
|
22073
|
+
if (criterionId === void 0 || surfaceArg === void 0) {
|
|
22074
|
+
fail12(`kritik: finding open takes <criterion> <surface>
|
|
22075
|
+
|
|
22076
|
+
${FINDING_USAGE}`);
|
|
22077
|
+
}
|
|
22078
|
+
const library = loadLibraryOrFail(common.root);
|
|
22079
|
+
const profile = profileOrFail(common.root);
|
|
22080
|
+
criterionOrFail(library, criterionId);
|
|
22081
|
+
const surface = surfaceOrFail(profile, surfaceArg, { allowCrossSurface: true });
|
|
22082
|
+
if (single.title === void 0 || single.title.trim() === "") {
|
|
22083
|
+
fail12(`kritik: --title is required \u2014 one line naming the actual defect, not the category.
|
|
22084
|
+
|
|
22085
|
+
${FINDING_USAGE}`);
|
|
22086
|
+
}
|
|
22087
|
+
if (single.evidence === void 0 || single.evidence.trim() === "") {
|
|
22088
|
+
fail12(`kritik: --evidence is required \u2014 file:line citations someone can check.
|
|
22089
|
+
|
|
22090
|
+
${FINDING_USAGE}`);
|
|
22091
|
+
}
|
|
22092
|
+
const impact = intOrFail("--impact", single.impact, 1, 5);
|
|
22093
|
+
const likelihood = intOrFail("--likelihood", single.likelihood, 1, 5);
|
|
22094
|
+
const cost = single.cost;
|
|
22095
|
+
if (cost === void 0 || !REMEDIATION_COSTS.includes(cost)) {
|
|
22096
|
+
fail12(
|
|
22097
|
+
`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.`
|
|
22098
|
+
);
|
|
22099
|
+
}
|
|
22100
|
+
const auditId = auditForWrite(common.root, single.audit);
|
|
22101
|
+
const file2 = loadFindings(common.root, auditId);
|
|
22102
|
+
const taken = new Set(file2.findings.map((f) => f.id));
|
|
22103
|
+
const id = single.id ?? mintFindingId(auditId, criterionId, surface, taken, library);
|
|
22104
|
+
if (single.id !== void 0 && taken.has(single.id)) {
|
|
22105
|
+
fail12(`kritik: finding "${single.id}" already exists in ${auditId} \u2014 resolve or accept it rather than reopening the id.`);
|
|
22106
|
+
}
|
|
22107
|
+
const nodes = single.nodes?.split(",").map((n) => n.trim()).filter((n) => n !== "");
|
|
22108
|
+
const finding = {
|
|
22109
|
+
id,
|
|
22110
|
+
criterion_id: criterionId,
|
|
22111
|
+
surface,
|
|
22112
|
+
title: single.title,
|
|
22113
|
+
detail: single.detail ?? single.title,
|
|
22114
|
+
evidence: textOrFile(single.evidence),
|
|
22115
|
+
impact,
|
|
22116
|
+
likelihood,
|
|
22117
|
+
cost,
|
|
22118
|
+
status: "open",
|
|
22119
|
+
...single.remediation !== void 0 ? { remediation: single.remediation } : {},
|
|
22120
|
+
...nodes !== void 0 && nodes.length > 0 ? { node_ids: nodes } : {},
|
|
22121
|
+
...single["issue-url"] !== void 0 ? { issue_url: single["issue-url"] } : {}
|
|
22122
|
+
};
|
|
22123
|
+
const { findings } = upsertFinding(file2.findings, finding);
|
|
22124
|
+
saveFindings(common.root, auditId, {
|
|
22125
|
+
...file2,
|
|
22126
|
+
audit_id: auditId,
|
|
22127
|
+
framework_version: file2.framework_version ?? library.version,
|
|
22128
|
+
findings
|
|
22129
|
+
});
|
|
22130
|
+
const severity = severityOf(finding, library);
|
|
22131
|
+
const priority = priorityOf(finding, library);
|
|
22132
|
+
console.log(
|
|
22133
|
+
`
|
|
22134
|
+
opened ${id} \u2014 ${finding.title}
|
|
22135
|
+
${severity} / ${priority} (impact ${impact} x likelihood ${likelihood} = ${impact * likelihood}, cost ${cost}) on ${surface}
|
|
22136
|
+
${findings.length} finding${findings.length === 1 ? "" : "s"} in ${auditId}`
|
|
22137
|
+
);
|
|
22138
|
+
reportJournal(common.root, [findingOpenedInput(finding, library)], common);
|
|
22139
|
+
if (priority === "P0") {
|
|
22140
|
+
console.log(` P0: file the issue now \u2014 \`arkaik kritik issue ${criterionId} --surface ${surface} --finding ${id}\``);
|
|
22141
|
+
}
|
|
22142
|
+
console.log("");
|
|
22143
|
+
process.exit(0);
|
|
22144
|
+
}
|
|
22145
|
+
function runFindingResolve(args, common) {
|
|
22146
|
+
const { single, flags, positionals } = collect(args);
|
|
22147
|
+
if (flags.has("help")) {
|
|
22148
|
+
console.log(FINDING_USAGE);
|
|
22149
|
+
process.exit(0);
|
|
22150
|
+
}
|
|
22151
|
+
const id = positionals[0];
|
|
22152
|
+
if (id === void 0) fail12(`kritik: finding resolve takes <finding-id>
|
|
22153
|
+
|
|
22154
|
+
${FINDING_USAGE}`);
|
|
22155
|
+
const located = locateFinding(common.root, id);
|
|
22156
|
+
if (!located) fail12(`kritik: no finding "${id}" in any audit under ${common.root}/docs/quality/audits/`);
|
|
22157
|
+
if (located.finding.status === "resolved") {
|
|
22158
|
+
console.log(`
|
|
22159
|
+
${id} is already resolved \u2014 nothing written.
|
|
22160
|
+
`);
|
|
22161
|
+
process.exit(0);
|
|
22162
|
+
}
|
|
22163
|
+
const { findings, finding } = resolveFinding(located.file.findings, id, single.by);
|
|
22164
|
+
saveFindings(common.root, located.auditId, { ...located.file, findings });
|
|
22165
|
+
console.log(`
|
|
22166
|
+
resolved ${id} \u2014 ${located.finding.title}` + (single.by ? `
|
|
22167
|
+
by ${single.by}` : ""));
|
|
22168
|
+
if (single.by === void 0) {
|
|
22169
|
+
console.log(` no --by: without the PR or commit that closed it, "resolved" means "we stopped looking".`);
|
|
22170
|
+
}
|
|
22171
|
+
reportJournal(common.root, [findingResolvedInput(finding ?? located.finding, single.by)], common);
|
|
22172
|
+
console.log("");
|
|
22173
|
+
process.exit(0);
|
|
22174
|
+
}
|
|
22175
|
+
function runFindingAccept(args, common) {
|
|
22176
|
+
const { single, flags, positionals } = collect(args);
|
|
22177
|
+
if (flags.has("help")) {
|
|
22178
|
+
console.log(FINDING_USAGE);
|
|
22179
|
+
process.exit(0);
|
|
22180
|
+
}
|
|
22181
|
+
const id = positionals[0];
|
|
22182
|
+
if (id === void 0) fail12(`kritik: finding accept takes <finding-id>
|
|
22183
|
+
|
|
22184
|
+
${FINDING_USAGE}`);
|
|
22185
|
+
if (single.note === void 0 || single.note.trim() === "") {
|
|
22186
|
+
fail12(`kritik: --note is required \u2014 an accepted risk is a decision and reads like one.`);
|
|
22187
|
+
}
|
|
22188
|
+
const located = locateFinding(common.root, id);
|
|
22189
|
+
if (!located) fail12(`kritik: no finding "${id}" in any audit under ${common.root}/docs/quality/audits/`);
|
|
22190
|
+
const { findings } = acceptFinding(located.file.findings, id, single.note);
|
|
22191
|
+
saveFindings(common.root, located.auditId, { ...located.file, findings });
|
|
22192
|
+
console.log(
|
|
22193
|
+
`
|
|
22194
|
+
accepted ${id} as a known risk \u2014 ${located.finding.title}
|
|
22195
|
+
${single.note}
|
|
22196
|
+
no journal event: acceptance is a state of the finding, not something that happened to the product.
|
|
22197
|
+
`
|
|
22198
|
+
);
|
|
22199
|
+
process.exit(0);
|
|
22200
|
+
}
|
|
22201
|
+
function runMatrix(args, common) {
|
|
22202
|
+
const { flags, positionals } = collect(args, [], ["json", "record"]);
|
|
22203
|
+
if (flags.has("help")) {
|
|
22204
|
+
console.log(MATRIX_USAGE);
|
|
22205
|
+
process.exit(0);
|
|
22206
|
+
}
|
|
22207
|
+
const library = loadLibraryOrFail(common.root);
|
|
22208
|
+
let auditId;
|
|
22209
|
+
try {
|
|
22210
|
+
auditId = positionals[0] ?? newestAuditId(common.root);
|
|
22211
|
+
} catch (error51) {
|
|
22212
|
+
return fail12(`kritik: ${error51.message}`);
|
|
22213
|
+
}
|
|
22214
|
+
let computed;
|
|
22215
|
+
try {
|
|
22216
|
+
computed = computeAuditMatrix(common.root, auditId, library);
|
|
22217
|
+
} catch (error51) {
|
|
22218
|
+
return fail12(`kritik: ${error51.message}`);
|
|
22219
|
+
}
|
|
22220
|
+
const { section, matrix, file: file2 } = computed;
|
|
22221
|
+
if (flags.has("json")) {
|
|
22222
|
+
console.log(JSON.stringify(file2, null, 2));
|
|
22223
|
+
} else {
|
|
22224
|
+
const domainNames = new Map((library.domains ?? []).map((d) => [d.code, d.name]));
|
|
22225
|
+
console.log(`
|
|
22226
|
+
${renderMatrixMarkdown(matrix, domainNames)}
|
|
22227
|
+
`);
|
|
22228
|
+
const open = section.findings.filter(isOpenFinding);
|
|
22229
|
+
const lanes = { P0: 0, P1: 0, P2: 0, P3: 0 };
|
|
22230
|
+
for (const finding of open) lanes[priorityOf(finding, library)]++;
|
|
22231
|
+
const counts = matrix.finding_counts;
|
|
22232
|
+
console.log(
|
|
22233
|
+
`${section.assessments.length} assessments \xB7 ${open.length} open findings (${counts.critical} critical, ${counts.high} high, ${counts.medium} medium, ${counts.low} low)
|
|
22234
|
+
lanes: P0 ${lanes.P0} \xB7 P1 ${lanes.P1} \xB7 P2 ${lanes.P2} \xB7 P3 ${lanes.P3}`
|
|
22235
|
+
);
|
|
22236
|
+
const p0 = open.filter((finding) => priorityOf(finding, library) === "P0");
|
|
22237
|
+
if (p0.length > 0) {
|
|
22238
|
+
console.log(`
|
|
22239
|
+
P0 \u2014 fix first:`);
|
|
22240
|
+
for (const finding of p0) {
|
|
22241
|
+
console.log(` [${severityOf(finding, library)}] ${finding.surface} \xB7 ${finding.id} \u2014 ${finding.title}`);
|
|
22242
|
+
}
|
|
22243
|
+
}
|
|
22244
|
+
console.log(`
|
|
22245
|
+
wrote ${matrixPath(common.root, auditId)}`);
|
|
22246
|
+
}
|
|
22247
|
+
if (flags.has("record")) {
|
|
22248
|
+
reportJournal(
|
|
22249
|
+
common.root,
|
|
22250
|
+
[auditCompletedInput(matrix, { audit_id: auditId, framework_version: file2.framework_version, ...file2.commit !== void 0 ? { commit: file2.commit } : {} })],
|
|
22251
|
+
common
|
|
22252
|
+
);
|
|
22253
|
+
}
|
|
22254
|
+
console.log("");
|
|
22255
|
+
process.exit(0);
|
|
22256
|
+
}
|
|
22257
|
+
function tripsSinceLastAudit(root, common) {
|
|
22258
|
+
const journal = resolveJournal(root, common.bundlePath);
|
|
22259
|
+
if (!journal.present) return [];
|
|
22260
|
+
const events = orderEvents(readFullJournalEvents(journal.journalPath));
|
|
22261
|
+
const lastAudit = events.map((event) => event.type).lastIndexOf("quality.audit.completed");
|
|
22262
|
+
return events.slice(lastAudit + 1).filter((event) => event.type === "quality.signal.tripped");
|
|
22263
|
+
}
|
|
22264
|
+
function runSignals(args, common) {
|
|
22265
|
+
const { single, flags } = collect(args, [], ["json"]);
|
|
22266
|
+
if (flags.has("help")) {
|
|
22267
|
+
console.log(SIGNALS_USAGE);
|
|
22268
|
+
process.exit(0);
|
|
22269
|
+
}
|
|
22270
|
+
const library = loadLibraryOrFail(common.root);
|
|
22271
|
+
const profile = profileOrFail(common.root);
|
|
22272
|
+
if (single.trip !== void 0) {
|
|
22273
|
+
const criterion = criterionOrFail(library, single.trip);
|
|
22274
|
+
if (single.surface === void 0) fail12(`kritik: --trip needs --surface`);
|
|
22275
|
+
const surface = surfaceOrFail(profile, single.surface);
|
|
22276
|
+
if (single.signal === void 0) fail12(`kritik: --trip needs --signal (a run-sheet index, or the statement itself)`);
|
|
22277
|
+
const signals = criterion.signals ?? [];
|
|
22278
|
+
const index = Number(single.signal);
|
|
22279
|
+
const signal = Number.isInteger(index) && index >= 0 && index < signals.length ? signals[index] : single.signal;
|
|
22280
|
+
console.log(`
|
|
22281
|
+
tripped ${criterion.id} x ${surface}
|
|
22282
|
+
${signal}`);
|
|
22283
|
+
reportJournal(
|
|
22284
|
+
common.root,
|
|
22285
|
+
[signalTrippedInput({ criterion_id: criterion.id, surface, signal, ...single.detail !== void 0 ? { detail: single.detail } : {} })],
|
|
22286
|
+
common
|
|
22287
|
+
);
|
|
22288
|
+
console.log(` a tripped signal is not a finding \u2014 it is the prompt to go look.
|
|
22289
|
+
`);
|
|
22290
|
+
process.exit(0);
|
|
22291
|
+
}
|
|
22292
|
+
const filter = {
|
|
22293
|
+
...single.surface !== void 0 ? { surface: surfaceOrFail(profile, single.surface) } : {},
|
|
22294
|
+
...single.criterion !== void 0 ? { criterion: criterionOrFail(library, single.criterion).id } : {},
|
|
22295
|
+
...single.domain !== void 0 ? { domain: single.domain } : {}
|
|
22296
|
+
};
|
|
22297
|
+
const rows = signalRunSheet(library, profile.surfaces ?? [], filter);
|
|
22298
|
+
const trips = tripsSinceLastAudit(common.root, common);
|
|
22299
|
+
if (flags.has("json")) {
|
|
22300
|
+
console.log(JSON.stringify({ signals: rows, tripped_since_last_audit: trips }, null, 2));
|
|
22301
|
+
process.exit(trips.length > 0 ? 1 : 0);
|
|
22302
|
+
}
|
|
22303
|
+
const filtered = Object.keys(filter).length > 0;
|
|
22304
|
+
if (filtered) {
|
|
22305
|
+
console.log("");
|
|
22306
|
+
let current = "";
|
|
22307
|
+
for (const row of rows) {
|
|
22308
|
+
const key = `${row.criterion_id} x ${row.surface}`;
|
|
22309
|
+
if (key !== current) {
|
|
22310
|
+
current = key;
|
|
22311
|
+
console.log(` ${key}`);
|
|
22312
|
+
}
|
|
22313
|
+
console.log(` [${row.index}] ${row.signal}`);
|
|
22314
|
+
}
|
|
22315
|
+
console.log(`
|
|
22316
|
+
${rows.length} check${rows.length === 1 ? "" : "s"}.`);
|
|
22317
|
+
} else {
|
|
22318
|
+
const criteria = new Set(rows.map((row) => row.criterion_id)).size;
|
|
22319
|
+
console.log(
|
|
22320
|
+
`
|
|
22321
|
+
${rows.length} checks across ${criteria} criteria and ${(profile.surfaces ?? []).length} surfaces.
|
|
22322
|
+
Narrow it (--surface, --criterion, --domain) to read them, or --json to take the lot.` + (listAuditIds(common.root).length > 1 ? `
|
|
22323
|
+
Comparing two audits is \`arkaik kritik regressions\`.` : "")
|
|
22324
|
+
);
|
|
22325
|
+
}
|
|
22326
|
+
if (trips.length > 0) {
|
|
22327
|
+
console.log(`
|
|
22328
|
+
tripped since the last recorded audit:`);
|
|
22329
|
+
for (const trip of trips) {
|
|
22330
|
+
const event = trip;
|
|
22331
|
+
console.log(` ${event.criterion_id} x ${event.surface} \u2014 ${event.signal}`);
|
|
22332
|
+
}
|
|
22333
|
+
console.log("");
|
|
22334
|
+
process.exit(1);
|
|
22335
|
+
}
|
|
22336
|
+
console.log("");
|
|
22337
|
+
process.exit(0);
|
|
22338
|
+
}
|
|
22339
|
+
function auditPair(root, from, to) {
|
|
22340
|
+
const audits = listAuditIds(root);
|
|
22341
|
+
if (audits.length < 2) {
|
|
22342
|
+
fail12(
|
|
22343
|
+
`kritik: regressions needs two audits to compare \u2014 ${audits.length === 0 ? "docs/quality/audits/ holds none" : `only "${audits[0]}" exists`}.
|
|
22344
|
+
A regression is the difference between two readings; one reading is a baseline.`
|
|
22345
|
+
);
|
|
22346
|
+
}
|
|
22347
|
+
const known = (id) => {
|
|
22348
|
+
if (!audits.includes(id)) fail12(`kritik: no audit "${id}" under docs/quality/audits/ (have: ${audits.join(", ")})`);
|
|
22349
|
+
return id;
|
|
22350
|
+
};
|
|
22351
|
+
const newer = to === void 0 ? audits[audits.length - 1] : known(to);
|
|
22352
|
+
const older = from === void 0 ? audits[audits.indexOf(newer) - 1] : known(from);
|
|
22353
|
+
if (older === void 0) {
|
|
22354
|
+
fail12(`kritik: "${newer}" is the oldest audit \u2014 there is nothing before it to compare against.`);
|
|
22355
|
+
}
|
|
22356
|
+
if (older === newer) {
|
|
22357
|
+
fail12(`kritik: --from and --to name the same audit ("${newer}") \u2014 a regression needs two readings.`);
|
|
22358
|
+
}
|
|
22359
|
+
if (audits.indexOf(older) > audits.indexOf(newer)) {
|
|
22360
|
+
fail12(`kritik: --from "${older}" is newer than --to "${newer}" \u2014 swap them, or the comparison inverts.`);
|
|
22361
|
+
}
|
|
22362
|
+
return { from: older, to: newer };
|
|
22363
|
+
}
|
|
22364
|
+
function runRegressions(args, common) {
|
|
22365
|
+
const { single, flags } = collect(args, [], ["json", "record"]);
|
|
22366
|
+
if (flags.has("help")) {
|
|
22367
|
+
console.log(REGRESSIONS_USAGE);
|
|
22368
|
+
process.exit(0);
|
|
22369
|
+
}
|
|
22370
|
+
const library = loadLibraryOrFail(common.root);
|
|
22371
|
+
profileOrFail(common.root);
|
|
22372
|
+
const { from, to } = auditPair(common.root, single.from, single.to);
|
|
22373
|
+
let regressions;
|
|
22374
|
+
try {
|
|
22375
|
+
regressions = detectRegressions(
|
|
22376
|
+
loadQualitySection(common.root, from, library),
|
|
22377
|
+
loadQualitySection(common.root, to, library),
|
|
22378
|
+
library
|
|
22379
|
+
);
|
|
22380
|
+
} catch (error51) {
|
|
22381
|
+
return fail12(`kritik: ${error51.message}`);
|
|
22382
|
+
}
|
|
22383
|
+
if (flags.has("json")) {
|
|
22384
|
+
console.log(JSON.stringify({ from, to, total: regressions.length, regressions }, null, 2));
|
|
22385
|
+
} else if (regressions.length === 0) {
|
|
22386
|
+
console.log(`
|
|
22387
|
+
nothing regressed between ${from} and ${to}.
|
|
22388
|
+
`);
|
|
22389
|
+
} else {
|
|
22390
|
+
console.log("");
|
|
22391
|
+
for (const regression of regressions) {
|
|
22392
|
+
console.log(` [${regression.kind}] ${regression.criterion_id} x ${regression.surface}`);
|
|
22393
|
+
console.log(` ${regression.detail}`);
|
|
22394
|
+
}
|
|
22395
|
+
console.log(`
|
|
22396
|
+
${regressions.length} regression${regressions.length === 1 ? "" : "s"} between ${from} and ${to}.`);
|
|
22397
|
+
}
|
|
22398
|
+
if (flags.has("record") && regressions.length > 0) {
|
|
22399
|
+
reportJournal(
|
|
22400
|
+
common.root,
|
|
22401
|
+
regressions.map(
|
|
22402
|
+
(regression) => signalTrippedInput({
|
|
22403
|
+
criterion_id: regression.criterion_id,
|
|
22404
|
+
surface: regression.surface,
|
|
22405
|
+
signal: regression.signal,
|
|
22406
|
+
detail: regression.detail
|
|
22407
|
+
})
|
|
22408
|
+
),
|
|
22409
|
+
common
|
|
22410
|
+
);
|
|
22411
|
+
console.log(` a tripped signal is not a finding \u2014 it is the prompt to go look.
|
|
22412
|
+
`);
|
|
22413
|
+
}
|
|
22414
|
+
process.exit(regressions.length > 0 ? 1 : 0);
|
|
22415
|
+
}
|
|
22416
|
+
function runIssue(args, common) {
|
|
22417
|
+
const { single, flags, positionals } = collect(args);
|
|
22418
|
+
if (flags.has("help")) {
|
|
22419
|
+
console.log(ISSUE_USAGE);
|
|
22420
|
+
process.exit(0);
|
|
22421
|
+
}
|
|
22422
|
+
const criterionId = positionals[0];
|
|
22423
|
+
if (criterionId === void 0) fail12(`kritik: issue takes <criterion>
|
|
22424
|
+
|
|
22425
|
+
${ISSUE_USAGE}`);
|
|
22426
|
+
if (single.surface === void 0) fail12(`kritik: --surface is required
|
|
22427
|
+
|
|
22428
|
+
${ISSUE_USAGE}`);
|
|
22429
|
+
const library = loadLibraryOrFail(common.root);
|
|
22430
|
+
const criterion = criterionOrFail(library, criterionId);
|
|
22431
|
+
const declared = loadProfile(common.root);
|
|
22432
|
+
if (declared) surfaceOrFail(declared, single.surface, { allowCrossSurface: true });
|
|
22433
|
+
let finding;
|
|
22434
|
+
if (single.finding !== void 0) {
|
|
22435
|
+
const located = locateFinding(common.root, single.finding);
|
|
22436
|
+
if (!located) fail12(`kritik: no finding "${single.finding}" in any audit`);
|
|
22437
|
+
finding = located.finding;
|
|
22438
|
+
}
|
|
22439
|
+
const rendered = renderIssue(criterion, {
|
|
22440
|
+
surface: single.surface,
|
|
22441
|
+
...single.level !== void 0 ? { level: single.level } : {},
|
|
22442
|
+
...finding !== void 0 ? { finding, library } : {}
|
|
22443
|
+
});
|
|
22444
|
+
process.stdout.write(`Title: ${rendered.title}
|
|
22445
|
+
`);
|
|
22446
|
+
process.stdout.write(`Labels: ${rendered.labels.join(", ")}
|
|
22447
|
+
|
|
22448
|
+
`);
|
|
22449
|
+
process.stdout.write(`${rendered.body}
|
|
22450
|
+
`);
|
|
22451
|
+
if (rendered.remediation) process.stdout.write(`
|
|
22452
|
+
<!-- Typical remediation: ${rendered.remediation} -->
|
|
22453
|
+
`);
|
|
22454
|
+
process.exit(0);
|
|
22455
|
+
}
|
|
22456
|
+
function runCriterion(args, common) {
|
|
22457
|
+
const [action, ...rest] = args;
|
|
22458
|
+
if (action === void 0 || action === "--help" || action === "-h") {
|
|
22459
|
+
console.log(CRITERION_USAGE);
|
|
22460
|
+
process.exit(action === void 0 ? 1 : 0);
|
|
22461
|
+
}
|
|
22462
|
+
if (action !== "add") fail12(`kritik: unknown criterion action "${action}" (only \`add\`)
|
|
22463
|
+
|
|
22464
|
+
${CRITERION_USAGE}`);
|
|
22465
|
+
const { single, many, flags } = collect(rest, ["anchor", "signal", "check", "label"], ["template", "force"]);
|
|
22466
|
+
if (flags.has("help")) {
|
|
22467
|
+
console.log(CRITERION_USAGE);
|
|
22468
|
+
process.exit(0);
|
|
22469
|
+
}
|
|
22470
|
+
const { library: pack } = (() => {
|
|
22471
|
+
try {
|
|
22472
|
+
const loaded = loadKritikLibrary(common.root);
|
|
22473
|
+
return { library: loaded.pack.library };
|
|
22474
|
+
} catch (error51) {
|
|
22475
|
+
return fail12(`kritik: ${error51.message}`);
|
|
22476
|
+
}
|
|
22477
|
+
})();
|
|
22478
|
+
if (flags.has("template")) {
|
|
22479
|
+
console.log(JSON.stringify(CRITERION_TEMPLATE, null, 2));
|
|
22480
|
+
process.exit(0);
|
|
22481
|
+
}
|
|
22482
|
+
let criterion;
|
|
22483
|
+
try {
|
|
22484
|
+
if (single.from !== void 0) {
|
|
22485
|
+
if (!existsSync16(single.from)) fail12(`kritik: no file at ${single.from}`);
|
|
22486
|
+
criterion = JSON.parse(readFileSync15(single.from, "utf8"));
|
|
22487
|
+
} else {
|
|
22488
|
+
const anchors = {};
|
|
22489
|
+
for (const spec of many.anchor) {
|
|
22490
|
+
const at2 = spec.indexOf("=");
|
|
22491
|
+
if (at2 <= 0) fail12(`kritik: --anchor wants lN=<text> (got "${spec}")`);
|
|
22492
|
+
anchors[spec.slice(0, at2).trim()] = spec.slice(at2 + 1);
|
|
22493
|
+
}
|
|
22494
|
+
const draft = {
|
|
22495
|
+
id: single.id ?? "",
|
|
22496
|
+
domain: single.domain ?? "",
|
|
22497
|
+
...single.subcategory !== void 0 ? { subcategory: single.subcategory } : {},
|
|
22498
|
+
name: single.name ?? "",
|
|
22499
|
+
question: single.question ?? "",
|
|
22500
|
+
...single.definition !== void 0 ? { definition: single.definition } : {},
|
|
22501
|
+
...single.rationale !== void 0 ? { rationale: single.rationale } : {},
|
|
22502
|
+
appliesTo: (single["applies-to"] ?? "").split(",").map((s) => s.trim()).filter(Boolean),
|
|
22503
|
+
anchors,
|
|
22504
|
+
...single.weight !== void 0 ? { weight: Number(single.weight) } : {},
|
|
22505
|
+
...single.impact !== void 0 ? { impact: Number(single.impact) } : {},
|
|
22506
|
+
signals: many.signal,
|
|
22507
|
+
checklist: many.check,
|
|
22508
|
+
...single.remediation !== void 0 ? { remediation: single.remediation } : {},
|
|
22509
|
+
labels: many.label
|
|
22510
|
+
};
|
|
22511
|
+
criterion = buildCriterion(draft);
|
|
22512
|
+
}
|
|
22513
|
+
} catch (error51) {
|
|
22514
|
+
return fail12(`kritik: ${error51.message}
|
|
22515
|
+
|
|
22516
|
+
${CRITERION_USAGE}`);
|
|
22517
|
+
}
|
|
22518
|
+
let written;
|
|
22519
|
+
try {
|
|
22520
|
+
written = addCriterionToOverlay(common.root, pack, criterion, {
|
|
22521
|
+
force: flags.has("force"),
|
|
22522
|
+
...single["domain-name"] !== void 0 ? { domainName: single["domain-name"] } : {}
|
|
22523
|
+
});
|
|
22524
|
+
} catch (error51) {
|
|
22525
|
+
return fail12(`kritik: ${error51.message}`);
|
|
22526
|
+
}
|
|
22527
|
+
console.log(
|
|
22528
|
+
`
|
|
22529
|
+
${written.replaced ? "replaced" : "added"} ${criterion.id} (${criterion.domain}) -> ${written.path}
|
|
22530
|
+
applies to ${(criterion.applies_to ?? []).join(", ") || "every surface"}` + (written.addedDomain !== void 0 ? `
|
|
22531
|
+
declared a new domain: ${written.addedDomain}` : "") + `
|
|
22532
|
+
it scores and rolls up exactly like a pack criterion \u2014 \`arkaik kritik issue ${criterion.id} --surface <s>\`
|
|
22533
|
+
`
|
|
22534
|
+
);
|
|
22535
|
+
process.exit(0);
|
|
22536
|
+
}
|
|
22537
|
+
function runKritik(args) {
|
|
22538
|
+
const { rest, common } = takeCommon(args);
|
|
22539
|
+
const [sub, ...subArgs] = rest;
|
|
22540
|
+
if (sub === void 0 || sub === "--help" || sub === "-h" || sub === "help") {
|
|
22541
|
+
console.log(USAGE13);
|
|
22542
|
+
process.exit(sub === void 0 ? 1 : 0);
|
|
22543
|
+
}
|
|
22544
|
+
switch (sub) {
|
|
22545
|
+
case "profile":
|
|
22546
|
+
return runProfile(subArgs, common);
|
|
22547
|
+
case "score":
|
|
22548
|
+
return runScore(subArgs, common);
|
|
22549
|
+
case "finding":
|
|
22550
|
+
return runFinding(subArgs, common);
|
|
22551
|
+
case "matrix":
|
|
22552
|
+
return runMatrix(subArgs, common);
|
|
22553
|
+
case "signals":
|
|
22554
|
+
return runSignals(subArgs, common);
|
|
22555
|
+
case "regressions":
|
|
22556
|
+
return runRegressions(subArgs, common);
|
|
22557
|
+
case "issue":
|
|
22558
|
+
return runIssue(subArgs, common);
|
|
22559
|
+
case "criterion":
|
|
22560
|
+
return runCriterion(subArgs, common);
|
|
22561
|
+
default:
|
|
22562
|
+
fail12(`kritik: unknown subcommand "${sub}"
|
|
22563
|
+
|
|
22564
|
+
${USAGE13}`);
|
|
22565
|
+
}
|
|
22566
|
+
}
|
|
22567
|
+
|
|
20026
22568
|
// src/index.ts
|
|
20027
|
-
var
|
|
22569
|
+
var USAGE14 = `arkaik \u2014 CLI for Arkaik project bundles
|
|
20028
22570
|
|
|
20029
22571
|
Usage:
|
|
20030
22572
|
arkaik <command> [options]
|
|
@@ -20036,25 +22578,26 @@ Commands:
|
|
|
20036
22578
|
release <version> [path] Tag a release (append release.tagged) and draft its notes.
|
|
20037
22579
|
deliverable <title> [path] Record a deliverable (append deliverable.shipped).
|
|
20038
22580
|
sync [options] [path] Mirror external ref status (GitHub issues/PRs) into node refs.
|
|
20039
|
-
pack [options] [path] Produce a
|
|
22581
|
+
pack [options] [path] Produce a self-contained interchange bundle (embeds journal + quality).
|
|
20040
22582
|
open [options] [path] Validate, then hand off the packed bundle to arkaik.app import.
|
|
20041
|
-
push [options] [path] Validate, pack (journal stripped), and publish to Publik.
|
|
22583
|
+
push [options] [path] Validate, pack (journal + quality stripped), and publish to Publik.
|
|
20042
22584
|
--delete <id> --key <owner_key> removes a snapshot.
|
|
20043
22585
|
link [options] [path] Point this repo at a hosted project so an agent can edit it.
|
|
20044
22586
|
--list shows the projects your token can reach.
|
|
20045
|
-
restore [options] [path] Replace the linked
|
|
22587
|
+
restore [options] [path] Replace the linked project's bundle, journal + quality (backs up first).
|
|
20046
22588
|
bootstrap <sub> [options] One-time onboarding: mine, plan, slice, merge a map from a repo.
|
|
22589
|
+
kritik <sub> [options] Quality audits: score criteria, open findings, roll up the matrix.
|
|
20047
22590
|
|
|
20048
22591
|
Options:
|
|
20049
22592
|
-h, --help Show this help.
|
|
20050
22593
|
-v, --version Print the version.
|
|
20051
22594
|
|
|
20052
22595
|
Run "arkaik <command> --help" for command-specific help.`;
|
|
20053
|
-
var VERSION = "0.
|
|
22596
|
+
var VERSION = "0.3.0";
|
|
20054
22597
|
function main(argv) {
|
|
20055
22598
|
const [command, ...rest] = argv;
|
|
20056
22599
|
if (command === void 0 || command === "--help" || command === "-h" || command === "help") {
|
|
20057
|
-
console.log(
|
|
22600
|
+
console.log(USAGE14);
|
|
20058
22601
|
process.exit(0);
|
|
20059
22602
|
}
|
|
20060
22603
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
@@ -20098,10 +22641,13 @@ function main(argv) {
|
|
|
20098
22641
|
case "bootstrap":
|
|
20099
22642
|
runBootstrap(rest);
|
|
20100
22643
|
return;
|
|
22644
|
+
case "kritik":
|
|
22645
|
+
runKritik(rest);
|
|
22646
|
+
return;
|
|
20101
22647
|
default:
|
|
20102
22648
|
console.error(`Unknown command: ${command}
|
|
20103
22649
|
`);
|
|
20104
|
-
console.error(
|
|
22650
|
+
console.error(USAGE14);
|
|
20105
22651
|
process.exit(1);
|
|
20106
22652
|
}
|
|
20107
22653
|
}
|