filegrc 0.6.3 → 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 +1 -1
- package/src/batch-review.js +142 -17
- package/src/cli.js +2 -2
- package/src/collection-review.js +16 -46
- package/src/collection-revision.js +83 -0
- package/src/collection-scope.js +182 -0
- package/src/git.js +54 -5
- package/src/reconciliation.js +3 -1
- package/src/server.js +4 -3
- package/src/validate.js +17 -20
- package/src/web.js +27 -16
- package/src/workflow.js +6 -1
package/package.json
CHANGED
package/src/batch-review.js
CHANGED
|
@@ -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 {
|
|
3
|
-
import {
|
|
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
|
|
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
|
|
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({
|
|
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:
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
|
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.`);
|
package/src/collection-review.js
CHANGED
|
@@ -1,32 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { collectionRevision } from "./collection-revision.js";
|
|
3
|
+
import { scopedCollectionRecords } from "./collection-scope.js";
|
|
2
4
|
import { applyResourceBatch } from "./files.js";
|
|
3
5
|
import { getGitSummary } from "./git.js";
|
|
4
6
|
import { loadWorkspace } from "./workspace.js";
|
|
5
|
-
import {
|
|
7
|
+
import { resolveProgram } from "./program.js";
|
|
6
8
|
|
|
7
|
-
export
|
|
8
|
-
const program = resolveProgram(loaded, options.programId);
|
|
9
|
-
const scopedIds = new Set(scopedCollectionRecords(loaded, resourceType, program).map(({ id }) => id));
|
|
10
|
-
const records = loaded.entries
|
|
11
|
-
.filter(({ record }) => record.type === resourceType && scopedIds.has(record.id))
|
|
12
|
-
.map(({ record, source }) => ({
|
|
13
|
-
id: record.id,
|
|
14
|
-
revision: createHash("sha256").update(source).digest("hex")
|
|
15
|
-
}))
|
|
16
|
-
.sort((left, right) => left.id.localeCompare(right.id));
|
|
17
|
-
const workspaceScope = {
|
|
18
|
-
programId: program?.id ?? null,
|
|
19
|
-
assuranceGoal: program?.assuranceGoal ?? null,
|
|
20
|
-
candidateCoverage: program?.candidateCoverage ?? null,
|
|
21
|
-
systemIds: [...(program?.systemIds || [])].sort(),
|
|
22
|
-
frameworkIds: [...(program?.frameworkIds || [])].sort(),
|
|
23
|
-
requirementIds: [...selectedRequirementIds(program || {}, loaded.model)].sort(),
|
|
24
|
-
controlIds: [...(program?.controlIds || [])].sort()
|
|
25
|
-
};
|
|
26
|
-
return createHash("sha256")
|
|
27
|
-
.update(JSON.stringify({ resourceType, records, workspaceScope }))
|
|
28
|
-
.digest("hex");
|
|
29
|
-
}
|
|
9
|
+
export { collectionRevision };
|
|
30
10
|
|
|
31
11
|
export function assessCollectionReviews(input, options = {}) {
|
|
32
12
|
const loaded = input?.resources && input?.model && input?.entries
|
|
@@ -49,7 +29,13 @@ export function assessCollectionReview(loaded, resourceType, options = {}) {
|
|
|
49
29
|
&& (String(loaded.model.modelVersion) !== "4" || (record.scopeResourceIds || []).includes(program.id))
|
|
50
30
|
));
|
|
51
31
|
const review = reviewEntry?.record || null;
|
|
52
|
-
const
|
|
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
|
+
});
|
|
53
39
|
const allowedDecisions = configuration.decisions || ["complete"];
|
|
54
40
|
const allowsEmptyCollection = allowedDecisions.some((decision) => (
|
|
55
41
|
decision === "zero-population" || decision === "externally-managed"
|
|
@@ -146,6 +132,10 @@ export async function planCollectionReview(input = process.cwd(), options = {})
|
|
|
146
132
|
));
|
|
147
133
|
if (!system) throw new Error(`${configuration.title} review needs an active authoritative ${v4 ? "Component" : "System"}.`);
|
|
148
134
|
}
|
|
135
|
+
const currentRevision = collectionRevision(loaded, resourceType, {
|
|
136
|
+
programId: program.id,
|
|
137
|
+
authoritativeSourceId: decision === "externally-managed" ? authoritativeSourceId : null
|
|
138
|
+
});
|
|
149
139
|
const existing = assessment.review;
|
|
150
140
|
const record = {
|
|
151
141
|
...(existing || {
|
|
@@ -160,7 +150,7 @@ export async function planCollectionReview(input = process.cwd(), options = {})
|
|
|
160
150
|
rationale,
|
|
161
151
|
reviewedByIds,
|
|
162
152
|
reviewedOn,
|
|
163
|
-
collectionRevision:
|
|
153
|
+
collectionRevision: currentRevision,
|
|
164
154
|
scopeRevision,
|
|
165
155
|
...(decision === "externally-managed"
|
|
166
156
|
? { [v4 ? "authoritativeComponentId" : "authoritativeSystemId"]: authoritativeSourceId }
|
|
@@ -200,26 +190,6 @@ export async function applyCollectionReview(input = process.cwd(), options = {})
|
|
|
200
190
|
};
|
|
201
191
|
}
|
|
202
192
|
|
|
203
|
-
function scopedCollectionRecords(loaded, resourceType, program) {
|
|
204
|
-
if (String(loaded.model.modelVersion) !== "4") {
|
|
205
|
-
return loaded.resources.filter((record) => record.type === resourceType);
|
|
206
|
-
}
|
|
207
|
-
const components = programComponents(loaded, program);
|
|
208
|
-
const componentIds = new Set(components.map(({ id }) => id));
|
|
209
|
-
const selected = {
|
|
210
|
-
system: new Set(program.systemIds || []),
|
|
211
|
-
component: componentIds,
|
|
212
|
-
framework: new Set(program.frameworkIds || []),
|
|
213
|
-
vendor: new Set(components.map(({ vendorId }) => vendorId).filter(Boolean)),
|
|
214
|
-
asset: new Set(loaded.resources.filter((record) => (
|
|
215
|
-
record.type === "asset" && (record.componentIds || []).some((id) => componentIds.has(id))
|
|
216
|
-
)).map(({ id }) => id))
|
|
217
|
-
}[resourceType];
|
|
218
|
-
return loaded.resources.filter((record) => (
|
|
219
|
-
record.type === resourceType && (!selected || selected.has(record.id))
|
|
220
|
-
));
|
|
221
|
-
}
|
|
222
|
-
|
|
223
193
|
function requiredType(loaded, value) {
|
|
224
194
|
const resourceType = String(value || "").trim();
|
|
225
195
|
if (!loaded.model.collectionReviews?.[resourceType]) {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { programComponents, selectedRequirementIds } from "./program.js";
|
|
2
|
+
|
|
3
|
+
export function scopedCollectionRecords(loaded, resourceType, program) {
|
|
4
|
+
if (String(loaded.model.modelVersion) !== "4") {
|
|
5
|
+
return loaded.resources.filter((record) => record.type === resourceType);
|
|
6
|
+
}
|
|
7
|
+
if (resourceType === "vendor") {
|
|
8
|
+
return loaded.resources.filter((record) => record.type === "vendor");
|
|
9
|
+
}
|
|
10
|
+
const scopedProgram = program || {};
|
|
11
|
+
const components = programComponents(loaded, scopedProgram);
|
|
12
|
+
const componentIds = new Set(components.map(({ id }) => id));
|
|
13
|
+
const systemIds = new Set(scopedProgram.systemIds || []);
|
|
14
|
+
const controlIds = new Set(scopedProgram.controlIds || []);
|
|
15
|
+
const selected = {
|
|
16
|
+
system: systemIds,
|
|
17
|
+
component: componentIds,
|
|
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)),
|
|
29
|
+
asset: new Set(loaded.resources.filter((record) => (
|
|
30
|
+
record.type === "asset" && (record.componentIds || []).some((id) => componentIds.has(id))
|
|
31
|
+
)).map(({ id }) => id))
|
|
32
|
+
}[resourceType];
|
|
33
|
+
return loaded.resources.filter((record) => (
|
|
34
|
+
record.type === resourceType && (!selected || selected.has(record.id))
|
|
35
|
+
));
|
|
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
|
|
188
|
-
|
|
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;
|
package/src/reconciliation.js
CHANGED
|
@@ -91,7 +91,9 @@ const TRANSITIONS = {
|
|
|
91
91
|
};
|
|
92
92
|
|
|
93
93
|
export async function planReconciliation(input = process.cwd()) {
|
|
94
|
-
const loaded =
|
|
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 {
|
|
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) =>
|
|
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
|
@@ -2,6 +2,8 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { readFile, stat } from "node:fs/promises";
|
|
3
3
|
import { performance } from "node:perf_hooks";
|
|
4
4
|
import { getResourceDefinition } from "../model/index.js";
|
|
5
|
+
import { scopedCollectionRecords } from "./collection-scope.js";
|
|
6
|
+
import { collectionRevision } from "./collection-revision.js";
|
|
5
7
|
import { isSafeGitName } from "./git-name.js";
|
|
6
8
|
import { isCanonicalDataPath, resolveDataPath } from "./paths.js";
|
|
7
9
|
import { parseCalendarDate, validCalendarRecurrence } from "./recurrence.js";
|
|
@@ -78,7 +80,7 @@ async function validateWorkspaceUnmeasured(input) {
|
|
|
78
80
|
if (record.type === "control") validateControlComponents(record, byId, displayPath, diagnostics);
|
|
79
81
|
if (record.type === "audit") validateAuditSubservices(record, byId, displayPath, diagnostics);
|
|
80
82
|
if (record.type === "collection-review") {
|
|
81
|
-
validateCollectionReview(record, loaded
|
|
83
|
+
validateCollectionReview(record, loaded, byId, displayPath, diagnostics);
|
|
82
84
|
}
|
|
83
85
|
if (record.type === "obligation") validateObligation(record, loaded.model, byId, displayPath, diagnostics);
|
|
84
86
|
if (record.type === "action-item") {
|
|
@@ -158,8 +160,9 @@ async function validateWorkspaceUnmeasured(input) {
|
|
|
158
160
|
};
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
function validateCollectionReview(record,
|
|
163
|
+
function validateCollectionReview(record, loaded, byId, path, diagnostics) {
|
|
162
164
|
if (record.status !== "active") return;
|
|
165
|
+
const { model } = loaded;
|
|
163
166
|
const configuration = model.collectionReviews?.[record.resourceType];
|
|
164
167
|
if (!configuration) return;
|
|
165
168
|
const allowedDecisions = configuration.decisions || ["complete"];
|
|
@@ -174,29 +177,22 @@ function validateCollectionReview(record, model, resources, byId, path, diagnost
|
|
|
174
177
|
const program = String(model.modelVersion) === "4"
|
|
175
178
|
? (record.scopeResourceIds || []).map((id) => byId.get(id)).find(({ type } = {}) => type === "program")
|
|
176
179
|
: null;
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
vendor: new Set(resources.filter(({ id }) => componentIds.has(id)).map(({ vendorId }) => vendorId).filter(Boolean)),
|
|
187
|
-
asset: new Set(resources.filter(({ type, componentIds: ids }) => type === "asset" && (ids || []).some((id) => componentIds.has(id))).map(({ id }) => id))
|
|
188
|
-
}[record.resourceType];
|
|
189
|
-
const recordCount = resources.filter(({ type, id }) => (
|
|
190
|
-
type === record.resourceType && (!program || !selectedIds || selectedIds.has(id))
|
|
191
|
-
)).length;
|
|
192
|
-
if (!recordCount && record.decision === "complete") {
|
|
180
|
+
const recordCount = scopedCollectionRecords(loaded, record.resourceType, program).length;
|
|
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") {
|
|
193
189
|
diagnostics.push(error(
|
|
194
190
|
"invalid-collection-review-decision",
|
|
195
191
|
path,
|
|
196
192
|
`${configuration.title} has no records and cannot use the complete conclusion.`
|
|
197
193
|
));
|
|
198
194
|
}
|
|
199
|
-
if (recordCount && record.decision === "zero-population") {
|
|
195
|
+
if (current && recordCount && record.decision === "zero-population") {
|
|
200
196
|
diagnostics.push(error(
|
|
201
197
|
"invalid-collection-review-decision",
|
|
202
198
|
path,
|
|
@@ -204,7 +200,8 @@ function validateCollectionReview(record, model, resources, byId, path, diagnost
|
|
|
204
200
|
));
|
|
205
201
|
}
|
|
206
202
|
if (
|
|
207
|
-
|
|
203
|
+
current
|
|
204
|
+
&& record.decision === "externally-managed"
|
|
208
205
|
&& byId.get(record.authoritativeComponentId || record.authoritativeSystemId)?.status !== "active"
|
|
209
206
|
) {
|
|
210
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
|
|
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><
|
|
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
|
|
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
|
|
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
|
-
|
|
3710
|
-
|
|
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:
|
|
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}.
|
|
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
|
|
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)
|