@cassiomc1/forgeloop 0.1.6 → 0.1.8

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.
@@ -5,13 +5,15 @@ import { requiredGatesForGuides } from "./guide-metadata.js";
5
5
  import { assertRouteInvariants } from "./router.js";
6
6
  import { assertSourceProvenance } from "./sources.js";
7
7
  import { readPersistedRoute } from "./route-artifact.js";
8
- import { appendProtocolEvent, LIFECYCLE_MILESTONES, validateEventLedger } from "./events.js";
8
+ import { appendProtocolEvent, validateEventLedger } from "./events.js";
9
9
  import { readWorkState } from "./work-state.js";
10
10
  import { readConfig } from "./config.js";
11
11
  import { assertSafePath, ensureWithin, fileExists, readBytes } from "./filesystem.js";
12
12
  import { sha256 } from "./manifest.js";
13
- import { validateProfileSources } from "./profile.js";
13
+ import { findProfilePath, validateProfileSources } from "./profile.js";
14
14
  import { assertStateIdentity } from "./completion-relationships.js";
15
+ import { ensureResumableState, synchronizePreflightState } from "./resumability.js";
16
+ import { PROFILE_PATH } from "./target-layout.js";
15
17
 
16
18
  const PREVIEW_DECISION_LIMIT = 10;
17
19
  const PREVIEW_DECISION_MAX_LENGTH = 240;
@@ -91,10 +93,9 @@ export function validatePersistedPreflight(persisted, current) {
91
93
  }
92
94
 
