filegrc 0.16.16 → 0.16.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "filegrc",
3
- "version": "0.16.16",
3
+ "version": "0.16.18",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/agent.js CHANGED
@@ -122,13 +122,15 @@ export function buildAgentGuide(loaded, type, options = {}) {
122
122
  oneOf: definition.oneOf ?? [],
123
123
  markdown,
124
124
  workflow: [
125
- "Inspect existing records and relation candidates before writing.",
125
+ "Inspect the recommended action, existing records, relationships, source Markdown, and available evidence. Do the research, drafting, record edits, and validation you can do yourself.",
126
126
  definition.singleton
127
127
  ? "Open the existing singleton record and update only gaps supported by facts from an authoritative source."
128
128
  : "Reuse and update a suitable existing record. Create a scaffold only when no record covers the work, then replace every null value and empty required array with facts from an authoritative source.",
129
129
  recommendedMarkdown.length
130
130
  ? "Keep model fields in JSON and use the recommended Markdown companion for the detailed work, decisions, results, exceptions, and follow-up that apply to this record."
131
131
  : "Keep the current facts and lifecycle state in JSON. Add optional Record Markdown only when the model fields cannot explain the record clearly.",
132
+ "If a material fact, external action, approval, or management decision is still missing, ask the user one specific question. State what you inspected, the exact missing input, and what it unlocks. Do not hand off FileGRC commands, routine record entry, or a vague request to confirm. Continue the workflow after the answer without asking for another handoff.",
133
+ ...(type === "control" ? ["Separate implementation from operation: document the working configuration, scope, owner, procedure, source, and enabled rules when its operation pattern needs them before marking Implemented. Record dated operating work and evidence only after a real cycle or event occurs; a ready schedule is not proof of operation."] : []),
132
134
  ...(type === "program" && modelSupports(loaded.model, "program-scope")
133
135
  ? ["Review Requirement applicability with npx filegrc review-applicability --type requirement --scaffold, then preview and apply the reviewed decisions as one validated batch."]
134
136
  : []),
package/src/cli.js CHANGED
@@ -1743,7 +1743,7 @@ function agentOverview(model) {
1743
1743
  commit: "git diff --check && git diff && git add <reviewed-paths> && git commit -m <reason>"
1744
1744
  };
1745
1745
  return {
1746
- rule: "Treat data/ as the source of truth. Run guide before creating an unfamiliar type, validate after every write, review the Git diff, then commit a focused change.",
1746
+ rule: "Treat data/ as the source of truth. Inspect existing records and sources, then do the research, edits, and validation you can do. Ask the user only for a specific material fact, external action, approval, or decision you cannot establish; explain what it unlocks and continue after the answer. Run guide before creating an unfamiliar type, validate every write, review the Git diff, and commit a focused change.",
1747
1747
  programPath: buildAgentProgramPath(model),
1748
1748
  actions: Object.fromEntries(Object.entries(commands).map(([name, command]) => [
1749
1749
  name,
@@ -0,0 +1,20 @@
1
+ export function obligationRule(obligation, byId, options = {}) {
2
+ const proposedId = options.includeProposed
3
+ ? [...(obligation.ruleIds || [])].reverse().find((id) => ["proposed", "approved"].includes(byId.get(id)?.status))
4
+ : null;
5
+ if (obligation?.scheduleMode !== "rule" && !proposedId) return null;
6
+ let rule = byId.get(obligation.activeRuleId || proposedId);
7
+ if (rule?.status === "active" && rule.effectiveAt && options.now && new Date(rule.effectiveAt) > new Date(options.now)) {
8
+ const prior = byId.get(rule.supersedesId);
9
+ return prior?.type === "obligation-rule"
10
+ && prior.obligationId === obligation.id
11
+ && ["active", "retired"].includes(prior.status)
12
+ ? prior
13
+ : null;
14
+ }
15
+ return rule?.type === "obligation-rule"
16
+ && rule.obligationId === obligation.id
17
+ && (rule.status === "active" || (options.includeProposed && ["proposed", "approved"].includes(rule.status)))
18
+ ? rule
19
+ : null;
20
+ }
@@ -21,7 +21,8 @@ import {
21
21
  } from "./recurrence.js";
22
22
  import { currentCalendarDate, isRfc3339Timestamp, localDateTimeValue, timestampFromLocalDateTime } from "./time.js";
23
23
  import { loadWorkspace } from "./workspace.js";
24
- import { obligationGovernedContent, obligationProgramStatus } from "./program-lifecycle.js";
24
+ import { obligationRule } from "./obligation-rule.js";
25
+ import { governedContentIsOperating, obligationGovernedContent, obligationProgramStatus } from "./program-lifecycle.js";
25
26
  import { resolveProgram } from "./program.js";
26
27
  import { currentPartyPeople } from "./parties.js";
27
28
  import { serializeWorkspaceMutation } from "./mutation.js";
@@ -84,9 +85,9 @@ export function planObligations(resources, options = {}) {
84
85
  throw new Error("Obligation planning requires programId when more than one Program is active.");
85
86
  }
86
87
  const asOf = requireDate(options.asOf ?? currentCalendarDate(workspace?.timezone || "UTC"), "as-of date");
87
- const defaultNow = options.asOf
88
- ? timestampFromLocalDateTime(`${asOf}T23:59:59`, workspace?.timezone || "UTC")
89
- : new Date().toISOString();
88
+ const defaultNow = asOf === currentCalendarDate(workspace?.timezone || "UTC")
89
+ ? new Date().toISOString()
90
+ : timestampFromLocalDateTime(`${asOf}T23:59:59`, workspace?.timezone || "UTC");
90
91
  const now = requireTimestamp(options.now ?? defaultNow, "current timestamp");
91
92
  const through = requireDate(options.through ?? addCalendarDays(asOf, 90), "through date");
92
93
  const requestedFrom = options.from ? requireDate(options.from, "from date") : null;
@@ -102,7 +103,7 @@ export function planObligations(resources, options = {}) {
102
103
  && obligationBelongsToProgram(record, obligationProgram, model)
103
104
  ));
104
105
  const programStatusByObligationId = new Map(obligations.map((obligation) => [
105
- obligation.id, obligationProgramStatus(obligation, byId, asOf, model)
106
+ obligation.id, obligationProgramStatus(obligation, byId, asOf, model, now)
106
107
  ]));
107
108
  const obligationIds = new Set(obligations.map(({ id }) => id));
108
109
  if (obligations.length > MAX_PLANNED_ITEMS) {
@@ -120,14 +121,17 @@ export function planObligations(resources, options = {}) {
120
121
  };
121
122
 
122
123
  for (const obligation of obligations) {
124
+ const selectedRule = byId.get(obligation.activeRuleId);
123
125
  const rule = obligation.scheduleMode === "rule"
124
126
  ? obligationRule(obligation, byId, { now, includeProposed: true })
127
+ || (selectedRule?.status === "active" && selectedRule.effectiveAt
128
+ && new Date(selectedRule.effectiveAt) > new Date(now) ? selectedRule : null)
125
129
  : null;
126
130
  const schedule = rule || obligation;
127
131
  const activity = obligationActivity(model, obligation);
128
132
  const expectedCompletionTypes = activity.completionResourceTypes;
129
133
  const programStatus = programStatusByObligationId.get(obligation.id);
130
- const programBlocker = programStatus === "proposed" ? obligationProgramBlocker(obligation, byId, asOf) : null;
134
+ const programBlocker = programStatus === "proposed" ? obligationProgramBlocker(obligation, byId, asOf, model, now) : null;
131
135
  if (schedule.recurrence?.mode === "event" && schedule.recurrence.eventType) {
132
136
  const eventType = schedule.recurrence.eventType;
133
137
  const group = triggerGroups.get(eventType) ?? {
@@ -298,7 +302,7 @@ export function planObligations(resources, options = {}) {
298
302
  const overdueOn = addCalendarDays(dueWindowEnd, 1);
299
303
  const window = { dueWindowStart, dueWindowEnd, overdueOn };
300
304
  const programStatus = programStatusByObligationId.get(obligation.id);
301
- const programBlocker = programStatus === "proposed" ? obligationProgramBlocker(obligation, byId, asOf) : null;
305
+ const programBlocker = programStatus === "proposed" ? obligationProgramBlocker(obligation, byId, asOf, model, now) : null;
302
306
  const activity = obligationActivity(model, obligation);
303
307
  const completedMemberIds = (occurrence.members || [])
304
308
  .filter(({ disposition, result }) => disposition === "expected" && result === "passed")
@@ -420,10 +424,17 @@ export function planObligations(resources, options = {}) {
420
424
  };
421
425
  }
422
426
 
423
- function obligationProgramBlocker(obligation, byId, asOf) {
427
+ function obligationProgramBlocker(obligation, byId, asOf, model, now = `${asOf}T23:59:59Z`) {
424
428
  if (obligation.status !== "active") {
425
429
  return { type: "obligation", id: obligation.id, label: "Activate obligation" };
426
430
  }
431
+ if (obligation.scheduleMode === "rule" && !obligationRule(obligation, byId, { now })) {
432
+ const selectedRule = byId.get(obligation.activeRuleId);
433
+ return {
434
+ type: "obligation-rule", id: obligation.activeRuleId,
435
+ label: selectedRule?.status === "active" ? "Wait for rule effective time" : "Activate rule"
436
+ };
437
+ }
427
438
  if (currentPartyPeople(obligation.ownerIds || [], byId).size === 0) {
428
439
  return { type: "obligation", id: obligation.id, label: "Assign current owner" };
429
440
  }
@@ -433,6 +444,11 @@ function obligationProgramBlocker(obligation, byId, asOf) {
433
444
  return { type: "policy", id, label: "Activate policy" };
434
445
  }
435
446
  }
447
+ const inactiveContent = obligationGovernedContent(obligation, byId, model)
448
+ .find((record) => !governedContentIsOperating(record, asOf, model));
449
+ if (inactiveContent) {
450
+ return { type: inactiveContent.type, id: inactiveContent.id, label: `Activate ${inactiveContent.type}` };
451
+ }
436
452
  const controls = (obligation.controlIds || []).map((id) => byId.get(id)).filter(Boolean);
437
453
  if (controls.length && !controls.some(({ status }) => status === "implemented")) {
438
454
  return { type: "control", id: controls[0].id, label: "Implement control" };
@@ -466,6 +482,27 @@ async function createObligationEventUnlocked(input, options) {
466
482
  if (riskLevel && !["normal", "high"].includes(riskLevel)) {
467
483
  throw new Error("A departure risk level must be normal or high.");
468
484
  }
485
+ if (!occurredAt) {
486
+ const intradayCutover = records.find((record) => {
487
+ if (
488
+ record.type !== "obligation"
489
+ || !["active", "proposed"].includes(record.status)
490
+ || (program && !obligationBelongsToProgram(record, program, loaded.model))
491
+ || (Array.isArray(record.eventRiskLevels) && !record.eventRiskLevels.includes(riskLevel))
492
+ ) return false;
493
+ const rule = byId.get(record.activeRuleId);
494
+ return rule?.type === "obligation-rule"
495
+ && rule.status === "active"
496
+ && rule.recurrence?.mode === "event"
497
+ && rule.recurrence.eventType === eventType
498
+ && rule.effectiveAt
499
+ && timestampCalendarDate(rule.effectiveAt, loaded.workspace.timezone) === occurredOn;
500
+ });
501
+ if (intradayCutover) {
502
+ throw new Error(`Event type "${eventType}" requires occurredAt on ${occurredOn} because a linked rule takes effect during that date. Record the real event time with its timezone.`);
503
+ }
504
+ }
505
+ const eventTime = occurredAt || `${occurredOn}T23:59:59Z`;
469
506
  const eventSchedules = new Map();
470
507
  const templates = records.filter((record) => {
471
508
  if (
@@ -473,16 +510,43 @@ async function createObligationEventUnlocked(input, options) {
473
510
  || record.status !== "active"
474
511
  || (program && !obligationBelongsToProgram(record, program, loaded.model))
475
512
  ) return false;
476
- const schedule = obligationRule(record, byId, { now: occurredAt || `${occurredOn}T23:59:59Z` }) || record;
513
+ const schedule = obligationRule(record, byId, { now: eventTime }) || record;
477
514
  const matches = schedule.recurrence?.mode === "event"
478
515
  && schedule.recurrence.eventType === eventType
479
516
  && (!Array.isArray(record.eventRiskLevels) || record.eventRiskLevels.includes(riskLevel));
480
517
  if (matches) eventSchedules.set(record.id, schedule);
481
518
  return matches;
482
519
  });
483
- if (!eventType || templates.length === 0) throw new Error(`No active obligations use event type "${eventType}".`);
484
- if (templates.some((record) => obligationProgramStatus(record, byId, occurredOn, loaded.model) === "proposed")) {
485
- throw new Error(`Event type "${eventType}" still has starter proposals. Make every governing Policy and required governed-content record active and effective, then implement at least one linked Control before starting this workflow.`);
520
+ const proposed = records.find((record) => {
521
+ if (
522
+ record.type !== "obligation"
523
+ || !["active", "proposed"].includes(record.status)
524
+ || (program && !obligationBelongsToProgram(record, program, loaded.model))
525
+ || (Array.isArray(record.eventRiskLevels) && !record.eventRiskLevels.includes(riskLevel))
526
+ ) return false;
527
+ const rule = obligationRule(record, byId, { now: eventTime, includeProposed: true });
528
+ const selectedRule = byId.get(record.activeRuleId);
529
+ const futureRule = selectedRule?.status === "active" && selectedRule.effectiveAt
530
+ && new Date(selectedRule.effectiveAt) > new Date(eventTime);
531
+ const schedule = rule || (futureRule ? selectedRule : record);
532
+ return schedule.recurrence?.mode === "event"
533
+ && schedule.recurrence.eventType === eventType
534
+ && (record.status === "proposed" || (rule && rule.status !== "active") || (futureRule && !rule));
535
+ });
536
+ if (!eventType || templates.length === 0 || proposed) {
537
+ const selectedRule = byId.get(proposed?.activeRuleId);
538
+ const futureRule = selectedRule?.status === "active" && selectedRule.effectiveAt
539
+ && new Date(selectedRule.effectiveAt) > new Date(eventTime);
540
+ throw new Error(futureRule
541
+ ? `Event type "${eventType}" is dormant until rule ${selectedRule.id} takes effect at ${selectedRule.effectiveAt}. Trigger only a real event after that time.`
542
+ : proposed
543
+ ? `Event type "${eventType}" is still a proposed workflow (${proposed.id}). Confirm its owner, deadline, and required proof, then enable its rule in Step 3. It stays dormant until governing content is effective; trigger only a real event after cutover.`
544
+ : `No active obligations use event type "${eventType}".`);
545
+ }
546
+ const dormant = templates.find((record) => obligationProgramStatus(record, byId, occurredOn, loaded.model, eventTime) === "proposed");
547
+ if (dormant) {
548
+ const blocker = obligationProgramBlocker(dormant, byId, occurredOn, loaded.model, eventTime);
549
+ throw new Error(`Event type "${eventType}" is dormant until ${blocker?.label?.toLowerCase() || "its prerequisites are ready"}${blocker?.id ? ` (${blocker.id})` : ""}. Complete the cutover before starting event work; do not create a past event to prove a prospective Control.`);
486
550
  }
487
551
  if (templates.some((record) => normalizedEventWindow(eventSchedules.get(record.id)?.window).precision === "timestamp") && !occurredAt) {
488
552
  throw new Error(`Event type "${eventType}" has hour-based deadlines and requires an RFC 3339 occurredAt timestamp.`);
@@ -1893,26 +1957,7 @@ function obligationActivity(model, obligation) {
1893
1957
  };
1894
1958
  }
1895
1959
 
1896
- export function obligationRule(obligation, byId, options = {}) {
1897
- const proposedId = options.includeProposed
1898
- ? [...(obligation.ruleIds || [])].reverse().find((id) => ["proposed", "approved"].includes(byId.get(id)?.status))
1899
- : null;
1900
- if (obligation?.scheduleMode !== "rule" && !proposedId) return null;
1901
- let rule = byId.get(obligation.activeRuleId || proposedId);
1902
- if (rule?.status === "active" && rule.effectiveAt && options.now && new Date(rule.effectiveAt) > new Date(options.now)) {
1903
- const prior = byId.get(rule.supersedesId);
1904
- return prior?.type === "obligation-rule"
1905
- && prior.obligationId === obligation.id
1906
- && ["active", "retired"].includes(prior.status)
1907
- ? prior
1908
- : null;
1909
- }
1910
- return rule?.type === "obligation-rule"
1911
- && rule.obligationId === obligation.id
1912
- && (rule.status === "active" || (options.includeProposed && ["proposed", "approved"].includes(rule.status)))
1913
- ? rule
1914
- : null;
1915
- }
1960
+ export { obligationRule } from "./obligation-rule.js";
1916
1961
 
1917
1962
  function currentOccurrence(records, occurrenceKey, obligationId, ruleId, window) {
1918
1963
  return records.find((record) => (
@@ -1,6 +1,7 @@
1
1
  import { currentPartyPeople } from "./parties.js";
2
2
  import { modelSupports } from "../model/index.js";
3
3
  import { revisionsMatch } from "./revisions.js";
4
+ import { obligationRule } from "./obligation-rule.js";
4
5
 
5
6
  const requiredDocumentsByControlCache = new WeakMap();
6
7
 
@@ -131,8 +132,9 @@ function indexRequiredDocumentsByControl(byId, model) {
131
132
  return index;
132
133
  }
133
134
 
134
- export function obligationProgramStatus(obligation, byId, asOf, model) {
135
+ export function obligationProgramStatus(obligation, byId, asOf, model, now = `${asOf}T23:59:59Z`) {
135
136
  if (obligation.status !== "active") return "proposed";
137
+ if (!obligationRuleIsEnabled(obligation, byId, now)) return "proposed";
136
138
  if (currentPartyPeople(obligation.ownerIds || [], byId).size === 0) return "proposed";
137
139
  const policyIds = obligation.policyIds || [];
138
140
  const policiesReady = policyIds.every((id) => {
@@ -153,12 +155,18 @@ export function obligationProgramStatus(obligation, byId, asOf, model) {
153
155
  : "proposed";
154
156
  }
155
157
 
156
- export function obligationIsRunning(obligation, byId, asOf, model) {
158
+ export function obligationIsRunning(obligation, byId, asOf, model, now) {
157
159
  return obligation?.type === "obligation"
158
160
  && obligation.status === "active"
159
- && obligationProgramStatus(obligation, byId, asOf, model) === "accepted";
161
+ && obligationProgramStatus(obligation, byId, asOf, model, now) === "accepted";
160
162
  }
161
163
 
162
164
  export function obligationIsEnabled(obligation) {
163
165
  return obligation?.type === "obligation" && obligation.status === "active";
164
166
  }
167
+
168
+ export function obligationRuleIsEnabled(obligation, byId, now) {
169
+ return obligationIsEnabled(obligation)
170
+ && (obligation.scheduleMode !== "rule"
171
+ || obligationRule(obligation, byId, { now }) !== null);
172
+ }
@@ -4,11 +4,11 @@ export const RESOURCE_INSTRUCTIONS = {
4
4
  program: "Set the Program goal, owners, Systems, Frameworks, criteria decisions, and risk method.",
5
5
  person: "Record the person’s actual job title and current status.",
6
6
  appointment: "Assign a named authority to one Person with its start date and scope.",
7
- team: "Confirm the Team’s members and chair.",
7
+ team: "Use the current Person and Appointment records to set the Team’s members and chair; ask who holds a role only when those sources cannot establish it.",
8
8
  system: "Define the service boundary, exclusions, owners, and information it handles.",
9
9
  component: "Record a material service or evidence source and its role in each linked System.",
10
10
  vendor: "Record each material external provider and the Components it supplies.",
11
- classification: "Confirm the handling categories and apply them to information and evidence.",
11
+ classification: "Use current Policy content and recorded information needs to propose handling categories, then apply the management decision to information and evidence. Ask about any category those sources do not settle.",
12
12
  "information-type": "Name a stable category of information, set its default Classification, and link its uses.",
13
13
  framework: "Select the criteria Framework and version used by the Program.",
14
14
  requirement: "Review whether this criterion applies to the Program and record the decision and rationale.",
@@ -22,7 +22,7 @@ export const RESOURCE_INSTRUCTIONS = {
22
22
  evidence: "Record a real artifact or approved reference, link its source and supported work, then have another person verify it.",
23
23
  "risk-assessment": "Assess the in-scope service, record conclusions, and obtain an independent review.",
24
24
  risk: "Name the threat and impact, assign an owner, rate the risk, and record its response.",
25
- obligation: "Confirm the owner, cadence or trigger, deadline, and proof, then enable the rule when its Control is ready.",
25
+ obligation: "Read the linked Policy and Control, fill the owner, cadence or trigger, deadline, and proof from those sources, then enable the rule when its Control is ready. Ask only for an unresolved management choice.",
26
26
  "retention-schedule-item": "Set the Information Types, scope, cutoff, period, disposition, and sources for one retention rule.",
27
27
  "obligation-event": "Record the actual event date and subject, then complete its linked Work Queue actions.",
28
28
  "policy-review": "Record the review date, reviewer, decision, and any required follow-up.",
@@ -101,11 +101,11 @@ export const RESOURCE_OUTPUTS = {
101
101
  };
102
102
 
103
103
  export const RESOURCE_PAGE_SUMMARIES = {
104
- person: "Confirm who works on the program.",
104
+ person: "Use current people already recorded; ask for a missing person's identity or role only when needed.",
105
105
  appointment: "Assign named authority.",
106
- team: "Confirm shared owners and members.",
106
+ team: "Set shared owners and members from current People and Appointments.",
107
107
  program: "Choose the goal, scope, criteria, controls, owners, and risk method.",
108
- framework: "Confirm the SOC 2 framework.",
108
+ framework: "Select the applicable SOC 2 framework and version from the installed catalog.",
109
109
  requirement: "Decide which SOC 2 criteria apply.",
110
110
  commitment: "Record customer promises that affect scope.",
111
111
  "requirement-mapping": "Review how supplemental promises relate to Requirements and Controls.",
@@ -134,8 +134,8 @@ export const PROGRAM_PATH = [
134
134
  description: "Ownership, criteria, and service boundary",
135
135
  summary: "A reviewed Program scope with owners, criteria, Systems, and material providers.",
136
136
  sections: [
137
- { id: "ownership", title: "Program Ownership", description: "Confirm who owns the program, plus the normal security reporting channel and its fallback.", steps: ["Confirm the program lead, policy owner, independent reviewer, and oversight team.", "Replace the reporting-channel placeholders with real normal and fallback routes, then commit the proposal."], types: ["person", "appointment", "team", "reporting-route-set"], defaultOpen: true },
138
- { id: "criteria", title: "Program and Criteria", description: "Define the Program, confirm its Frameworks, record Program-scoped Requirement applicability, and connect customer commitments that shape the System or Control design.", steps: ["Set the Program goal, owners, risk method, and tentative timing.", "Review each selected criterion for this service and record its applicability decision.", "Replace the starter Commitment prompt with the real promise; add only needed supplemental mappings."], types: ["program", "framework", "requirement", "commitment", "requirement-mapping"], defaultOpen: true },
137
+ { id: "ownership", title: "Program Ownership", description: "Assign program owners and set the normal security reporting channel and its fallback.", steps: ["Inspect current People and Appointments, then assign the program lead, policy owner, independent reviewer, and oversight team. Ask only for missing assignments.", "Use the actual normal and fallback routes to replace reporting-channel placeholders, then commit the proposal."], types: ["person", "appointment", "team", "reporting-route-set"], defaultOpen: true },
138
+ { id: "criteria", title: "Program and Criteria", description: "Define the Program, select its Frameworks, record Program-scoped Requirement applicability, and connect customer commitments that shape the System or Control design.", steps: ["Set the Program goal, owners, risk method, and tentative timing from current records; ask for unresolved management choices.", "Compare each selected criterion with the service scope and record its applicability decision.", "Replace the starter Commitment prompt with the actual promise; add only needed supplemental mappings."], types: ["program", "framework", "requirement", "commitment", "requirement-mapping"], defaultOpen: true },
139
139
  { id: "boundary", title: "System Boundary", description: "Start with the bounded System. Add Components that materially deliver the service, support Controls, produce authoritative Evidence, or support relevant operations. Keep Vendor relationships and specific Assets separate.", steps: ["Define the service and exclusions in a bounded System selected by the Program.", "Add material Components and Vendors with their actual System relationships.", "Record the Information Types and Classifications the service uses."], types: ["system", "component", "vendor", "classification", "information-type"], defaultOpen: false }
140
140
  ],
141
141
  resourceTypes: ["person", "appointment", "team", "reporting-route-set", "program", "framework", "requirement", "commitment", "requirement-mapping", "system", "component", "vendor", "classification", "information-type"],
@@ -160,7 +160,7 @@ export const PROGRAM_PATH = [
160
160
  description: "Approve governed content and retention decisions",
161
161
  summary: "Approved Policies, program Documents, Training content, and retention decisions.",
162
162
  sections: [
163
- { id: "policy-content", title: "Policies", description: "Approve the program's governing content.", instructions: "Open each Policy, program Document, and Training record. Replace placeholders, confirm the owner and intended values, then have a separate person approve the exact Markdown revision.", output: "Approved content bound to the reviewed revisions.", steps: ["Replace placeholders with the company's real rules, plans, and training material.", "Confirm the owner, separate approver, linked Controls, audience, and intended values.", "Record the independent approval and date against each exact Markdown revision."], types: [], relatedLinks: [{ type: "policy", label: "Policies", href: "#/policies" }], defaultOpen: true },
163
+ { id: "policy-content", title: "Policies", description: "Approve the program's governing content.", instructions: "Open each Policy, program Document, and Training record. Draft from current approved decisions, replace placeholders, and resolve only material choices with management. A separate person then approves the exact Markdown revision.", output: "Approved content bound to the reviewed revisions.", steps: ["Draft the company's real rules, plans, and training material from current scope and approved decisions.", "Resolve missing owner, separate approver, Control links, audience, or intended values from authoritative records; ask only when a material decision remains.", "Have the independent approver review the exact Markdown revision and record their actual decision and date."], types: [], relatedLinks: [{ type: "policy", label: "Policies", href: "#/policies" }], defaultOpen: true },
164
164
  { id: "retention", title: "Data Retention Schedule", description: "Approve the schedule document and its completed rows together.", steps: ["Review the governing schedule and any legal hold or exception rules.", "Complete a row for each distinct information use, cutoff, retention period, and disposition.", "Have a separate reviewer approve the complete document and row set."], types: [], utility: "retention-schedule", defaultOpen: true }
165
165
  ],
166
166
  resourceTypes: [],
@@ -184,7 +184,7 @@ export const PROGRAM_PATH = [
184
184
  description: "Implement and prepare to operate",
185
185
  summary: "Implemented Controls with ready evidence sources and enabled work schedules.",
186
186
  sections: [
187
- { id: "controls", title: "Controls", description: "Put each selected Control into use and record what is actually in place.", steps: ["Open each Control for its specific setup action and current missing checks.", "Configure the activity and record its procedure, Systems, owner, and evidence sources.", "Mark it Implemented with its real start date after its sources and schedules are ready."], types: ["control"], defaultOpen: true },
187
+ { id: "controls", title: "Controls", description: "Put each selected Control into use and record its working design. Dated operating proof follows when work occurs.", steps: ["Open each Control, linked records, and current missing checks; do the setup that can be established from those sources.", "Configure the activity and record its procedure, Systems, owner, evidence sources, and any work rules its operation pattern requires.", "Mark it Implemented with its real start date when it works. Record later cycles and events separately with dated proof."], types: ["control"], defaultOpen: true },
188
188
  { id: "complementary-controls", title: "Complementary Controls", description: "Record customer or provider responsibilities that form part of the implementation boundary, or confirm there are none.", steps: ["Identify any customer or carved-out provider action an in-scope Control depends on.", "Record each dependency, or confirm that the current scope has none."], types: ["complementary-control"], defaultOpen: true },
189
189
  { id: "evidence-sources", title: "Evidence Sources", description: "Check that every Control points to an authoritative source that can produce its expected evidence.", steps: ["For each Control family, link an active Component with the required source kind, access owners, and retrieval instructions."], types: [], utility: "evidence-sources", defaultOpen: true },
190
190
  { id: "obligations", title: "Obligations", description: "Configure the calendar and event schedules that operate the Controls.", steps: ["Check the owner, trigger or cadence, due window, and required proof for each Obligation.", "Enable the rules that will run the implemented Controls."], types: ["obligation"], defaultOpen: true }
@@ -13,6 +13,7 @@ import {
13
13
  documentIsAuditSpecific,
14
14
  governedDocumentIsOperating,
15
15
  obligationIsEnabled,
16
+ obligationRuleIsEnabled,
16
17
  obligationIsRunning
17
18
  } from "./program-lifecycle.js";
18
19
  import { currentPartyPeople, partiesIndependent, partyPeople } from "./parties.js";
@@ -136,6 +137,10 @@ export async function assessProgramReadiness(input, options = {}) {
136
137
  const workspace = loaded.workspace || records.find((record) => record.type === "workspace");
137
138
  const program = resolveProgram(loaded, options.programId);
138
139
  const asOf = options.asOf || currentCalendarDate(workspace?.timezone || "UTC");
140
+ const currentDate = currentCalendarDate(workspace?.timezone || "UTC");
141
+ const asOfTime = asOf === currentDate
142
+ ? options.generatedAt || new Date().toISOString()
143
+ : timestampFromLocalDateTime(`${asOf}T23:59:59`, workspace?.timezone || "UTC");
139
144
  const requestedAudit = options.auditId
140
145
  ? records.find((record) => record.type === "audit" && record.id === options.auditId)
141
146
  : null;
@@ -155,7 +160,7 @@ export async function assessProgramReadiness(input, options = {}) {
155
160
  };
156
161
 
157
162
  const policyStage = await policiesStage(scope, records, byId, readMarkdown, loaded.model);
158
- const controlStage = await controlsStage(scope, byId, readMarkdown, asOf, loaded.model, loaded.root);
163
+ const controlStage = await controlsStage(scope, byId, readMarkdown, asOf, loaded.model, loaded.root, asOfTime);
159
164
  controlStage.items.unshift(...collectionReviews
160
165
  .filter(({ resourceType }) => resourceType === "complementary-control")
161
166
  .map(collectionReviewReadinessItem));
@@ -185,7 +190,7 @@ export async function assessProgramReadiness(input, options = {}) {
185
190
  } else {
186
191
  controlStage.items.push(...retentionItems, ...retentionReviewItems);
187
192
  }
188
- const governedContent = await governedContentItems(scope, records, byId, readMarkdown, asOf, loaded.model);
193
+ const governedContent = await governedContentItems(scope, records, byId, readMarkdown, asOf, loaded.model, asOfTime);
189
194
  const policyActivations = await assessPolicyActivations(
190
195
  requiredPolicies(scope, byId),
191
196
  scope.controls,
@@ -193,7 +198,9 @@ export async function assessProgramReadiness(input, options = {}) {
193
198
  byId,
194
199
  readMarkdown,
195
200
  asOf,
196
- loaded.model
201
+ loaded.model,
202
+ controlStage.items,
203
+ asOfTime
197
204
  );
198
205
  const controlOversight = collectionReviews.find(({ resourceType }) => resourceType === "control");
199
206
  const oversightEligible = Boolean(controlOversight && controlOversightEligible(controlStage.items));
@@ -210,7 +217,7 @@ export async function assessProgramReadiness(input, options = {}) {
210
217
  ? item
211
218
  : { ...item, status: "blocked", message: `Complete the Control collection review before activation. ${item.message}` };
212
219
  }));
213
- controlStage.description = `Each implemented Control needs an owner, actual procedure, scope, operation pattern, mappings, an implementation date, enabled Obligations, and complete authoritative source ${modelSupports(loaded.model, "component-sources") ? "Components" : "Systems"}. When implementation is ready, perform one review of the implemented Control collection. Management then activates the unchanged approved program content as the Step 3 cutover.`;
220
+ controlStage.description = `Each implemented Control needs an owner, actual procedure, scope, operation pattern, mappings, an implementation date, enabled Obligations for scheduled or event work, and complete authoritative source ${modelSupports(loaded.model, "component-sources") ? "Components" : "Systems"}. When implementation is ready, perform one review of the implemented Control collection. Management then activates the unchanged approved program content as the Step 3 cutover.`;
214
221
  const scopeReadinessStage = scopeStage(
215
222
  program,
216
223
  scope,
@@ -268,7 +275,7 @@ export async function assessProgramReadiness(input, options = {}) {
268
275
  program?.assuranceGoal === "soc-2-type-2"
269
276
  && progressWindow?.start <= asOf
270
277
  );
271
- const obligations = planObligations(records, { programId: program.id, asOf, through: asOf, model: loaded.model });
278
+ const obligations = planObligations(records, { programId: program.id, asOf, through: asOf, now: asOfTime, model: loaded.model });
272
279
  const policyLibrary = await assessPolicyLibraryUpgrades(loaded);
273
280
  const operating = Boolean(
274
281
  evidenceReady
@@ -589,7 +596,7 @@ function scopeStage(workspace, scope, records, byId, model, collectionReviews =
589
596
  items.push(item(
590
597
  "criteria",
591
598
  criteriaComplete ? "complete" : "action",
592
- "Confirm Trust Services criteria, Description Criteria, and Controls",
599
+ "Resolve criteria decisions and Control coverage",
593
600
  criteriaComplete
594
601
  ? `${selectedTrustServicesRequirements.length} applicable Trust Services criteria, ${selectedDescriptionRequirements.length} SOC 2 Description Criteria, and ${scope.controls.length} Controls are in scope. Every applicable Trust Services criterion has at least one selected Control; Description Criteria govern the system description and do not map to Controls.`
595
602
  : missingRequiredSecurityReferences.length || missingRequiredDescriptionReferences.length
@@ -985,7 +992,7 @@ function programOwnershipItem(records, byId) {
985
992
  return item(
986
993
  "program-ownership",
987
994
  complete ? "complete" : "action",
988
- "Confirm program owners and oversight",
995
+ "Assign program owners and oversight",
989
996
  complete
990
997
  ? `${currentOwners.size} current ${currentOwners.size === 1 ? "person owns" : "people own"} the program records.${oversight ? " Security and Risk Oversight has a separate current chair." : ""}`
991
998
  : detail.join(" "),
@@ -1266,10 +1273,11 @@ function requiredGovernedDocuments(scope, records, byId, model) {
1266
1273
  ));
1267
1274
  }
1268
1275
 
1269
- async function governedContentItems(scope, records, byId, readMarkdown, asOf, model) {
1276
+ async function governedContentItems(scope, records, byId, readMarkdown, asOf, model, asOfTime) {
1270
1277
  const enabledObligations = records.filter((record) => (
1271
1278
  record.type === "obligation" && obligationIsEnabled(record)
1272
1279
  ));
1280
+ const operatingSchedules = enabledObligations.filter((record) => obligationRuleIsEnabled(record, byId, asOfTime));
1273
1281
  const requiredGovernedIds = new Set(enabledObligations.flatMap((record) => [
1274
1282
  ...(record.scopeResourceIds || []),
1275
1283
  ...(record.templateResourceId ? [record.templateResourceId] : [])
@@ -1332,7 +1340,7 @@ async function governedContentItems(scope, records, byId, readMarkdown, asOf, mo
1332
1340
  owner: currentPartyPeople(record.ownerIds, byId).size > 0,
1333
1341
  active: record.status === "active",
1334
1342
  requirementsImplemented: linkedControlIds.length > 0 && missingImplementationControlIds.length === 0,
1335
- assignmentScheduled: enabledObligations.some((obligation) => (
1343
+ assignmentScheduled: operatingSchedules.some((obligation) => (
1336
1344
  obligation.templateResourceId === record.id
1337
1345
  || (obligation.scopeResourceIds || []).includes(record.id)
1338
1346
  )),
@@ -1459,7 +1467,7 @@ function documentActivationLabel(state) {
1459
1467
  })[state] || state;
1460
1468
  }
1461
1469
 
1462
- async function assessPolicyActivations(policies, controls, records, byId, readMarkdown, asOf, model) {
1470
+ async function assessPolicyActivations(policies, controls, records, byId, readMarkdown, asOf, model, controlItems, asOfTime) {
1463
1471
  const sourceType = modelSupports(model, "component-sources") ? "component" : "system";
1464
1472
  const sourceField = modelSupports(model, "component-sources") ? "evidenceSourceComponentIds" : "evidenceSourceIds";
1465
1473
  const assessments = [];
@@ -1469,6 +1477,11 @@ async function assessPolicyActivations(policies, controls, records, byId, readMa
1469
1477
  const plannedOrPartialControlIds = linkedControls
1470
1478
  .filter((control) => ["planned", "partially-implemented"].includes(control.status))
1471
1479
  .map(({ id }) => id);
1480
+ const missingLaunchControlIds = linkedControls
1481
+ .filter((control) => controlItems.some((item) => (
1482
+ item.id === `control-${control.id}` && item.implementationState === "implemented-with-gaps"
1483
+ )))
1484
+ .map(({ id }) => id);
1472
1485
  const missingComponentControlIds = modelSupports(model, "component-sources")
1473
1486
  ? linkedControls.filter((control) => ![
1474
1487
  ...(control.componentIds || []),
@@ -1497,7 +1510,7 @@ async function assessPolicyActivations(policies, controls, records, byId, readMa
1497
1510
  ["scheduled", "event-driven", "mixed"].includes(control.operationPattern)
1498
1511
  && !records.some((record) => (
1499
1512
  record.type === "obligation"
1500
- && obligationIsEnabled(record)
1513
+ && obligationRuleIsEnabled(record, byId, asOfTime)
1501
1514
  && (record.controlIds || []).includes(control.id)
1502
1515
  && (record.policyIds || []).includes(policy.id)
1503
1516
  ))
@@ -1540,6 +1553,7 @@ async function assessPolicyActivations(policies, controls, records, byId, readMa
1540
1553
  : null
1541
1554
  ].filter(Boolean);
1542
1555
  const gapCount = plannedOrPartialControlIds.length
1556
+ + missingLaunchControlIds.length
1543
1557
  + missingComponentControlIds.length
1544
1558
  + missingEvidenceSourceControlIds.length
1545
1559
  + missingScheduleControlIds.length
@@ -1564,6 +1578,7 @@ async function assessPolicyActivations(policies, controls, records, byId, readMa
1564
1578
  proposedEffectiveOn: policy.proposedEffectiveOn || null,
1565
1579
  linkedControlIds,
1566
1580
  plannedOrPartialControlIds,
1581
+ missingLaunchControlIds,
1567
1582
  missingComponentControlIds,
1568
1583
  missingEvidenceSourceControlIds,
1569
1584
  missingScheduleControlIds,
@@ -1596,6 +1611,7 @@ function policyActivationLabel(state) {
1596
1611
  function policyActivationItem(assessment) {
1597
1612
  const counts = [
1598
1613
  [assessment.plannedOrPartialControlIds.length, "planned or partial Controls"],
1614
+ [assessment.missingLaunchControlIds.length, "implemented Controls with launch gaps"],
1599
1615
  [assessment.missingComponentControlIds.length, "Controls missing active Components"],
1600
1616
  [assessment.missingEvidenceSourceControlIds.length, "Controls missing ready evidence sources"],
1601
1617
  [assessment.missingScheduleControlIds.length, "Controls missing enabled Obligations"],
@@ -1603,7 +1619,7 @@ function policyActivationItem(assessment) {
1603
1619
  [assessment.unresolvedExceptionIds.length, "unresolved Exceptions"]
1604
1620
  ].filter(([count]) => count).map(([count, label]) => `${count} ${label}`);
1605
1621
  const message = assessment.state === "active-and-operating"
1606
- ? `Active and effective ${assessment.effectiveOn}; all ${assessment.linkedControlIds.length} linked Controls are implemented with Components, evidence sources, and enabled Obligations.`
1622
+ ? `Active and effective ${assessment.effectiveOn}; all ${assessment.linkedControlIds.length} linked Controls are implemented with Components, evidence sources, and enabled Obligations where scheduled or event work requires them.`
1607
1623
  : assessment.state === "ready-to-activate"
1608
1624
  ? "Implementation checks are complete. Include this approved Policy in the Step 3 cutover when you are ready for it to take effect."
1609
1625
  : `${assessment.label}: ${counts.join(", ") || assessment.timingWarnings.join(" ")}. ${assessment.activationWarning || ""}`.trim();
@@ -1644,7 +1660,7 @@ function legacyPolicyLibraryProposals(records) {
1644
1660
  }];
1645
1661
  }
1646
1662
 
1647
- async function controlsStage(scope, byId, readMarkdown, asOf, model, root = null) {
1663
+ async function controlsStage(scope, byId, readMarkdown, asOf, model, root = null, asOfTime) {
1648
1664
  const items = [];
1649
1665
  const families = selectedControlFamilies(scope.controls, model);
1650
1666
  if (!scope.controls.length) {
@@ -1698,12 +1714,24 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model, root = null
1698
1714
  criteriaMapping: (control.requirementIds || []).length > 0,
1699
1715
  ...(["scheduled", "event-driven", "mixed"].includes(control.operationPattern) ? {
1700
1716
  workQueue: queueSchedules.length > 0
1701
- && queueSchedules.some(obligationIsEnabled)
1717
+ && queueSchedules.some((obligation) => obligationRuleIsEnabled(obligation, byId, asOfTime))
1702
1718
  } : {})
1703
1719
  };
1720
+ const launchChecks = Object.fromEntries(Object.entries(checks).filter(([name]) => ![
1721
+ "implemented", "implementationDate"
1722
+ ].includes(name)));
1723
+ const launchReady = Object.values(launchChecks).every(Boolean);
1724
+ const implementationState = control.status === "implemented"
1725
+ ? Object.values(checks).every(Boolean) ? "implemented" : "implemented-with-gaps"
1726
+ : control.status === "partially-implemented" ? "partial"
1727
+ : launchReady ? "ready-to-implement" : "planned";
1704
1728
  const missing = Object.entries(checks).filter(([, value]) => !value).map(([name]) => controlCheckLabel(name));
1705
1729
  const nextSteps = controlImplementationSteps(control, checks, {
1706
- missingSourceFamilies
1730
+ missingSourceFamilies,
1731
+ targets: [...new Set([
1732
+ ...(control.systemIds || []),
1733
+ ...(control.componentIds || [])
1734
+ ].map((id) => byId.get(id)?.title).filter(Boolean))]
1707
1735
  });
1708
1736
  items.push(item(
1709
1737
  `control-${control.id}`,
@@ -1715,17 +1743,20 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model, root = null
1715
1743
  control,
1716
1744
  {
1717
1745
  checks,
1746
+ launchChecks,
1747
+ implementationState,
1748
+ implementationAssertion: "Implemented means the stated control works for the named Systems, its owner and procedure match current practice, and its evidence sources can produce proof. Scheduled or event-driven Controls have at least one enabled linked Obligation. It does not assert that a scheduled cycle or event has already occurred. Record dated operating proof when it does.",
1718
1749
  nextSteps,
1719
1750
  commands: [`npx filegrc get ${shellArgument(control.id)} --mutation`],
1720
1751
  workQueue: queueSchedules.length ? {
1721
- enabled: queueSchedules.filter(obligationIsEnabled).length,
1722
- running: queueSchedules.filter((obligation) => obligationIsRunning(obligation, byId, asOf, model)).length,
1752
+ enabled: queueSchedules.filter((obligation) => obligationRuleIsEnabled(obligation, byId, asOfTime)).length,
1753
+ running: queueSchedules.filter((obligation) => obligationIsRunning(obligation, byId, asOf, model, asOfTime)).length,
1723
1754
  total: queueSchedules.length
1724
1755
  } : null
1725
1756
  }
1726
1757
  ));
1727
1758
  }
1728
- return stage("controls", "Implement Controls", "Put each selected Control in place, record how it works, and mark it Implemented when it is working.", items);
1759
+ return stage("controls", "Implement Controls", "Put each selected Control in place. Check its launch conditions, then mark it Implemented when it is working. Dated proof follows during operation.", items);
1729
1760
  }
1730
1761
 
1731
1762
  async function evidenceSourcesStage(scope, byId, model, readMarkdown) {
@@ -1739,10 +1770,6 @@ async function evidenceSourcesStage(scope, byId, model, readMarkdown) {
1739
1770
  .map((id) => byId.get(id))
1740
1771
  .filter((record) => (
1741
1772
  record?.type === sourceType
1742
- && (
1743
- !(record.evidenceSourceKinds || []).length
1744
- || family.sourceKinds.some((kind) => (record.evidenceSourceKinds || []).includes(kind))
1745
- )
1746
1773
  ));
1747
1774
  const completeSources = [];
1748
1775
  const sourceChecks = [];
@@ -1780,7 +1807,7 @@ async function evidenceSourcesStage(scope, byId, model, readMarkdown) {
1780
1807
  const coveredControls = controlMappings.filter(({ complete }) => complete);
1781
1808
  const complete = completeSources.length > 0 && coveredControls.length === family.controls.length;
1782
1809
  const commands = [
1783
- ...(selectedSources.length
1810
+ ...(complete ? [] : selectedSources.length
1784
1811
  ? sourceChecks.filter(({ complete: sourceComplete }) => !sourceComplete).flatMap((sourceCheck) => {
1785
1812
  const sourceId = sourceCheck.sourceComponentId || sourceCheck.sourceSystemId;
1786
1813
  return [
@@ -2087,21 +2114,21 @@ function controlCheckLabel(name) {
2087
2114
  procedureEffective: "procedure effective date",
2088
2115
  policyMapping: "policy mapping",
2089
2116
  criteriaMapping: "criteria mapping",
2090
- workQueue: "running Obligation schedules"
2117
+ workQueue: "enabled Obligation rules (work starts after cutover)"
2091
2118
  })[name] || name;
2092
2119
  }
2093
2120
 
2094
2121
  const starterControlChecks = {
2095
- "control-security-governance": "Open the security review calendar and attendee roles. Check a reviewer separate from owners and operators examines risks, incidents, findings, and overdue work on the approved schedule.",
2096
- "control-policy-management": "Open the Control inventory and a recently revised Policy. Check owners are named, the approver is separate, and the change went through review before approval.",
2097
- "control-security-communication": "Trace how a security notice or Policy change reaches staff and affected outside parties. Check that the approved reporting routes can be used.",
2122
+ "control-security-governance": "Set a security review calendar and name a reviewer separate from the owners. Make sure the agenda covers risks, incidents, findings, and overdue work when those records exist.",
2123
+ "control-policy-management": "Name Control owners and a separate Policy approver. Set the path for reviewing and approving future Policy changes before they take effect.",
2124
+ "control-security-communication": "Set working normal and fallback routes for security notices and Policy changes to staff and affected outside parties.",
2098
2125
  "control-workforce-expectations": "Review onboarding for roles with sensitive access. Check that required screening, agreements, and Policy acknowledgements happen before access.",
2099
- "control-security-training": "Compare the worker roster with training assignments. Check starters and role changes get training on time, with repeats tied to current content and the approved schedule.",
2100
- "control-risk-assessment": "Open the risk assessment against in-scope Systems and Vendors. Check threats, fraud, dependencies, and changes have owners and high risks get the required review.",
2101
- "control-monitoring-remediation": "Open the latest Control review, or schedule the first. Check incidents, test results, Exceptions, and overdue work, with owners and verified closure for gaps.",
2126
+ "control-security-training": "Set up the worker roster, training audience, assignment rule, and owner. Check that starters and role changes will receive the approved content on time.",
2127
+ "control-risk-assessment": "Set the risk assessment scope for the in-scope Systems and Vendors. Name who reviews threats, fraud, dependencies, changes, and high risks.",
2128
+ "control-monitoring-remediation": "Schedule the first Control review. Name who will inspect incidents, tests, Exceptions, overdue work, and follow-up closure when those records exist.",
2102
2129
  "control-access-authorization": "Open access lists for in-scope Systems and Vendor tools. Check that accounts use unique identities and only needed permissions, and new grants require a business reason and approval.",
2103
2130
  "control-strong-authentication": "Open the System and Vendor lists. For each tool used for production, source code, email, identity, or sensitive data, check MFA, unique accounts, default credentials, and admin roles.",
2104
- "control-access-review-offboarding": "Export access lists from key Systems and Vendor tools. Check privileged and other access reviews meet their approved schedules, and departures lose access within the required window.",
2131
+ "control-access-review-offboarding": "Check whether key Systems and Vendor tools can export access lists. Set the first access review and name who removes access after a departure.",
2105
2132
  "control-physical-workspace-security": "Walk through actual work areas and visitor access. Check who can enter nonpublic spaces and how devices, papers, screens, and conversations are protected.",
2106
2133
  "control-data-classification-inventory": "List important data stores in Systems and Vendor tools. Check each has an owner, classification, approved purpose, and recorded location.",
2107
2134
  "control-encryption-transmission": "For Systems, Vendor tools, and devices holding Confidential or Restricted data, check encryption at rest and in transit and who can manage the keys.",
@@ -2109,33 +2136,35 @@ const starterControlChecks = {
2109
2136
  "control-inventory-configuration": "Compare actual services, devices, software, service accounts, Vendors, and data stores with the inventory. Check owners, secure settings, and unsupported assets.",
2110
2137
  "control-endpoint-protection": "Review devices allowed into company Systems. Check encryption, screen lock, updates, and malware protection against the approved settings.",
2111
2138
  "control-network-security": "Open production network and remote-access rules. Check each path has a business reason, approved authentication, and separation; identify rules due for review.",
2112
- "control-change-management": "Follow a software or infrastructure change from request to deployment. Check the path includes risk review, testing, approval, and a way to recover.",
2139
+ "control-change-management": "Set the path for software and infrastructure changes: risk review, testing, approval, deployment, and recovery. Check teams use it for the next real change.",
2113
2140
  "control-vulnerability-management": "List internet-facing and important Systems. Check each has vulnerability scan coverage or another approved check, and findings have owners and target dates.",
2114
2141
  "control-penetration-testing": "Review the service's exposure, major changes, and customer commitments. Decide whether independent testing is needed and, if so, set its scope and cadence.",
2115
2142
  "control-logging-monitoring": "For each important System, open security logs, alert rules, and service-health checks where needed. Check retention and whether test alerts reach an owner.",
2116
2143
  "control-incident-response": "Walk through how a worker reports an incident. Check who triages, escalates, contains, communicates, and closes it using the approved plan.",
2117
- "control-incident-exercise": "Send a test alert through a representative incident path. Check acknowledgement, escalation, and fallback, then record participants, failures, and follow-up.",
2118
- "control-backup-restoration": "For each important System, inspect backup or alternate recovery settings and failure alerts. Run or review a restore test against its recovery needs.",
2144
+ "control-incident-exercise": "Set the incident exercise owner, scenario, and schedule. Check the reporting, escalation, and fallback paths are ready for the first exercise.",
2145
+ "control-backup-restoration": "For each important System, inspect backup or alternate recovery settings and failure alerts. Schedule the first restore test against its recovery needs.",
2119
2146
  "control-continuity-exercise": "Review recovery priorities, contacts, and owners. Check the plan covers loss of an important System or provider and has an exercise date.",
2120
- "control-vendor-due-diligence": "Open the Vendor list. For each with sensitive data or a material service role, check its security review and contract terms; review older Vendors already in use.",
2121
- "control-vendor-monitoring": "Open critical and high-risk Vendors. Check each has a current review, an owner for open issues, and timely reassessment after a material change or incident.",
2147
+ "control-vendor-due-diligence": "Identify Vendors with sensitive data or a material service role. Set the review and contract checks before approving a new Vendor; triage existing Vendors for the first review.",
2148
+ "control-vendor-monitoring": "Identify critical and high-risk Vendors, assign review owners and cadence, and set a reassessment trigger for material changes or incidents.",
2122
2149
  "control-security-exceptions": "Review departures from security rules. Check each has a reason, owner, risk assessment, compensating measure, approval, and expiry or review date."
2123
2150
  };
2124
2151
 
2125
2152
  export function controlImplementationSteps(control, checks, options = {}) {
2126
2153
  const steps = [];
2127
- if (!checks.implemented) steps.push(starterControlChecks[control.id]
2128
- || "Inspect the systems, people, vendors, or work this Control covers. Compare current practice with its statement and fix any gaps.");
2154
+ if (!checks.implemented) steps.push(control.id === "control-strong-authentication" && options.targets?.length
2155
+ ? `Ensure MFA and unique accounts are enabled for ${options.targets.join(", ")}; check default credentials and admin roles.`
2156
+ : starterControlChecks[control.id]
2157
+ || `Inspect the Systems, people, Vendors, and work linked to ${control.title || control.id}. Compare the actual configuration with this Control's statement and fix the specific gaps.`);
2129
2158
  const record = [];
2130
- if (!checks.owner) record.push("choose an owner");
2131
- if (!checks.scope) record.push("select the Systems this covers");
2132
- if (!checks.operationPattern) record.push("set when it runs");
2133
- if (!checks.procedure) record.push("write who does what and when in Procedure");
2134
- if (!checks.evidenceSource) record.push("link the tool or system that can show it happened");
2159
+ if (!checks.owner) record.push("assign the actual owner from existing People or Appointments");
2160
+ if (!checks.scope) record.push("link the in-scope Systems already in the Program");
2161
+ if (!checks.operationPattern) record.push("record whether it runs continuously, per transaction, on a schedule, or after an event");
2162
+ if (!checks.procedure) record.push("write the current actors, configuration, and procedure in Record Markdown");
2163
+ if (!checks.evidenceSource) record.push("link the authoritative Component that can produce proof");
2135
2164
  if (!checks.policyMapping && checks.policyMapping !== undefined) record.push("link the Policy it implements");
2136
2165
  if (!checks.criteriaMapping && checks.criteriaMapping !== undefined) record.push("link the criteria it covers");
2137
- if (checks.implemented && !checks.implementationDate) record.push("enter the date it started working");
2138
- if (checks.implemented && (checks.procedureRevision === false || checks.procedureEffective === false)) {
2166
+ if (!checks.implementationDate) record.push("record the date the working implementation took effect when marking Implemented");
2167
+ if (checks.procedureRevision === false || checks.procedureEffective === false) {
2139
2168
  record.push("record the Procedure revision and effective date");
2140
2169
  }
2141
2170
  if (record.length) steps.push(`In this Control, ${record.join("; ")}.`);
@@ -2145,18 +2174,16 @@ export function controlImplementationSteps(control, checks, options = {}) {
2145
2174
  ? `Cover the missing evidence families: ${sourceTargets.length ? sourceTargets.join(" and ") : "this Control"}. For each, link an active Component with the matching kind, evidence owners, and report retrieval instructions.`
2146
2175
  : null;
2147
2176
  const queueStep = checks.workQueue === false
2148
- ? "Check the linked Obligations. Enable one with the owner and schedule for this Control's recurring or event work, or add one if none covers it."
2177
+ ? "Inspect linked Obligations and their Policy basis. Enable a suitable existing rule with its owner and deadline, or add a rule only if no existing one covers this work. A scheduled rule does not prove a cycle occurred."
2149
2178
  : null;
2150
2179
  if (!checks.implemented) {
2151
2180
  if (sourceStep || queueStep) steps.push([sourceStep, queueStep].filter(Boolean).join(" "));
2152
- else if (checks.applicability === false) steps.push("Confirm this Control applies to the current Program scope with `review-applicability --scaffold --type control`.");
2153
- else steps.push(checks.procedureRevision === undefined
2154
- ? "When it works, mark this Control Implemented and enter its start date."
2155
- : "When it works, mark this Control Implemented. Enter its start date and the Procedure revision and effective date.");
2181
+ else if (checks.applicability === false) steps.push("Compare this Control with the current Program scope and record its applicability decision with `review-applicability --scaffold --type control`; ask management only if the decision cannot be established from the scope and commitments.");
2182
+ else steps.push("When these launch conditions hold, mark the Control Implemented with its real effective date. Record dated proof later, when a cycle or event actually occurs.");
2156
2183
  } else {
2157
2184
  if (sourceStep) steps.push(sourceStep);
2158
2185
  if (queueStep) steps.push(queueStep);
2159
- if (checks.applicability === false) steps.push("Confirm this Control applies to the current Program scope with `review-applicability --scaffold --type control`.");
2186
+ if (checks.applicability === false) steps.push("Compare this Control with the current Program scope and record its applicability decision with `review-applicability --scaffold --type control`; ask management only if the decision cannot be established from the scope and commitments.");
2160
2187
  }
2161
2188
  return steps;
2162
2189
  }
package/src/validate.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  } from "./recurrence.js";
22
22
  import { resourceReviewRevisionMatches, resourceReviewRevisions, retentionReviewResourceIds } from "./retention.js";
23
23
  import { retentionScheduleApprovalIssues } from "./retention-schedule-approval.js";
24
- import { obligationIsEnabled } from "./program-lifecycle.js";
24
+ import { obligationIsEnabled, obligationRuleIsEnabled } from "./program-lifecycle.js";
25
25
  import { currentPartyPeople, partyPeople } from "./parties.js";
26
26
  import { isMarkdownChoice, markdownEntries } from "./resource-markdown.js";
27
27
  import { currentCalendarDate, isRfc3339Timestamp, localDateTimeValue, timestampFromLocalDateTime } from "./time.js";
@@ -222,7 +222,7 @@ async function validateWorkspaceUnmeasured(input) {
222
222
  validateIndependentApproval(record, byId, displayPath, diagnostics);
223
223
  validateCompletedObligationEvent(record, byId, loaded.model, displayPath, diagnostics);
224
224
  validateActionObligationRule(record, byId, displayPath, diagnostics);
225
- validateImplementedControlSchedules(record, obligationsByControl, displayPath, diagnostics);
225
+ validateImplementedControlSchedules(record, obligationsByControl, byId, new Date().toISOString(), displayPath, diagnostics);
226
226
  serialContentDiagnosticTasks.push(async () => {
227
227
  const markdownDiagnostics = [];
228
228
  const approvalDiagnostics = [];
@@ -1538,7 +1538,7 @@ function assertFingerprintBudget(options, uncachedBytes) {
1538
1538
  }
1539
1539
  }
1540
1540
 
1541
- function validateImplementedControlSchedules(record, obligationsByControl, path, diagnostics) {
1541
+ function validateImplementedControlSchedules(record, obligationsByControl, byId, now, path, diagnostics) {
1542
1542
  if (record.type !== "control" || record.status !== "implemented") return;
1543
1543
  const schedules = obligationsByControl.get(record.id) || [];
1544
1544
  if (!schedules.length) {
@@ -1550,16 +1550,20 @@ function validateImplementedControlSchedules(record, obligationsByControl, path,
1550
1550
  ));
1551
1551
  return;
1552
1552
  }
1553
- if (schedules.some(obligationIsEnabled)) return;
1554
- const stopped = schedules.filter((obligation) => !obligationIsEnabled(obligation));
1553
+ if (schedules.some((obligation) => obligationRuleIsEnabled(obligation, byId, now))) return;
1554
+ const stopped = schedules.filter((obligation) => !obligationRuleIsEnabled(obligation, byId, now));
1555
1555
  const paused = stopped.filter((obligation) => obligation.status === "paused");
1556
1556
  const proposed = stopped.filter((obligation) => obligation.status === "proposed");
1557
+ const pendingRules = stopped.filter((obligation) => obligationIsEnabled(obligation) && obligation.scheduleMode === "rule");
1557
1558
  const reasons = [
1558
1559
  proposed.length
1559
1560
  ? `${proposed.length} linked ${proposed.length === 1 ? "schedule is" : "schedules are"} still proposed. Enable ${proposed.length === 1 ? "it" : "them"} before implementing the control; the schedule will remain dormant until its governing Policy is active and effective.`
1560
1561
  : "",
1561
1562
  paused.length
1562
1563
  ? `${paused.length} linked ${paused.length === 1 ? "schedule is" : "schedules are"} paused. Enable ${paused.length === 1 ? "it" : "them"} before implementing the control.`
1564
+ : "",
1565
+ pendingRules.length
1566
+ ? `${pendingRules.length} linked ${pendingRules.length === 1 ? "rule is" : "rules are"} not active. Activate the selected rule before implementing the control.`
1563
1567
  : ""
1564
1568
  ].filter(Boolean).join(" ");
1565
1569
  diagnostics.push(error(
package/src/web.js CHANGED
@@ -1098,6 +1098,7 @@ function renderPolicyActivationAssessments() {
1098
1098
  return '<article class="policy-activation-card ' + esc(assessment.state) + '"><div class="evidence-map-card-head"><div><span class="badge ' + (operating ? "good" : "warn") + '">' + esc(assessment.label) + '</span><h3><a href="#/resource/policy/' + encodeURIComponent(assessment.policyId) + '">' + esc(assessment.title) + '</a></h3></div><small>' + assessment.gapCount + ' ' + pluralize("gap", assessment.gapCount) + '</small></div>' + warnings +
1099
1099
  '<div class="policy-activation-gaps">' +
1100
1100
  gapGroup("Planned or partial Controls", assessment.plannedOrPartialControlIds, "control") +
1101
+ gapGroup("Implemented Controls with launch gaps", assessment.missingLaunchControlIds || [], "control") +
1101
1102
  gapGroup("Missing active Components", assessment.missingComponentControlIds, "control") +
1102
1103
  gapGroup("Missing ready evidence sources", assessment.missingEvidenceSourceControlIds, "control") +
1103
1104
  gapGroup("Missing enabled Obligations", assessment.missingScheduleControlIds, "control") +
@@ -1212,9 +1213,9 @@ function workflowGuidance(options = {}) {
1212
1213
  const renderRow = (item) => {
1213
1214
  const href = workflowItemHref(item);
1214
1215
  const guidance = options.type === "control" && item.nextSteps?.length
1215
- ? '<ol class="control-next-steps">' + item.nextSteps.map((step) => '<li>' + esc(step) + '</li>').join("") + '</ol>'
1216
+ ? '<p class="workflow-status">' + esc(controlLaunchLabel(item.implementationState)) + '</p><ol class="control-next-steps">' + item.nextSteps.map((step) => '<li>' + esc(step) + '</li>').join("") + '</ol><small>' + esc(item.implementationAssertion || "") + '</small>'
1216
1217
  : '<small>' + esc(item.message || workflowItemDetail(item)) + '</small>';
1217
- const body = '<span class="workflow-finding-status ' + esc(item.state) + '">' + esc(properCase(item.state)) + '</span><span><strong>' + esc(item.title) + '</strong>' + guidance + '</span>';
1218
+ const body = '<span class="workflow-finding-status ' + esc(item.state) + '">' + esc(options.type === "control" && item.implementationState ? controlLaunchLabel(item.implementationState).split(":")[0] : properCase(item.state)) + '</span><span><strong>' + esc(item.title) + '</strong>' + guidance + '</span>';
1218
1219
  return href ? '<a href="' + href + '">' + body + '</a>' : '<div>' + body + '</div>';
1219
1220
  };
1220
1221
  const rows = visible.map(renderRow).join("");
@@ -1226,7 +1227,27 @@ function workflowGuidance(options = {}) {
1226
1227
  return '<section class="workflow-guidance panel detail-support-panel detail-workflow-panel"><div class="panel-head"><div><p class="kicker">To-do</p><h3>' + esc(options.title || "Checklist") + '</h3></div><span class="badge ' + (blocking.length ? "warn" : "good") + '">' + (blocking.length ? "Needs work" : "Current") + '</span></div><p class="workflow-status">' + esc(status) + (completionRequirements ? '. ' + completionRequirements : "") + '</p><div class="workflow-findings">' + rows + '</div>' + more + '</section>';
1227
1228
  }
1228
1229
 
1230
+ function controlLaunchLabel(state) {
1231
+ return ({
1232
+ planned: "Planned: launch conditions remain",
1233
+ partial: "Partially implemented: launch conditions remain",
1234
+ "ready-to-implement": "Launch checks ready: check the live configuration, then record its start date",
1235
+ "implemented-with-gaps": "Implemented with gaps: fix the launch checks",
1236
+ implemented: "Implemented"
1237
+ })[state] || "Control launch";
1238
+ }
1239
+
1240
+ function controlLaunchState(id) {
1241
+ return state.programReadiness?.stages?.find(({ id: stageId }) => stageId === "controls")?.items
1242
+ .find(({ id: itemId }) => itemId === "control-" + id)?.implementationState;
1243
+ }
1244
+
1229
1245
  function recordCompletionState(record) {
1246
+ if (record.type === "control") {
1247
+ const item = state.programReadiness?.stages?.find(({ id }) => id === "controls")?.items
1248
+ .find(({ id }) => id === "control-" + record.id);
1249
+ if (item?.implementationState === "implemented") return '<section class="record-completion-state panel detail-support-panel"><p class="kicker">Control launch</p><h3>Implemented</h3><p>' + esc(item.implementationAssertion) + '</p><p>Dated evidence belongs to the operating work that produces it.</p></section>';
1250
+ }
1230
1251
  const completeStatuses = new Set(["approved", "active", "complete", "completed", "closed", "done", "reconciled"]);
1231
1252
  if (!completeStatuses.has(record.status)) return "";
1232
1253
  return '<section class="record-completion-state panel detail-support-panel"><p class="kicker">Record status</p><h3>' + esc(properCase(record.status)) + '</h3><p>This record has no direct next step.</p></section>';
@@ -1408,8 +1429,13 @@ function recordWorkflowCell(type, entry, detailContext = "") {
1408
1429
  const item = items[0];
1409
1430
  let href = workflowItemHref(item) || "#/resource/" + encodeURIComponent(type) + "/" + encodeURIComponent(entry.record.id);
1410
1431
  if (detailContext && href.startsWith("#/resource/" + encodeURIComponent(type) + "/") && !href.includes("?")) href += detailContext;
1411
- const title = type === "control" ? "Finish implementation" : item.title;
1412
- return '<a class="record-workflow-action" href="' + href + '"><span class="workflow-finding-status ' + esc(item.state) + '">' + esc(properCase(item.state)) + '</span><span><strong>' + esc(title) + '</strong><small>' + esc(stagePageItemDetail(item)) + (items.length > 1 ? " +" + (items.length - 1) + " more" : "") + '</small></span></a>';
1432
+ const title = type === "control"
1433
+ ? item.implementationState === "implemented-with-gaps" ? "Fix Control launch gaps" : "Finish implementation"
1434
+ : item.title;
1435
+ const statusLabel = type === "control" && item.implementationState
1436
+ ? controlLaunchLabel(item.implementationState).split(":")[0]
1437
+ : properCase(item.state);
1438
+ return '<a class="record-workflow-action" href="' + href + '"><span class="workflow-finding-status ' + esc(item.state) + '">' + esc(statusLabel) + '</span><span><strong>' + esc(title) + '</strong><small>' + esc(stagePageItemDetail(item)) + (items.length > 1 ? " +" + (items.length - 1) + " more" : "") + '</small></span></a>';
1413
1439
  }
1414
1440
 
1415
1441
  function openCollectionReviewDialog(type) {
@@ -1839,7 +1865,7 @@ function stagePageCard(stage, destination, index) {
1839
1865
  const taskPreview = items.length
1840
1866
  ? '<div class="stage-page-tasks">' + items.slice(0, 3).map((item) => {
1841
1867
  const href = destination.utility === "evidence-sources" ? destination.href : item.href || workflowItemHref(item) || destination.href;
1842
- return '<a href="' + href + '"><span class="workflow-finding-status ' + esc(item.state) + '">' + esc(properCase(item.state)) + '</span><span><strong>' + esc(item.title) + '</strong><small>' + esc(stagePageItemDetail(item)) + '</small></span></a>';
1868
+ return '<a href="' + href + '"><span class="workflow-finding-status ' + esc(item.state) + '">' + esc(item.implementationState ? controlLaunchLabel(item.implementationState).split(":")[0] : properCase(item.state)) + '</span><span><strong>' + esc(item.title) + '</strong><small>' + esc(stagePageItemDetail(item)) + '</small></span></a>';
1843
1869
  }).join("") + (items.length > 3 ? '<small class="stage-page-tasks-more">+' + (items.length - 3) + ' more on this page</small>' : "") + '</div>'
1844
1870
  : "";
1845
1871
  return '<article class="stage-page-card ' + (complete ? "complete" : "") + '"><div class="stage-page-card-head"><div><small>' + esc(stepLabel) + '</small><h3>' + esc(destination.label) + '</h3></div><div class="stage-page-card-actions">' + completionState + '<a class="stage-page-open" href="' + destination.href + '" aria-label="Open ' + esc(destination.label) + '"><span>Open ›</span></a></div></div><p>' + esc(summary) + '</p>' + taskPreview + '</article>';
@@ -2277,8 +2303,9 @@ function policyEventTrigger(trigger, index, collapsed = false, scope = "events")
2277
2303
  const tooltipId = "policy-event-tooltip-" + scope + "-" + index;
2278
2304
  const proposed = trigger.programStatus === "proposed";
2279
2305
  const unavailable = state.readOnly || proposed;
2306
+ const blocker = trigger.steps.find(({ programBlocker }) => programBlocker)?.programBlocker;
2280
2307
  const availability = proposed
2281
- ? "Available after its governing policies are effective, at least one linked control is implemented, and every task has a current owner."
2308
+ ? "Dormant until " + (blocker ? blocker.label.toLowerCase() + " (" + blocker.id + ")" : "its governing content is active and effective") + ". Use this workflow for the first real event after cutover."
2282
2309
  : state.readOnly
2283
2310
  ? "Open this workspace in writable mode to trigger the workflow."
2284
2311
  : trigger.steps.length + " " + pluralize("task", trigger.steps.length) + " will be added to the Work Queue.";
@@ -3219,7 +3246,7 @@ function renderList(main, type, params = new URLSearchParams()) {
3219
3246
  const start = (pageNumber - 1) * LIST_PAGE_SIZE;
3220
3247
  const visible = filtered.slice(start, start + LIST_PAGE_SIZE);
3221
3248
  main.querySelector("#result-count").textContent = filtered.length + (filtered.length === 1 ? " record" : " records");
3222
- main.querySelector("#record-rows").innerHTML = filtered.length ? visible.map((entry) => '<tr><td data-label="' + esc(fieldLabel(type, "title")) + '" data-primary-field><a class="record-title" href="#/resource/' + encodeURIComponent(type) + '/' + encodeURIComponent(entry.record.id) + detailContext + '">' + esc(entry.record.title) + '</a></td>' + fields.map((name) => '<td data-label="' + esc(fieldLabel(type, name)) + '">' + (name === "$workQueueStatus" ? obligationWorkQueueStatus(entry.record) : formatValue(name === "status" ? displayStatus(entry.record) : entry.record[name], name, type, true)) + '</td>').join("") + '<td data-label="Next action">' + recordWorkflowCell(type, entry) + '</td><td data-label="Git file"><code>' + esc(entry.relativePath.replace(/^data\//, "")) + '</code></td></tr>').join("") : '<tr><td colspan="' + (fields.length + 3) + '">' + empty(entries.length ? "No records match this filter." : collectionEmptyState(type, definition)) + '</td></tr>';
3249
+ main.querySelector("#record-rows").innerHTML = filtered.length ? visible.map((entry) => '<tr><td data-label="' + esc(fieldLabel(type, "title")) + '" data-primary-field><a class="record-title" href="#/resource/' + encodeURIComponent(type) + '/' + encodeURIComponent(entry.record.id) + detailContext + '">' + esc(entry.record.title) + '</a></td>' + fields.map((name) => '<td data-label="' + esc(fieldLabel(type, name)) + '">' + (name === "$workQueueStatus" ? obligationWorkQueueStatus(entry.record) : formatValue(name === "status" ? type === "control" ? controlLaunchState(entry.record.id) || displayStatus(entry.record) : displayStatus(entry.record) : entry.record[name], name, type, true)) + '</td>').join("") + '<td data-label="Next action">' + recordWorkflowCell(type, entry) + '</td><td data-label="Git file"><code>' + esc(entry.relativePath.replace(/^data\//, "")) + '</code></td></tr>').join("") : '<tr><td colspan="' + (fields.length + 3) + '">' + empty(entries.length ? "No records match this filter." : collectionEmptyState(type, definition)) + '</td></tr>';
3223
3250
  pagination.hidden = totalPages === 1;
3224
3251
  previous.disabled = pageNumber === 1;
3225
3252
  next.disabled = pageNumber === totalPages;
package/src/workflow.js CHANGED
@@ -466,13 +466,17 @@ function recordIncompleteReason(record, loaded, program) {
466
466
  return {
467
467
  state: "ready",
468
468
  requiredness: appointmentRequiredness(record, loaded.model),
469
- message: "Assign a holder, confirm the authority and independence needed, then activate this Appointment on its actual start date."
469
+ message: "Check current People and scoped Appointments, assign a qualified holder, and record the actual authority and start date. Ask management only if the holder or independence decision is unresolved."
470
470
  };
471
471
  }
472
472
  const messages = {
473
473
  draft: "Add the required details, relationships, and Markdown before review.",
474
474
  planned: "Check this planned record against how your program works, then add the missing details.",
475
- proposed: "Confirm the work, owner, schedule, and completion requirements before activation.",
475
+ proposed: ["obligation", "obligation-rule"].includes(record.type)
476
+ ? "Read the linked Policy and Control, fill the actual owner, schedule, deadline, and proof requirement, then activate the rule. Ask management only for a choice those sources do not settle."
477
+ : record.type === "reporting-route-set"
478
+ ? "Fill the normal and fallback reporting routes from approved contact details, commit the proposal, and obtain the required separate approval before it takes effect. Ask only for missing route or authority decisions."
479
+ : "Inspect the source facts and type guide, complete the proposal, and obtain the required review or approval before activation.",
476
480
  "in-review": "Have an independent reviewer approve the exact content revision.",
477
481
  open: "Complete this work with the required proof, or formally close it without completion.",
478
482
  "in-progress": "Finish the work, record the result, and link the completion proof.",
@@ -724,7 +728,7 @@ function reconciliationFindings(reconciliation) {
724
728
  state: "ready",
725
729
  severity: "warning",
726
730
  requiredness: "conditional",
727
- title: `Confirm ${fieldLabel(candidate.eventType)}`,
731
+ title: `Resolve ${fieldLabel(candidate.eventType)} transition`,
728
732
  message: candidate.message,
729
733
  subject: candidate.subject,
730
734
  fieldPath: candidate.sourcePath,
@@ -1139,6 +1143,11 @@ function normalizeFinding(code, item, context) {
1139
1143
  title: item.title,
1140
1144
  message: item.message,
1141
1145
  ...(item.nextSteps?.length ? { nextSteps: item.nextSteps } : {}),
1146
+ ...(item.implementationState ? {
1147
+ implementationState: item.implementationState,
1148
+ launchChecks: item.launchChecks,
1149
+ implementationAssertion: item.implementationAssertion
1150
+ } : {}),
1142
1151
  ...(item.checks ? { unmetChecks: Object.entries(item.checks)
1143
1152
  .filter(([, passed]) => passed === false)
1144
1153
  .map(([name]) => name) } : {}),
@@ -1625,12 +1634,12 @@ function periodFinding(key, title, message, state, subject, actions, dependencie
1625
1634
  function auditClosureNextStep(status) {
1626
1635
  const steps = {
1627
1636
  planned: {
1628
- title: "Confirm the engagement and start audit preparation",
1629
- message: "Link the agreed engagement terms, confirm scope and management acknowledgement, then move the audit to in progress."
1637
+ title: "Record the agreed engagement and start audit preparation",
1638
+ message: "Read the signed engagement terms, link them, record the agreed scope and management acknowledgement, then move the audit to in progress. Ask for missing terms or acknowledgement only if unavailable."
1630
1639
  },
1631
1640
  "in-progress": {
1632
1641
  title: "Begin fieldwork",
1633
- message: "Finish management preparation, confirm the fieldwork dates, and move the audit to fieldwork when the CPA firm begins testing."
1642
+ message: "Finish management preparation, record the fieldwork dates agreed with the CPA firm, and move the audit to fieldwork when testing begins. Ask for the dates only if they are not in the engagement correspondence."
1634
1643
  },
1635
1644
  fieldwork: {
1636
1645
  title: "Finish fieldwork and prepare the report",
@@ -1642,7 +1651,7 @@ function auditClosureNextStep(status) {
1642
1651
  },
1643
1652
  issued: {
1644
1653
  title: "Record final report delivery",
1645
- message: "Confirm the issued report evidence, opinion, report date, authorized recipient, and delivery receipt, then move the audit to delivered."
1654
+ message: "Read the issued report and delivery record, enter the opinion, report date, authorized recipient, and receipt, then move the audit to delivered. Ask only for missing delivery facts."
1646
1655
  },
1647
1656
  delivered: {
1648
1657
  title: "Close the audit",