@skyf0xx/hedgehog 6.3.2 → 6.3.4
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/bin/cli.mjs +40 -6
- package/package.json +1 -1
- package/src/db/friction.mjs +38 -2
- package/src/db/satisfied.mjs +116 -0
- package/src/db/schema.mjs +19 -5
- package/src/db/status.mjs +22 -0
- package/src/hosts/gemini/gemini-extension.json +1 -1
- package/src/registry/cores.json +4 -4
package/bin/cli.mjs
CHANGED
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
formatMissingRequirements,
|
|
59
59
|
} from '../src/db/requires.mjs';
|
|
60
60
|
import { whyPath, formatWhy } from '../src/db/why.mjs';
|
|
61
|
-
import { addFriction, listFriction } from '../src/db/friction.mjs';
|
|
61
|
+
import { addFriction, listFriction, resolveFriction } from '../src/db/friction.mjs';
|
|
62
62
|
import { addDebt, listDebt, resolveDebt } from '../src/db/debt.mjs';
|
|
63
63
|
import { addDecision, listDecisions } from '../src/db/decision.mjs';
|
|
64
64
|
import {
|
|
@@ -668,7 +668,8 @@ ${bold('Usage')}
|
|
|
668
668
|
npx @skyf0xx/hedgehog graph --no-open start (or reuse) the server; print the URL instead
|
|
669
669
|
npx @skyf0xx/hedgehog why <path> provenance chain for a file
|
|
670
670
|
npx @skyf0xx/hedgehog friction add "<note>" log a friction note [--task <task-id>]
|
|
671
|
-
npx @skyf0xx/hedgehog friction list
|
|
671
|
+
npx @skyf0xx/hedgehog friction list [--all] list open friction, oldest first (--all includes resolved)
|
|
672
|
+
npx @skyf0xx/hedgehog friction resolve <friction-id> --reason "<why>" mark a friction note resolved
|
|
672
673
|
npx @skyf0xx/hedgehog debt add <task-id> "<note>" declare debt that lands in dependent tasks' packets
|
|
673
674
|
npx @skyf0xx/hedgehog debt list [<task-id>] [--all] list open debt, oldest first (--all includes resolved)
|
|
674
675
|
npx @skyf0xx/hedgehog debt resolve <debt-id> --reason "<why>" mark a debt note resolved
|
|
@@ -3299,27 +3300,60 @@ async function frictionCommand(args) {
|
|
|
3299
3300
|
}
|
|
3300
3301
|
|
|
3301
3302
|
if (sub === 'list') {
|
|
3303
|
+
const includeResolved = args.includes('--all') || args.includes('--resolved');
|
|
3302
3304
|
const db = openDb();
|
|
3303
3305
|
let entries;
|
|
3304
3306
|
try {
|
|
3305
|
-
entries = listFriction(db);
|
|
3307
|
+
entries = listFriction(db, { includeResolved });
|
|
3306
3308
|
} finally {
|
|
3307
3309
|
db.close();
|
|
3308
3310
|
}
|
|
3309
3311
|
|
|
3310
3312
|
if (entries.length === 0) {
|
|
3311
|
-
console.log(`${dim('No friction logged.')}\n`);
|
|
3313
|
+
console.log(`${dim(includeResolved ? 'No friction logged.' : 'No open friction.')}\n`);
|
|
3312
3314
|
return;
|
|
3313
3315
|
}
|
|
3314
3316
|
for (const entry of entries) {
|
|
3315
3317
|
console.log(`#${entry.id} ${dim(entry.loggedAt)}${entry.taskId ? ` ${bold(entry.taskId)}` : ''}`);
|
|
3316
|
-
console.log(` ${entry.note}
|
|
3318
|
+
console.log(` ${entry.note}`);
|
|
3319
|
+
if (entry.resolvedAt) {
|
|
3320
|
+
console.log(` ${green('resolved')} ${dim(entry.resolvedAt)} — ${entry.resolvedReason}`);
|
|
3321
|
+
}
|
|
3322
|
+
console.log('');
|
|
3317
3323
|
}
|
|
3318
3324
|
return;
|
|
3319
3325
|
}
|
|
3320
3326
|
|
|
3327
|
+
if (sub === 'resolve') {
|
|
3328
|
+
const frictionId = args[1];
|
|
3329
|
+
const reasonIdx = args.indexOf('--reason');
|
|
3330
|
+
const reason = reasonIdx !== -1 ? args[reasonIdx + 1] : undefined;
|
|
3331
|
+
|
|
3332
|
+
if (!frictionId || frictionId.startsWith('--') || !reason) {
|
|
3333
|
+
console.error(`${red('Usage:')} hedgehog friction resolve <friction-id> --reason "<why>"\n`);
|
|
3334
|
+
process.exitCode = 1;
|
|
3335
|
+
return;
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
const db = openDb();
|
|
3339
|
+
let result;
|
|
3340
|
+
try {
|
|
3341
|
+
result = await resolveFriction(db, { frictionId: Number(frictionId), reason });
|
|
3342
|
+
} catch (err) {
|
|
3343
|
+
console.error(`${red('Failed to resolve friction:')} ${err.message}\n`);
|
|
3344
|
+
process.exitCode = 1;
|
|
3345
|
+
return;
|
|
3346
|
+
} finally {
|
|
3347
|
+
db.close();
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
console.log(` ${green('resolved')} #${result.id}${result.taskId ? ` (${result.taskId})` : ''}`);
|
|
3351
|
+
console.log(` ${dim(result.note)}`);
|
|
3352
|
+
return;
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3321
3355
|
console.error(
|
|
3322
|
-
`${red('Unknown friction subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog friction add "<note>" [--task <task-id>]\n or: hedgehog friction list\n`,
|
|
3356
|
+
`${red('Unknown friction subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog friction add "<note>" [--task <task-id>]\n or: hedgehog friction list [--all]\n or: hedgehog friction resolve <friction-id> --reason "<why>"\n`,
|
|
3323
3357
|
);
|
|
3324
3358
|
process.exitCode = 1;
|
|
3325
3359
|
}
|
package/package.json
CHANGED
package/src/db/friction.mjs
CHANGED
|
@@ -33,8 +33,44 @@ export async function addFriction(db, { note, taskId }) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Returns every friction row, oldest first, for tweaker's review pass.
|
|
36
|
-
|
|
36
|
+
// Resolved rows are excluded by default — same convention as
|
|
37
|
+
// listDebt — and included when `includeResolved` is set (`--all`).
|
|
38
|
+
export function listFriction(db, { includeResolved = false } = {}) {
|
|
39
|
+
const where = includeResolved ? '' : 'WHERE resolved_at IS NULL';
|
|
37
40
|
return db
|
|
38
|
-
.prepare(
|
|
41
|
+
.prepare(
|
|
42
|
+
`SELECT id, task_id AS taskId, note, logged_at AS loggedAt,
|
|
43
|
+
resolved_at AS resolvedAt, resolved_reason AS resolvedReason
|
|
44
|
+
FROM friction ${where} ORDER BY id ASC`,
|
|
45
|
+
)
|
|
39
46
|
.all();
|
|
40
47
|
}
|
|
48
|
+
|
|
49
|
+
// Resolves one friction row by id: marks it resolved in the DB and
|
|
50
|
+
// appends the marker to the same committed log (FRICTION_LOG_PATH) the
|
|
51
|
+
// original entry was written to — friction has no `.hedgehog/notes/`
|
|
52
|
+
// record the way debt does, so this is the row's only committed source
|
|
53
|
+
// and `db rebuild` leaves friction rows untouched (see rebuild.mjs).
|
|
54
|
+
export async function resolveFriction(db, { frictionId, reason }) {
|
|
55
|
+
if (!frictionId) throw new Error('friction resolve requires a friction id');
|
|
56
|
+
if (!reason) throw new Error('friction resolve requires a --reason');
|
|
57
|
+
|
|
58
|
+
const row = db
|
|
59
|
+
.prepare('SELECT id, task_id AS taskId, note, resolved_at AS resolvedAt FROM friction WHERE id = ?')
|
|
60
|
+
.get(frictionId);
|
|
61
|
+
if (!row) throw new Error(`no such friction entry: #${frictionId}`);
|
|
62
|
+
if (row.resolvedAt) throw new Error(`friction #${frictionId} is already resolved`);
|
|
63
|
+
|
|
64
|
+
const resolvedAt = new Date().toISOString();
|
|
65
|
+
db.prepare('UPDATE friction SET resolved_at = ?, resolved_reason = ? WHERE id = ?').run(
|
|
66
|
+
resolvedAt,
|
|
67
|
+
reason,
|
|
68
|
+
frictionId,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
await mkdir(FRICTION_DIR, { recursive: true });
|
|
72
|
+
const header = `## ${resolvedAt} resolved #${frictionId}${row.taskId ? ` ${row.taskId}` : ''}`;
|
|
73
|
+
await appendFile(FRICTION_LOG_PATH, `${header}\n\n${reason}\n\n`);
|
|
74
|
+
|
|
75
|
+
return { id: row.id, taskId: row.taskId, note: row.note, resolvedAt, resolvedReason: reason };
|
|
76
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Possibly-satisfied detection: a `planned` task whose scope_globs are
|
|
2
|
+
// already fully covered by git-tracked, committed files with nothing
|
|
3
|
+
// pending inside that scope — the heuristic nudge toward `hedgehog
|
|
4
|
+
// reconcile` for work that landed under a different task's or a
|
|
5
|
+
// different intent's commit, so `db rebuild`'s exact-commit-subject
|
|
6
|
+
// attribution (rebuild.mjs#markCompletedTasks) never had a subject to
|
|
7
|
+
// credit it against and the task sits `planned` forever with no signal.
|
|
8
|
+
//
|
|
9
|
+
// This is evidence, not proof, exactly like reconcile.mjs's
|
|
10
|
+
// gatherEvidence: a scope glob resolving to tracked, clean paths says
|
|
11
|
+
// the files a task would have written already exist and nothing is
|
|
12
|
+
// mid-edit there. It says nothing about whether the task's objective was
|
|
13
|
+
// actually met by that content — a `planned` task can legitimately share
|
|
14
|
+
// a glob with pre-existing, unrelated code that still needs real work.
|
|
15
|
+
// False positives are expected and acceptable; this only flags "look at
|
|
16
|
+
// this", it never completes anything itself.
|
|
17
|
+
//
|
|
18
|
+
// Uses the same git pathspec mechanism verify.mjs's own scope gate uses
|
|
19
|
+
// (`:(glob)<glob>` handed to git literally, no in-process glob dialect of
|
|
20
|
+
// its own to keep in sync with core.mjs's), rather than matchesGlob's
|
|
21
|
+
// witness-path walk — a witness path proves non-containment against a
|
|
22
|
+
// *set* of globs (core.mjs's own use, and coverage's), not "which real
|
|
23
|
+
// files in this repo currently match one glob", which only git's own
|
|
24
|
+
// pathspec matcher can answer without re-walking the whole tree.
|
|
25
|
+
|
|
26
|
+
import { execFileSync } from 'node:child_process';
|
|
27
|
+
import { isEngineStatePath } from './engineState.mjs';
|
|
28
|
+
|
|
29
|
+
function git(args) {
|
|
30
|
+
return execFileSync('git', args, { encoding: 'utf8' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function pathspecsFor(scopeGlobs) {
|
|
34
|
+
return scopeGlobs.map((glob) => `:(glob)${glob}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Tracked files under a task's scope, minus engine-state paths — the
|
|
38
|
+
// same exclusion verify.mjs's gate applies, so a task whose scope happens
|
|
39
|
+
// to reach into `.hedgehog/` never gets flagged on the build graph's own
|
|
40
|
+
// bookkeeping.
|
|
41
|
+
function trackedPathsInScope(scopeGlobs) {
|
|
42
|
+
let output;
|
|
43
|
+
try {
|
|
44
|
+
output = git(['ls-files', '--', ...pathspecsFor(scopeGlobs)]);
|
|
45
|
+
} catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
return output
|
|
49
|
+
.split('\n')
|
|
50
|
+
.map((p) => p.trim())
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.filter((p) => !isEngineStatePath(p));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Modified, staged, or untracked paths under a task's scope — anything
|
|
56
|
+
// `git status --porcelain` reports there means the scope is still
|
|
57
|
+
// mid-edit, the opposite of "already satisfied".
|
|
58
|
+
function dirtyPathsInScope(scopeGlobs) {
|
|
59
|
+
let output;
|
|
60
|
+
try {
|
|
61
|
+
output = git(['status', '--porcelain', '--', ...pathspecsFor(scopeGlobs)]);
|
|
62
|
+
} catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
return output
|
|
66
|
+
.split('\n')
|
|
67
|
+
.map((line) => line.slice(3).trim())
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.filter((p) => !isEngineStatePath(p));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Every `planned` task whose scope_globs resolve to at least one tracked
|
|
73
|
+
// file and zero dirty ones — a task with an empty scope match (nothing in
|
|
74
|
+
// the repo yet touches its globs at all) is not flagged, since that is
|
|
75
|
+
// simply unstarted work, not possibly-satisfied work.
|
|
76
|
+
//
|
|
77
|
+
// Only `planned` is read, not `ready`: a `ready` task already cleared
|
|
78
|
+
// every dependency and is next.mjs/claim.mjs's own queue to hand out —
|
|
79
|
+
// flagging it here would tell an operator to reconcile work a claim is
|
|
80
|
+
// about to pick up anyway. `building`/`verifying` are leased and mid-flight
|
|
81
|
+
// by definition; `blocked` has its own NEEDS ATTENTION path; `complete` is
|
|
82
|
+
// done. `planned` is the one status this signal exists for: work nothing
|
|
83
|
+
// has looked at yet, sitting behind whatever unlocked it.
|
|
84
|
+
const PLANNED_TASKS_SQL = `
|
|
85
|
+
SELECT id, layer, objective, scope_globs FROM tasks
|
|
86
|
+
WHERE status = 'planned'
|
|
87
|
+
ORDER BY priority, id;
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
export function possiblySatisfiedTasks(db) {
|
|
91
|
+
const tasks = db.prepare(PLANNED_TASKS_SQL).all();
|
|
92
|
+
const flagged = [];
|
|
93
|
+
for (const task of tasks) {
|
|
94
|
+
const scopeGlobs = JSON.parse(task.scope_globs);
|
|
95
|
+
if (scopeGlobs.length === 0) continue;
|
|
96
|
+
const tracked = trackedPathsInScope(scopeGlobs);
|
|
97
|
+
if (tracked.length === 0) continue;
|
|
98
|
+
const dirty = dirtyPathsInScope(scopeGlobs);
|
|
99
|
+
if (dirty.length > 0) continue;
|
|
100
|
+
flagged.push({ id: task.id, layer: task.layer, objective: task.objective, paths: tracked });
|
|
101
|
+
}
|
|
102
|
+
return flagged;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function formatPossiblySatisfied(flagged) {
|
|
106
|
+
const lines = [`POSSIBLY SATISFIED ${flagged.length}`];
|
|
107
|
+
for (const { id, layer, paths } of flagged) {
|
|
108
|
+
lines.push(` ${id} ${layer} ${paths.length} tracked path${paths.length === 1 ? '' : 's'} in scope, none pending`);
|
|
109
|
+
}
|
|
110
|
+
lines.push('');
|
|
111
|
+
lines.push(
|
|
112
|
+
' Scope glob already covered by clean, tracked files — not proof the objective was met.' +
|
|
113
|
+
' See: hedgehog reconcile',
|
|
114
|
+
);
|
|
115
|
+
return lines.join('\n');
|
|
116
|
+
}
|
package/src/db/schema.mjs
CHANGED
|
@@ -127,10 +127,12 @@ CREATE TABLE IF NOT EXISTS decisions (
|
|
|
127
127
|
);
|
|
128
128
|
|
|
129
129
|
CREATE TABLE IF NOT EXISTS friction (
|
|
130
|
-
id
|
|
131
|
-
task_id
|
|
132
|
-
note
|
|
133
|
-
logged_at
|
|
130
|
+
id INTEGER PRIMARY KEY,
|
|
131
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
132
|
+
note TEXT NOT NULL,
|
|
133
|
+
logged_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
134
|
+
resolved_at TEXT,
|
|
135
|
+
resolved_reason TEXT
|
|
134
136
|
);
|
|
135
137
|
`;
|
|
136
138
|
|
|
@@ -178,7 +180,7 @@ export function ensureTaskColumns(db) {
|
|
|
178
180
|
// hand-set past what MIGRATIONS actually covers, since runMigrations
|
|
179
181
|
// trusts this number to mean "every migration through this version has
|
|
180
182
|
// run."
|
|
181
|
-
export const CURRENT_SCHEMA_VERSION =
|
|
183
|
+
export const CURRENT_SCHEMA_VERSION = 4;
|
|
182
184
|
|
|
183
185
|
// Forward migrations, applied in order to bring a graph's user_version up
|
|
184
186
|
// to CURRENT_SCHEMA_VERSION. Unlike the CREATE TABLE IF NOT EXISTS /
|
|
@@ -223,6 +225,18 @@ const MIGRATIONS = [
|
|
|
223
225
|
}
|
|
224
226
|
},
|
|
225
227
|
},
|
|
228
|
+
{
|
|
229
|
+
version: 4,
|
|
230
|
+
// Same shape as version 3's debt columns, for friction — an existing
|
|
231
|
+
// graph's rows default to open the moment these columns exist.
|
|
232
|
+
migrate: (db) => {
|
|
233
|
+
const existing = new Set(db.prepare('PRAGMA table_info(friction)').all().map((row) => row.name));
|
|
234
|
+
if (!existing.has('resolved_at')) db.exec('ALTER TABLE friction ADD COLUMN resolved_at TEXT');
|
|
235
|
+
if (!existing.has('resolved_reason')) {
|
|
236
|
+
db.exec('ALTER TABLE friction ADD COLUMN resolved_reason TEXT');
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
},
|
|
226
240
|
];
|
|
227
241
|
|
|
228
242
|
// Brings a graph's `PRAGMA user_version` up to CURRENT_SCHEMA_VERSION,
|
package/src/db/status.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { listFriction } from './friction.mjs';
|
|
|
17
17
|
import { orphanedOverrides } from './overrides.mjs';
|
|
18
18
|
import { RECONCILED_DIR, RECONCILED_NOTE_PREFIX } from './reconcile.mjs';
|
|
19
19
|
import { formatMissingRequirements } from './requires.mjs';
|
|
20
|
+
import { possiblySatisfiedTasks, formatPossiblySatisfied } from './satisfied.mjs';
|
|
20
21
|
import { readyTasks, heldBackReason } from './ready.mjs';
|
|
21
22
|
import { worktreeStatus } from './worktree.mjs';
|
|
22
23
|
import { findClaimableTasks } from './claim.mjs';
|
|
@@ -219,6 +220,15 @@ function countFriction(db) {
|
|
|
219
220
|
// in every count and list here. Reported unconditionally, not as a
|
|
220
221
|
// warning: reconciling is a supported act, and the point is that the
|
|
221
222
|
// distinction stays visible after the session that made it is gone.
|
|
223
|
+
//
|
|
224
|
+
// `possiblySatisfied` (satisfied.mjs) is a `planned` task whose scope
|
|
225
|
+
// globs already resolve to tracked, clean files — a heuristic nudge, not
|
|
226
|
+
// a completion: `db rebuild`'s attribution only credits a task whose own
|
|
227
|
+
// commit_message appears as some commit's subject (rebuild.mjs), so work
|
|
228
|
+
// that landed inside a commit made for a different task never has a
|
|
229
|
+
// subject to match, and the task stays `planned` with no other signal
|
|
230
|
+
// that its scope may already be covered. Read-only, like drift and
|
|
231
|
+
// orphaned overrides — nothing here is ever auto-completed.
|
|
222
232
|
// Synchronous, as it always was: `boundary.mjs#boundaryState` (a hot path
|
|
223
233
|
// that only ever reads `graph.inFlight`) depends on that, and every other
|
|
224
234
|
// section here is a plain SQL read with no I/O to await. The worktree
|
|
@@ -236,6 +246,7 @@ export function graphStatus(db, { core = null, overrides = new Map() } = {}) {
|
|
|
236
246
|
const debt = loadDebtByTask(db);
|
|
237
247
|
const frictionCount = countFriction(db);
|
|
238
248
|
const reconciled = loadReconciledTasks(db);
|
|
249
|
+
const possiblySatisfied = possiblySatisfiedTasks(db);
|
|
239
250
|
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
240
251
|
return {
|
|
241
252
|
counts,
|
|
@@ -248,6 +259,7 @@ export function graphStatus(db, { core = null, overrides = new Map() } = {}) {
|
|
|
248
259
|
debt,
|
|
249
260
|
frictionCount,
|
|
250
261
|
reconciled,
|
|
262
|
+
possiblySatisfied,
|
|
251
263
|
total,
|
|
252
264
|
};
|
|
253
265
|
}
|
|
@@ -293,6 +305,7 @@ export function formatStatus({
|
|
|
293
305
|
debt = [],
|
|
294
306
|
frictionCount = 0,
|
|
295
307
|
reconciled = [],
|
|
308
|
+
possiblySatisfied = [],
|
|
296
309
|
worktrees = { active: [], orphaned: [] },
|
|
297
310
|
total,
|
|
298
311
|
missingRequirements,
|
|
@@ -381,6 +394,15 @@ export function formatStatus({
|
|
|
381
394
|
lines.push(formatDrift(drift));
|
|
382
395
|
}
|
|
383
396
|
|
|
397
|
+
// Below drift, same family of "condition of the graph as a whole" —
|
|
398
|
+
// and above orphaned overrides, since a possibly-satisfied task is work
|
|
399
|
+
// an operator can act on right now (`hedgehog reconcile`), where an
|
|
400
|
+
// orphaned override is only ever inert.
|
|
401
|
+
if (possiblySatisfied.length > 0) {
|
|
402
|
+
lines.push('');
|
|
403
|
+
lines.push(formatPossiblySatisfied(possiblySatisfied));
|
|
404
|
+
}
|
|
405
|
+
|
|
384
406
|
// Below drift because it's the rarer of the two and never blocks the
|
|
385
407
|
// build: an orphaned override is work the operator asked for that isn't
|
|
386
408
|
// happening, not work the graph is getting wrong. `hedgehog override
|
package/src/registry/cores.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"name": "full-stack-app",
|
|
5
5
|
"flag": "--ts-full-stack-app",
|
|
6
6
|
"package": "@skyf0xx/hedgehog-core-full-stack-app",
|
|
7
|
-
"version": "^1.0
|
|
7
|
+
"version": "^1.1.0",
|
|
8
8
|
"language": "typescript",
|
|
9
9
|
"repository": "https://github.com/skyf0xx/hedgehog-core-full-stack-app",
|
|
10
10
|
"selects_when": "The description names server-side logic across most of the app: authorization more expressive than per-object row-level security, background jobs, scheduled work or webhook receivers as the app's primary function, server-rendered or SEO-critical pages beyond an app shell, or a data model whose working set doesn't sensibly fit on a device. Persistent domain data alone is not the signal — a tracker, journal, notebook, or planner whose data fits on the user's device is pwa-app, even with sharing, accounts, or multi-device sync in scope, and even with one or two entities that must be server-authoritative. If the project has both a marketing page and a real app behind it, this is still full-stack-app: the page becomes routes inside apps/web, not a separate project."
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"name": "pwa-app",
|
|
14
14
|
"flag": "--pwa-app",
|
|
15
15
|
"package": "@skyf0xx/hedgehog-core-pwa-app",
|
|
16
|
-
"version": "^1.0
|
|
16
|
+
"version": "^1.1.0",
|
|
17
17
|
"language": "typescript",
|
|
18
18
|
"repository": "https://github.com/skyf0xx/hedgehog-core-pwa-app",
|
|
19
19
|
"selects_when": "The description names an app whose data model fits on the user's device and whose reads and writes are the user's own — a tracker, journal, notebook, planner, offline reference, or utility. Offline capability or installability named explicitly is a strong signal. Sharing, collaboration, accounts, and multi-device sync do NOT disqualify a project: Dexie Cloud provides sync, authentication, and server-enforced per-object access control, so a shared list, a family calendar, or a small team's board is still this core. A small number of entities that must be server-authoritative — a points balance, a reward ledger, anything a client must not write to directly — do NOT disqualify a project either: those entities are declared --remote and backed by Supabase (Postgres + RLS + Edge Functions) behind the same repository interface, while the rest of the app stays local-first. What routes a project to full-stack-app is server-side logic across most of the app, not the presence of one or two such entities: authorization beyond row-level policies, background jobs or webhooks as the app's primary function, server-rendered pages, or a working set too large for a device."
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
{
|
|
40
40
|
"name": "authored",
|
|
41
41
|
"package": "@skyf0xx/hedgehog-core-authored",
|
|
42
|
-
"version": "^1.
|
|
42
|
+
"version": "^1.2.0",
|
|
43
43
|
"language": "typescript",
|
|
44
44
|
"repository": "https://github.com/skyf0xx/hedgehog-core-authored",
|
|
45
45
|
"selects_when": "Neither shipped core fits, but the description names a real artifact a Builder step would produce — just not in either shipped core's shape. This core is designed by the planner rather than chosen from a fixed set: hedgehog-planning-intake's Phase 0 elicits the drivers first, then hedgehog-core-design names the system shape, picks the stack, derives the layers, and writes .hedgehog/core.yaml. It carries the same enforcement as a shipped core — ordered layers, scoped file access, verification before completion — but the sequence is designed for this project rather than battle-tested across many. This is the core most often confused with adopted: authored designs a workspace from scratch for a project being built new, while adopted brings Hedgehog's discipline to a repo that already exists — route here only when there is no existing codebase this work is being added to."
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
{
|
|
48
48
|
"name": "adopted",
|
|
49
49
|
"package": "@skyf0xx/hedgehog-core-adopted",
|
|
50
|
-
"version": "
|
|
50
|
+
"version": ">=1.0.0 <2.0.0",
|
|
51
51
|
"language": "typescript",
|
|
52
52
|
"repository": "https://github.com/skyf0xx/hedgehog-core-adopted",
|
|
53
53
|
"selects_when": "The description is about bringing Hedgehog's discipline to a codebase that already exists, rather than building something new — the repo already has real source files, or the user says so explicitly: \"adopt this repo\", \"add Hedgehog to my existing project\", \"I want scope/verify enforcement on my changes here\". Not chosen by matching a `when` paragraph the way a shipped core is: hedgehog-adopt reads the repo read-only, proposes a linear-chain .hedgehog/core.yaml whose verify commands are the repo's own, and writes only .hedgehog/ — never a workspace, never a stack migration. This is the core most often confused with authored: adopted brings discipline to an existing repo, while authored designs and scaffolds a workspace from scratch for something being built new — route here only when the work is landing on a codebase that already exists."
|