filegrc 0.6.4 → 0.6.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "filegrc",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,7 +1,13 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
1
3
  import { applyResourceBatch } from "./files.js";
2
- import { getGitSummary } from "./git.js";
3
- import { loadWorkspace } from "./workspace.js";
4
+ import { getWorkspaceRevisionSnapshot } from "./git.js";
5
+ import { serializeWorkspaceMutation } from "./mutation.js";
6
+ import { resolveDataPath } from "./paths.js";
4
7
  import { resolveProgram } from "./program.js";
8
+ import { markdownEntries } from "./resource-markdown.js";
9
+ import { assessWorkflow, buildWorkflowDelta } from "./workflow.js";
10
+ import { loadWorkspace } from "./workspace.js";
5
11
 
6
12
  const REVIEWABLE_TYPES = new Set([
7
13
  "requirement",
@@ -11,7 +17,8 @@ const REVIEWABLE_TYPES = new Set([
11
17
  ]);
12
18
 
13
19
  export async function scaffoldApplicabilityReview(input = process.cwd(), options = {}) {
14
- const loaded = await loadWorkspace(input);
20
+ const context = await applicabilityReviewContext(input);
21
+ const { loaded } = context;
15
22
  if (!["3", "4"].includes(String(loaded.model.modelVersion))) {
16
23
  throw new Error("Batch applicability review requires a model v3 or v4 workspace.");
17
24
  }
@@ -32,6 +39,7 @@ export async function scaffoldApplicabilityReview(input = process.cwd(), options
32
39
  && !["retired", "superseded"].includes(record.status)
33
40
  ));
34
41
  return {
42
+ basis: context.basis,
35
43
  reviewedByIds: [],
36
44
  reviewedOn: null,
37
45
  decisions: records
@@ -45,14 +53,25 @@ export async function scaffoldApplicabilityReview(input = process.cwd(), options
45
53
  }
46
54
 
47
55
  export async function planApplicabilityReview(input = process.cwd(), options = {}) {
48
- const loaded = await loadWorkspace(input);
56
+ const context = await applicabilityReviewContext(input);
57
+ return planApplicabilityReviewWithContext(context, options);
58
+ }
59
+
60
+ function planApplicabilityReviewWithContext(context, options) {
61
+ const { basis, loaded } = context;
49
62
  if (!["3", "4"].includes(String(loaded.model.modelVersion))) {
50
63
  throw new Error("Batch applicability review requires a model v3 or v4 workspace.");
51
64
  }
52
65
  if (!Array.isArray(options.decisions) || !options.decisions.length) {
53
66
  throw new Error("Applicability review needs at least one decision.");
54
67
  }
68
+ if (options.basis !== undefined) assertApplicabilityReviewBasis(options.basis, basis);
55
69
  const byId = new Map(loaded.resources.map((record) => [record.id, record]));
70
+ const revisionById = new Map(loaded.entries.map((entry) => [entry.record.id, entry.revision]));
71
+ const expectedRevisions = options.expectedRevisions ?? {};
72
+ if (Array.isArray(expectedRevisions) || typeof expectedRevisions !== "object" || expectedRevisions === null) {
73
+ throw new Error("Batch expected revisions must be keyed by resource ID.");
74
+ }
56
75
  const program = resolveProgram(loaded, options.programId);
57
76
  const v4RequirementDecisions = [];
58
77
  const update = options.decisions.flatMap((decision) => {
@@ -62,12 +81,6 @@ export async function planApplicabilityReview(input = process.cwd(), options = {
62
81
  }
63
82
  const reviewedByIds = [...new Set((decision.reviewedByIds || options.reviewedByIds || []).map(String))];
64
83
  const reviewedOn = String(decision.reviewedOn || options.reviewedOn || "").trim();
65
- const scopeRevision = String(
66
- decision.scopeRevision
67
- || options.scopeRevision
68
- || getGitSummary(loaded.root).commit
69
- || "uncommitted"
70
- ).trim();
71
84
  const rationale = String(decision.rationale || "").trim();
72
85
  const result = String(decision.decision || "").trim();
73
86
  if (!["applicable", "not-applicable", "externally-managed", "zero-population"].includes(result)) {
@@ -83,7 +96,7 @@ export async function planApplicabilityReview(input = process.cwd(), options = {
83
96
  rationale,
84
97
  reviewedByIds,
85
98
  reviewedOn,
86
- scopeRevision
99
+ scopeRevision: basis.scopeRevision
87
100
  }
88
101
  };
89
102
  if (record.type === "requirement") {
@@ -91,7 +104,14 @@ export async function planApplicabilityReview(input = process.cwd(), options = {
91
104
  throw new Error(`Requirement "${record.id}" must be applicable or not-applicable.`);
92
105
  }
93
106
  if (String(loaded.model.modelVersion) === "4") {
94
- v4RequirementDecisions.push({ requirementId: record.id, decision: result, rationale, reviewedByIds, reviewedOn, scopeRevision });
107
+ v4RequirementDecisions.push({
108
+ requirementId: record.id,
109
+ decision: result,
110
+ rationale,
111
+ reviewedByIds,
112
+ reviewedOn,
113
+ scopeRevision: basis.scopeRevision
114
+ });
95
115
  return [];
96
116
  }
97
117
  next.applicability = result;
@@ -113,20 +133,125 @@ export async function planApplicabilityReview(input = process.cwd(), options = {
113
133
  }
114
134
  return {
115
135
  operation: "applicability-review",
136
+ basis,
116
137
  reviewedIds: options.decisions.map(({ id }) => id),
117
138
  changes: {
118
139
  update,
119
- expectedRevisions: options.expectedRevisions || {},
140
+ expectedRevisions: Object.fromEntries(update.map((record) => [
141
+ record.id,
142
+ expectedRevisions[record.id] || revisionById.get(record.id)
143
+ ])),
120
144
  validateWholeWorkspace: true
121
145
  }
122
146
  };
123
147
  }
124
148
 
125
- export async function applyApplicabilityReview(input = process.cwd(), options = {}) {
149
+ export function applyApplicabilityReview(input = process.cwd(), options = {}) {
150
+ return applyApplicabilityReviewWithContext(input, options);
151
+ }
152
+
153
+ export async function applyApplicabilityReviewWithContext(input = process.cwd(), options = {}, contextOptions = {}) {
126
154
  if (options.confirmed !== true) {
127
155
  throw new Error("Preview the applicability decisions and confirm the write.");
128
156
  }
129
- const plan = await planApplicabilityReview(input, options);
130
- const result = await applyResourceBatch(input, plan.changes);
131
- return { ...plan, result };
157
+ return serializeWorkspaceMutation(input, async (root) => {
158
+ const context = await applicabilityReviewContext(root, contextOptions);
159
+ if (options.basis === undefined) {
160
+ throw new Error("Applicability review apply requires the basis returned by scaffold or --preview --json. Generate the decisions again before confirming the write.");
161
+ }
162
+ const plan = planApplicabilityReviewWithContext(context, options);
163
+ const before = contextOptions.includeWorkflowDelta
164
+ ? await assessWorkflow(context.loaded, { git: workflowGitState(context.repository) })
165
+ : null;
166
+ const result = await applyResourceBatch(root, plan.changes);
167
+ if (!before) return { ...plan, result };
168
+ const after = await assessWorkflow(result.validation.loaded, {
169
+ git: workflowGitState(context.repository),
170
+ validation: result.validation
171
+ });
172
+ return {
173
+ ...plan,
174
+ result,
175
+ workflowDelta: buildWorkflowDelta(before, after)
176
+ };
177
+ });
178
+ }
179
+
180
+ async function applicabilityReviewContext(input, options = {}) {
181
+ const loaded = await loadWorkspace(input);
182
+ const repository = options.repositorySnapshot ?? await getWorkspaceRevisionSnapshot(loaded.root);
183
+ return {
184
+ basis: await applicabilityReviewBasis(loaded, repository),
185
+ loaded,
186
+ repository
187
+ };
188
+ }
189
+
190
+ function workflowGitState(repository) {
191
+ return {
192
+ ...repository,
193
+ commit: repository.currentCommit ?? repository.commit ?? null
194
+ };
195
+ }
196
+
197
+ async function applicabilityReviewBasis(loaded, repository) {
198
+ const scopeFingerprint = await applicabilityScopeFingerprint(loaded);
199
+ const commit = repository.currentCommit ?? repository.commit ?? null;
200
+ const clean = repository.clean === true || repository.wholeWorktreeClean === true;
201
+ return {
202
+ scopeRevision: commit && clean ? commit : `uncommitted:${scopeFingerprint}`,
203
+ scopeFingerprint
204
+ };
205
+ }
206
+
207
+ function assertApplicabilityReviewBasis(expected, current) {
208
+ if (
209
+ !expected
210
+ || Array.isArray(expected)
211
+ || typeof expected !== "object"
212
+ || typeof expected.scopeRevision !== "string"
213
+ || typeof expected.scopeFingerprint !== "string"
214
+ ) {
215
+ throw new Error("Applicability review basis must include its scope revision and scope fingerprint. Generate a new scaffold or preview.");
216
+ }
217
+ if (
218
+ expected.scopeRevision !== current.scopeRevision
219
+ || expected.scopeFingerprint !== current.scopeFingerprint
220
+ ) {
221
+ throw new Error("The workspace scope changed after this applicability review was prepared. Generate a new preview before confirming the write.");
222
+ }
223
+ }
224
+
225
+ async function applicabilityScopeFingerprint(loaded) {
226
+ const hash = createHash("sha256");
227
+ hash.update(`model\0${loaded.model.modelVersion}\0`);
228
+ for (const entry of [...loaded.entries].sort((left, right) => compareText(left.relativePath, right.relativePath))) {
229
+ hash.update(`record\0${entry.relativePath}\0${stableJson(entry.record)}\0`);
230
+ for (const markdown of markdownEntries(loaded.model, entry.record).sort((left, right) => (
231
+ compareText(left.path, right.path)
232
+ ))) {
233
+ try {
234
+ const source = await readFile(resolveDataPath(loaded.root, markdown.path), "utf8");
235
+ hash.update(`markdown\0${markdown.path}\0${source.length}\0${source}\0`);
236
+ } catch (error) {
237
+ if (error.code !== "ENOENT") throw error;
238
+ hash.update(`markdown-missing\0${markdown.path}\0`);
239
+ }
240
+ }
241
+ }
242
+ return hash.digest("hex");
243
+ }
244
+
245
+ function compareText(left, right) {
246
+ return left < right ? -1 : left > right ? 1 : 0;
247
+ }
248
+
249
+ function stableJson(value) {
250
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
251
+ if (value && typeof value === "object") {
252
+ return `{${Object.keys(value).sort().map((key) => (
253
+ `${JSON.stringify(key)}:${stableJson(value[key])}`
254
+ )).join(",")}}`;
255
+ }
256
+ return JSON.stringify(value);
132
257
  }
package/src/cli.js CHANGED
@@ -6,7 +6,7 @@ import { buildAgentGuide, findResourceReferences, listResourceTypes, scaffoldRes
6
6
  import { assessAuditPreparation, prepareAuditWorkspace } from "./audit-preparation.js";
7
7
  import { createNextAuditCycle, planNextAuditCycle } from "./audit-transition.js";
8
8
  import {
9
- applyApplicabilityReview,
9
+ applyApplicabilityReviewWithContext,
10
10
  planApplicabilityReview,
11
11
  scaffoldApplicabilityReview
12
12
  } from "./batch-review.js";
@@ -561,7 +561,7 @@ export async function runCli(argv = process.argv.slice(2)) {
561
561
  const options = { ...payload, confirmed: flags.yes === true };
562
562
  const result = flags.preview
563
563
  ? await planApplicabilityReview(root, options)
564
- : await withWorkflowDelta(root, () => applyApplicabilityReview(root, options));
564
+ : await applyApplicabilityReviewWithContext(root, options, { includeWorkflowDelta: true });
565
565
  if (flags.json) console.log(JSON.stringify(result, null, 2));
566
566
  else if (flags.preview) console.log(`Applicability preview: ${result.reviewedIds.length} decisions.`);
567
567
  else console.log(`Recorded ${result.reviewedIds.length} reviewed applicability decisions.`);
@@ -1,33 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { collectionRevision } from "./collection-revision.js";
2
3
  import { scopedCollectionRecords } from "./collection-scope.js";
3
4
  import { applyResourceBatch } from "./files.js";
4
5
  import { getGitSummary } from "./git.js";
5
6
  import { loadWorkspace } from "./workspace.js";
6
- import { resolveProgram, selectedRequirementIds } from "./program.js";
7
+ import { resolveProgram } from "./program.js";
7
8
 
8
- export function collectionRevision(loaded, resourceType, options = {}) {
9
- const program = resolveProgram(loaded, options.programId);
10
- const scopedIds = new Set(scopedCollectionRecords(loaded, resourceType, program).map(({ id }) => id));
11
- const records = loaded.entries
12
- .filter(({ record }) => record.type === resourceType && scopedIds.has(record.id))
13
- .map(({ record, source }) => ({
14
- id: record.id,
15
- revision: createHash("sha256").update(source).digest("hex")
16
- }))
17
- .sort((left, right) => left.id.localeCompare(right.id));
18
- const workspaceScope = {
19
- programId: program?.id ?? null,
20
- assuranceGoal: program?.assuranceGoal ?? null,
21
- candidateCoverage: program?.candidateCoverage ?? null,
22
- systemIds: [...(program?.systemIds || [])].sort(),
23
- frameworkIds: [...(program?.frameworkIds || [])].sort(),
24
- requirementIds: [...selectedRequirementIds(program || {}, loaded.model)].sort(),
25
- controlIds: [...(program?.controlIds || [])].sort()
26
- };
27
- return createHash("sha256")
28
- .update(JSON.stringify({ resourceType, records, workspaceScope }))
29
- .digest("hex");
30
- }
9
+ export { collectionRevision };
31
10
 
32
11
  export function assessCollectionReviews(input, options = {}) {
33
12
  const loaded = input?.resources && input?.model && input?.entries
@@ -50,7 +29,13 @@ export function assessCollectionReview(loaded, resourceType, options = {}) {
50
29
  && (String(loaded.model.modelVersion) !== "4" || (record.scopeResourceIds || []).includes(program.id))
51
30
  ));
52
31
  const review = reviewEntry?.record || null;
53
- const currentRevision = collectionRevision(loaded, resourceType, { programId: program.id });
32
+ const authoritativeSourceId = review?.decision === "externally-managed"
33
+ ? review.authoritativeComponentId || review.authoritativeSystemId
34
+ : null;
35
+ const currentRevision = collectionRevision(loaded, resourceType, {
36
+ programId: program.id,
37
+ authoritativeSourceId
38
+ });
54
39
  const allowedDecisions = configuration.decisions || ["complete"];
55
40
  const allowsEmptyCollection = allowedDecisions.some((decision) => (
56
41
  decision === "zero-population" || decision === "externally-managed"
@@ -147,6 +132,10 @@ export async function planCollectionReview(input = process.cwd(), options = {})
147
132
  ));
148
133
  if (!system) throw new Error(`${configuration.title} review needs an active authoritative ${v4 ? "Component" : "System"}.`);
149
134
  }
135
+ const currentRevision = collectionRevision(loaded, resourceType, {
136
+ programId: program.id,
137
+ authoritativeSourceId: decision === "externally-managed" ? authoritativeSourceId : null
138
+ });
150
139
  const existing = assessment.review;
151
140
  const record = {
152
141
  ...(existing || {
@@ -161,7 +150,7 @@ export async function planCollectionReview(input = process.cwd(), options = {})
161
150
  rationale,
162
151
  reviewedByIds,
163
152
  reviewedOn,
164
- collectionRevision: assessment.collectionRevision,
153
+ collectionRevision: currentRevision,
165
154
  scopeRevision,
166
155
  ...(decision === "externally-managed"
167
156
  ? { [v4 ? "authoritativeComponentId" : "authoritativeSystemId"]: authoritativeSourceId }
@@ -0,0 +1,83 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import {
4
+ collectionRevisionInputs,
5
+ collectionScopeRevisionFacts
6
+ } from "./collection-scope.js";
7
+ import { resolveDataPath } from "./paths.js";
8
+ import { resolveProgram } from "./program.js";
9
+ import { markdownEntries } from "./resource-markdown.js";
10
+
11
+ export function collectionRevision(loaded, resourceType, options = {}) {
12
+ const program = Object.hasOwn(options, "program")
13
+ ? options.program
14
+ : resolveProgram(loaded, options.programId);
15
+ const inputs = new Map(collectionRevisionInputs(loaded, resourceType, program)
16
+ .map((input) => [input.record.id, input]));
17
+ const authoritativeSource = loaded.resources.find(({ id }) => id === options.authoritativeSourceId);
18
+ if (authoritativeSource) {
19
+ inputs.set(authoritativeSource.id, { record: authoritativeSource, value: authoritativeSource });
20
+ }
21
+ const records = [...inputs.values()]
22
+ .map(({ record, value }) => ({
23
+ id: record.id,
24
+ revision: createHash("sha256")
25
+ .update(JSON.stringify(canonicalRecordValue(loaded.model, record.type, value)))
26
+ .digest("hex"),
27
+ contentRevisions: markdownEntries(loaded.model, record).flatMap(({ path }) => {
28
+ try {
29
+ const content = readFileSync(resolveDataPath(loaded.root, path), "utf8");
30
+ return [{ path, revision: createHash("sha256").update(content).digest("hex") }];
31
+ } catch (error) {
32
+ if (error.code === "ENOENT") return [];
33
+ throw error;
34
+ }
35
+ })
36
+ }))
37
+ .sort((left, right) => left.id.localeCompare(right.id));
38
+ const workspaceScope = collectionScopeRevisionFacts(loaded, resourceType, program);
39
+ return createHash("sha256")
40
+ .update(JSON.stringify({ resourceType, records, workspaceScope }))
41
+ .digest("hex");
42
+ }
43
+
44
+ function canonicalRecordValue(model, resourceType, value) {
45
+ const fields = {
46
+ ...model.commonFields,
47
+ ...model.resources[resourceType]?.fields
48
+ };
49
+ return canonicalObject(model, value, fields);
50
+ }
51
+
52
+ function canonicalObject(model, value, fields = {}) {
53
+ return Object.fromEntries(Object.keys(value).sort().map((name) => [
54
+ name,
55
+ canonicalFieldValue(model, value[name], fields[name])
56
+ ]));
57
+ }
58
+
59
+ function canonicalFieldValue(model, value, field) {
60
+ if (Array.isArray(value)) {
61
+ const objectType = field?.itemObjectType;
62
+ const itemFields = objectType ? model.objectTypes?.[objectType]?.properties : undefined;
63
+ const items = value.map((item) => (
64
+ item && typeof item === "object" && !Array.isArray(item)
65
+ ? canonicalObject(model, item, itemFields)
66
+ : item
67
+ ));
68
+ return field?.type === "array"
69
+ ? items.sort(compareCanonicalValues)
70
+ : items;
71
+ }
72
+ if (value && typeof value === "object") {
73
+ const objectType = field?.objectType;
74
+ return canonicalObject(model, value, objectType ? model.objectTypes?.[objectType]?.properties : undefined);
75
+ }
76
+ return value;
77
+ }
78
+
79
+ function compareCanonicalValues(left, right) {
80
+ const leftValue = JSON.stringify(left);
81
+ const rightValue = JSON.stringify(right);
82
+ return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
83
+ }
@@ -1,4 +1,4 @@
1
- import { programComponents } from "./program.js";
1
+ import { programComponents, selectedRequirementIds } from "./program.js";
2
2
 
3
3
  export function scopedCollectionRecords(loaded, resourceType, program) {
4
4
  if (String(loaded.model.modelVersion) !== "4") {
@@ -10,10 +10,22 @@ export function scopedCollectionRecords(loaded, resourceType, program) {
10
10
  const scopedProgram = program || {};
11
11
  const components = programComponents(loaded, scopedProgram);
12
12
  const componentIds = new Set(components.map(({ id }) => id));
13
+ const systemIds = new Set(scopedProgram.systemIds || []);
14
+ const controlIds = new Set(scopedProgram.controlIds || []);
13
15
  const selected = {
14
- system: new Set(scopedProgram.systemIds || []),
16
+ system: systemIds,
15
17
  component: componentIds,
16
18
  framework: new Set(scopedProgram.frameworkIds || []),
19
+ "complementary-control": new Set(loaded.resources.filter((record) => (
20
+ record.type === "complementary-control"
21
+ && record.status !== "superseded"
22
+ && record.status !== "retired"
23
+ && (
24
+ (record.systemIds || []).some((id) => systemIds.has(id))
25
+ || (record.relatedControlIds || []).some((id) => controlIds.has(id))
26
+ || (record.componentIds || []).some((id) => componentIds.has(id))
27
+ )
28
+ )).map(({ id }) => id)),
17
29
  asset: new Set(loaded.resources.filter((record) => (
18
30
  record.type === "asset" && (record.componentIds || []).some((id) => componentIds.has(id))
19
31
  )).map(({ id }) => id))
@@ -22,3 +34,149 @@ export function scopedCollectionRecords(loaded, resourceType, program) {
22
34
  record.type === resourceType && (!selected || selected.has(record.id))
23
35
  ));
24
36
  }
37
+
38
+ export function collectionRevisionInputs(loaded, resourceType, program) {
39
+ const reviewed = scopedCollectionRecords(loaded, resourceType, program);
40
+ if (String(loaded.model.modelVersion) !== "4") {
41
+ return reviewed.map((record) => ({ record, value: record }));
42
+ }
43
+ const byId = new Map(loaded.resources.map((record) => [record.id, record]));
44
+ const records = new Map(reviewed.map((record) => [record.id, record]));
45
+ const reviewedIds = new Set(records.keys());
46
+ const addIds = (ids) => {
47
+ for (const id of ids || []) {
48
+ const record = byId.get(id);
49
+ if (record) records.set(record.id, record);
50
+ }
51
+ };
52
+
53
+ if (resourceType === "framework") {
54
+ addIds(program?.systemIds);
55
+ addIds(selectedRequirementIds(program || {}, loaded.model));
56
+ }
57
+ if (resourceType === "vendor") {
58
+ for (const record of reviewed) {
59
+ addIds([record.agreementDocumentId, record.classificationId]);
60
+ addIds(record.informationTypeIds);
61
+ }
62
+ }
63
+ if (resourceType === "system") {
64
+ for (const record of reviewed) {
65
+ addIds([record.classificationId]);
66
+ addIds(record.informationTypeIds);
67
+ }
68
+ }
69
+ if (resourceType === "component") {
70
+ addIds(program?.systemIds);
71
+ addIds(program?.controlIds);
72
+ for (const record of reviewed) {
73
+ addIds([record.vendorId, record.classificationId]);
74
+ addIds((record.informationUses || []).map(({ informationTypeId }) => informationTypeId));
75
+ }
76
+ }
77
+ if (resourceType === "complementary-control") {
78
+ addIds(program?.systemIds);
79
+ addIds(program?.controlIds);
80
+ for (const record of reviewed) {
81
+ addIds([record.vendorId]);
82
+ addIds(record.requirementIds);
83
+ addIds(record.commitmentIds);
84
+ addIds(record.sourceDocumentIds);
85
+ addIds(record.componentIds);
86
+ }
87
+ }
88
+ return [...records.values()].map((record) => ({
89
+ record,
90
+ value: reviewedIds.has(record.id)
91
+ ? record
92
+ : dependencyRevisionValue(resourceType, record)
93
+ }));
94
+ }
95
+
96
+ export function collectionScopeRevisionFacts(loaded, resourceType, program) {
97
+ if (String(loaded.model.modelVersion) !== "4") {
98
+ const common = { programId: program?.id ?? null };
99
+ if (resourceType === "framework") {
100
+ return {
101
+ ...common,
102
+ assuranceGoal: program?.assuranceGoal ?? null,
103
+ systemIds: sorted(program?.systemIds),
104
+ frameworkIds: sorted(program?.frameworkIds),
105
+ requirementIds: sorted(selectedRequirementIds(program || {}, loaded.model))
106
+ };
107
+ }
108
+ if (resourceType === "vendor" || resourceType === "system" || resourceType === "complementary-control") {
109
+ return {
110
+ ...common,
111
+ systemIds: sorted(program?.systemIds),
112
+ controlIds: sorted(program?.controlIds)
113
+ };
114
+ }
115
+ return common;
116
+ }
117
+ const common = { programId: program?.id ?? null };
118
+ if (resourceType === "framework") {
119
+ return {
120
+ ...common,
121
+ assuranceGoal: program?.assuranceGoal ?? null,
122
+ systemIds: sorted(program?.systemIds),
123
+ frameworkIds: sorted(program?.frameworkIds),
124
+ requirementIds: sorted(selectedRequirementIds(program || {}, loaded.model))
125
+ };
126
+ }
127
+ if (resourceType === "system") {
128
+ return { ...common, systemIds: sorted(program?.systemIds) };
129
+ }
130
+ if (resourceType === "component" || resourceType === "complementary-control") {
131
+ return {
132
+ ...common,
133
+ systemIds: sorted(program?.systemIds),
134
+ controlIds: sorted(program?.controlIds)
135
+ };
136
+ }
137
+ return common;
138
+ }
139
+
140
+ const dependencyFields = {
141
+ framework: {
142
+ system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "informationTypeIds", "classificationId", "internetExposed"],
143
+ requirement: ["id", "type", "title", "frameworkId", "reference", "description", "parentRequirementId"]
144
+ },
145
+ vendor: {
146
+ document: ["id", "type", "status", "documentKind", "version", "effectiveOn", "approvedOn", "approvedContentRevisions", "systemIds", "controlIds", "componentIds", "classificationId"],
147
+ classification: ["id", "type", "status", "rank", "description", "handlingRequirements"],
148
+ "information-type": ["id", "type", "status", "classificationId", "description"]
149
+ },
150
+ system: {
151
+ classification: ["id", "type", "status", "rank", "description", "handlingRequirements"],
152
+ "information-type": ["id", "type", "status", "classificationId", "description"]
153
+ },
154
+ component: {
155
+ system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "criticality", "informationTypeIds", "classificationId", "internetExposed", "continuityObjectives"],
156
+ control: ["id", "type", "status", "statement", "activity", "controlType", "operationMode", "operationPattern", "systemIds", "componentIds", "evidenceSourceComponentIds"],
157
+ vendor: ["id", "type", "status", "category", "criticality", "description", "standardAgreement", "agreementDocumentId", "startDate", "endDate", "classificationId", "informationTypeIds"],
158
+ classification: ["id", "type", "status", "rank", "description", "handlingRequirements"],
159
+ "information-type": ["id", "type", "status", "classificationId", "description"]
160
+ },
161
+ "complementary-control": {
162
+ system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "informationTypeIds", "classificationId", "internetExposed"],
163
+ control: ["id", "type", "status", "statement", "activity", "controlType", "operationMode", "operationPattern", "systemIds", "componentIds", "evidenceSourceComponentIds"],
164
+ vendor: ["id", "type", "status", "category", "criticality", "description", "standardAgreement", "agreementDocumentId", "startDate", "endDate", "classificationId", "informationTypeIds"],
165
+ requirement: ["id", "type", "title", "frameworkId", "reference", "description", "parentRequirementId"],
166
+ commitment: ["id", "type", "status", "commitmentKind", "statement", "systemIds", "requirementIds", "controlIds", "customerFacing", "effectiveOn"],
167
+ document: ["id", "type", "status", "documentKind", "version", "effectiveOn", "approvedOn", "approvedContentRevisions", "systemIds", "controlIds", "componentIds", "classificationId"],
168
+ component: ["id", "type", "status", "componentKind", "description", "vendorId", "systemUses", "informationUses", "internetExposed"]
169
+ }
170
+ };
171
+
172
+ function dependencyRevisionValue(resourceType, record) {
173
+ const fields = dependencyFields[resourceType]?.[record.type];
174
+ if (!fields) return { id: record.id, type: record.type };
175
+ return Object.fromEntries(fields
176
+ .filter((field) => record[field] !== undefined)
177
+ .map((field) => [field, record[field]]));
178
+ }
179
+
180
+ function sorted(values) {
181
+ return [...(values || [])].sort();
182
+ }
package/src/git.js CHANGED
@@ -7,7 +7,7 @@ import { performance } from "node:perf_hooks";
7
7
  import { isSafeGitName } from "./git-name.js";
8
8
  import { serializeWorkspaceMutation, withDeferredWorkspaceValidation } from "./mutation.js";
9
9
  import { resolveWorkspaceRoot } from "./paths.js";
10
- import { measureTiming, recordTiming, timingEnabled } from "./timing.js";
10
+ import { measureTiming, measureTimingSync, recordTiming, timingEnabled } from "./timing.js";
11
11
  import { fingerprintWorkspace, validateWorkspace } from "./validate.js";
12
12
  import { loadWorkspace } from "./workspace.js";
13
13
 
@@ -44,7 +44,7 @@ export class GitOperationError extends Error {
44
44
  export function getGitSummary(input = process.cwd()) {
45
45
  const root = resolveWorkspaceRoot(input);
46
46
  try {
47
- const topLevel = git(root, ["rev-parse", "--show-toplevel"]);
47
+ const topLevel = measureTimingSync("git-discovery", () => git(root, ["rev-parse", "--show-toplevel"]));
48
48
  const status = git(root, ["status", "--porcelain=v1", "--", "."]);
49
49
  const commit = tryGit(root, ["rev-parse", "HEAD"]) || null;
50
50
  const branch = tryGit(root, ["symbolic-ref", "--short", "HEAD"]) || null;
@@ -181,12 +181,40 @@ export function getRepositorySnapshot(input = process.cwd(), options = {}) {
181
181
  return snapshot;
182
182
  }
183
183
 
184
+ export async function getWorkspaceRevisionSnapshot(input = process.cwd()) {
185
+ const root = resolveWorkspaceRoot(input);
186
+ try {
187
+ const source = await measureTiming("repository-revision", () => runGitCommand(root, [
188
+ "status",
189
+ "--porcelain=v2",
190
+ "--branch",
191
+ "-z",
192
+ "--untracked-files=all",
193
+ "--",
194
+ "data"
195
+ ], { operation: "resolve the workspace revision" }));
196
+ const parsed = parseWorkspaceRevision(source);
197
+ return {
198
+ available: true,
199
+ commit: parsed.commit,
200
+ shortCommit: parsed.commit?.slice(0, 8) ?? "no commits",
201
+ clean: parsed.changePaths.length === 0,
202
+ changes: parsed.changePaths,
203
+ workspaceChangePaths: parsed.changePaths
204
+ };
205
+ } catch (error) {
206
+ return unavailableSnapshot(error, { workspaceChangePaths: [] });
207
+ }
208
+ }
209
+
184
210
  async function buildRepositorySnapshot(root) {
185
211
  let repositoryPaths;
186
212
  try {
187
- repositoryPaths = await runGitCommand(root, ["rev-parse", "--show-toplevel", "--absolute-git-dir"], {
188
- operation: "locate the repository"
189
- });
213
+ repositoryPaths = await measureTiming("git-discovery", () => runGitCommand(
214
+ root,
215
+ ["rev-parse", "--show-toplevel", "--absolute-git-dir"],
216
+ { operation: "locate the repository" }
217
+ ));
190
218
  } catch (error) {
191
219
  return unavailableSnapshot(error);
192
220
  }
@@ -1058,6 +1086,27 @@ function parsePorcelainV2(source, topLevel, root) {
1058
1086
  return { commit, branch, upstream, ahead, behind, allChanges, workspaceChanges };
1059
1087
  }
1060
1088
 
1089
+ function parseWorkspaceRevision(source) {
1090
+ const fields = source.split("\0").filter(Boolean);
1091
+ let commit = null;
1092
+ const changePaths = [];
1093
+ for (let index = 0; index < fields.length; index += 1) {
1094
+ const field = fields[index];
1095
+ if (field.startsWith("# branch.oid ")) {
1096
+ commit = field.slice(13) === "(initial)" ? null : field.slice(13);
1097
+ } else if (/^[12u?!] /.test(field)) {
1098
+ changePaths.push(porcelainV2Path(field));
1099
+ if (field.startsWith("2 ")) {
1100
+ changePaths.push(fields[++index]);
1101
+ }
1102
+ }
1103
+ }
1104
+ return {
1105
+ commit,
1106
+ changePaths: [...new Set(changePaths.filter(Boolean))].sort()
1107
+ };
1108
+ }
1109
+
1061
1110
  function porcelainV2Path(field) {
1062
1111
  if (field.startsWith("? ") || field.startsWith("! ")) return field.slice(2);
1063
1112
  const requiredSpaces = field.startsWith("2 ") ? 9 : field.startsWith("u ") ? 10 : 8;
@@ -91,7 +91,9 @@ const TRANSITIONS = {
91
91
  };
92
92
 
93
93
  export async function planReconciliation(input = process.cwd()) {
94
- const loaded = await loadWorkspace(input);
94
+ const loaded = input?.resources && input?.model && input?.entries
95
+ ? input
96
+ : await loadWorkspace(input);
95
97
  if (!["3", "4"].includes(String(loaded.model.modelVersion))) {
96
98
  return {
97
99
  contractVersion: 1,
package/src/server.js CHANGED
@@ -6,7 +6,7 @@ import { performance } from "node:perf_hooks";
6
6
  import { getResourceDefinition } from "../model/index.js";
7
7
  import { prepareAuditWorkspace } from "./audit-preparation.js";
8
8
  import { createNextAuditCycle, planNextAuditCycle } from "./audit-transition.js";
9
- import { applyApplicabilityReview, planApplicabilityReview } from "./batch-review.js";
9
+ import { applyApplicabilityReviewWithContext, planApplicabilityReview } from "./batch-review.js";
10
10
  import { applyCollectionReview, planCollectionReview } from "./collection-review.js";
11
11
  import { generateEvidencePacket, prepareEvidencePacket } from "./evidence-packet.js";
12
12
  import { FAVICON_PNG, LOGO_MARK_PNG } from "./favicon.js";
@@ -174,10 +174,11 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
174
174
  message: (result) => `Record ${result.reviewedIds?.length || 0} applicability decisions`,
175
175
  fastResponse: prefersFastMutation(request),
176
176
  prefetchToken
177
- }, (_root, mutationContext) => applyApplicabilityReview(input, {
177
+ }, (_root, mutationContext) => applyApplicabilityReviewWithContext(input, {
178
178
  ...reviewPayload,
179
- scopeRevision: mutationContext?.repositorySnapshot?.currentCommit,
180
179
  confirmed: payload.confirmed === true
180
+ }, {
181
+ repositorySnapshot: mutationContext?.repositorySnapshot
181
182
  })));
182
183
  }
183
184
  if (request.method === "POST" && url.pathname === "/api/collection-review/preview") {
package/src/validate.js CHANGED
@@ -3,6 +3,7 @@ import { readFile, stat } from "node:fs/promises";
3
3
  import { performance } from "node:perf_hooks";
4
4
  import { getResourceDefinition } from "../model/index.js";
5
5
  import { scopedCollectionRecords } from "./collection-scope.js";
6
+ import { collectionRevision } from "./collection-revision.js";
6
7
  import { isSafeGitName } from "./git-name.js";
7
8
  import { isCanonicalDataPath, resolveDataPath } from "./paths.js";
8
9
  import { parseCalendarDate, validCalendarRecurrence } from "./recurrence.js";
@@ -177,14 +178,21 @@ function validateCollectionReview(record, loaded, byId, path, diagnostics) {
177
178
  ? (record.scopeResourceIds || []).map((id) => byId.get(id)).find(({ type } = {}) => type === "program")
178
179
  : null;
179
180
  const recordCount = scopedCollectionRecords(loaded, record.resourceType, program).length;
180
- if (!recordCount && record.decision === "complete") {
181
+ const currentRevision = collectionRevision(loaded, record.resourceType, {
182
+ program,
183
+ authoritativeSourceId: record.decision === "externally-managed"
184
+ ? record.authoritativeComponentId || record.authoritativeSystemId
185
+ : null
186
+ });
187
+ const current = record.collectionRevision === currentRevision;
188
+ if (current && !recordCount && record.decision === "complete") {
181
189
  diagnostics.push(error(
182
190
  "invalid-collection-review-decision",
183
191
  path,
184
192
  `${configuration.title} has no records and cannot use the complete conclusion.`
185
193
  ));
186
194
  }
187
- if (recordCount && record.decision === "zero-population") {
195
+ if (current && recordCount && record.decision === "zero-population") {
188
196
  diagnostics.push(error(
189
197
  "invalid-collection-review-decision",
190
198
  path,
@@ -192,7 +200,8 @@ function validateCollectionReview(record, loaded, byId, path, diagnostics) {
192
200
  ));
193
201
  }
194
202
  if (
195
- record.decision === "externally-managed"
203
+ current
204
+ && record.decision === "externally-managed"
196
205
  && byId.get(record.authoritativeComponentId || record.authoritativeSystemId)?.status !== "active"
197
206
  ) {
198
207
  diagnostics.push(error(
package/src/web.js CHANGED
@@ -184,7 +184,7 @@ function buildNavigation(route) {
184
184
  const organizationCurrent = route.name === "organization" || route.name === "repository" || ["workspace", "renderer-settings"].includes(route.type);
185
185
  const organizationName = state.workspace.organizationName || "Organization";
186
186
  const initial = organizationName.trim().charAt(0).toUpperCase() || "O";
187
- return '<aside class="sidebar" id="sidebar-navigation"><button class="nav-close" type="button" aria-label="Close navigation">×</button><a href="#/" class="brand"' + (route.name === "home" ? ' aria-current="page"' : "") + '><img class="mark" src="./logo-mark-white.png" alt="" width="39" height="39"><span><strong>filegrc</strong><small>SOC 2 workspace</small></span></a><nav class="sidebar-nav">' + stages + '</nav><div class="sidebar-footer"><a class="organization-nav ' + (organizationCurrent ? "current" : "") + '" href="#/organization"><span class="organization-mark">' + esc(initial) + '</span><span><strong>' + esc(organizationName) + '</strong><small>Organization</small></span><span class="organization-arrow">›</span></a></div></aside><button class="nav-scrim" type="button" aria-label="Close navigation"></button>';
187
+ return '<aside class="sidebar" id="sidebar-navigation"><button class="nav-close" type="button" aria-label="Close navigation">×</button><a href="#/" class="brand"' + (route.name === "home" ? ' aria-current="page"' : "") + '><img class="mark" src="./logo-mark-white.png" alt="" width="39" height="39"><span><strong>filegrc</strong><small>SOC 2 workspace</small></span></a><label class="search mobile-sidebar-search"><span aria-hidden="true">⌕</span><input data-global-search type="search" placeholder="Search records" aria-label="Search records"></label><nav class="sidebar-nav">' + stages + '</nav><div class="sidebar-footer"><a class="organization-nav ' + (organizationCurrent ? "current" : "") + '" href="#/organization"><span class="organization-mark">' + esc(initial) + '</span><span><strong>' + esc(organizationName) + '</strong><small>Organization</small></span><span class="organization-arrow">›</span></a></div></aside><button class="nav-scrim" type="button" aria-label="Close navigation"></button>';
188
188
  }
189
189
 
190
190
  function readinessStageForRoute(route) {
@@ -249,7 +249,13 @@ function topbar(route) {
249
249
  const repositoryTone = state.repository?.mode === "trunk"
250
250
  ? repositoryStatusTone(state.repository.status)
251
251
  : state.git.clean ? "good" : "warn";
252
- return '<button class="mobile-nav" type="button" aria-label="Open navigation" aria-controls="sidebar-navigation" aria-expanded="false">☰</button><div><small class="eyebrow">' + esc(state.workspace.organizationName) + '</small><h1>' + esc(titleCase(title)) + '</h1></div><label class="search"><span aria-hidden="true">⌕</span><input id="global-search" type="search" placeholder="Search records" aria-label="Search records"><kbd>/</kbd></label><div class="topbar-status"><a class="validation-chip" href="#/repository"><span class="status-dot ' + (state.validation.ok ? "good" : "bad") + '"></span>' + (state.validation.ok ? "Data valid" : state.validation.counts.errors + " validation errors") + '</a><a class="repo-chip" href="#/repository"><span class="status-dot ' + repositoryTone + '"></span>' + esc(repositoryLabel) + '</a></div>';
252
+ return '<button class="mobile-nav" type="button" aria-label="Open navigation" aria-controls="sidebar-navigation" aria-expanded="false">☰</button><div><small class="eyebrow">' + esc(state.workspace.organizationName) + '</small><h1>' + esc(titleCase(title)) + '</h1></div><div class="topbar-status">' + topbarProgramReadiness() + '<label class="search topbar-search"><span aria-hidden="true">⌕</span><input data-global-search type="search" placeholder="Search records" aria-label="Search records"><kbd>/</kbd></label><a class="validation-chip" href="#/repository"><span class="status-dot ' + (state.validation.ok ? "good" : "bad") + '"></span>' + (state.validation.ok ? "Data valid" : state.validation.counts.errors + " validation errors") + '</a><a class="repo-chip" href="#/repository"><span class="status-dot ' + repositoryTone + '"></span>' + esc(repositoryLabel) + '</a></div>';
253
+ }
254
+
255
+ function topbarProgramReadiness() {
256
+ const progress = dashboardProgramReadiness(state.programReadiness);
257
+ const detail = progress.complete + " of " + progress.total + " readiness items complete";
258
+ return '<a class="topbar-readiness" href="' + nextProgramStageHref() + '" aria-label="' + esc("Program readiness: " + progress.percent + "%. " + detail + ". " + progress.status + ".") + '"><span class="topbar-readiness-copy"><span>Program readiness</span><strong>' + progress.percent + '%</strong></span><span class="progress" aria-hidden="true"><span style="width:' + progress.percent + '%"></span></span></a>';
253
259
  }
254
260
 
255
261
  function repositoryStatusTone(status) {
@@ -310,7 +316,6 @@ function initialSetupBanner() {
310
316
  }
311
317
 
312
318
  function readinessOverview() {
313
- const progress = dashboardProgramReadiness(state.programReadiness);
314
319
  const nextHref = nextProgramStageHref();
315
320
  const programStage = (id, href) => {
316
321
  const stage = READINESS_STAGES.find((candidate) => candidate.id === id);
@@ -326,7 +331,7 @@ function readinessOverview() {
326
331
  programStage("run", "#/stage/run"),
327
332
  programStage("audit", "#/stage/audit")
328
333
  ];
329
- return '<section class="readiness-map"><div class="readiness-map-head"><div><p class="kicker">SOC 2 program path</p><h3>Prepare, Operate, Then Audit</h3></div><div class="readiness-progress-summary"><div><span>Program readiness</span><strong>' + progress.percent + '%</strong><b class="badge ' + esc(progress.tone) + '">' + esc(progress.status) + '</b><div class="progress"><span style="width:' + progress.percent + '%"></span></div><small>' + esc(progress.complete + " of " + progress.total + " readiness items complete") + '</small></div><a class="button primary" href="' + nextHref + '">Continue</a></div></div><div class="readiness-flow">' + stages.map(([title, body, href, status, tone], index) => '<a href="' + href + '"><span>' + (index + 1) + '</span><strong>' + esc(title) + '</strong><small>' + esc(body) + '</small><b class="readiness-state ' + esc(tone) + '">' + esc(status) + '</b></a>').join("") + '</div></section>';
334
+ return '<section class="readiness-map"><div class="readiness-map-head"><div><p class="kicker">SOC 2 program path</p><h3>Prepare, Operate, Then Audit</h3></div><a class="button primary" href="' + nextHref + '">Continue</a></div><div class="readiness-flow">' + stages.map(([title, body, href, status, tone], index) => '<a href="' + href + '"><span>' + (index + 1) + '</span><strong>' + esc(title) + '</strong><small>' + esc(body) + '</small><b class="readiness-state ' + esc(tone) + '">' + esc(status) + '</b></a>').join("") + '</div></section>';
330
335
  }
331
336
 
332
337
  function nextProgramStageHref() {
@@ -1511,7 +1516,6 @@ function openApplicabilityReviewDialog(type, entries) {
1511
1516
  decisions,
1512
1517
  reviewedByIds: [form.elements.reviewerId.value],
1513
1518
  reviewedOn: form.elements.reviewedOn.value,
1514
- scopeRevision: state.git?.commit || undefined,
1515
1519
  expectedRevisions: Object.fromEntries([
1516
1520
  ...entries.filter((entry) => decisionIds.has(entry.record.id)).map((entry) => [entry.record.id, entry.revision]),
1517
1521
  ...(type === "requirement" && String(state.model.modelVersion) === "4"
@@ -1529,7 +1533,7 @@ function openApplicabilityReviewDialog(type, entries) {
1529
1533
  });
1530
1534
  if (!response.ok) throw new Error(await responseMessage(response));
1531
1535
  const preview = await response.json();
1532
- previewedPayload = payload;
1536
+ previewedPayload = { ...payload, basis: preview.basis };
1533
1537
  dialog.querySelector(".workflow-preview").innerHTML = '<strong>Review before saving</strong><p>' + preview.reviewedIds.length + ' ' + pluralize("decision", preview.reviewedIds.length) + ' will be saved with the reviewer, review date, and current scope recorded automatically.</p>';
1534
1538
  dialog.querySelector("[data-preview-review]").textContent = "Confirm and save";
1535
1539
  } else {
@@ -3666,7 +3670,7 @@ function bindCommon() {
3666
3670
  setNavigation(false);
3667
3671
  navButton?.focus();
3668
3672
  });
3669
- const search = root.querySelector("#global-search");
3673
+ const searches = [...root.querySelectorAll("[data-global-search]")];
3670
3674
  const sidebarNavigation = root.querySelector(".sidebar-nav");
3671
3675
  sidebarNavigation?.addEventListener("scroll", () => {
3672
3676
  navigationScrollTop = sidebarNavigation.scrollTop;
@@ -3676,12 +3680,13 @@ function bindCommon() {
3676
3680
  // Browser storage may be unavailable; navigation still preserves the position in memory.
3677
3681
  }
3678
3682
  }, { passive: true });
3679
- search?.addEventListener("keydown", (event) => {
3683
+ searches.forEach((search) => search.addEventListener("keydown", (event) => {
3680
3684
  if (event.key === "Enter" && search.value.trim()) {
3681
3685
  event.preventDefault();
3686
+ if (search.closest(".sidebar")) setNavigation(false);
3682
3687
  globalSearch(search.value);
3683
3688
  }
3684
- });
3689
+ }));
3685
3690
  document.onkeydown = (event) => {
3686
3691
  if (event.key === "Escape" && sidebar.classList.contains("shown")) {
3687
3692
  setNavigation(false);
@@ -3690,6 +3695,9 @@ function bindCommon() {
3690
3695
  }
3691
3696
  if (event.key === "/" && !/input|textarea/i.test(document.activeElement?.tagName)) {
3692
3697
  event.preventDefault();
3698
+ const mobile = window.matchMedia("(max-width: 760px)").matches;
3699
+ const search = root.querySelector(mobile ? ".mobile-sidebar-search input" : ".topbar-search input");
3700
+ if (mobile) setNavigation(true);
3693
3701
  search?.focus();
3694
3702
  }
3695
3703
  };
@@ -3706,8 +3714,9 @@ function globalSearch(query) {
3706
3714
  document.body.append(dialog);
3707
3715
  dialog.showModal();
3708
3716
  const clearSearch = () => {
3709
- const search = root.querySelector("#global-search");
3710
- if (search?.value.trim() === query) search.value = "";
3717
+ root.querySelectorAll("[data-global-search]").forEach((search) => {
3718
+ if (search.value.trim() === query) search.value = "";
3719
+ });
3711
3720
  };
3712
3721
  dialog.querySelector(".icon-button").onclick = () => {
3713
3722
  clearSearch();
@@ -4297,8 +4306,8 @@ function esc(value) { return String(value ?? "").replace(/[&<>"']/g, (character)
4297
4306
 
4298
4307
  export const APP_STYLES = String.raw`
4299
4308
  :root{--ink:#151827;--muted:#5d6475;--line:#dfe3ef;--paper:#f6f7fb;--panel:#fff;--accent:#0000a5;--accent-soft:#eef1ff;--accent-light:#8aa1ff;--focus:#0000e0;--amber:#8a5200;--red:#a13a31;--sidebar:linear-gradient(135deg,#000070 0%,#000035 60%);--primary-gradient:linear-gradient(135deg,#000070 0%,#000035 60%);--surface-soft:#f2f4fa;--surface-muted:#eceff7;--field:#fff;--field-readonly:#eef0f6;--code-bg:#10162b;--code-ink:#e8ebff;--shadow:0 8px 28px rgba(0,0,53,.08);color-scheme:light dark;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--ink);background:var(--paper);font-synthesis:none}
4300
- *{box-sizing:border-box}body{margin:0;min-width:320px;background:var(--paper)}button,input,select,textarea{font:inherit}a{color:inherit}.skip-link{position:fixed;left:1rem;top:-4rem;z-index:100;padding:.7rem 1rem;background:#fff}.skip-link:focus{top:1rem}.loading,.fatal{padding:3rem}.shell{display:grid;grid-template-columns:248px 1fr;min-height:100vh}.sidebar{position:fixed;inset:0 auto 0 0;width:248px;background:var(--sidebar);color:#eef1ff;padding:25px 18px 18px;overflow:auto;z-index:20}.brand{display:flex;align-items:center;gap:12px;text-decoration:none;margin:0 7px 27px}.brand .mark{display:block;width:39px;height:39px;border-radius:10px}.brand strong,.brand small{display:block}.brand strong{color:#fff;font-size:18px}.brand small{font-size:13.2px;color:#c5cae2;margin-top:2px}.nav-home,.nav-items a{display:flex;justify-content:space-between;align-items:center;text-decoration:none;border-radius:7px;padding:8px 10px;font-size:15.6px;color:#d5d9ed}.nav-home{margin-bottom:9px}.nav-home:hover,.nav-items a:hover,.nav-home.current,.nav-items a.current{background:#202066;color:#fff}.nav-heading{width:100%;border:0;background:none;color:#b4bbdc;text-transform:uppercase;letter-spacing:.11em;font-size:12px;font-weight:750;display:flex;align-items:center;justify-content:space-between;padding:13px 10px 5px;cursor:pointer}.chevron{display:grid;place-items:center;width:14px;height:22px;font-size:0;line-height:1;transform:none}.chevron:before{content:"";width:6px;height:6px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transform-origin:center;transition:transform .15s}.nav-items{display:none}.nav-group.open .nav-items{display:block}.nav-items small{font-size:12px;color:#b8bed7}.side-foot{position:sticky;bottom:-18px;margin:25px -18px -18px;padding:17px 25px;background:#000024;border-top:1px solid #34345f;color:#cbd0e5;font-size:13.2px;display:flex;align-items:center;gap:8px}.status-dot{width:8px;height:8px;border-radius:50%;background:#9aa39f;display:inline-block;flex:0 0 auto}.status-dot.good,.badge.good{background:#6abf8c}.status-dot.warn,.badge.warn{background:#e9a445}.status-dot.bad,.badge.bad{background:#dc6c5d}.status-dot.neutral{background:#9aabff}.workspace{grid-column:2;min-width:0}.topbar{height:86px;background:rgba(255,255,255,.88);backdrop-filter:blur(10px);border-bottom:1px solid var(--line);padding:0 32px;display:flex;align-items:center;gap:23px;position:sticky;top:0;z-index:10}.topbar>div:first-of-type{min-width:190px}.topbar h1{font-size:20.4px;line-height:1.1;margin:3px 0 0}.eyebrow,.kicker{color:var(--accent);text-transform:uppercase;letter-spacing:.12em;font-weight:760;font-size:10.8px;margin:0}.search{height:39px;max-width:480px;flex:1;margin-left:auto;display:flex;align-items:center;gap:9px;background:#f2f4fa;border:1px solid #dfe3ef;border-radius:8px;padding:0 10px;color:#5d6475}.search input{border:0;outline:0;background:none;min-width:0;flex:1;font-size:15.6px}.search kbd{background:#fff;border:1px solid #dfe3ef;border-radius:4px;padding:1px 5px;font-size:12px}.repo-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:8px;padding:10px 12px;color:var(--muted);font-size:13.2px;white-space:nowrap;text-decoration:none}.mobile-nav{display:none}.page{padding:30px 34px 70px;max-width:1510px;margin:auto}.hero{color:#f8f9ff;background:linear-gradient(120deg,#000070,#000035);border-radius:13px;padding:28px 31px;display:flex;justify-content:space-between;align-items:end;min-height:158px;box-shadow:var(--shadow);position:relative;overflow:hidden}.hero:after{content:"";position:absolute;width:270px;height:270px;border:55px solid rgba(138,161,255,.1);border-radius:50%;right:-80px;top:-145px}.hero .kicker{color:#cbd3ff}.hero h2{font-family:Georgia,serif;font-weight:500;font-size:33.6px;margin:10px 0 8px;letter-spacing:-.02em}.hero p:not(.kicker){margin:0;color:#dde1f4;font-size:15.6px;max-width:650px}.hero-meta{display:flex;gap:15px;position:relative;z-index:1}.hero-meta span{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#e6e8f7;border-left:1px solid #6874ab;padding-left:15px}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:14px 0}.metric{background:#fff;border:1px solid var(--line);border-radius:10px;padding:16px 18px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.metric-label{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);display:flex;align-items:center;gap:7px}.metric>strong{display:block;font-family:Georgia,serif;font-size:30px;font-weight:500;margin:8px 0 2px}.metric>small{font-size:12px;color:#697184}.dashboard-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.panel{background:#fff;border:1px solid var(--line);border-radius:11px;padding:21px;min-width:0;box-shadow:0 2px 8px rgba(21,40,33,.025)}.span-2{grid-column:span 2}.panel-head{display:flex;align-items:start;justify-content:space-between;gap:15px;margin-bottom:18px}.panel-head h3{font-size:16.8px;margin:4px 0 0}.panel-head>a{font-size:13.2px;color:var(--accent);font-weight:700}.audit-progress{display:grid;grid-template-columns:105px 1fr;gap:11px 20px;align-items:end}.progress-number strong{font-family:Georgia,serif;font-size:36px;font-weight:500;display:block}.progress-number span{font-size:12px;color:var(--muted)}.progress{height:9px;background:#eceff7;border-radius:9px;overflow:hidden}.progress span{display:block;height:100%;background:linear-gradient(90deg,#0000a5,var(--accent-light));border-radius:9px}.progress-meta{grid-column:2;display:flex;justify-content:space-between;font-size:10.8px;text-transform:uppercase;letter-spacing:.08em;color:#5d6475}.due-list{display:grid}.due-list a{display:grid;grid-template-columns:60px 1fr;text-decoration:none;border-top:1px solid #e8ebf3;padding:10px 0;align-items:center}.due-list a:first-child{border:0;padding-top:0}.due-list time{font-size:12px;color:var(--accent);font-weight:750}.due-list strong,.due-list small{display:block}.due-list strong{font-size:13.2px}.due-list small{font-size:10.8px;color:var(--muted);margin-top:3px}.resource-bars{display:grid;gap:11px}.resource-bars a{display:grid;grid-template-columns:105px 1fr 20px;gap:9px;align-items:center;text-decoration:none;font-size:12px}.resource-bars i{height:5px;background:#e8ebf3;border-radius:5px;overflow:hidden}.resource-bars b{display:block;height:100%;background:#6676dd;border-radius:5px}.resource-bars strong{text-align:right}.catalog{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}.catalog a{display:flex;justify-content:space-between;text-decoration:none;padding:9px 11px;background:#f2f4fa;border-radius:6px;font-size:12px}.catalog a:hover{background:var(--accent-soft)}.page-intro{display:flex;justify-content:space-between;align-items:end;margin-bottom:25px}.page-intro h2,.detail-head h2{font-family:Georgia,serif;font-size:37.2px;font-weight:500;margin:7px 0}.page-intro p:not(.kicker){color:var(--muted);max-width:700px;font-size:15.6px;margin:0}.button{border:1px solid #d0d5e3;background:#fff;border-radius:7px;padding:9px 13px;cursor:pointer;font-size:14.4px;font-weight:650}.button.primary{background:var(--accent);border-color:var(--accent);color:#fff}.button.danger{color:var(--red)}.list-tools{display:flex;align-items:center;gap:10px;margin-bottom:12px}.list-tools label{flex:1}.list-tools input,.list-tools select{width:100%;border:1px solid var(--line);border-radius:7px;background:#fff;padding:10px 12px;font-size:14.4px}.list-tools select{width:auto}.list-tools>span{color:var(--muted);font-size:12px}.record-table-wrap{background:#fff;border:1px solid var(--line);border-radius:10px;overflow:auto}.record-table{width:100%;border-collapse:collapse;font-size:13.2px}.record-table th{background:#f2f4fa;text-align:left;text-transform:uppercase;letter-spacing:.08em;color:#75817b;font-size:10.8px;padding:11px 14px;border-bottom:1px solid var(--line)}.record-table td{padding:13px 14px;border-bottom:1px solid #e8ebf3;vertical-align:top}.record-table tr:last-child td{border-bottom:0}.record-table code{font-size:10.8px;color:#5d6475}.record-title{display:block;color:var(--ink);font-weight:700;text-decoration:none}.record-table td>small{display:block;color:#6a7181;margin-top:3px}.record-table td[data-label="Description"]{min-width:260px;max-width:520px;color:var(--muted);line-height:1.45}.badge,.tag,.type-pill{display:inline-block;border-radius:99px;background:#eceff7;padding:3px 7px;font-size:10.8px;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap}.tag{text-transform:none;margin:1px}.badge.status-active,.badge.status-approved,.badge.status-complete,.badge.status-passed,.badge.status-accepted{background:#ddefe5;color:#176143}.badge.status-open,.badge.status-high,.badge.status-critical,.badge.status-failed{background:#f5ded9;color:#8d352c}.badge.status-draft,.badge.status-planned,.badge.status-in-progress,.badge.status-medium{background:#f7e9cf;color:#855717}.breadcrumbs{display:flex;gap:8px;color:var(--muted);font-size:13.2px;margin-bottom:20px}.detail-head{display:flex;justify-content:space-between;align-items:end;margin-bottom:22px}.detail-head h2{margin-bottom:4px}.detail-head>div>code{font-size:12px;color:var(--muted)}.actions{display:flex;gap:7px}.detail-grid{display:grid;grid-template-columns:minmax(0,2fr) minmax(270px,1fr);gap:14px}.detail-grid aside{display:grid;gap:14px;align-content:start}.detail-main{padding:29px}.content-label{color:#75817b;text-transform:uppercase;letter-spacing:.08em;font-size:10.8px;border-bottom:1px solid var(--line);padding-bottom:13px;margin-bottom:23px}.markdown{max-width:790px}.markdown h1{font-family:Georgia,serif;font-size:34.8px;font-weight:500}.markdown h2{font-family:Georgia,serif;font-size:27.6px;font-weight:500;margin-top:1.8em}.markdown h3{font-size:18px;margin-top:1.7em}.markdown p,.markdown li{font-size:15.6px;line-height:1.65;color:#272c3b}.markdown code{background:#eef0f6;border-radius:3px;padding:1px 4px}.markdown pre{padding:15px;background:#10162b;color:#e8ebff;border-radius:7px;overflow:auto}.markdown blockquote{border-left:3px solid var(--accent-light);padding:4px 15px;color:var(--muted);margin-left:0}.table-wrap{overflow:auto}.markdown table{border-collapse:collapse;width:100%;font-size:13.2px}.markdown th,.markdown td{border:1px solid var(--line);padding:8px;text-align:left}.metadata{margin:0}.metadata>div{display:grid;grid-template-columns:105px 1fr;gap:10px;border-top:1px solid #e8ebf3;padding:10px 0}.metadata>div:first-child{border-top:0;padding-top:0}.metadata dt{font-size:10.8px;text-transform:uppercase;letter-spacing:.06em;color:#5d6475}.metadata dd{margin:0;font-size:13.2px;min-width:0}.compact-json{white-space:pre-wrap;font-size:10.8px}.git-panel>code{font-size:10.8px;word-break:break-all}.git-panel p{font-size:12px;color:var(--muted)}.relation{color:var(--accent);text-decoration:none}.history{display:grid}.history>div{display:grid;grid-template-columns:60px 1fr;gap:8px;padding:8px 0;border-top:1px solid #e8ebf3}.history>div:first-child{border-top:0}.history code{font-size:10.8px;color:var(--accent)}.history strong,.history small{display:block}.history strong{font-size:12px}.history small{font-size:10.8px;color:var(--muted);margin-top:2px}.empty{padding:25px;color:#697184;text-align:center;font-size:13.2px;background:#f4f5fa;border-radius:7px}.changes{padding-left:18px}.changes li{margin:8px 0}.diagnostics>div{display:grid;grid-template-columns:58px minmax(120px,180px) minmax(0,1fr);gap:10px;align-items:start;border-top:1px solid var(--line);padding:10px 0}.diagnostics p{margin:0;font-size:13.2px;overflow-wrap:anywhere}.diagnostics code{font-size:10.8px;overflow-wrap:anywhere}.editor,.search-results{width:min(760px,calc(100vw - 30px));border:0;border-radius:12px;padding:0;box-shadow:0 25px 80px rgba(0,0,24,.28)}dialog::backdrop{background:rgba(0,0,24,.55)}.editor form,.search-results{padding:23px}.dialog-head{display:flex;justify-content:space-between;align-items:start}.dialog-head h2{font-family:Georgia,serif;font-weight:500;margin:5px 0 0}.icon-button{border:0;background:#eceff7;width:32px;height:32px;border-radius:50%;font-size:26.4px;cursor:pointer}.editor form>p{font-size:13.2px;color:var(--muted)}.editor textarea{width:100%;height:440px;border:1px solid var(--line);border-radius:7px;background:#10162b;color:#e8ebff;padding:15px;font:13.2px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;tab-size:2}.dialog-actions{display:flex;justify-content:end;gap:8px;margin-top:14px}.dialog-error{color:var(--red);font-size:13.2px;min-height:18px;margin-top:7px}.result-list{display:grid;margin-top:17px;max-height:60vh;overflow:auto}.result-list a{display:block;text-decoration:none;padding:11px;border-top:1px solid var(--line)}.result-list strong,.result-list small{display:block}.result-list small{color:var(--muted);margin-top:3px}.muted{color:#737a8b}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
4301
- .topbar-status{display:flex;align-items:center;gap:8px}.repo-chip,.validation-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:8px;padding:10px 12px;color:var(--muted);font-size:13.2px;white-space:nowrap;text-decoration:none}.repo-chip:hover,.validation-chip:hover{color:var(--ink);border-color:var(--accent-light)}
4309
+ *{box-sizing:border-box}body{margin:0;min-width:320px;background:var(--paper)}button,input,select,textarea{font:inherit}a{color:inherit}.skip-link{position:fixed;left:1rem;top:-4rem;z-index:100;padding:.7rem 1rem;background:#fff}.skip-link:focus{top:1rem}.loading,.fatal{padding:3rem}.shell{display:grid;grid-template-columns:248px 1fr;min-height:100vh}.sidebar{position:fixed;inset:0 auto 0 0;width:248px;background:var(--sidebar);color:#eef1ff;padding:25px 18px 18px;overflow:auto;z-index:20}.brand{display:flex;align-items:center;gap:12px;text-decoration:none;margin:0 7px 27px}.brand .mark{display:block;width:39px;height:39px;border-radius:10px}.brand strong,.brand small{display:block}.brand strong{color:#fff;font-size:18px}.brand small{font-size:13.2px;color:#c5cae2;margin-top:2px}.nav-home,.nav-items a{display:flex;justify-content:space-between;align-items:center;text-decoration:none;border-radius:7px;padding:8px 10px;font-size:15.6px;color:#d5d9ed}.nav-home{margin-bottom:9px}.nav-home:hover,.nav-items a:hover,.nav-home.current,.nav-items a.current{background:#202066;color:#fff}.nav-heading{width:100%;border:0;background:none;color:#b4bbdc;text-transform:uppercase;letter-spacing:.11em;font-size:12px;font-weight:750;display:flex;align-items:center;justify-content:space-between;padding:13px 10px 5px;cursor:pointer}.chevron{display:grid;place-items:center;width:14px;height:22px;font-size:0;line-height:1;transform:none}.chevron:before{content:"";width:6px;height:6px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transform-origin:center;transition:transform .15s}.nav-items{display:none}.nav-group.open .nav-items{display:block}.nav-items small{font-size:12px;color:#b8bed7}.side-foot{position:sticky;bottom:-18px;margin:25px -18px -18px;padding:17px 25px;background:#000024;border-top:1px solid #34345f;color:#cbd0e5;font-size:13.2px;display:flex;align-items:center;gap:8px}.status-dot{width:8px;height:8px;border-radius:50%;background:#9aa39f;display:inline-block;flex:0 0 auto}.status-dot.good,.badge.good{background:#6abf8c}.status-dot.warn,.badge.warn{background:#e9a445}.status-dot.bad,.badge.bad{background:#dc6c5d}.status-dot.neutral{background:#9aabff}.workspace{grid-column:2;min-width:0}.topbar{height:86px;background:rgba(255,255,255,.88);backdrop-filter:blur(10px);border-bottom:1px solid var(--line);padding:0 32px;display:flex;align-items:center;gap:23px;position:sticky;top:0;z-index:10}.topbar>div:first-of-type{min-width:190px}.topbar h1{font-size:20.4px;line-height:1.1;margin:3px 0 0}.eyebrow,.kicker{color:var(--accent);text-transform:uppercase;letter-spacing:.12em;font-weight:760;font-size:10.8px;margin:0}.search{height:39px;max-width:240px;flex:1;margin-left:auto;display:flex;align-items:center;gap:9px;background:#f2f4fa;border:1px solid #dfe3ef;border-radius:8px;padding:0 10px;color:#5d6475}.search input{border:0;outline:0;background:none;min-width:0;flex:1;font-size:15.6px}.search kbd{background:#fff;border:1px solid #dfe3ef;border-radius:4px;padding:1px 5px;font-size:12px}.mobile-sidebar-search{display:none}.repo-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:8px;padding:10px 12px;color:var(--muted);font-size:13.2px;white-space:nowrap;text-decoration:none}.mobile-nav{display:none}.page{padding:30px 34px 70px;max-width:1510px;margin:auto}.hero{color:#f8f9ff;background:linear-gradient(120deg,#000070,#000035);border-radius:13px;padding:28px 31px;display:flex;justify-content:space-between;align-items:end;min-height:158px;box-shadow:var(--shadow);position:relative;overflow:hidden}.hero:after{content:"";position:absolute;width:270px;height:270px;border:55px solid rgba(138,161,255,.1);border-radius:50%;right:-80px;top:-145px}.hero .kicker{color:#cbd3ff}.hero h2{font-family:Georgia,serif;font-weight:500;font-size:33.6px;margin:10px 0 8px;letter-spacing:-.02em}.hero p:not(.kicker){margin:0;color:#dde1f4;font-size:15.6px;max-width:650px}.hero-meta{display:flex;gap:15px;position:relative;z-index:1}.hero-meta span{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#e6e8f7;border-left:1px solid #6874ab;padding-left:15px}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:14px 0}.metric{background:#fff;border:1px solid var(--line);border-radius:10px;padding:16px 18px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.metric-label{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);display:flex;align-items:center;gap:7px}.metric>strong{display:block;font-family:Georgia,serif;font-size:30px;font-weight:500;margin:8px 0 2px}.metric>small{font-size:12px;color:#697184}.dashboard-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.panel{background:#fff;border:1px solid var(--line);border-radius:11px;padding:21px;min-width:0;box-shadow:0 2px 8px rgba(21,40,33,.025)}.span-2{grid-column:span 2}.panel-head{display:flex;align-items:start;justify-content:space-between;gap:15px;margin-bottom:18px}.panel-head h3{font-size:16.8px;margin:4px 0 0}.panel-head>a{font-size:13.2px;color:var(--accent);font-weight:700}.audit-progress{display:grid;grid-template-columns:105px 1fr;gap:11px 20px;align-items:end}.progress-number strong{font-family:Georgia,serif;font-size:36px;font-weight:500;display:block}.progress-number span{font-size:12px;color:var(--muted)}.progress{height:9px;background:#eceff7;border-radius:9px;overflow:hidden}.progress span{display:block;height:100%;background:linear-gradient(90deg,#0000a5,var(--accent-light));border-radius:9px}.progress-meta{grid-column:2;display:flex;justify-content:space-between;font-size:10.8px;text-transform:uppercase;letter-spacing:.08em;color:#5d6475}.due-list{display:grid}.due-list a{display:grid;grid-template-columns:60px 1fr;text-decoration:none;border-top:1px solid #e8ebf3;padding:10px 0;align-items:center}.due-list a:first-child{border:0;padding-top:0}.due-list time{font-size:12px;color:var(--accent);font-weight:750}.due-list strong,.due-list small{display:block}.due-list strong{font-size:13.2px}.due-list small{font-size:10.8px;color:var(--muted);margin-top:3px}.resource-bars{display:grid;gap:11px}.resource-bars a{display:grid;grid-template-columns:105px 1fr 20px;gap:9px;align-items:center;text-decoration:none;font-size:12px}.resource-bars i{height:5px;background:#e8ebf3;border-radius:5px;overflow:hidden}.resource-bars b{display:block;height:100%;background:#6676dd;border-radius:5px}.resource-bars strong{text-align:right}.catalog{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}.catalog a{display:flex;justify-content:space-between;text-decoration:none;padding:9px 11px;background:#f2f4fa;border-radius:6px;font-size:12px}.catalog a:hover{background:var(--accent-soft)}.page-intro{display:flex;justify-content:space-between;align-items:end;margin-bottom:25px}.page-intro h2,.detail-head h2{font-family:Georgia,serif;font-size:37.2px;font-weight:500;margin:7px 0}.page-intro p:not(.kicker){color:var(--muted);max-width:700px;font-size:15.6px;margin:0}.button{border:1px solid #d0d5e3;background:#fff;border-radius:7px;padding:9px 13px;cursor:pointer;font-size:14.4px;font-weight:650}.button.primary{background:var(--accent);border-color:var(--accent);color:#fff}.button.danger{color:var(--red)}.list-tools{display:flex;align-items:center;gap:10px;margin-bottom:12px}.list-tools label{flex:1}.list-tools input,.list-tools select{width:100%;border:1px solid var(--line);border-radius:7px;background:#fff;padding:10px 12px;font-size:14.4px}.list-tools select{width:auto}.list-tools>span{color:var(--muted);font-size:12px}.record-table-wrap{background:#fff;border:1px solid var(--line);border-radius:10px;overflow:auto}.record-table{width:100%;border-collapse:collapse;font-size:13.2px}.record-table th{background:#f2f4fa;text-align:left;text-transform:uppercase;letter-spacing:.08em;color:#75817b;font-size:10.8px;padding:11px 14px;border-bottom:1px solid var(--line)}.record-table td{padding:13px 14px;border-bottom:1px solid #e8ebf3;vertical-align:top}.record-table tr:last-child td{border-bottom:0}.record-table code{font-size:10.8px;color:#5d6475}.record-title{display:block;color:var(--ink);font-weight:700;text-decoration:none}.record-table td>small{display:block;color:#6a7181;margin-top:3px}.record-table td[data-label="Description"]{min-width:260px;max-width:520px;color:var(--muted);line-height:1.45}.badge,.tag,.type-pill{display:inline-block;border-radius:99px;background:#eceff7;padding:3px 7px;font-size:10.8px;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap}.tag{text-transform:none;margin:1px}.badge.status-active,.badge.status-approved,.badge.status-complete,.badge.status-passed,.badge.status-accepted{background:#ddefe5;color:#176143}.badge.status-open,.badge.status-high,.badge.status-critical,.badge.status-failed{background:#f5ded9;color:#8d352c}.badge.status-draft,.badge.status-planned,.badge.status-in-progress,.badge.status-medium{background:#f7e9cf;color:#855717}.breadcrumbs{display:flex;gap:8px;color:var(--muted);font-size:13.2px;margin-bottom:20px}.detail-head{display:flex;justify-content:space-between;align-items:end;margin-bottom:22px}.detail-head h2{margin-bottom:4px}.detail-head>div>code{font-size:12px;color:var(--muted)}.actions{display:flex;gap:7px}.detail-grid{display:grid;grid-template-columns:minmax(0,2fr) minmax(270px,1fr);gap:14px}.detail-grid aside{display:grid;gap:14px;align-content:start}.detail-main{padding:29px}.content-label{color:#75817b;text-transform:uppercase;letter-spacing:.08em;font-size:10.8px;border-bottom:1px solid var(--line);padding-bottom:13px;margin-bottom:23px}.markdown{max-width:790px}.markdown h1{font-family:Georgia,serif;font-size:34.8px;font-weight:500}.markdown h2{font-family:Georgia,serif;font-size:27.6px;font-weight:500;margin-top:1.8em}.markdown h3{font-size:18px;margin-top:1.7em}.markdown p,.markdown li{font-size:15.6px;line-height:1.65;color:#272c3b}.markdown code{background:#eef0f6;border-radius:3px;padding:1px 4px}.markdown pre{padding:15px;background:#10162b;color:#e8ebff;border-radius:7px;overflow:auto}.markdown blockquote{border-left:3px solid var(--accent-light);padding:4px 15px;color:var(--muted);margin-left:0}.table-wrap{overflow:auto}.markdown table{border-collapse:collapse;width:100%;font-size:13.2px}.markdown th,.markdown td{border:1px solid var(--line);padding:8px;text-align:left}.metadata{margin:0}.metadata>div{display:grid;grid-template-columns:105px 1fr;gap:10px;border-top:1px solid #e8ebf3;padding:10px 0}.metadata>div:first-child{border-top:0;padding-top:0}.metadata dt{font-size:10.8px;text-transform:uppercase;letter-spacing:.06em;color:#5d6475}.metadata dd{margin:0;font-size:13.2px;min-width:0}.compact-json{white-space:pre-wrap;font-size:10.8px}.git-panel>code{font-size:10.8px;word-break:break-all}.git-panel p{font-size:12px;color:var(--muted)}.relation{color:var(--accent);text-decoration:none}.history{display:grid}.history>div{display:grid;grid-template-columns:60px 1fr;gap:8px;padding:8px 0;border-top:1px solid #e8ebf3}.history>div:first-child{border-top:0}.history code{font-size:10.8px;color:var(--accent)}.history strong,.history small{display:block}.history strong{font-size:12px}.history small{font-size:10.8px;color:var(--muted);margin-top:2px}.empty{padding:25px;color:#697184;text-align:center;font-size:13.2px;background:#f4f5fa;border-radius:7px}.changes{padding-left:18px}.changes li{margin:8px 0}.diagnostics>div{display:grid;grid-template-columns:58px minmax(120px,180px) minmax(0,1fr);gap:10px;align-items:start;border-top:1px solid var(--line);padding:10px 0}.diagnostics p{margin:0;font-size:13.2px;overflow-wrap:anywhere}.diagnostics code{font-size:10.8px;overflow-wrap:anywhere}.editor,.search-results{width:min(760px,calc(100vw - 30px));border:0;border-radius:12px;padding:0;box-shadow:0 25px 80px rgba(0,0,24,.28)}dialog::backdrop{background:rgba(0,0,24,.55)}.editor form,.search-results{padding:23px}.dialog-head{display:flex;justify-content:space-between;align-items:start}.dialog-head h2{font-family:Georgia,serif;font-weight:500;margin:5px 0 0}.icon-button{border:0;background:#eceff7;width:32px;height:32px;border-radius:50%;font-size:26.4px;cursor:pointer}.editor form>p{font-size:13.2px;color:var(--muted)}.editor textarea{width:100%;height:440px;border:1px solid var(--line);border-radius:7px;background:#10162b;color:#e8ebff;padding:15px;font:13.2px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;tab-size:2}.dialog-actions{display:flex;justify-content:end;gap:8px;margin-top:14px}.dialog-error{color:var(--red);font-size:13.2px;min-height:18px;margin-top:7px}.result-list{display:grid;margin-top:17px;max-height:60vh;overflow:auto}.result-list a{display:block;text-decoration:none;padding:11px;border-top:1px solid var(--line)}.result-list strong,.result-list small{display:block}.result-list small{color:var(--muted);margin-top:3px}.muted{color:#737a8b}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
4310
+ .topbar-readiness{display:grid;grid-template-columns:minmax(0,1fr);gap:3px;flex:0 1 220px;min-width:155px;padding:5px 8px;border:1px solid transparent;border-radius:8px;color:var(--ink);text-decoration:none}.topbar-readiness:hover{border-color:var(--accent-light);background:var(--accent-soft)}.topbar-readiness-copy{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:7px;align-items:center}.topbar-readiness-copy>span{overflow:hidden;color:var(--muted);font-size:9.6px;font-weight:700;text-overflow:ellipsis;text-transform:uppercase;letter-spacing:.08em;white-space:nowrap}.topbar-readiness-copy>strong{font-size:10.8px;line-height:1}.topbar-readiness>.progress{width:100%;height:5px}.topbar-status{display:flex;flex:1 1 auto;min-width:0;align-items:center;justify-content:flex-end;gap:8px;margin-left:auto}.topbar-status .topbar-search{flex:0 1 240px;width:240px;min-width:140px;margin-left:0}.repo-chip,.validation-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:8px;padding:10px 12px;color:var(--muted);font-size:13.2px;white-space:nowrap;text-decoration:none}.repo-chip:hover,.validation-chip:hover{color:var(--ink);border-color:var(--accent-light)}
4302
4311
  .audit-progress-empty{padding:11px 13px;border-radius:8px;background:var(--surface-soft)}.audit-progress-empty strong,.audit-progress-empty span{display:block}.audit-progress-empty strong{font-size:13.2px}.audit-progress-empty span{margin-top:4px;color:var(--muted);font-size:10.8px}
4303
4312
  .icon-button{position:relative;display:grid;place-items:center;padding:0;color:var(--ink);font-size:0}.icon-button:before,.icon-button:after{content:"";position:absolute;width:13px;height:2px;border-radius:2px;background:currentColor;transform:rotate(45deg)}.icon-button:after{transform:rotate(-45deg)}
4304
4313
  html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(0,1fr);height:100vh;min-height:0}.sidebar{display:flex;flex-direction:column;height:100vh;overflow:hidden;overscroll-behavior:contain}.workspace{height:100vh;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch}
@@ -4322,6 +4331,7 @@ html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(
4322
4331
  .record-content-action{display:flex;justify-content:flex-start;margin-top:20px}.record-content-details{border-top:1px solid var(--line);margin-top:18px;padding-top:13px}.record-content-details summary{cursor:pointer;color:var(--accent);font-size:13.2px;font-weight:750}.record-content-details>p{color:var(--muted);font-size:12px}.record-content-editor>span small{color:var(--muted);font-size:10.8px;font-weight:500}
4323
4332
  .program-setup{grid-template-columns:minmax(250px,.75fr) minmax(440px,1.4fr)}.setup-steps{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.setup-steps a{display:grid;grid-template-columns:24px minmax(0,1fr);gap:9px;align-items:start;padding:10px;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--ink);text-decoration:none}.setup-steps a:hover{border-color:var(--accent-light)}.setup-steps a>span:first-child{display:grid;place-items:center;width:24px;height:24px;border-radius:50%;background:var(--accent-soft);color:var(--accent);font-size:12px}.setup-steps a.done>span:first-child{background:#dcefe4;color:#125733}.setup-steps strong,.setup-steps small{display:block}.setup-steps strong{font-size:12px}.setup-steps small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4;font-weight:500}
4324
4333
  .readiness-map{margin:14px 0;background:var(--panel);border:1px solid var(--line);border-radius:11px;padding:20px 22px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.readiness-map-head{display:grid;grid-template-columns:minmax(220px,1fr) minmax(320px,420px);gap:28px;align-items:center;margin-bottom:17px}.readiness-map-head h3{font-size:18px;margin:5px 0 0}.readiness-progress-summary{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px 14px;align-items:center}.readiness-progress-summary>div{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:3px 8px;align-items:baseline}.readiness-progress-summary>div>span{color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}.readiness-progress-summary>div>strong{font-size:9.6px;font-weight:700;line-height:1.2}.readiness-progress-summary>div>.badge{font-size:8.4px}.readiness-progress-summary .progress,.readiness-progress-summary small{grid-column:1/-1}.readiness-progress-summary small{color:var(--muted);font-size:9.6px}.readiness-progress-summary>.button{white-space:nowrap}.readiness-flow{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px}.readiness-flow a{display:grid;grid-template-columns:23px minmax(0,1fr);column-gap:8px;align-content:start;min-width:0;padding:11px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft);text-decoration:none}.readiness-flow a:hover{border-color:var(--accent-light);background:var(--accent-soft)}.readiness-flow a>span{grid-row:1/4;display:grid;place-items:center;width:23px;height:23px;border-radius:50%;background:var(--primary-gradient);color:#fff;font-size:9.6px;font-weight:800}.readiness-flow strong{font-size:12px;line-height:1.25}.readiness-flow small{grid-column:2;color:var(--muted);font-size:9.6px;line-height:1.4;margin-top:3px}.readiness-state{grid-column:2;justify-self:start;margin-top:8px;padding:3px 6px;border-radius:99px;background:var(--surface-muted);color:var(--muted);font-size:8.4px;line-height:1.2}.readiness-state.good{background:#dcefe4;color:#125733}.readiness-state.warn{background:#f6e8c9;color:#79500f}.readiness-state.bad{background:#f7dfdc;color:#873027}.audit-engagement{display:grid;grid-template-columns:minmax(210px,1fr) minmax(260px,1.25fr) auto;gap:20px;align-items:center;padding:14px 15px;border-radius:8px;background:var(--surface-soft)}.audit-engagement strong{font-size:13.2px}.audit-engagement p,.audit-engagement li{color:var(--muted);font-size:10.8px;line-height:1.5}.audit-engagement p{margin:5px 0 0}.audit-engagement ul{margin:0;padding-left:18px}.audit-engagement .button{white-space:nowrap;text-decoration:none}.resource-directory{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.resource-directory>section{min-width:0;padding:12px;border-radius:8px;background:var(--surface-soft)}.resource-directory h4{margin:0 0 7px;color:var(--muted);font-size:10.8px;text-transform:uppercase;letter-spacing:.08em}.resource-directory a{display:flex;justify-content:space-between;gap:10px;padding:5px 0;border-top:1px solid var(--line);font-size:10.8px;text-decoration:none}.resource-directory a:first-of-type{border-top:0}.resource-directory a:hover span{color:var(--accent)}.resource-directory a strong{color:var(--muted);font-size:9.6px}.record-prose{max-width:790px}.record-prose section{padding:0 0 20px}.record-prose section+section{padding-top:20px;border-top:1px solid var(--line)}.record-prose h3{margin:0 0 7px;color:var(--muted);font-size:10.8px;text-transform:uppercase;letter-spacing:.08em}.record-prose p{margin:0;font-size:16.8px;line-height:1.65;white-space:pre-wrap}.connections-panel .panel-head>span{display:grid;place-items:center;min-width:22px;height:22px;border-radius:99px;background:var(--surface-muted);color:var(--muted);font-size:9.6px}.connections{display:grid}.connections a{display:block;padding:9px 0;border-top:1px solid var(--line);text-decoration:none}.connections a:first-child{padding-top:0;border-top:0}.connections strong,.connections small{display:block}.connections strong{font-size:12px}.connections small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.connections a:hover strong{color:var(--accent)}.connections-more{margin:9px 0 0;color:var(--muted);font-size:9.6px;line-height:1.4}.external-source{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;color:var(--accent);text-decoration:none}.external-source span,.external-source strong,.external-source small{display:block}.external-source strong{font-size:12px;line-height:1.35}.external-source small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.35;overflow-wrap:anywhere}.external-source b{font-size:13.2px}.external-source:hover strong{text-decoration:underline}
4334
+ .readiness-map-head{display:flex;justify-content:space-between}.readiness-map-head>.button{flex:0 0 auto;white-space:nowrap}
4325
4335
  .page-title-line{display:flex;align-items:center;gap:8px}.guide-trigger{display:grid;place-items:center;width:24px;height:24px;flex:0 0 auto;padding:0;border:1px solid var(--line);border-radius:50%;background:var(--panel);color:var(--muted);cursor:pointer}.guide-trigger svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round}.guide-trigger:hover{border-color:var(--accent-light);color:var(--accent)}.guide-trigger:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.page-guide{display:grid;grid-template-columns:1.05fr 1.25fr 1fr;gap:0;margin:0;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.resource-guide-popover{position:fixed;z-index:40;overflow:auto;box-shadow:0 18px 50px rgba(0,0,24,.24)}.resource-guide-popover[hidden]{display:none}.page-guide>div{padding:14px 16px;border-left:1px solid var(--line);min-width:0}.page-guide>div:first-child{border-left:0}.page-guide>div>span{display:block;color:var(--accent);text-transform:uppercase;letter-spacing:.09em;font-size:9.6px;font-weight:780;margin-bottom:6px}.page-guide p{color:var(--muted);font-size:12px;line-height:1.5;margin:0}.page-guide>.guide-review{grid-column:1/-1;border-top:1px solid var(--line);border-left:0}.guide-review ul{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px 28px;margin:0;padding-left:18px;color:var(--muted);font-size:11px;line-height:1.45}.guide-links{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}.guide-links a{color:var(--accent);background:var(--accent-soft);border-radius:99px;padding:4px 7px;text-decoration:none;font-size:9.6px;font-weight:700}
4326
4336
  .operation-tracking{display:grid;gap:2px;min-width:0;text-decoration:none}.operation-tracking strong,.operation-tracking small{display:block;overflow-wrap:anywhere}.operation-tracking small{color:var(--muted);line-height:1.35}.operation-tracking.running strong{color:#176143}.operation-tracking.waiting strong,.operation-tracking.mixed strong{color:var(--amber)}.operation-tracking.paused strong{color:var(--red)}a.operation-tracking:hover strong{text-decoration:underline}
4327
4337
  .page-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px;flex-wrap:wrap}.repository-sync-status{min-height:18px;margin:7px 0 0;color:var(--muted);font-size:13.2px}.repository-sync-status.error{color:var(--red)}.repository-sync-alert{display:flex;align-items:flex-start;gap:9px;padding:10px 22px;border-bottom:1px solid #e9c888;background:#fff8e8;color:var(--ink);font-size:12px;line-height:1.45}.repository-sync-alert.syncing{border-color:var(--line);background:var(--surface-soft)}.repository-sync-alert .status-dot{flex:0 0 auto;margin-top:4px}.repository-sync-alert a{margin-left:auto;white-space:nowrap}.repository-state-banner,.repository-override{display:flex;align-items:flex-start;gap:13px;margin-bottom:14px}.repository-state-banner>.status-dot,.repository-override>.status-dot{margin-top:5px}.repository-state-banner h3{margin:3px 0 5px}.repository-state-banner p:last-child,.repository-override p{margin:0;color:var(--muted);line-height:1.5}.repository-override{padding:14px 17px;border:1px solid #e9c888;border-radius:9px;background:#fff8e8;font-size:13.2px}.repository-override strong{display:block;margin-bottom:4px}.onboarding-dialog{width:min(470px,calc(100vw - 30px));max-height:calc(100vh - 32px);margin:0;border:1px solid var(--line);border-radius:13px;padding:0;background:var(--panel);color:var(--ink);box-shadow:0 28px 90px rgba(0,0,24,.38);overflow:hidden}.onboarding-dialog[open]{display:flex;flex-direction:column}.onboarding-dialog::backdrop{background:transparent;backdrop-filter:none}.onboarding-shade{position:fixed;inset:0;z-index:60;pointer-events:none}.onboarding-shade span{position:absolute;background:rgba(0,0,24,.58)}.onboarding-progress{display:grid;flex:0 0 auto;grid-template-columns:repeat(var(--onboarding-step-count),1fr);gap:5px;padding:18px 24px 0}.onboarding-progress span{height:3px;border-radius:3px;background:var(--surface-muted)}.onboarding-progress span.active{background:var(--accent-light)}.onboarding-scroll{min-height:0;overflow-y:auto}.onboarding-head{padding:22px 25px 0}.onboarding-head h2{font-family:Georgia,serif;font-size:30px;font-weight:500;letter-spacing:-.015em;margin:8px 0 0}.onboarding-body{color:var(--muted);font-size:14.4px;line-height:1.6;margin:13px 25px 0}.onboarding-body+.onboarding-body{margin-top:8px}.onboarding-points{display:grid;gap:9px;margin:18px 25px 4px;padding-left:19px}.onboarding-points li{font-size:13.2px;line-height:1.5;padding-left:3px}.onboarding-sections{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:16px 25px 4px}.onboarding-sections section{padding:13px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft)}.onboarding-sections strong{font-size:13.2px}.onboarding-sections p{margin:6px 0 0;color:var(--muted);font-size:12px;line-height:1.5}.onboarding-actions{flex:0 0 auto;flex-wrap:wrap;padding:12px 25px 23px;border-top:1px solid var(--line);background:var(--panel)}.onboarding-skip{margin-right:auto;color:var(--muted);text-transform:none;letter-spacing:0;font-size:13.2px}.onboarding-form{display:grid;grid-template-columns:1fr 1fr;gap:13px;margin:18px 25px 0}.onboarding-form label{display:block;min-width:0}.onboarding-form label.wide{grid-column:1/-1}.onboarding-form label>span{display:block;color:var(--ink);font-size:12px;font-weight:720;margin-bottom:6px}.onboarding-form input,.onboarding-form select,.onboarding-form textarea{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:14.4px}.onboarding-form textarea{min-height:78px;resize:vertical}.onboarding-form small{display:block;color:var(--muted);font-size:10.8px;line-height:1.45;margin-top:5px}.onboarding-write-note{color:var(--muted);font-size:10.8px;line-height:1.5;margin:12px 25px 16px}.onboarding-scroll>.dialog-error{margin:8px 25px 0}.onboarding-focus{outline:4px solid var(--accent-light)!important;outline-offset:5px;scroll-margin-top:102px}
@@ -4331,15 +4341,16 @@ html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(
4331
4341
  .not-found{max-width:620px;padding:28px}.not-found h2{font-family:Georgia,serif;font-size:37.2px;font-weight:500;margin:7px 0}.not-found>p:not(.kicker){margin:0;color:var(--muted);font-size:15.6px}.not-found .page-actions{justify-content:flex-start;margin-top:22px}
4332
4342
  .button.danger-action{background:var(--red);border-color:var(--red);color:#fff}
4333
4343
  @media(max-width:1200px){.readiness-flow{grid-template-columns:repeat(3,minmax(0,1fr))}.audit-engagement{grid-template-columns:1fr 1fr}.audit-engagement .button{grid-column:1/-1;justify-self:start}}
4334
- @media(max-width:1100px){.search{display:none}.topbar-status{margin-left:auto}.metrics{grid-template-columns:repeat(2,1fr)}.dashboard-grid,.organization-grid{grid-template-columns:repeat(2,1fr)}.catalog{grid-template-columns:repeat(3,1fr)}.span-2{grid-column:span 2}.resource-directory{grid-template-columns:repeat(2,minmax(0,1fr))}}
4335
- @media(max-width:760px){.shell{display:block}.sidebar{transform:translateX(-100%);transition:.2s;box-shadow:8px 0 30px rgba(0,0,0,.2)}.sidebar.shown{transform:translateX(0)}.workspace{min-width:0}.mobile-nav{display:block;border:0;background:none;font-size:24px}.topbar{height:72px;padding:0 16px}.topbar>div:first-of-type{flex:1;min-width:0}.topbar h1{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.topbar-status{display:none}.search{display:flex;flex:0 1 180px;min-width:110px;max-width:none}.search kbd,.topbar .eyebrow{display:none}.page{padding:20px 15px 60px}.hero{display:block;padding:23px}.hero-meta{margin-top:22px;flex-wrap:wrap}.metrics,.dashboard-grid,.organization-grid{grid-template-columns:1fr}.span-2{grid-column:auto}.catalog{grid-template-columns:repeat(2,1fr)}.detail-grid{grid-template-columns:1fr}.page-intro,.detail-head{display:block}.page-intro>.button,.actions{margin-top:15px}.page-intro>.list-header-tools{justify-content:flex-start;margin:15px 0 0}.list-header-tools label{max-width:none}.record-table{min-width:720px}.readiness-map{padding:17px}.readiness-map-head{grid-template-columns:1fr;gap:8px}.readiness-flow{grid-template-columns:repeat(2,minmax(0,1fr))}.audit-engagement{grid-template-columns:1fr}.audit-engagement .button{grid-column:auto}.resource-directory{grid-template-columns:1fr}}
4344
+ @media(max-width:1100px){.topbar-search{display:none}.topbar-status{margin-left:auto}.metrics{grid-template-columns:repeat(2,1fr)}.dashboard-grid,.organization-grid{grid-template-columns:repeat(2,1fr)}.catalog{grid-template-columns:repeat(3,1fr)}.span-2{grid-column:span 2}.resource-directory{grid-template-columns:repeat(2,minmax(0,1fr))}}
4345
+ @media(max-width:920px){.topbar-status>.validation-chip,.topbar-status>.repo-chip{display:none}}
4346
+ @media(max-width:760px){.shell{display:block}.sidebar{transform:translateX(-100%);transition:.2s;box-shadow:8px 0 30px rgba(0,0,0,.2)}.sidebar.shown{transform:translateX(0)}.workspace{min-width:0}.mobile-nav{display:block;border:0;background:none;font-size:24px}.topbar{height:72px;padding:0 16px}.topbar>div:first-of-type{flex:1;min-width:0}.topbar h1{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.topbar-search,.topbar-status>.validation-chip,.topbar-status>.repo-chip{display:none}.topbar-status{display:flex;margin-left:auto}.sidebar .mobile-sidebar-search{display:flex;flex:0 0 auto;width:100%;max-width:none;min-width:0;margin:0 0 14px;background:rgba(255,255,255,.1);border-color:rgba(255,255,255,.2);color:#d5d9ed}.mobile-sidebar-search input{color:#fff}.mobile-sidebar-search input::placeholder{color:#c5cae2}.topbar .eyebrow{display:none}.page{padding:20px 15px 60px}.hero{display:block;padding:23px}.hero-meta{margin-top:22px;flex-wrap:wrap}.metrics,.dashboard-grid,.organization-grid{grid-template-columns:1fr}.span-2{grid-column:auto}.catalog{grid-template-columns:repeat(2,1fr)}.detail-grid{grid-template-columns:1fr}.page-intro,.detail-head{display:block}.page-intro>.button,.actions{margin-top:15px}.page-intro>.list-header-tools{justify-content:flex-start;margin:15px 0 0}.list-header-tools label{max-width:none}.record-table{min-width:720px}.readiness-map{padding:17px}.readiness-map-head{grid-template-columns:1fr;gap:8px}.readiness-flow{grid-template-columns:repeat(2,minmax(0,1fr))}.audit-engagement{grid-template-columns:1fr}.audit-engagement .button{grid-column:auto}.resource-directory{grid-template-columns:1fr}}
4336
4347
  @media(max-width:760px){.setup-banner,.page-guide,.stage-overview-hero,.relationship-note,.group-destination-card,.stage-page-grid,.evidence-map-expectation,.evidence-map-links,.external-evidence-list{grid-template-columns:1fr}.context-workflow,.evidence-map-head{align-items:stretch;flex-direction:column}.evidence-map-actions{flex-wrap:wrap}.page-guide>div{border-left:0;border-top:1px solid var(--line)}.page-guide>div:first-child{border-top:0}.group-overview-head{display:block}.stage-progress-card,.stage-status-link{margin-top:15px}.destination-rollup{text-align:left}.form-grid{grid-template-columns:1fr}.record-table{min-width:0}.record-table thead{display:none}.record-table,.record-table tbody,.record-table tr{display:block}.record-table tr{padding:8px 12px;border-bottom:1px solid var(--line)}.record-table tr:last-child{border-bottom:0}.record-table td:not([data-label]){display:block}.record-table td[data-label]{display:grid;grid-template-columns:105px minmax(0,1fr);gap:10px;border:0;padding:7px 0;align-items:start}.record-table td[data-label]::before{content:attr(data-label);color:#75817b;text-transform:uppercase;letter-spacing:.07em;font-size:9.6px;font-weight:700}.record-table td[data-primary-field]{display:block;padding:8px 0 10px}.record-table td[data-primary-field]::before{display:none}.content-label{align-items:flex-start}.editor form{padding:18px}.diagnostics>div{grid-template-columns:58px minmax(0,1fr)}.diagnostics p{grid-column:1/-1}.changes code{overflow-wrap:anywhere}.onboarding-dialog{max-height:56vh}}
4337
4348
  @media(max-width:760px){.guide-review ul,.resource-review-criteria ul{grid-template-columns:1fr}.collection-review-head,.collection-review-foot{align-items:stretch;flex-direction:column}.record-workflow-action{min-width:0}}
4338
4349
  @media(max-width:520px){.onboarding-form,.onboarding-sections,.setup-steps{grid-template-columns:1fr}.onboarding-form label.wide{grid-column:auto}.onboarding-actions{flex-wrap:wrap}.onboarding-skip{width:100%;order:3;margin:3px 0 0}.readiness-flow{grid-template-columns:1fr}.obligation-card-foot{align-items:flex-start;flex-direction:column}.obligation-action{align-self:flex-start}}
4339
4350
  @media(min-width:761px){.detail-grid{grid-template-columns:minmax(270px,1fr) minmax(0,2fr)}.detail-grid aside{grid-column:1;grid-row:1}.detail-main{grid-column:2;grid-row:1}}
4340
4351
  @media(min-width:761px){.detail-grid.detail-grid-structured{grid-template-columns:1fr}.detail-grid-structured aside{grid-column:1;grid-row:1;grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}.detail-grid-structured aside>.panel{align-self:start}}
4341
4352
  @media(max-width:760px){.sidebar{visibility:hidden;transition:transform .2s,visibility 0s .2s}.sidebar.shown{visibility:visible;transition-delay:0s}.nav-close{display:grid;place-items:center;position:absolute;top:25px;right:18px;width:34px;height:34px;border:1px solid #5966a4;border-radius:50%;background:#11174a;color:#eef1ff;font-size:24px;cursor:pointer}.nav-scrim{display:block;position:fixed;inset:0;border:0;background:rgba(0,0,24,.38);opacity:0;pointer-events:none;transition:opacity .2s;z-index:15}.sidebar.shown+.nav-scrim{opacity:1;pointer-events:auto}.pagination{justify-content:space-between;gap:8px}.page-status{min-width:0}}
4342
- @media(max-width:760px){.topbar{height:56px}.topbar>div:first-of-type{display:none}.search{flex:1;min-width:0}.nav-close{font-size:0}.nav-close:before,.nav-close:after{content:"";position:absolute;width:13px;height:2px;border-radius:2px;background:currentColor;transform:rotate(45deg)}.nav-close:after{transform:rotate(-45deg)}}
4353
+ @media(max-width:760px){.topbar{height:56px}.topbar>div:first-of-type{display:none}.topbar-readiness{flex:0 1 220px;min-width:120px}.nav-close{font-size:0}.nav-close:before,.nav-close:after{content:"";position:absolute;width:13px;height:2px;border-radius:2px;background:currentColor;transform:rotate(45deg)}.nav-close:after{transform:rotate(-45deg)}}
4343
4354
 
4344
4355
  .connection-group+.connection-group{margin-top:15px;padding-top:13px;border-top:1px solid var(--line)}.connection-group h4{margin:0 0 8px;color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.08em}
4345
4356
  body,button,input,select,textarea,dialog{color:var(--ink)}
package/src/workflow.js CHANGED
@@ -23,6 +23,7 @@ import { assessProgramReadiness } from "./program-readiness.js";
23
23
  import { resolveProgram } from "./program.js";
24
24
  import { planReconciliation } from "./reconciliation.js";
25
25
  import { currentCalendarDate } from "./time.js";
26
+ import { measureTiming } from "./timing.js";
26
27
  import { validateWorkspace } from "./validate.js";
27
28
  import { loadWorkspace } from "./workspace.js";
28
29
 
@@ -77,6 +78,10 @@ const OWNER_FIELDS = [
77
78
  * the complete contract for cross-interface workflow guidance.
78
79
  */
79
80
  export async function assessWorkflow(input, options = {}) {
81
+ return measureTiming("workflow-assessment", () => assessWorkflowUnmeasured(input, options));
82
+ }
83
+
84
+ async function assessWorkflowUnmeasured(input, options = {}) {
80
85
  const loaded = input?.resources && input?.model && input?.entries
81
86
  ? input
82
87
  : await loadWorkspace(input);
@@ -116,7 +121,7 @@ export async function assessWorkflow(input, options = {}) {
116
121
  model: loaded.model
117
122
  });
118
123
  const validation = options.validation || await validateWorkspace(loaded);
119
- const reconciliation = options.reconciliation || await planReconciliation(loaded.root);
124
+ const reconciliation = options.reconciliation || await planReconciliation(loaded);
120
125
  const coverage = normalizeCoverage(
121
126
  options.coverage
122
127
  || (options.auditId ? audits[0]?.coverage : null)