93
95
  async function readProfile(target) {
94
- const relativePath = "PROJECT_PROFILE.md";
95
- await assertSafePath(target, relativePath);
96
+ const relativePath = await findProfilePath(target);
97
+ if (!relativePath) return { status: "missing", fingerprint: null };
96
98
  const filePath = ensureWithin(target, relativePath);
97
- if (!(await fileExists(filePath))) return { status: "missing", fingerprint: null };
98
99
  const bytes = await readBytes(filePath);
99
100
  const text = bytes.toString("utf8");
100
101
  const mode = text.match(/^profile-mode:\s*([^\s]+)\s*$/m)?.[1] ?? null;
@@ -204,7 +205,7 @@ export async function evaluatePreflight({ target, packageRoot, strict = false }
204
205
  const config = await optionalConfig(target, packageRoot, errors);
205
206
  const effectiveStrict = strict || config.complianceMode === "strict";
206
207
  if (effectiveStrict && profile.status !== "verified") {
207
- errors.push(issue("E_PROFILE_UNVERIFIED", "Strict preflight requires a verified project profile", ["PROJECT_PROFILE.md"]));
208
+ errors.push(issue("E_PROFILE_UNVERIFIED", "Strict preflight requires a verified project profile", [PROFILE_PATH]));
208
209
  }
209
210
  const unresolvedDecisions = contract?.value?.unresolvedDecisions ?? [];
210
211
  if (unresolvedDecisions.length > 0) {
@@ -348,62 +349,235 @@ function sameReadyPreflightEvent(event, result) {
348
349
  && sameStringSet(event.details?.satisfiedGates, result.satisfiedGates);
349
350
  }
350
351
 
351
- function planReadyPreflightLifecycleWrite(ledger, result) {
352
- if (!ledger) return { appendEvents: false };
353
- const existing = ledger.events.find((event) => event.event === "PREFLIGHT_READY");
354
- if (existing) {
355
- if (sameReadyPreflightEvent(existing, result)) return { appendEvents: false };
352
+ export async function validateReadyProtocolConsistency({
353
+ target,
354
+ packageRoot,
355
+ persisted,
356
+ current = null,
357
+ } = {}) {
358
+ if (persisted?.status !== "READY") return [];
359
+ const result = current ?? await evaluatePreflight({ target, packageRoot });
360
+ const errors = [...validatePersistedPreflight(persisted, result)];
361
+
362
+ let state = null;
363
+ try {
364
+ state = await readWorkState(target, packageRoot);
365
+ } catch (error) {
366
+ errors.push(issue("E_STATE_INVALID", error.message, [ARTIFACT_PATHS.state]));
367
+ }
368
+ if (!state) {
369
+ errors.push(issue(
370
+ "E_STATE_MISSING_AFTER_PREFLIGHT_READY",
371
+ "A persisted READY preflight must have a resumable work-state checkpoint",
372
+ [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.state],
373
+ ));
374
+ } else {
375
+ if (state.taskId !== persisted.taskId) {
376
+ errors.push(issue(
377
+ "E_STATE_TASK_MISMATCH",
378
+ "The resumable checkpoint does not belong to the READY preflight task",
379
+ [ARTIFACT_PATHS.state, ARTIFACT_PATHS.preflight],
380
+ ));
381
+ }
382
+ if (state.contractFingerprint !== result.fingerprints.contract) {
383
+ errors.push(issue(
384
+ "E_CONTRACT_STALE",
385
+ "The resumable checkpoint does not match the READY contract fingerprint",
386
+ [ARTIFACT_PATHS.state, ARTIFACT_PATHS.contract],
387
+ ));
388
+ }
389
+ if (state.routeFingerprint !== result.fingerprints.routing) {
390
+ errors.push(issue(
391
+ "E_ROUTE_STALE",
392
+ "The resumable checkpoint does not match the READY routing fingerprint",
393
+ [ARTIFACT_PATHS.state, ARTIFACT_PATHS.route],
394
+ ));
395
+ }
396
+ if (JSON.stringify(state.selectedGuides) !== JSON.stringify(result.routing.guides)) {
397
+ errors.push(issue(
398
+ "E_ROUTE_GUIDE_MISMATCH",
399
+ "The resumable checkpoint guides do not match the READY routing result",
400
+ [ARTIFACT_PATHS.state, ARTIFACT_PATHS.route],
401
+ ));
402
+ }
403
+ if (!sameStringSet(state.requiredGates ?? [], persisted.requiredGates)
404
+ || !sameStringSet(state.satisfiedGates ?? [], persisted.satisfiedGates)) {
405
+ errors.push(issue(
406
+ "E_PREFLIGHT_GATES_STALE",
407
+ "The resumable checkpoint gate sets do not match the READY preflight",
408
+ [ARTIFACT_PATHS.state, ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.gates],
409
+ ));
410
+ }
411
+ }
412
+
413
+ const ledger = await validateEventLedger(target, packageRoot);
414
+ if (!ledger.valid) {
415
+ errors.push(...ledger.errors.map((error) => issue(
416
+ error.code ?? "E_EVENT_INVALID",
417
+ error.message,
418
+ [ARTIFACT_PATHS.events],
419
+ )));
420
+ }
421
+ const events = ledger.events ?? [];
422
+ for (const requiredEvent of ["CONTRACT_VALIDATED", "ROUTE_VALIDATED"]) {
423
+ if (!events.some((event) => event.event === requiredEvent && event.taskId === persisted.taskId)) {
424
+ errors.push(issue(
425
+ "E_PREFLIGHT_EVENT_MISSING",
426
+ `READY preflight is missing lifecycle event: ${requiredEvent}`,
427
+ [ARTIFACT_PATHS.events, ARTIFACT_PATHS.preflight],
428
+ ));
429
+ }
430
+ }
431
+ for (const gate of persisted.satisfiedGates ?? []) {
432
+ if (!events.some((event) => event.event === "GATE_SATISFIED"
433
+ && event.taskId === persisted.taskId
434
+ && event.details?.gate === gate)) {
435
+ errors.push(issue(
436
+ "E_PREFLIGHT_GATE_EVENT_MISSING",
437
+ `READY preflight is missing lifecycle gate event: ${gate}`,
438
+ [ARTIFACT_PATHS.events, `${ARTIFACT_PATHS.gates}/${gate}.json`],
439
+ ));
440
+ }
441
+ }
442
+ const readyEvents = events.filter((event) => event.event === "PREFLIGHT_READY" && event.taskId === persisted.taskId);
443
+ if (readyEvents.length === 0) {
444
+ errors.push(issue(
445
+ "E_PREFLIGHT_READY_EVENT_MISSING",
446
+ "Persisted READY preflight is missing the matching PREFLIGHT_READY lifecycle event",
447
+ [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events],
448
+ ));
449
+ } else if (!readyEvents.some((event) => sameReadyPreflightEvent(event, result))) {
450
+ errors.push(issue(
451
+ "E_PREFLIGHT_READY_EVENT_MISMATCH",
452
+ "PREFLIGHT_READY lifecycle details do not match the persisted READY preflight",
453
+ [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events],
454
+ ));
455
+ }
456
+ return sortIssues(errors);
457
+ }
458
+
459
+ function sameBlockedPreflightEvent(event, result) {
460
+ return event.event === "PREFLIGHT_BLOCKED"
461
+ && event.taskId === result.taskId
462
+ && event.fingerprint === (result.fingerprints.contract ?? undefined)
463
+ && sameStringSet(event.details?.requiredGates, result.requiredGates)
464
+ && sameStringSet(event.details?.satisfiedGates, result.satisfiedGates)
465
+ && event.details?.routingFingerprint === result.fingerprints.routing;
466
+ }
467
+
468
+ function assertExistingReadyLifecycleCompatibility(ledger, result) {
469
+ if (result.status !== "READY") return;
470
+ const existingReady = ledger?.events?.find((event) => event.event === "PREFLIGHT_READY" && event.taskId === result.taskId);
471
+ if (existingReady && !sameReadyPreflightEvent(existingReady, result)) {
356
472
  throw preflightError(
357
473
  "E_PHASE_CHRONOLOGY_INVALID",
358
474
  "PREFLIGHT_READY already exists with different READY preflight details; repair the contract, route, or gate lifecycle before refreshing preflight",
359
475
  [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events, ARTIFACT_PATHS.contract, ARTIFACT_PATHS.route, ARTIFACT_PATHS.gates],
360
476
  );
361
477
  }
362
- const lastMilestone = ledger.events.reduce(
363
- (last, event) => Math.max(last, LIFECYCLE_MILESTONES.indexOf(event.event)),
364
- -1,
365
- );
366
- const routeMilestone = LIFECYCLE_MILESTONES.indexOf("ROUTE_VALIDATED");
367
- const preflightMilestone = LIFECYCLE_MILESTONES.indexOf("PREFLIGHT_READY");
368
- if (lastMilestone < routeMilestone) return { appendEvents: false };
369
- if (lastMilestone !== preflightMilestone - 1) {
370
- throw preflightError(
371
- "E_PHASE_CHRONOLOGY_INVALID",
372
- "PREFLIGHT_READY cannot be appended after the current lifecycle ledger",
373
- [ARTIFACT_PATHS.events],
374
- );
375
- }
376
- return { appendEvents: true };
377
478
  }
378
479
 
379
- export async function runPreflight({ target, packageRoot, strict = false, persist = true } = {}) {
380
- const result = await evaluatePreflight({ target, packageRoot, strict });
381
- if (persist) {
382
- const ledger = await assertPreflightPersistenceSafety(target, packageRoot, result.taskId);
383
- assertPreflightResultPersistenceSafety(result);
384
- const lifecycleWrite = result.status === "READY"
385
- ? planReadyPreflightLifecycleWrite(ledger, result)
386
- : { appendEvents: true };
387
- await writeJsonArtifact(target, ARTIFACT_PATHS.preflight, result, "preflight", packageRoot);
388
- if (result.taskId !== "unknown" && lifecycleWrite.appendEvents) {
389
- for (const gate of result.satisfiedGates) {
390
- await appendProtocolEvent(target, {
391
- taskId: result.taskId,
392
- event: "GATE_SATISFIED",
393
- details: { gate },
394
- }, packageRoot);
395
- }
396
- await appendProtocolEvent(target, {
480
+ async function appendActivationEvents(target, packageRoot, ledger, result) {
481
+ const events = [...(ledger?.events ?? [])];
482
+ const hasEvent = (eventName) => events.some((event) => event.event === eventName && event.taskId === result.taskId);
483
+ const append = async (input) => {
484
+ const event = await appendProtocolEvent(target, input, packageRoot);
485
+ events.push(event);
486
+ };
487
+
488
+ if (events.length === 0) {
489
+ await append({ taskId: result.taskId, event: "TASK_RECEIVED" });
490
+ }
491
+ if (result.contract.status === "valid" && !hasEvent("CONTRACT_VALIDATED")) {
492
+ await append({ taskId: result.taskId, event: "CONTRACT_VALIDATED", fingerprint: result.fingerprints.contract });
493
+ }
494
+ if (result.routing.status === "valid" && !hasEvent("ROUTE_VALIDATED")) {
495
+ await append({ taskId: result.taskId, event: "ROUTE_VALIDATED", fingerprint: result.fingerprints.routing });
496
+ }
497
+ for (const gate of result.satisfiedGates) {
498
+ if (!events.some((event) => event.event === "GATE_SATISFIED"
499
+ && event.taskId === result.taskId
500
+ && event.details?.gate === gate)) {
501
+ await append({ taskId: result.taskId, event: "GATE_SATISFIED", details: { gate } });
502
+ }
503
+ }
504
+
505
+ const existingReady = events.find((event) => event.event === "PREFLIGHT_READY" && event.taskId === result.taskId);
506
+ if (result.status === "READY") {
507
+ if (existingReady && !sameReadyPreflightEvent(existingReady, result)) {
508
+ throw preflightError(
509
+ "E_PHASE_CHRONOLOGY_INVALID",
510
+ "PREFLIGHT_READY already exists with different READY preflight details; repair the contract, route, or gate lifecycle before refreshing preflight",
511
+ [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events, ARTIFACT_PATHS.contract, ARTIFACT_PATHS.route, ARTIFACT_PATHS.gates],
512
+ );
513
+ }
514
+ if (!existingReady) {
515
+ await append({
397
516
  taskId: result.taskId,
398
- event: result.status === "READY" ? "PREFLIGHT_READY" : "PREFLIGHT_BLOCKED",
517
+ event: "PREFLIGHT_READY",
399
518
  fingerprint: result.fingerprints.contract ?? undefined,
400
519
  details: {
401
520
  requiredGates: result.requiredGates,
402
521
  satisfiedGates: result.satisfiedGates,
403
522
  routingFingerprint: result.fingerprints.routing,
404
523
  },
405
- }, packageRoot);
524
+ });
525
+ }
526
+ } else if (!events.some((event) => sameBlockedPreflightEvent(event, result))) {
527
+ await append({
528
+ taskId: result.taskId,
529
+ event: "PREFLIGHT_BLOCKED",
530
+ fingerprint: result.fingerprints.contract ?? undefined,
531
+ details: {
532
+ requiredGates: result.requiredGates,
533
+ satisfiedGates: result.satisfiedGates,
534
+ routingFingerprint: result.fingerprints.routing,
535
+ },
536
+ });
537
+ }
538
+ return events;
539
+ }
540
+
541
+ export async function runPreflight({ target, packageRoot, strict = false, persist = true } = {}) {
542
+ let result = await evaluatePreflight({ target, packageRoot, strict });
543
+ if (!persist) return result;
544
+
545
+ let ledger = await assertPreflightPersistenceSafety(target, packageRoot, result.taskId);
546
+ assertPreflightResultPersistenceSafety(result);
547
+ assertExistingReadyLifecycleCompatibility(ledger, result);
548
+
549
+ let contract = null;
550
+ let route = null;
551
+ if (result.contract.status === "valid" && result.routing.status === "valid") {
552
+ contract = await readContract(target, packageRoot);
553
+ route = await readPersistedRoute(target, packageRoot);
554
+ const state = await ensureResumableState({ target, packageRoot, contract, route });
555
+ if (state) {
556
+ await synchronizePreflightState({
557
+ target,
558
+ packageRoot,
559
+ state,
560
+ contract,
561
+ route,
562
+ requiredGates: result.requiredGates,
563
+ satisfiedGates: result.satisfiedGates,
564
+ complianceMode: result.policy?.complianceMode,
565
+ });
566
+ result = await evaluatePreflight({ target, packageRoot, strict });
567
+ assertPreflightResultPersistenceSafety(result);
568
+ assertExistingReadyLifecycleCompatibility(ledger, result);
569
+ ledger = await assertPreflightPersistenceSafety(target, packageRoot, result.taskId);
570
+ }
571
+ }
572
+
573
+ if (result.taskId !== "unknown") {
574
+ await appendActivationEvents(target, packageRoot, ledger, result);
575
+ const afterEvents = await validateEventLedger(target, packageRoot);
576
+ if (!afterEvents.valid) {
577
+ const first = afterEvents.errors[0];
578
+ throw preflightError(first.code, first.message, [ARTIFACT_PATHS.events]);
406
579
  }
407
580
  }
581
+ await writeJsonArtifact(target, ARTIFACT_PATHS.preflight, result, "preflight", packageRoot);
408
582
  return result;
409
583
  }
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
3
3
  import { ARTIFACT_PATHS, readJsonArtifact } from "./artifacts.js";
4
4
  import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
5
5
  import { assertSourceProvenance } from "./sources.js";
6
+ import { LEGACY_PROFILE_PATH, PROFILE_PATH } from "./target-layout.js";
6
7
 
7
8
  const SOURCE_ID_PATTERN = /\b(?:USER|FILE|CMD|DECISION|OBS|INFER|UNKNOWN)-[A-Z0-9_-]+\b/g;
8
9
  const DIRECTIVE_PATTERN = /forgeloop-source:\s*([A-Z0-9_-]+)\s+kind=([a-z-]+)/gi;
@@ -12,12 +13,11 @@ function issue(code, message, artifacts = []) {
12
13
  }
13
14
 
14
15
  export async function validateProfileSources(target, packageRoot) {
15
- const profilePath = "PROJECT_PROFILE.md";
16
- await assertSafePath(target, profilePath);
17
- const absolutePath = ensureWithin(target, profilePath);
18
- if (!(await fileExists(absolutePath))) {
16
+ const profilePath = await findProfilePath(target);
17
+ if (!profilePath) {
19
18
  return { status: "missing", refs: [], errors: [] };
20
19
  }
20
+ const absolutePath = ensureWithin(target, profilePath);
21
21
  const text = await readFile(absolutePath, "utf8");
22
22
  const refs = [...new Set(text.match(SOURCE_ID_PATTERN) ?? [])].sort();
23
23
  const directives = [...text.matchAll(DIRECTIVE_PATTERN)].map((match) => ({ id: match[1], kind: match[2] }));
@@ -46,3 +46,11 @@ export async function validateProfileSources(target, packageRoot) {
46
46
  }
47
47
  return { status: errors.length === 0 ? "valid" : "invalid", refs, errors };
48
48
  }
49
+
50
+ export async function findProfilePath(target) {
51
+ for (const relativePath of [PROFILE_PATH, LEGACY_PROFILE_PATH]) {
52
+ await assertSafePath(target, relativePath);
53
+ if (await fileExists(ensureWithin(target, relativePath))) return relativePath;
54
+ }
55
+ return null;
56
+ }
@@ -13,6 +13,7 @@ export const FAILURE_CLASSES = Object.freeze([
13
13
  "ENVIRONMENT_FAILURE",
14
14
  "EXTERNAL_SERVICE_FAILURE",
15
15
  "STALE_STATE_FAILURE",
16
+ "OPERATOR_INTERRUPTION",
16
17
  ]);
17
18
 
18
19
  export const FAILURE_CODES = Object.freeze([
@@ -47,6 +48,12 @@ export const FAILURE_CODES = Object.freeze([
47
48
  "E_COMPLETION_REJECTED",
48
49
  "E_PUBLICATION_CLAIM_UNVERIFIED",
49
50
  "E_PRODUCTION_READINESS_UNVERIFIED",
51
+ "E_STATE_MISSING_AFTER_PREFLIGHT_READY",
52
+ "E_PREFLIGHT_READY_EVENT_MISSING",
53
+ "E_PREFLIGHT_READY_EVENT_MISMATCH",
54
+ "E_PREFLIGHT_EVENT_MISSING",
55
+ "E_PREFLIGHT_GATE_EVENT_MISSING",
56
+ "E_PREFLIGHT_GATES_STALE",
50
57
  ]);
51
58
 
52
59
  export const WORK_PHASES = Object.freeze([
@@ -74,6 +81,7 @@ export const GUIDE_IDS = Object.freeze([
74
81
  "performance",
75
82
  "accessibility",
76
83
  "games",
84
+ "taste",
77
85
  ]);
78
86
 
79
87
  export const GUIDE_ORDER = GUIDE_IDS;
@@ -2,9 +2,12 @@ import { readFile } from "node:fs/promises";
2
2
 
3
3
  import { evaluateAudit } from "./audit.js";
4
4
  import { fileExists, ensureWithin } from "./filesystem.js";
5
+ import { findProfilePath } from "./profile.js";
5
6
 
6
7
  async function profileStatus(target) {
7
- const profilePath = ensureWithin(target, "PROJECT_PROFILE.md");
8
+ const relativePath = await findProfilePath(target);
9
+ if (!relativePath) return "NOT_VERIFIED";
10
+ const profilePath = ensureWithin(target, relativePath);
8
11
  if (!(await fileExists(profilePath))) return "NOT_VERIFIED";
9
12
  const text = await readFile(profilePath, "utf8");
10
13
  return /^profile-status:\s*verified\s*$/m.test(text) ? "PASS" : "NOT_VERIFIED";
@@ -0,0 +1,61 @@
1
+ import { currentRepositoryFingerprint } from "./repository.js";
2
+ import { createWorkState, readWorkState, writeWorkState } from "./work-state.js";
3
+
4
+ const DEFAULT_PENDING_STEPS = ["planning", "implementation", "verification"];
5
+
6
+ export async function ensureResumableState({ target, packageRoot, contract, route }) {
7
+ if (!contract || !route) return null;
8
+ const existing = await readWorkState(target, packageRoot);
9
+ if (existing) return existing;
10
+
11
+ const state = createWorkState({
12
+ taskId: contract.value.taskId,
13
+ contractFingerprint: contract.fingerprint,
14
+ routeFingerprint: route.fingerprint,
15
+ repositoryFingerprint: await currentRepositoryFingerprint(target),
16
+ phase: "ROUTED",
17
+ selectedGuides: route.value.guides,
18
+ completedSteps: ["contract", "route"],
19
+ pendingSteps: DEFAULT_PENDING_STEPS,
20
+ checks: [],
21
+ failures: [],
22
+ blockers: [],
23
+ verificationEvidence: [],
24
+ });
25
+ await writeWorkState(target, state, { packageRoot });
26
+ return state;
27
+ }
28
+
29
+ export async function synchronizePreflightState({
30
+ target,
31
+ packageRoot,
32
+ state,
33
+ contract,
34
+ route,
35
+ requiredGates,
36
+ satisfiedGates,
37
+ complianceMode,
38
+ }) {
39
+ const candidate = createWorkState({
40
+ ...state,
41
+ taskId: contract.value.taskId,
42
+ contractFingerprint: contract.fingerprint,
43
+ routeFingerprint: route.fingerprint,
44
+ selectedGuides: route.value.guides,
45
+ requiredGates: [...requiredGates],
46
+ satisfiedGates: [...satisfiedGates],
47
+ ...(complianceMode ? { complianceMode } : {}),
48
+ lastUpdated: state.lastUpdated,
49
+ });
50
+ const withoutTimestamp = (value) => {
51
+ const copy = structuredClone(value);
52
+ delete copy.lastUpdated;
53
+ return copy;
54
+ };
55
+ if (JSON.stringify(withoutTimestamp(candidate)) === JSON.stringify(withoutTimestamp(state))) {
56
+ return state;
57
+ }
58
+ const next = { ...candidate, lastUpdated: new Date().toISOString() };
59
+ await writeWorkState(target, next, { packageRoot });
60
+ return next;
61
+ }
@@ -1,13 +1,16 @@
1
1
  import { assertRouteInvariants } from "./router.js";
2
2
  import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
3
3
  import { readContract } from "./contract.js";
4
+ import { ensureResumableState } from "./resumability.js";
4
5
 
5
6
  export async function persistRoute(target, route, packageRoot, options = {}) {
6
7
  assertRouteInvariants(route);
7
8
  let { contractFingerprint, ...writeOptions } = options;
9
+ let contractArtifact = null;
8
10
  if (contractFingerprint === undefined) {
9
11
  try {
10
- contractFingerprint = (await readContract(target, packageRoot)).fingerprint;
12
+ contractArtifact = await readContract(target, packageRoot);
13
+ contractFingerprint = contractArtifact.fingerprint;
11
14
  } catch (error) {
12
15
  if (error.code !== "ARTIFACT_MISSING") throw error;
13
16
  }
@@ -16,7 +19,7 @@ export async function persistRoute(target, route, packageRoot, options = {}) {
16
19
  ? route
17
20
  : { ...route, contractFingerprint };
18
21
  assertRouteInvariants(value);
19
- return writeJsonArtifact(
22
+ const artifact = await writeJsonArtifact(
20
23
  target,
21
24
  ARTIFACT_PATHS.route,
22
25
  value,
@@ -24,6 +27,10 @@ export async function persistRoute(target, route, packageRoot, options = {}) {
24
27
  packageRoot,
25
28
  writeOptions,
26
29
  );
30
+ if (contractArtifact && contractArtifact.fingerprint === artifact.value.contractFingerprint) {
31
+ await ensureResumableState({ target, packageRoot, contract: contractArtifact, route: artifact });
32
+ }
33
+ return artifact;
27
34
  }
28
35
 
29
36
  export async function readPersistedRoute(target, packageRoot) {
@@ -84,7 +84,7 @@ export const PLATFORM_SEMANTICS = Object.freeze({
84
84
  });
85
85
 
86
86
  const WORK_GUIDES = Object.freeze({
87
- "complete-website": ["premium", "design", "accessibility", "clean", "test", "security", "performance"],
87
+ "complete-website": ["premium", "design", "taste", "accessibility", "clean", "test", "security", "performance"],
88
88
  "api-auth": ["clean", "test", "security", "performance"],
89
89
  api: ["clean", "test"],
90
90
  backend: ["clean", "test"],
@@ -240,6 +240,7 @@ export function evaluateRoute(input = {}) {
240
240
  else if (guide === "performance") excluded[guide] = ["NO_MEASURABLE_PERFORMANCE_RISK"];
241
241
  else if (guide === "design" || guide === "accessibility") excluded[guide] = ["NO_UI_SURFACE"];
242
242
  else if (guide === "premium" || guide === "games") excluded[guide] = ["NO_PRIMARY_WORK_TYPE"];
243
+ else if (guide === "taste") excluded[guide] = ["NO_TASTE_FRONTEND_CONTEXT"];
243
244
  else excluded[guide] = ["NO_BEHAVIOR_OR_EXECUTABLE_CHANGE"];
244
245
  }
245
246
 
@@ -0,0 +1,38 @@
1
+ export const LAYOUT_VERSION = 2;
2
+ export const LEGACY_LAYOUT_VERSION = 1;
3
+ export const FORGELOOP_DIR = ".forgeloop";
4
+ export const FORGELOOP_KIT_DIR = ".forgeloop/kit";
5
+ export const PROFILE_PATH = `${FORGELOOP_KIT_DIR}/PROJECT_PROFILE.md`;
6
+ export const LEGACY_PROFILE_PATH = "PROJECT_PROFILE.md";
7
+
8
+ export const NATIVE_ADAPTER_PATHS = Object.freeze([
9
+ "AGENTS.md",
10
+ "CLAUDE.md",
11
+ ".cursor/rules/project-loop.mdc",
12
+ ".github/copilot-instructions.md",
13
+ ]);
14
+
15
+ const NATIVE_ADAPTER_SET = new Set(NATIVE_ADAPTER_PATHS);
16
+
17
+ export function isNativeAdapterPath(relativePath) {
18
+ return NATIVE_ADAPTER_SET.has(relativePath);
19
+ }
20
+
21
+ export function targetPathForSource(sourcePath) {
22
+ if (sourcePath === ".forgeloop/.gitignore" || isNativeAdapterPath(sourcePath)) {
23
+ return sourcePath;
24
+ }
25
+ return `${FORGELOOP_KIT_DIR}/${sourcePath}`;
26
+ }
27
+
28
+ export function legacyPathForSource(sourcePath) {
29
+ return sourcePath === ".forgeloop/forgeloop.gitignore" ? ".forgeloop/.gitignore" : sourcePath;
30
+ }
31
+
32
+ export function isKitPath(relativePath) {
33
+ return relativePath === FORGELOOP_KIT_DIR || relativePath.startsWith(`${FORGELOOP_KIT_DIR}/`);
34
+ }
35
+
36
+ export function profilePathForLayout(layoutVersion = LAYOUT_VERSION) {
37
+ return layoutVersion >= LAYOUT_VERSION ? PROFILE_PATH : LEGACY_PROFILE_PATH;
38
+ }
@@ -2,6 +2,13 @@ import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
5
+ import {
6
+ isNativeAdapterPath,
7
+ legacyPathForSource,
8
+ targetPathForSource,
9
+ } from "./target-layout.js";
10
+ import { nativeShim } from "./native-adapters.js";
11
+
5
12
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
6
13
 
7
14
  const TEMPLATE_SOURCE_PATHS = Object.freeze({
@@ -35,6 +42,7 @@ export const TEMPLATE_PATHS = [
35
42
  "ENG/games-code-design-web-eng.md",
36
43
  "ENG/perf-code-eng.md",
37
44
  "ENG/premium-sites-studio-eng.md",
45
+ "ENG/taste-frontend-eng.md",
38
46
  "ENG/sec-code-eng.md",
39
47
  "ENG/test-code-eng.md",
40
48
  "schemas/routing-input.schema.json",
@@ -66,8 +74,12 @@ export async function readTemplateEntries(packageRoot = PACKAGE_ROOT) {
66
74
  TEMPLATE_PATHS.map(async (relativePath) => {
67
75
  const sourcePath = TEMPLATE_SOURCE_PATHS[relativePath] ?? relativePath;
68
76
  return {
69
- relativePath,
70
- bytes: await readFile(path.join(packageRoot, sourcePath)),
77
+ relativePath: targetPathForSource(relativePath),
78
+ sourcePath,
79
+ legacyRelativePath: legacyPathForSource(sourcePath),
80
+ bytes: Buffer.from(isNativeAdapterPath(relativePath)
81
+ ? nativeShim(relativePath)
82
+ : await readFile(path.join(packageRoot, sourcePath))),
71
83
  };
72
84
  }),
73
85
  );